src/app/core/admin/config-cleanup/configurable-enum-cleanup/configurable-enum-cleanup.component.ts

Implements

OnInit

Metadata

Relationships

Depends on

  • MatCardModule
  • MatButtonModule
  • MatListModule
  • MatProgressSpinnerModule
  • MatTooltipModule
  • FontAwesomeModule
  • OnInit

Index

Properties
Methods

Methods

Protected Async deleteEnum
deleteEnum(unusedEnum: ConfigurableEnumUsageSummary)
Parameters :
Name Type Optional
unusedEnum ConfigurableEnumUsageSummary No
Returns : any
Protected enumLabel
enumLabel(enumSummary: ConfigurableEnumUsageSummary)
Parameters :
Name Type Optional
enumSummary ConfigurableEnumUsageSummary No
Returns : string
Protected isDeleting
isDeleting(enumId: string)
Parameters :
Name Type Optional
enumId string No
Returns : any
Protected openEnumOptions
openEnumOptions(enumEntity: ConfigurableEnum, event: Event)
Parameters :
Name Type Optional
enumEntity ConfigurableEnum No
event Event No
Returns : void
Protected Async reload
reload()
Returns : any

Properties

Protected Readonly errorMessage
Type : unknown
Default value : signal<string | undefined>(undefined)
Protected Readonly isLoading
Type : unknown
Default value : signal(false)
Protected Readonly totalEnums
Type : unknown
Default value : computed( () => this.analysis()?.totalEnums ?? 0, )
Protected Readonly unusedEnums
Type : unknown
Default value : computed( () => this.analysis()?.unusedEnums ?? [], )
Protected Readonly usedEnumDetails
Type : unknown
Default value : computed( () => this.analysis()?.usedEnumDetails ?? [], )
Protected Readonly usedEnums
Type : unknown
Default value : computed( () => this.analysis()?.usedEnums ?? 0, )
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  OnInit,
  signal,
} from "@angular/core";
import { MatCardModule } from "@angular/material/card";
import { MatButtonModule } from "@angular/material/button";
import { MatListModule } from "@angular/material/list";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { MatSnackBar } from "@angular/material/snack-bar";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatDialog } from "@angular/material/dialog";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { ConfirmationDialogService } from "../../../common-components/confirmation-dialog/confirmation-dialog.service";
import { Logging } from "#src/app/core/logging/logging.service";
import { ConfigurableEnum } from "../../../basic-datatypes/configurable-enum/configurable-enum";
import { ConfigureEnumPopupComponent } from "../../../basic-datatypes/configurable-enum/configure-enum-popup/configure-enum-popup.component";
import {
  ConfigCleanupAnalysis,
  ConfigurableEnumCleanupService,
  ConfigurableEnumUsageSummary,
} from "./configurable-enum-cleanup.service";

@Component({
  selector: "app-configurable-enum-cleanup",
  templateUrl: "./configurable-enum-cleanup.component.html",
  styleUrl: "./configurable-enum-cleanup.component.scss",
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    MatCardModule,
    MatButtonModule,
    MatListModule,
    MatProgressSpinnerModule,
    MatTooltipModule,
    FontAwesomeModule,
  ],
})
export class ConfigurableEnumCleanupComponent implements OnInit {
  private readonly configCleanupService = inject(
    ConfigurableEnumCleanupService,
  );
  private readonly confirmationDialog = inject(ConfirmationDialogService);
  private readonly snackBar = inject(MatSnackBar);
  private readonly dialog = inject(MatDialog);

  protected readonly isLoading = signal(false);
  protected readonly errorMessage = signal<string | undefined>(undefined);
  private readonly analysis = signal<ConfigCleanupAnalysis | undefined>(
    undefined,
  );
  private readonly deletingIds = signal<string[]>([]);

  protected readonly totalEnums = computed(
    () => this.analysis()?.totalEnums ?? 0,
  );
  protected readonly usedEnums = computed(
    () => this.analysis()?.usedEnums ?? 0,
  );
  protected readonly usedEnumDetails = computed(
    () => this.analysis()?.usedEnumDetails ?? [],
  );
  protected readonly unusedEnums = computed(
    () => this.analysis()?.unusedEnums ?? [],
  );

  ngOnInit(): void {
    void this.reload();
  }

  protected async reload() {
    this.isLoading.set(true);
    this.errorMessage.set(undefined);

    try {
      const analysis =
        await this.configCleanupService.analyzeUnusedConfigurableEnums();
      this.analysis.set(analysis);
    } catch (error) {
      Logging.error("Failed to analyze configurable enum cleanup data", error);
      this.errorMessage.set(
        $localize`Could not analyze configuration cleanup data. Please try again.`,
      );
    } finally {
      this.isLoading.set(false);
    }
  }

  protected isDeleting(enumId: string) {
    return this.deletingIds().includes(enumId);
  }

  protected enumLabel(enumSummary: ConfigurableEnumUsageSummary): string {
    return this.formatEnumIdAsLabel(enumSummary.enumEntity.getId(true));
  }

  protected openEnumOptions(enumEntity: ConfigurableEnum, event: Event) {
    event.stopPropagation();
    this.dialog.open(ConfigureEnumPopupComponent, { data: enumEntity });
  }

  private formatEnumIdAsLabel(enumId: string): string {
    return enumId
      .split(/[-_]+/)
      .filter(Boolean)
      .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
      .join(" ");
  }

  protected async deleteEnum(unusedEnum: ConfigurableEnumUsageSummary) {
    const enumId = unusedEnum.enumEntity.getId(true);
    const enumLabel = this.enumLabel(unusedEnum);
    const confirmed = await this.confirmationDialog.getConfirmation(
      $localize`Delete unused enum?`,
      $localize`Are you sure you want to delete the unused enum "${enumLabel}"?`,
    );

    if (!confirmed) {
      return;
    }

    this.deletingIds.update((ids) => [...ids, enumId]);

    try {
      const deleted =
        await this.configCleanupService.deleteUnusedConfigurableEnum(
          unusedEnum.enumEntity,
        );

      if (deleted) {
        this.snackBar.open(
          $localize`Enum "${enumLabel}" deleted successfully.`,
          undefined,
          {
            duration: 3000,
          },
        );
        // Reload after delete to refresh the list
        await this.reload();
      } else {
        this.snackBar.open(
          $localize`Enum is now used in schema and cannot be deleted.`,
          undefined,
          {
            duration: 5000,
          },
        );
      }
    } catch (error) {
      Logging.error("Failed to delete configurable enum", error);
      this.snackBar.open(
        $localize`Could not delete enum. Please try again.`,
        undefined,
        {
          duration: 5000,
        },
      );
    } finally {
      this.deletingIds.update((ids) => ids.filter((id) => id !== enumId));
    }
  }
}
<div class="flex-column gap-small">
  <div class="flex-row justify-space-between align-start flex-wrap gap-small">
    <button
      mat-stroked-button
      (click)="reload()"
      [disabled]="isLoading()"
      color="accent"
      i18n
    >
      Refresh Analysis
    </button>
  </div>

  @if (isLoading()) {
    <div class="flex-row align-center gap-small" aria-live="polite">
      <mat-spinner [diameter]="36" />
      <span i18n>Analyzing configuration ...</span>
    </div>
  } @else {
    @if (errorMessage(); as error) {
      <mat-card appearance="outlined">
        <mat-card-content>
          <p class="remove-margin-bottom">{{ error }}</p>
        </mat-card-content>
      </mat-card>
    }

    <mat-card appearance="outlined">
      <mat-card-header>
        <mat-card-title i18n>Summary</mat-card-title>
      </mat-card-header>
      <mat-card-content class="summary-grid">
        <div>
          <span class="summary-label" i18n>Total ConfigurableEnums</span>
          <strong>{{ totalEnums() }}</strong>
        </div>
        <div>
          <span class="summary-label" i18n>Used by Schema</span>
          <strong>{{ usedEnums() }}</strong>
        </div>
        <div>
          <span class="summary-label" i18n>Unused</span>
          <strong>{{ unusedEnums().length }}</strong>
        </div>
      </mat-card-content>
    </mat-card>

    <mat-card appearance="outlined">
      <mat-card-header>
        <mat-card-title i18n>Used ConfigurableEnums</mat-card-title>
        <mat-card-subtitle i18n>
          Includes configurable enums that are referenced in the active runtime
          schema.
        </mat-card-subtitle>
      </mat-card-header>

      <mat-card-content>
        @if (usedEnumDetails().length === 0) {
          <p i18n>No used configurable enums found.</p>
        } @else {
          <mat-list>
            @for (
              enumSummary of usedEnumDetails();
              track enumSummary.enumEntity.getId()
            ) {
              <mat-list-item>
                <div matListItemTitle>
                  {{ enumLabel(enumSummary) }}
                </div>
                <div matListItemMeta>
                  <button
                    mat-icon-button
                    color="accent"
                    (click)="openEnumOptions(enumSummary.enumEntity, $event)"
                    i18n-aria-label
                    aria-label="Configure enum options"
                    i18n-matTooltip
                    matTooltip="Configure options"
                  >
                    <fa-icon icon="wrench"></fa-icon>
                  </button>
                </div>
              </mat-list-item>
            }
          </mat-list>
        }
      </mat-card-content>
    </mat-card>

    <mat-card appearance="outlined">
      <mat-card-header>
        <mat-card-title i18n>Unused ConfigurableEnums</mat-card-title>
        <mat-card-subtitle i18n>
          Based on active runtime entity schemas.
        </mat-card-subtitle>
      </mat-card-header>

      <mat-card-content>
        @if (unusedEnums().length === 0) {
          <p i18n>No unused configurable enums found.</p>
        } @else {
          <mat-list>
            @for (
              unusedEnum of unusedEnums();
              track unusedEnum.enumEntity.getId()
            ) {
              <mat-list-item>
                <div matListItemTitle>
                  {{ enumLabel(unusedEnum) }}
                </div>
                <div matListItemMeta>
                  <button
                    mat-icon-button
                    color="accent"
                    (click)="openEnumOptions(unusedEnum.enumEntity, $event)"
                    i18n-aria-label
                    aria-label="Configure enum options"
                    i18n-matTooltip
                    matTooltip="Configure options"
                  >
                    <fa-icon icon="wrench"></fa-icon>
                  </button>
                  <button
                    mat-icon-button
                    color="accent"
                    (click)="deleteEnum(unusedEnum)"
                    [disabled]="isDeleting(unusedEnum.enumEntity.getId(true))"
                    i18n-aria-label
                    aria-label="Delete enum"
                    i18n-matTooltip
                    matTooltip="Delete"
                  >
                    <fa-icon icon="trash"></fa-icon>
                  </button>
                </div>
              </mat-list-item>
            }
          </mat-list>
        }
      </mat-card-content>
    </mat-card>
  }
</div>

./configurable-enum-cleanup.component.scss

.summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 1rem;
}

.summary-label {
  display: block;
  font-size: 0.875rem;
  color: rgb(0 0 0 / 60%);
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""