Description
A dumb component handling presentation of the sync indicator icon
and an additional details dropdown listing all currently running background processes.
Example
Relationships
Depends on
-
MatButtonModule
-
MatBadgeModule
-
MatMenuModule
-
MatProgressSpinnerModule
-
FontAwesomeModule
-
MatTooltipModule
-
MatDividerModule
|
backgroundProcesses
|
Type : BackgroundProcessState[]
|
|
Required : true
|
|
|
details on current background processes to be displayed to user
|
|
summarize
|
Type : boolean
|
Default value : true
|
|
|
whether processes of with the same title shall be summarized into one line
|
Methods
|
closeTaskListDropdown
|
closeTaskListDropdown()
|
|
|
|
|
|
markWasClosed
|
markWasClosed()
|
|
|
|
|
|
Async
resetSync
|
resetSync()
|
|
|
Clear sync checkpoints and trigger a full re-sync.
Returns : Promise<void>
|
|
allTasksFinished
|
Type : unknown
|
Default value : computed(() => this.taskCounter() === 0)
|
|
|
|
filteredProcesses
|
Type : unknown
|
Default value : computed(() =>
this.summarizeProcesses(this.backgroundProcesses()),
)
|
|
|
|
showManualSync
|
Type : unknown
|
Default value : computed(
() => environment.session_type !== SessionType.online,
)
|
|
|
whether to show the manual sync button (hide in pure online-only mode)
|
|
taskCounter
|
Type : unknown
|
Default value : computed(
() => this.filteredProcesses().filter((process) => process.pending).length,
)
|
|
|
|
taskListDropdownTrigger
|
Type : unknown
|
Default value : viewChild(MatMenuTrigger)
|
|
|
handle to programmatically open/close the details dropdown
|
|
wasClosed
|
Type : unknown
|
Default value : signal(false)
|
|
|
import {
computed,
Component,
effect,
inject,
ChangeDetectionStrategy,
input,
signal,
viewChild,
} from "@angular/core";
import { MatMenuModule, MatMenuTrigger } from "@angular/material/menu";
import { BackgroundProcessState } from "../background-process-state.interface";
import { MatButtonModule } from "@angular/material/button";
import { MatBadgeModule } from "@angular/material/badge";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatDividerModule } from "@angular/material/divider";
import { DatabaseResolverService } from "../../../database/database-resolver.service";
import { SessionType } from "../../../session/session-type";
import { environment } from "../../../../../environments/environment";
/**
* A dumb component handling presentation of the sync indicator icon
* and an additional details dropdown listing all currently running background processes.
*/
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-background-processing-indicator",
templateUrl: "./background-processing-indicator.component.html",
styleUrls: ["./background-processing-indicator.component.scss"],
imports: [
MatButtonModule,
MatBadgeModule,
MatMenuModule,
MatProgressSpinnerModule,
FontAwesomeModule,
MatTooltipModule,
MatDividerModule,
],
})
export class BackgroundProcessingIndicatorComponent {
/** details on current background processes to be displayed to user */
backgroundProcesses = input.required<BackgroundProcessState[]>();
/** whether processes of with the same title shall be summarized into one line */
summarize = input(true);
wasClosed = signal(false);
private readonly dbResolver = inject(DatabaseResolverService);
filteredProcesses = computed(() =>
this.summarizeProcesses(this.backgroundProcesses()),
);
/** whether to show the manual sync button (hide in pure online-only mode) */
showManualSync = computed(
() => environment.session_type !== SessionType.online,
);
taskCounter = computed(
() => this.filteredProcesses().filter((process) => process.pending).length,
);
allTasksFinished = computed(() => this.taskCounter() === 0);
/** handle to programmatically open/close the details dropdown */
taskListDropdownTrigger = viewChild(MatMenuTrigger);
private openMenuTimeout: ReturnType<typeof setTimeout> | undefined;
private readonly taskCounterEffect = effect((onCleanup) => {
const amount = this.taskCounter();
const taskListDropdownTrigger = this.taskListDropdownTrigger();
if (amount === 0) {
if (this.openMenuTimeout) {
clearTimeout(this.openMenuTimeout);
}
taskListDropdownTrigger?.closeMenu();
this.wasClosed.set(false);
} else {
if (!this.wasClosed()) {
// Need to wait for the change cycle that shows the sync button.
if (this.openMenuTimeout) {
clearTimeout(this.openMenuTimeout);
}
this.openMenuTimeout = setTimeout(() =>
taskListDropdownTrigger?.openMenu(),
);
}
}
onCleanup(() => {
if (this.openMenuTimeout) {
clearTimeout(this.openMenuTimeout);
}
});
});
/**
* Clear sync checkpoints and trigger a full re-sync.
*/
async resetSync(): Promise<void> {
await this.dbResolver.resetSync();
}
markWasClosed(): void {
this.wasClosed.set(true);
}
closeTaskListDropdown(): void {
this.taskListDropdownTrigger()?.closeMenu();
}
private combineProcesses(
first: BackgroundProcessState,
second: BackgroundProcessState,
): BackgroundProcessState {
return {
title: first.title,
pending: first.pending || second.pending,
};
}
private summarizeProcesses(
processes: BackgroundProcessState[],
): BackgroundProcessState[] {
if (!this.summarize()) {
return processes;
}
const accumulator: BackgroundProcessState[] = [];
for (const process of processes) {
const summaryEntry = accumulator.findIndex(
(i) => i.title === process.title,
);
if (summaryEntry === -1) {
accumulator.push(process);
} else {
accumulator[summaryEntry] = this.combineProcesses(
accumulator[summaryEntry],
process,
);
}
}
return accumulator;
}
}
<button
mat-icon-button
class="white"
[style.opacity]="allTasksFinished() ? 0.5 : 1"
[matMenuTriggerFor]="taskListDropdown"
(menuClosed)="markWasClosed()"
>
<span
[matBadge]="taskCounter()"
matBadgeColor="accent"
[matBadgeHidden]="allTasksFinished()"
>
<fa-icon class="white" icon="sync"></fa-icon>
</span>
</button>
<mat-menu #taskListDropdown="matMenu">
<div class="padding-left-regular padding-right-regular flex-column gap-small">
@if (allTasksFinished() === false) {
<div i18n class="details-header">
The following processes are still running in the background. Until these
are finished some pages may be slow or incomplete.
</div>
}
@for (process of filteredProcesses(); track process.title) {
<div class="flex-row gap-small align-center mat-subtitle-2 details-line">
<div>
@if (process.pending) {
<mat-spinner [diameter]="20"></mat-spinner>
}
@if (!process.pending) {
<fa-icon icon="check" class="process-checkmark"></fa-icon>
}
</div>
<div>
{{ process.title }}
@if (process.details) {
<span class="truncate-text">({{ process.details }})</span>
}
</div>
</div>
}
<div class="full-width flex-row align-center gap-small">
<button
mat-stroked-button
(click)="closeTaskListDropdown()"
i18n="Hide sync details"
class="flex-grow"
matTooltip="System will continue to sync data regularly"
i18n-matTooltip
>
Continue in background
</button>
@if (showManualSync()) {
<button
mat-icon-button
(click)="resetSync()"
matTooltip="Manually trigger re-sync"
i18n-matTooltip
>
<fa-icon icon="rotate"></fa-icon>
</button>
}
</div>
</div>
</mat-menu>
.details-header {
font-size: small;
text-align: justify;
line-height: normal;
}
.process-checkmark {
color: green;
}
.details-line {
line-height: normal;
}
Legend
Html element with directive