Change change detection for sidebar components

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/3/head
trivernis 3 years ago
parent 83f820e8ea
commit 12c0aa30f6
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -1,5 +1,5 @@
<ng-content></ng-content> <ng-content></ng-content>
<div *ngIf="this.busy" [class.blur]="this.blurBackground" [class.darken]="this.darkenBackground" <div *ngIf="this.busy" [class.blur]="this.blurBackground" [class.darken]="this.darkenBackground"
class="busy-indicator-overlay"> class="busy-indicator-overlay">
<mat-progress-spinner [mode]="mode" [value]="value" color="primary"></mat-progress-spinner> <mat-progress-spinner *ngIf="this.busy" [mode]="mode" [value]="value" color="primary"></mat-progress-spinner>
</div> </div>

@ -22,6 +22,10 @@
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
} }
hidden {
display: none;
}
::ng-deep app-busy-indicator { ::ng-deep app-busy-indicator {
width: 100%; width: 100%;
height: 100%; height: 100%;

@ -1,24 +1,33 @@
import {Component, Input} from "@angular/core"; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges} from "@angular/core";
import {ProgressSpinnerMode} from "@angular/material/progress-spinner"; import {ProgressSpinnerMode} from "@angular/material/progress-spinner";
@Component({ @Component({
selector: "app-busy-indicator", selector: "app-busy-indicator",
templateUrl: "./busy-indicator.component.html", templateUrl: "./busy-indicator.component.html",
styleUrls: ["./busy-indicator.component.scss"] styleUrls: ["./busy-indicator.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class BusyIndicatorComponent { export class BusyIndicatorComponent implements OnChanges {
@Input() busy: boolean = false; @Input() busy: boolean = false;
@Input() blurBackground: boolean = false; @Input() blurBackground: boolean = false;
@Input() darkenBackground: boolean = false; @Input() darkenBackground: boolean = false;
@Input() mode: ProgressSpinnerMode = "indeterminate"; @Input() mode: ProgressSpinnerMode = "indeterminate";
@Input() value: number | undefined; @Input() value: number | undefined;
constructor() { constructor(private changeDetector: ChangeDetectorRef) {
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes["busy"]) {
this.changeDetector.markForCheck();
}
} }
public setBusy(busy: boolean) { public setBusy(busy: boolean) {
this.busy = busy; if (busy != this.busy) {
this.busy = busy;
this.changeDetector.markForCheck();
}
} }
public wrapOperation<T>(operation: Function): T | undefined { public wrapOperation<T>(operation: Function): T | undefined {
@ -36,6 +45,7 @@ export class BusyIndicatorComponent {
public async wrapAsyncOperation<T>(operation: Function): Promise<T | undefined> { public async wrapAsyncOperation<T>(operation: Function): Promise<T | undefined> {
this.setBusy(true); this.setBusy(true);
console.log("busy");
try { try {
const result = await operation(); const result = await operation();
this.setBusy(false); this.setBusy(false);
@ -44,6 +54,7 @@ export class BusyIndicatorComponent {
return undefined; return undefined;
} finally { } finally {
this.setBusy(false); this.setBusy(false);
console.log("not busy");
} }
} }
} }

@ -1,10 +1,11 @@
import {Component, ViewChild,} from "@angular/core"; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewChild,} from "@angular/core";
import {MatMenuTrigger} from "@angular/material/menu"; import {MatMenuTrigger} from "@angular/material/menu";
@Component({ @Component({
selector: "app-context-menu", selector: "app-context-menu",
templateUrl: "./context-menu.component.html", templateUrl: "./context-menu.component.html",
styleUrls: ["./context-menu.component.scss"] styleUrls: ["./context-menu.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ContextMenuComponent { export class ContextMenuComponent {
@ -13,7 +14,7 @@ export class ContextMenuComponent {
@ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger; @ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger;
constructor() { constructor(private changeDetector: ChangeDetectorRef) {
} }
public onContextMenu(event: MouseEvent) { public onContextMenu(event: MouseEvent) {
@ -22,5 +23,6 @@ export class ContextMenuComponent {
this.y = event.clientY + "px"; this.y = event.clientY + "px";
this.menuTrigger.menu.focusFirstItem("mouse"); this.menuTrigger.menu.focusFirstItem("mouse");
this.menuTrigger.openMenu(); this.menuTrigger.openMenu();
this.changeDetector.markForCheck();
} }
} }

@ -1,4 +1,4 @@
import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from "@angular/core"; import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from "@angular/core";
import {Observable} from "rxjs"; import {Observable} from "rxjs";
import {FormControl} from "@angular/forms"; import {FormControl} from "@angular/forms";
import {Tag} from "../../../../../api/models/Tag"; import {Tag} from "../../../../../api/models/Tag";
@ -15,7 +15,8 @@ type AutocompleteEntry = {
@Component({ @Component({
selector: "app-filter-input", selector: "app-filter-input",
templateUrl: "./filter-input.component.html", templateUrl: "./filter-input.component.html",
styleUrls: ["./filter-input.component.scss"] styleUrls: ["./filter-input.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FilterInputComponent implements OnChanges { export class FilterInputComponent implements OnChanges {

@ -1,4 +1,14 @@
import {AfterViewChecked, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from "@angular/core"; import {
AfterViewChecked,
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild
} from "@angular/core";
import {SortKey} from "../../../../models/SortKey"; import {SortKey} from "../../../../models/SortKey";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {SortDialogComponent} from "./sort-dialog/sort-dialog.component"; import {SortDialogComponent} from "./sort-dialog/sort-dialog.component";
@ -18,7 +28,8 @@ import * as deepEqual from "fast-deep-equal";
@Component({ @Component({
selector: "app-file-search", selector: "app-file-search",
templateUrl: "./file-search.component.html", templateUrl: "./file-search.component.html",
styleUrls: ["./file-search.component.scss"] styleUrls: ["./file-search.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class FileSearchComponent implements AfterViewChecked, OnInit { export class FileSearchComponent implements AfterViewChecked, OnInit {
public sortExpression: SortKey[] = []; public sortExpression: SortKey[] = [];

@ -1,4 +1,4 @@
import {Component, Inject, OnChanges, SimpleChanges} from "@angular/core"; import {ChangeDetectionStrategy, Component, Inject, OnChanges, SimpleChanges} from "@angular/core";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {SortDialogComponent} from "../sort-dialog/sort-dialog.component"; import {SortDialogComponent} from "../sort-dialog/sort-dialog.component";
import {Tag} from "../../../../../../api/models/Tag"; import {Tag} from "../../../../../../api/models/Tag";
@ -13,7 +13,8 @@ type IndexableSelection<T> = {
@Component({ @Component({
selector: "app-filter-dialog", selector: "app-filter-dialog",
templateUrl: "./filter-dialog.component.html", templateUrl: "./filter-dialog.component.html",
styleUrls: ["./filter-dialog.component.scss"] styleUrls: ["./filter-dialog.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FilterDialogComponent implements OnChanges { export class FilterDialogComponent implements OnChanges {
public availableTags: Tag[] = []; public availableTags: Tag[] = [];

@ -1,14 +1,16 @@
import {Component, Inject} from "@angular/core"; import {ChangeDetectionStrategy, Component, Inject} from "@angular/core";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {SortKey} from "../../../../../models/SortKey"; import {SortKey} from "../../../../../models/SortKey";
import {CdkDragDrop, moveItemInArray} from "@angular/cdk/drag-drop"; import {CdkDragDrop, moveItemInArray} from "@angular/cdk/drag-drop";
import {Namespace} from "../../../../../../api/models/Namespace"; import {Namespace} from "../../../../../../api/models/Namespace";
import {TagService} from "../../../../../services/tag/tag.service"; import {TagService} from "../../../../../services/tag/tag.service";
import {compareSearchResults} from "../../../../../utils/compare-utils";
@Component({ @Component({
selector: "app-sort-dialog", selector: "app-sort-dialog",
templateUrl: "./sort-dialog.component.html", templateUrl: "./sort-dialog.component.html",
styleUrls: ["./sort-dialog.component.scss"] styleUrls: ["./sort-dialog.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class SortDialogComponent { export class SortDialogComponent {
@ -24,20 +26,6 @@ export class SortDialogComponent {
namespaces => this.namespaces = namespaces); namespaces => this.namespaces = namespaces);
} }
private static compareSuggestionNamespaces(query: string, l: string, r: string): number {
if (l.startsWith(query) && !r.startsWith(query)) {
return -1;
} else if (!l.startsWith(query) && r.startsWith(query)) {
return 1;
} else if (l.length < r.length) {
return -1;
} else if (l.length > r.length) {
return 1;
} else {
return l.localeCompare(r);
}
}
addNewSortKey() { addNewSortKey() {
const sortKey = new SortKey("FileName", "Ascending", undefined); const sortKey = new SortKey("FileName", "Ascending", undefined);
this.sortEntries.push(sortKey); this.sortEntries.push(sortKey);
@ -64,7 +52,7 @@ export class SortDialogComponent {
public updateAutocompleteSuggestions(value: string): void { public updateAutocompleteSuggestions(value: string): void {
this.suggestedNamespaces = this.namespaces.sort( this.suggestedNamespaces = this.namespaces.sort(
(a, b) => SortDialogComponent.compareSuggestionNamespaces(value, a.name, b.name)) (a, b) => compareSearchResults(value, a.name, b.name))
.slice(0, 50); .slice(0, 50);
} }
} }

@ -1,4 +1,4 @@
<div class="file-edit-inner" fxLayout="column"> <app-busy-indicator [blurBackground]="true" class="file-edit-inner" fxLayout="column">
<div class="file-edit-header" fxFlex="100px"> <div class="file-edit-header" fxFlex="100px">
<h1>Edit Tags</h1> <h1>Edit Tags</h1>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@ -33,5 +33,4 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<app-busy-indicator *ngIf="this.loading" [busy]="this.loading" [blurBackground]="true"></app-busy-indicator> </app-busy-indicator>
</div>

@ -12,6 +12,7 @@
.file-edit-header { .file-edit-header {
text-align: center; text-align: center;
h1 { h1 {
margin-top: 20px; margin-top: 20px;
} }
@ -75,6 +76,7 @@ mat-divider {
width: 100%; width: 100%;
} }
/*
app-busy-indicator { app-busy-indicator {
position: absolute; position: absolute;
top: 0; top: 0;
@ -83,3 +85,4 @@ app-busy-indicator {
width: 100%; width: 100%;
z-index: 99; z-index: 99;
} }
*/

@ -1,9 +1,10 @@
import { import {
AfterViewInit,
ChangeDetectionStrategy,
Component, Component,
EventEmitter, EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnInit,
Output, Output,
SimpleChanges, SimpleChanges,
ViewChild ViewChild
@ -12,31 +13,35 @@ import {File} from "../../../../../api/models/File";
import {Tag} from "../../../../../api/models/Tag"; import {Tag} from "../../../../../api/models/Tag";
import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling"; import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling";
import {TagService} from "../../../../services/tag/tag.service"; import {TagService} from "../../../../services/tag/tag.service";
import {ErrorBrokerService} from "../../../../services/error-broker/error-broker.service";
import {BusyIndicatorComponent} from "../../app-common/busy-indicator/busy-indicator.component";
@Component({ @Component({
selector: "app-tag-edit", selector: "app-tag-edit",
templateUrl: "./tag-edit.component.html", templateUrl: "./tag-edit.component.html",
styleUrls: ["./tag-edit.component.scss"] styleUrls: ["./tag-edit.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class TagEditComponent implements OnInit, OnChanges { export class TagEditComponent implements AfterViewInit, OnChanges {
@Input() files: File[] = []; @Input() files: File[] = [];
@Output() tagEditEvent = new EventEmitter<TagEditComponent>(); @Output() tagEditEvent = new EventEmitter<TagEditComponent>();
public tags: Tag[] = [];
@ViewChild("tagScroll") tagScroll!: CdkVirtualScrollViewport;
@ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent;
public tags: Tag[] = [];
public allTags: Tag[] = []; public allTags: Tag[] = [];
public editMode: string = "Toggle"; public editMode: string = "Toggle";
@ViewChild("tagScroll") tagScroll!: CdkVirtualScrollViewport;
private fileTags: { [key: number]: Tag[] } = {}; private fileTags: { [key: number]: Tag[] } = {};
public loading = false;
constructor( constructor(
private errorBroker: ErrorBrokerService,
private tagService: TagService, private tagService: TagService,
) { ) {
} }
async ngOnInit() { async ngAfterViewInit() {
this.tagService.tags.subscribe(tags => this.allTags = tags); this.tagService.tags.subscribe(tags => this.allTags = tags);
await this.tagService.loadTags(); await this.tagService.loadTags();
await this.tagService.loadNamespaces(); await this.tagService.loadNamespaces();
@ -50,7 +55,6 @@ export class TagEditComponent implements OnInit, OnChanges {
} }
public async editTag(tag: string): Promise<void> { public async editTag(tag: string): Promise<void> {
this.loading = true;
if (tag.length > 0) { if (tag.length > 0) {
let tagInstance = this.allTags.find( let tagInstance = this.allTags.find(
t => t.getNormalizedOutput() === tag); t => t.getNormalizedOutput() === tag);
@ -71,81 +75,87 @@ export class TagEditComponent implements OnInit, OnChanges {
break; break;
} }
} }
this.loading = false;
} }
async toggleTag(tag: Tag) { async toggleTag(tag: Tag) {
for (const file of this.files) { await this.wrapAsyncOperation(async () => {
const fileTags = this.fileTags[file.id]; for (const file of this.files) {
let addedTags = []; const fileTags = this.fileTags[file.id];
let removedTags = []; let addedTags = [];
if (fileTags.findIndex(i => i.id === tag.id) < 0) { let removedTags = [];
addedTags.push(tag.id); if (fileTags.findIndex(i => i.id === tag.id) < 0) {
} else { addedTags.push(tag.id);
removedTags.push(tag.id); } else {
removedTags.push(tag.id);
}
this.fileTags[file.id] = await this.tagService.changeFileTags(
file.id,
addedTags, removedTags
);
if (addedTags.length > 0) {
await this.tagService.loadTags();
await this.tagService.loadNamespaces();
}
} }
this.fileTags[file.id] = await this.tagService.changeFileTags( this.mapFileTagsToTagList();
file.id, const index = this.tags.indexOf(tag);
addedTags, removedTags); if (index >= 0) {
if (addedTags.length > 0) { this.tagScroll.scrollToIndex(index);
await this.tagService.loadTags();
await this.tagService.loadNamespaces();
} }
} });
this.mapFileTagsToTagList();
const index = this.tags.indexOf(tag);
if (index >= 0) {
this.tagScroll.scrollToIndex(index);
}
this.tagEditEvent.emit(this); this.tagEditEvent.emit(this);
} }
async addTag(tag: Tag) { async addTag(tag: Tag) {
for (const file of this.files) { await this.wrapAsyncOperation(async () => {
if ((this.fileTags[file.id] ?? []).findIndex(t => t.id === tag.id) < 0) { for (const file of this.files) {
this.fileTags[file.id] = await this.tagService.changeFileTags( if ((this.fileTags[file.id] ?? []).findIndex(t => t.id === tag.id) < 0) {
file.id, this.fileTags[file.id] = await this.tagService.changeFileTags(
[tag.id], []); file.id,
[tag.id], []
);
}
} }
} this.mapFileTagsToTagList();
this.mapFileTagsToTagList(); const index = this.tags.indexOf(tag);
const index = this.tags.indexOf(tag); if (index >= 0) {
if (index >= 0) { this.tagScroll.scrollToIndex(index);
this.tagScroll.scrollToIndex(index); }
} await this.tagService.loadTags();
await this.tagService.loadNamespaces();
});
this.tagEditEvent.emit(this); this.tagEditEvent.emit(this);
await this.tagService.loadTags();
await this.tagService.loadNamespaces();
} }
public async removeTag(tag: Tag) { public async removeTag(tag: Tag) {
this.loading = true; await this.wrapAsyncOperation(async () => {
for (const file of this.files) { for (const file of this.files) {
if (this.fileTags[file.id].findIndex(t => t.id === tag.id) >= 0) { if (this.fileTags[file.id].findIndex(t => t.id === tag.id) >= 0) {
this.fileTags[file.id] = await this.tagService.changeFileTags( this.fileTags[file.id] = await this.tagService.changeFileTags(
file.id, file.id,
[], [tag.id]); [], [tag.id]
);
}
} }
} this.mapFileTagsToTagList();
this.mapFileTagsToTagList(); });
this.loading = false;
this.tagEditEvent.emit(this); this.tagEditEvent.emit(this);
} }
private async loadFileTags() { private async loadFileTags() {
this.loading = true; await this.wrapAsyncOperation(async () => {
const promises = []; const promises = [];
const loadFn = async (file: File) => { const loadFn = async (file: File) => {
this.fileTags[file.id] = await this.tagService.getTagsForFiles( this.fileTags[file.id] = await this.tagService.getTagsForFiles(
[file.cd]); [file.cd]);
}; };
for (const file of this.files) { for (const file of this.files) {
promises.push(loadFn(file)); promises.push(loadFn(file));
} }
await Promise.all(promises); await Promise.all(promises);
this.mapFileTagsToTagList(); this.mapFileTagsToTagList();
this.loading = false; });
} }
private mapFileTagsToTagList() { private mapFileTagsToTagList() {
@ -160,4 +170,17 @@ export class TagEditComponent implements OnInit, OnChanges {
(a, b) => a.getNormalizedOutput() (a, b) => a.getNormalizedOutput()
.localeCompare(b.getNormalizedOutput())); .localeCompare(b.getNormalizedOutput()));
} }
private async wrapAsyncOperation<T>(cb: () => Promise<T>): Promise<T | undefined> {
if (!this.busyIndicator) {
try {
return cb();
} catch (err: any) {
this.errorBroker.showError(err);
return undefined;
}
} else {
return this.busyIndicator.wrapAsyncOperation(cb);
}
}
} }

Loading…
Cancel
Save