src/app/core/export/export-dialog/export-dialog.component.ts

Description

Dialog for selecting export format (CSV/XLSX) and data scope (filtered/all).

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods

Methods

applyColumnGroup
applyColumnGroup(groupName?: string)
Parameters :
Name Type Optional
groupName string Yes
Returns : void
clearSelection
clearSelection()
Returns : void
Async download
download()
Returns : any
includeAll
includeAll()
Returns : void

Properties

availableColumns
Type : ExportColumnConfig[]
Default value : this.buildAvailableColumns()

All available export column options (deduped, excludes internal fields).

columnGroups
Type : unknown
Default value : this.data.columnGroups

Column groups passed from the list (admin-configured)

columnToString
Type : unknown
Default value : () => {...}
columnToValue
Type : unknown
Default value : () => {...}
data
Type : unknown
Default value : inject<ExportDialogData>(MAT_DIALOG_DATA)
downloadError
Type : unknown
Default value : signal<string | null>(null)
format
Type : unknown
Default value : signal<FileDownloadFormat>("csv")
isLoading
Type : unknown
Default value : signal<boolean>(false)
scope
Type : unknown
Default value : signal<"filtered" | "all">("filtered")
selectedColumnCount
Type : unknown
Default value : computed(() => { const keys = this.selectedColumnKeys(); if (keys !== undefined) { return keys.length; } return this.availableColumns.length; })
selectedColumnKeys
Type : unknown
Default value : signal<string[] | undefined>( (this.data.preselectedExportConfig ?? this.data.exportConfig)?.map((c) => normalizeQueryKey(c.query), ), )

Selected export column keys in the order chosen by the user (undefined = use default passed config).

selectedGroupName
Type : unknown
Default value : signal<string | undefined>( this.columnGroups && this.columnGroups.groups.length > 0 ? this.columnGroups.groups[0].name : undefined, )

Currently selected column group name (if any)

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  signal,
} from "@angular/core";
import {
  MAT_DIALOG_DATA,
  MatDialogModule,
  MatDialogRef,
} from "@angular/material/dialog";
import { MatButtonModule } from "@angular/material/button";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatSelectModule } from "@angular/material/select";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { MatRadioModule } from "@angular/material/radio";
import { FormsModule } from "@angular/forms";
import { Logging } from "../../logging/logging.service";
import { BasicAutocompleteComponent } from "../../common-components/basic-autocomplete/basic-autocomplete.component";
import { DialogCloseComponent } from "../../common-components/dialog-close/dialog-close.component";
import {
  DownloadService,
  FileDownloadFormat,
} from "../download-service/download.service";
import {
  ExportColumnConfig,
  normalizeQueryKey,
} from "../data-transformation-service/export-column-config";
import { ColumnGroupsConfig } from "../../entity-list/EntityListConfig";

export interface ExportDialogData {
  /** Resolves with all records (unfiltered, permissions-limited) */
  allEntities: () => Promise<any[]>;
  /**
   * Resolves with the currently filtered/visible records.
   * When omitted, the scope selector is hidden and allEntities are exported directly.
   */
  filteredData?: () => Promise<any[]>;
  exportConfig?: ExportColumnConfig[];
  preselectedExportConfig?: ExportColumnConfig[];
  filename: string;
  columnGroups?: ColumnGroupsConfig;
}

/**
 * Dialog for selecting export format (CSV/XLSX) and data scope (filtered/all).
 */
@Component({
  selector: "app-export-dialog",
  templateUrl: "./export-dialog.component.html",
  styleUrls: ["./export-dialog.component.scss"],
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    MatDialogModule,
    MatButtonModule,
    MatFormFieldModule,
    MatSelectModule,
    MatProgressBarModule,
    MatRadioModule,
    FormsModule,
    BasicAutocompleteComponent,
    DialogCloseComponent,
  ],
})
export class ExportDialogComponent {
  private readonly dialogRef =
    inject<MatDialogRef<ExportDialogComponent>>(MatDialogRef);
  data = inject<ExportDialogData>(MAT_DIALOG_DATA);
  private readonly downloadService = inject(DownloadService);

  format = signal<FileDownloadFormat>("csv");
  scope = signal<"filtered" | "all">("filtered");
  isLoading = signal<boolean>(false);
  downloadError = signal<string | null>(null);

  /** All available export column options (deduped, excludes internal fields). */
  availableColumns: ExportColumnConfig[] = this.buildAvailableColumns();

  /** Selected export column keys in the order chosen by the user (undefined = use default passed config). */
  selectedColumnKeys = signal<string[] | undefined>(
    (this.data.preselectedExportConfig ?? this.data.exportConfig)?.map((c) =>
      normalizeQueryKey(c.query),
    ),
  );

  /** Column groups passed from the list (admin-configured) */
  columnGroups = this.data.columnGroups;

  /** Currently selected column group name (if any) */
  selectedGroupName = signal<string | undefined>(
    this.columnGroups && this.columnGroups.groups.length > 0
      ? this.columnGroups.groups[0].name
      : undefined,
  );

  applyColumnGroup(groupName?: string) {
    if (!groupName || !this.columnGroups) return;
    const grp = this.columnGroups.groups.find((g) => g.name === groupName);
    if (!grp) return;
    // Map group columns to normalized keys and set selection
    this.selectedColumnKeys.set(grp.columns.map((c) => normalizeQueryKey(c)));
  }

  selectedColumnCount = computed(() => {
    const keys = this.selectedColumnKeys();
    if (keys !== undefined) {
      return keys.length;
    }
    return this.availableColumns.length;
  });

  columnToString = (col: ExportColumnConfig) => col.label ?? col.query;
  columnToValue = (col: ExportColumnConfig) => normalizeQueryKey(col.query);

  clearSelection() {
    this.selectedColumnKeys.set([]);
  }

  includeAll() {
    this.selectedColumnKeys.set(
      this.availableColumns.map((c) => normalizeQueryKey(c.query)),
    );
  }

  /** Map selected column keys back to their full export column config (incl. label). */
  private columnsFromKeys(keys: string[]): ExportColumnConfig[] {
    const byKey = new Map<string, ExportColumnConfig>();
    for (const c of this.availableColumns) {
      byKey.set(normalizeQueryKey(c.query), c);
    }
    return keys
      .map((k) => byKey.get(normalizeQueryKey(k)))
      .filter((c): c is ExportColumnConfig => !!c);
  }

  private buildAvailableColumns(): ExportColumnConfig[] {
    const configs = this.data.exportConfig ?? [];
    const out: ExportColumnConfig[] = [];
    const seen = new Set<string>();
    for (const c of configs) {
      const key = normalizeQueryKey(c.query);
      if (key.startsWith("_")) continue;
      if (!seen.has(key)) {
        seen.add(key);
        out.push(c);
      }
    }
    return out;
  }

  async download() {
    // Only block when an explicit exportConfig was provided (columns UI visible).
    if (this.data.exportConfig && this.selectedColumnCount() === 0) {
      this.downloadError.set(
        $localize`Please select at least one column before downloading.`,
      );
      return;
    }

    this.isLoading.set(true);
    this.downloadError.set(null);
    await new Promise<void>((resolve) => setTimeout(resolve));
    try {
      const exportData =
        this.data.filteredData && this.scope() === "filtered"
          ? await this.data.filteredData()
          : await this.data.allEntities();
      const selectedKeys = this.selectedColumnKeys();
      await this.downloadService.triggerDownload(
        exportData,
        this.format(),
        this.data.filename,
        selectedKeys === undefined
          ? undefined
          : this.columnsFromKeys(selectedKeys),
      );
      this.dialogRef.close();
    } catch (e) {
      Logging.warn("Export download failed:", e);
      this.downloadError.set(
        $localize`Download failed [${e instanceof Error ? e.message : String(e)}]`,
      );
    } finally {
      this.isLoading.set(false);
    }
  }
}
<h1 mat-dialog-title i18n="Export dialog title">
  Download Data
  <app-dialog-close mat-dialog-close></app-dialog-close>
</h1>

@if (isLoading()) {
  <mat-progress-bar mode="indeterminate"></mat-progress-bar>
}

<div mat-dialog-content>
  <p i18n="Export dialog format label">Format</p>
  <mat-radio-group
    [ngModel]="format()"
    (ngModelChange)="format.set($event)"
    i18n-aria-label
    aria-label="Export format"
  >
    <mat-radio-button value="csv" i18n="Export format CSV option"
      >CSV</mat-radio-button
    >
    <mat-radio-button value="xlsx" i18n="Export format XLSX option"
      >XLSX (Excel)</mat-radio-button
    >
  </mat-radio-group>

  @if (data.filteredData) {
    <p i18n="Export dialog scope label">Records to export</p>
    <mat-radio-group
      [ngModel]="scope()"
      (ngModelChange)="scope.set($event)"
      aria-label="Export scope"
      i18n-aria-label
    >
      <mat-radio-button value="filtered" i18n="Export scope filtered option">
        Current (filtered)
      </mat-radio-button>
      <mat-radio-button value="all" i18n="Export scope all option">
        All records (including archived)
      </mat-radio-button>
    </mat-radio-group>
  }

  @if (data.exportConfig) {
    @if (columnGroups) {
      <mat-form-field class="full-width">
        <mat-label i18n="Export dialog presets label">Presets</mat-label>
        <mat-select
          [ngModel]="selectedGroupName()"
          (ngModelChange)="
            selectedGroupName.set($event); applyColumnGroup($event)
          "
        >
          @for (g of columnGroups.groups; track g.name) {
            <mat-option [value]="g.name">{{ g.name }}</mat-option>
          }
        </mat-select>
      </mat-form-field>
    }
    <mat-form-field floatLabel="always" class="full-width">
      <mat-label i18n="Export dialog columns label">Columns</mat-label>
      <app-basic-autocomplete
        [ngModel]="selectedColumnKeys()"
        (ngModelChange)="selectedColumnKeys.set($event)"
        [options]="availableColumns"
        [optionToString]="columnToString"
        [valueMapper]="columnToValue"
        placeholder="Type to search columns"
        i18n-placeholder="Placeholder text for export columns search"
        [multi]="true"
        [reorder]="true"
        [required]="true"
      ></app-basic-autocomplete>

      @if (selectedColumnCount() === 0) {
        <mat-error i18n="Error shown when no export columns are selected">
          Select at least one column to download.
        </mat-error>
      }
    </mat-form-field>

    <div class="flex-row gap-small margin-top-small">
      <button
        mat-raised-button
        type="button"
        (click)="includeAll()"
        color="accent"
        i18n
      >
        Include all columns
      </button>
      <button mat-stroked-button type="button" (click)="clearSelection()" i18n>
        Clear selection
      </button>
    </div>
  }
</div>

@if (downloadError()) {
  <mat-error class="margin-left-regular">{{ downloadError() }}</mat-error>
}

<div mat-dialog-actions>
  <button
    mat-raised-button
    color="accent"
    [disabled]="
      isLoading() || (data.exportConfig && selectedColumnCount() === 0)
    "
    (click)="download()"
    i18n="Download button"
  >
    Download
  </button>

  <button
    mat-stroked-button
    mat-dialog-close
    [disabled]="isLoading()"
    i18n="Cancel button"
  >
    Cancel
  </button>
</div>

./export-dialog.component.scss

Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""