src/app/features/template-export/template-export-selection-dialog/template-export-selection-dialog.component.ts

Description

Popup for user to select one of the available templates and manage the PDF/file generation process for a specific entity.

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods
Inputs

Inputs

entity
Type : Entity | Entity[]

Methods

Async requestFile
requestFile()
Returns : any

Properties

Readonly combineIntoSinglePdf
Type : unknown
Default value : signal<boolean>(false)

User-controlled toggle (bulk only): when on, the backend returns a single combined multi-page PDF instead of a ZIP of N independent files.

Readonly configureTemplatesRoute
Type : unknown
Default value : signal( getEntityRuntimeRoute(TemplateExport), )
Readonly currentEntity
Type : unknown
Default value : computed<Entity | undefined>( () => this.entities()[0], )
Readonly entities
Type : unknown
Default value : computed<Entity[]>(() => { const raw = this.entity() ?? this.dialogData; if (Array.isArray(raw)) return raw; return raw ? [raw as Entity] : []; })
Readonly failedEntityNames
Type : unknown
Default value : computed(() => this.failures() .map((f) => f.entity.toString()) .join(", "), )
Readonly failures
Type : unknown
Default value : signal<{ entity: Entity; error: unknown }[]>([])
Readonly isBulk
Type : unknown
Default value : computed(() => this.entities().length > 1)
isFeatureEnabled
Type : unknown
Default value : resource({ loader: () => this.templateExportService.isExportServerEnabled().catch(() => false), })
Readonly phase
Type : unknown
Default value : signal<"select" | "running" | "done">("select")
Readonly succeededCount
Type : unknown
Default value : computed( () => this.totalRecords() - this.failures().length, )
templateEntityFilter
Type : function
Default value : () => {...}
TemplateExport
Type : unknown
Default value : TemplateExport
templateSelectionForm
Type : FormControl
Default value : new FormControl()
Readonly totalRecords
Type : unknown
Default value : signal<number>(0)
import {
  Component,
  computed,
  inject,
  input,
  ChangeDetectionStrategy,
  signal,
  resource,
} from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { MatButton } from "@angular/material/button";
import { MatRadioModule } from "@angular/material/radio";
import {
  MAT_DIALOG_DATA,
  MatDialogActions,
  MatDialogClose,
  MatDialogContent,
  MatDialogRef,
} from "@angular/material/dialog";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatProgressBar } from "@angular/material/progress-bar";
import { RouterLink } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { Logging } from "#src/app/core/logging/logging.service";
import { AlertService } from "../../../core/alerts/alert.service";
import { EditEntityComponent } from "../../../core/basic-datatypes/entity/edit-entity/edit-entity.component";
import { FeatureDisabledInfoComponent } from "../../../core/common-components/feature-disabled-info/feature-disabled-info.component";
import { getEntityRuntimeRoute } from "../../../core/entity/entity-config.service";
import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service";
import { Entity } from "../../../core/entity/model/entity";
import { DownloadService } from "../../../core/export/download-service/download.service";
import { DisableEntityOperationDirective } from "../../../core/permissions/permission-directive/disable-entity-operation.directive";
import { TemplateExportApiService } from "../template-export-api/template-export-api.service";
import { TemplateExportService } from "../template-export-service/template-export.service";
import { TemplateExport } from "../template-export.entity";

/**
 * Popup for user to select one of the available templates
 * and manage the PDF/file generation process for a specific entity.
 */
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-file-template-selection-dialog-component",
  imports: [
    MatDialogContent,
    MatDialogActions,
    MatButton,
    EditEntityComponent,
    MatDialogClose,
    RouterLink,
    DisableEntityOperationDirective,
    MatProgressBar,
    FeatureDisabledInfoComponent,
    ReactiveFormsModule,
    MatFormFieldModule,
    MatRadioModule,
  ],
  templateUrl: "./template-export-selection-dialog.component.html",
  styleUrl: "./template-export-selection-dialog.component.scss",
})
export class TemplateExportSelectionDialogComponent {
  private readonly dialogData = inject<Entity | Entity[]>(MAT_DIALOG_DATA, {
    optional: true,
  });
  private templateExportApi = inject(TemplateExportApiService);
  private downloadService = inject(DownloadService);
  private alertService = inject(AlertService);
  private readonly dialogRef = inject(
    MatDialogRef<TemplateExportSelectionDialogComponent>,
  );
  private readonly templateExportService = inject(TemplateExportService);
  private readonly entityMapper = inject(EntityMapperService);

  entity = input<Entity | Entity[]>();

  templateSelectionForm: FormControl = new FormControl();
  TemplateExport = TemplateExport;
  readonly configureTemplatesRoute = signal(
    getEntityRuntimeRoute(TemplateExport),
  );
  readonly entities = computed<Entity[]>(() => {
    const raw = this.entity() ?? this.dialogData;
    if (Array.isArray(raw)) return raw;
    return raw ? [raw as Entity] : [];
  });
  readonly isBulk = computed(() => this.entities().length > 1);
  readonly currentEntity = computed<Entity | undefined>(
    () => this.entities()[0],
  );
  templateEntityFilter: (e: TemplateExport) => boolean = (e) =>
    e.applicableForEntityTypes.includes(this.currentEntity()?.getType() ?? "");

  readonly phase = signal<"select" | "running" | "done">("select");
  readonly totalRecords = signal<number>(0);
  readonly failures = signal<{ entity: Entity; error: unknown }[]>([]);

  /**
   * User-controlled toggle (bulk only): when on, the backend returns a single combined
   * multi-page PDF instead of a ZIP of N independent files.
   */
  readonly combineIntoSinglePdf = signal<boolean>(false);

  readonly succeededCount = computed(
    () => this.totalRecords() - this.failures().length,
  );
  readonly failedEntityNames = computed(() =>
    this.failures()
      .map((f) => f.entity.toString())
      .join(", "),
  );

  isFeatureEnabled = resource({
    loader: () =>
      this.templateExportService.isExportServerEnabled().catch(() => false),
  });

  async requestFile() {
    const templateId = this.templateSelectionForm.value;
    const entities = this.entities();
    if (entities.length === 0) {
      this.alertService.addWarning(
        $localize`No records selected for file generation.`,
      );
      return;
    }

    this.phase.set("running");
    this.totalRecords.set(entities.length);
    this.failures.set([]);

    try {
      const template = await this.entityMapper.load(TemplateExport, templateId);

      if (entities.length === 1) {
        const result = await firstValueFrom(
          this.templateExportApi.generatePdfFromTemplate(template, entities[0]),
        );
        await this.downloadService.triggerDownload(
          result.file,
          "pdf",
          result.filename ?? entities[0].toString(),
        );
      } else {
        const combined = this.combineIntoSinglePdf();
        const result = await firstValueFrom(
          this.templateExportApi.generateBatchFromTemplate(
            template,
            entities,
            combined ? "combined" : "zip",
          ),
        );
        await this.downloadService.triggerDownload(
          result.file,
          combined ? "pdf" : "zip",
          result.filename,
        );
      }
      this.dialogRef.close(true);

      this.alertService.addInfo(
        $localize`Generated ${entities.length} of ${entities.length} files.`,
      );
    } catch (error) {
      Logging.warn("Failed to generate files", error);
      this.failures.set(entities.map((entity) => ({ entity, error })));
      this.phase.set("done");
    }
  }
}
<mat-dialog-content>
  @if (isFeatureEnabled.value() !== true) {
    <app-feature-disabled-info
      i18n-featureName
      featureName="Export API"
      [featureEnabled]="isFeatureEnabled.value()"
    ></app-feature-disabled-info>
  } @else {
    @switch (phase()) {
      @case ("select") {
        <h2 i18n>Generate File from Template</h2>
        <p>
          <span i18n>
            Select the template based on which to generate a file for the
            current record or
          </span>
          <a
            [routerLink]="configureTemplatesRoute()"
            *appDisabledEntityOperation="{
              entity: TemplateExport,
              operation: 'update',
            }"
            matDialogClose
            i18n
            >configure available templates</a
          >
        </p>

        <mat-form-field class="full-width" floatLabel="always">
          <mat-label i18n>Template</mat-label>
          <app-edit-entity
            [formControl]="templateSelectionForm"
            [entityType]="TemplateExport.ENTITY_TYPE"
            [additionalFilter]="templateEntityFilter"
            [disableCreateNew]="true"
            placeholder="Select a file template"
            i18n-placeholder
          ></app-edit-entity>
        </mat-form-field>

        @if (isBulk()) {
          <mat-radio-group
            [value]="combineIntoSinglePdf()"
            (change)="combineIntoSinglePdf.set($event.value)"
            class="flex-column"
          >
            <mat-radio-button [value]="false" i18n>
              Create .zip file with multiple PDFs
            </mat-radio-button>
            <mat-radio-button
              data-testid="combine-into-single-pdf"
              [value]="true"
              i18n
            >
              Combine all records into a single PDF
            </mat-radio-button>
          </mat-radio-group>
        }
      }

      @case ("running") {
        <p data-testid="template-export-progress">
          @if (isBulk()) {
            <span i18n>Generating files for {{ totalRecords() }} records…</span>
          } @else {
            <span i18n>Generating file…</span>
          }
        </p>
        <mat-progress-bar mode="indeterminate"></mat-progress-bar>
      }

      @default {
        <div data-testid="template-export-summary">
          @if (failures().length === 0) {
            <p i18n>
              Generated {{ totalRecords() }} of {{ totalRecords() }} files.
            </p>
          } @else {
            <p i18n>
              Generated {{ succeededCount() }} of {{ totalRecords() }} files.
            </p>
            <p i18n>Failed: {{ failedEntityNames() }}</p>
          }
        </div>
      }
    }
  }
</mat-dialog-content>

<mat-dialog-actions>
  @switch (phase()) {
    @case ("select") {
      <button
        mat-raised-button
        color="accent"
        [disabled]="!templateSelectionForm.value"
        (click)="requestFile()"
      >
        <span i18n>Generate File</span>
      </button>
      <button mat-stroked-button i18n [matDialogClose]="false">Cancel</button>
    }

    @case ("running") {
      <!-- no actions while rendering -->
    }

    @default {
      <button mat-raised-button color="accent" [matDialogClose]="true" i18n>
        Close
      </button>
    }
  }
</mat-dialog-actions>

./template-export-selection-dialog.component.scss

mat-dialog-content {
  max-width: 1024px;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""