src/app/features/template-export/template-export-selection-dialog/template-export-selection-dialog.component.ts
Popup for user to select one of the available templates and manage the PDF/file generation process for a specific entity.
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-file-template-selection-dialog-component |
| standalone | true |
| imports |
MatDialogContent
MatDialogActions
MatButton
MatDialogClose
MatProgressBar
MatFormFieldModule
MatRadioModule
|
| styleUrls | ./template-export-selection-dialog.component.scss |
| templateUrl | ./template-export-selection-dialog.component.html |
| styleUrl | ./template-export-selection-dialog.component.scss |
No results matching.
MatDialogContent
MatDialogActions
MatButton
EditEntityComponent
MatDialogClose
RouterLink
DisableEntityOperationDirective
MatProgressBar
FeatureDisabledInfoComponent
ReactiveFormsModule
MatFormFieldModule
MatRadioModule
Properties |
|
Methods |
|
Inputs |
| entity | |
Type : Entity | Entity[]
|
|
| Async requestFile |
requestFile()
|
|
Returns :
any
|
| 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;
}