src/app/core/ui/sync-status/background-processing-indicator/background-processing-indicator.component.ts

Description

A dumb component handling presentation of the sync indicator icon and an additional details dropdown listing all currently running background processes.

Example

Metadata

Relationships

Depends on

  • MatButtonModule
  • MatBadgeModule
  • MatMenuModule
  • MatProgressSpinnerModule
  • FontAwesomeModule
  • MatTooltipModule
  • MatDividerModule

Index

Properties
Methods
Inputs

Inputs

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()
Returns : void
markWasClosed
markWasClosed()
Returns : void
Async resetSync
resetSync()

Clear sync checkpoints and trigger a full re-sync.

Returns : Promise<void>

Properties

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>

./background-processing-indicator.component.scss

.details-header {
  font-size: small;
  text-align: justify;
  line-height: normal;
}

.process-checkmark {
  color: green;
}

.details-line {
  line-height: normal;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""