src/app/core/entity-list/entity-list/entity-list.component.ts

Description

This component allows to create a full-blown table with pagination, filtering, searching and grouping. The filter and grouping settings are written into the URL params to allow going back to the previous view. The pagination settings are stored for each user. The columns can be any kind of component. The column components will be provided with the Entity object, the id for this column, as well as its static config.

The component can be either used inside a template, or directly in a route through the config object.

Implements

OnInit

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor()

Inputs

clickMode
Type : "navigate" | "popup" | "popup-details" | "none"
Default value : "navigate"
columnGroups
Type : ColumnGroupsConfig
columns
Type : (FormFieldConfig | string)[]
Default value : []
dataSource
Type : DataSourceType
defaultSort
Type : Sort
entityConstructor
Type : EntityConstructor<T>
entityType
Type : string
filterObj
Type : DataFilter<T>
filters
Type : FilterConfig[]
Default value : []
loaderMethod
Type : LoaderMethod

The special service or method to load data via an index or other special method.

selectedRows
Type : T[] | undefined
Default value : undefined
showEntityColor
Type : boolean
Default value : false

Whether the list's default row coloring should reflect each entity's color.

showInactive
Type : boolean

initial / default state whether to include archived records in the list

title
Type : string
Default value : ""

Outputs

addNewClick
Type : void
elementClick
Type : T
entityConstructor
Type : EntityConstructor<T>
filterObj
Type : DataFilter<T>
selectedRows
Type : T[] | undefined
title
Type : string

Methods

addNew
addNew(newEntity?: T)
Parameters :
Name Type Optional
newEntity T Yes
Returns : void
applyFilter
applyFilter(filterValue: string)
Parameters :
Name Type Optional
filterValue string No
Returns : void
Async copyPublicFormLinkForEntityType
copyPublicFormLinkForEntityType(config: PublicFormConfig)
Parameters :
Name Type Optional
config PublicFormConfig No
Returns : any
onRowClick
onRowClick(row: T)
Parameters :
Name Type Optional
row T No
Returns : void
openExportDialog
openExportDialog()
Returns : void
openFilterOverlay
openFilterOverlay()

Calling this function will display the filters in a popup

Returns : void
openPermissionsDialog
openPermissionsDialog()

Review and edit which user roles have access to this entity type.

Returns : void

Properties

Protected addNewAnalyticsAction
Type : unknown
Default value : computed( () => String(this.title() ?? "") .toLowerCase() .replace(" ", "_") + "_add_new", )

Analytics event name for the "Add New" button; title comes from config and may not be a string.

Protected Readonly auditRecordType
Type : unknown
Default value : AuditRecord.ENTITY_TYPE

the audit data the change log reads, as the permission subject to ask about

canImport
Type : unknown
Default value : computed(() => { if (!this.ability.initialized) { return true; } const entityConstructor = this.entityConstructor(); return ( !!entityConstructor && this.ability.can("create", entityConstructor) && this.ability.can("create", ImportMetadata) ); })

Whether the current user may import records of this type. Requires create permission on both the entity type and the ImportMetadata history record that every import writes at the end.

Readonly canManagePermissions
Type : unknown
Default value : toSignal( this.permissionsConfig.canManagePermissions$, { initialValue: false }, )

whether the current user may review and edit who can access this entity type

columnsToDisplay
Type : string[]
displayGroups
Type : { name: string; columns: string[] }[]
Default value : []

the same groups with their names resolved, for the tab labels

filterString
Type : string
Default value : ""
groups
Type : GroupConfig[]
Default value : []
isDesktop
Type : boolean
Readonly isFeatureType
Type : unknown
Default value : computed( () => !!this.entityConstructor()?.isInternalEntity, )

Internal entity types are "features" (e.g. Email Templates) that exist only to be configured by admins, so their permissions get a dedicated button here. For regular entity types the same dialog is offered in the overflow menu.

Public publicForms
Type : { config: PublicFormConfig; title: string }[]
Default value : []

public forms for this entity type, with titles resolved for display

recordsDataSource
Type : unknown
Default value : computed(() => resolveDataSource<T>( this.injector, this.dataSource(), this.loaderMethod(), this.entityConstructor(), ), )
selectedColumnGroupIndex_
Type : number
Default value : 0
showFreetextFilter
Type : unknown
Default value : computed( () => this.recordsDataSource() instanceof InMemoryDataSource, )

Accessors

selectedColumnGroupIndex
getselectedColumnGroupIndex()
setselectedColumnGroupIndex(newValue: number)
Parameters :
Name Type Optional
newValue number No
Returns : void
offsetFilterStyle
getoffsetFilterStyle()

defines the bottom margin of the topmost row in the desktop version. This has to be bigger when there are several column groups since there are tabs with zero top-padding in this case

Returns : object
import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  computed,
  effect,
  inject,
  Injector,
  input,
  model,
  OnInit,
  output,
  untracked,
} from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { AuditRecord } from "../../../features/change-history/model/audit-record";
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
import {
  ColumnGroupsConfig,
  FilterConfig,
  GroupConfig,
} from "../EntityListConfig";
import { Entity, EntityConstructor } from "../../entity/model/entity";
import { FormFieldConfig } from "../../common-components/entity-form/FormConfig";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { FilterOverlayComponent } from "../../filter/filter-overlay/filter-overlay.component";
import { MatDialog } from "@angular/material/dialog";
import { NgStyle, NgTemplateOutlet } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { Angulartics2OnModule } from "angulartics2";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MatMenuModule } from "@angular/material/menu";
import { MatTabsModule } from "@angular/material/tabs";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { FormsModule } from "@angular/forms";
import { FilterComponent } from "../../filter/filter/filter.component";
import { TabStateModule } from "../../../utils/tab-state/tab-state.module";
import { ViewTitleComponent } from "../../common-components/view-title/view-title.component";
import { ExportDialogComponent } from "../../export/export-dialog/export-dialog.component";
import { DisableEntityOperationDirective } from "../../permissions/permission-directive/disable-entity-operation.directive";
import { DuplicateRecordService } from "../duplicate-records/duplicate-records.service";
import { MatTooltipModule } from "@angular/material/tooltip";
import { Sort } from "@angular/material/sort";
import { ExportColumnsService } from "../../export/export-columns.service";
import { RouteTarget } from "../../../route-target";
import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component";
import { DataFilter } from "../../filter/filters/filters";
import { EntityCreateButtonComponent } from "../../common-components/entity-create-button/entity-create-button.component";
import { ViewActionsComponent } from "../../common-components/view-actions/view-actions.component";
import { LoaderMethod } from "../../entity/entity-special-loader/entity-special-loader.service";
import { AblePipe } from "../../permissions/permission-pipe/able.pipe";
import { FormDialogService } from "../../form-dialog/form-dialog.service";
import { PublicFormConfig } from "#src/app/features/public-form/public-form-config";
import { PublicFormsService } from "#src/app/features/public-form/public-forms.service";
import { EntityAbility } from "../../permissions/ability/entity-ability";
import { ImportMetadata } from "../../import/import-metadata";
import { EntityBulkActionsComponent } from "../../entity-details/entity-bulk-actions/entity-bulk-actions.component";
import { resolveLocaleText } from "../../language/active-locale";
import {
  FeaturePermissionDialogComponent,
  FeaturePermissionDialogData,
} from "../../permissions/feature-permission/feature-permission-dialog/feature-permission-dialog.component";
import { PermissionsConfigService } from "../../permissions/permissions-config.service";
import { DataSourceType } from "#src/app/core/common-components/entities-table/data-source/available-data-sources";
import { resolveDataSource } from "#src/app/core/common-components/entities-table/data-source/data-source-resolver";
import { InMemoryDataSource } from "#src/app/core/common-components/entities-table/data-source/in-memory-data-source";

/**
 * The column group shown on small screens: the one with the fewest columns
 * (the first of them if several have the same number).
 */
function indexOfGroupWithFewestColumns(groups: GroupConfig[]): number {
  return groups.reduce(
    (fewest, group, index) =>
      group.columns.length < groups[fewest].columns.length ? index : fewest,
    0,
  );
}

/**
 * This component allows to create a full-blown table with pagination, filtering, searching and grouping.
 * The filter and grouping settings are written into the URL params to allow going back to the previous view.
 * The pagination settings are stored for each user.
 * The columns can be any kind of component.
 * The column components will be provided with the Entity object, the id for this column, as well as its static config.
 *
 * The component can be either used inside a template, or directly in a route through the config object.
 */
@RouteTarget("EntityList")
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-entity-list",
  templateUrl: "./entity-list.component.html",
  styleUrls: ["./entity-list.component.scss"],
  providers: [DuplicateRecordService],
  imports: [
    NgStyle,
    MatButtonModule,
    Angulartics2OnModule,
    FontAwesomeModule,
    MatMenuModule,
    NgTemplateOutlet,
    MatTabsModule,
    MatFormFieldModule,
    MatInputModule,
    EntitiesTableComponent,
    FormsModule,
    FilterComponent,
    TabStateModule,
    ViewTitleComponent,
    DisableEntityOperationDirective,
    RouterLink,
    MatTooltipModule,
    EntityCreateButtonComponent,
    AblePipe,
    ViewActionsComponent,
    EntityBulkActionsComponent,
  ],
})
@UntilDestroy()
export class EntityListComponent<T extends Entity> implements OnInit {
  private screenWidthObserver = inject(ScreenWidthObserver);
  private router = inject(Router);
  private activatedRoute = inject(ActivatedRoute);
  private entities = inject(EntityRegistry);
  private dialog = inject(MatDialog);
  private readonly exportColumnsService = inject(ExportColumnsService);
  private readonly formDialog = inject(FormDialogService);
  private readonly cdr = inject(ChangeDetectorRef);
  private readonly publicFormsService = inject(PublicFormsService);
  private readonly ability = inject(EntityAbility);
  private readonly permissionsConfig = inject(PermissionsConfigService);
  private readonly injector = inject(Injector);

  /** the audit data the change log reads, as the permission subject to ask about */
  protected readonly auditRecordType = AuditRecord.ENTITY_TYPE;

  /** public forms for this entity type, with titles resolved for display */
  public publicForms: { config: PublicFormConfig; title: string }[] = [];

  /**
   * Whether the current user may import records of this type.
   * Requires create permission on both the entity type and the ImportMetadata
   * history record that every import writes at the end.
   */
  canImport = computed(() => {
    if (!this.ability.initialized) {
      return true;
    }
    const entityConstructor = this.entityConstructor();
    return (
      !!entityConstructor &&
      this.ability.can("create", entityConstructor) &&
      this.ability.can("create", ImportMetadata)
    );
  });

  /** whether the current user may review and edit who can access this entity type */
  readonly canManagePermissions = toSignal(
    this.permissionsConfig.canManagePermissions$,
    { initialValue: false },
  );

  /**
   * Internal entity types are "features" (e.g. Email Templates) that exist only to
   * be configured by admins, so their permissions get a dedicated button here.
   * For regular entity types the same dialog is offered in the overflow menu.
   */
  readonly isFeatureType = computed(
    () => !!this.entityConstructor()?.isInternalEntity,
  );

  entityType = input<string>();
  entityConstructor = model<EntityConstructor<T>>();
  defaultSort = input<Sort>();
  dataSource = input<DataSourceType>();
  recordsDataSource = computed(() =>
    resolveDataSource<T>(
      this.injector,
      this.dataSource(),
      this.loaderMethod(),
      this.entityConstructor(),
    ),
  );
  showFreetextFilter = computed(
    () => this.recordsDataSource() instanceof InMemoryDataSource,
  );

  /**
   * The special service or method to load data via an index or other special method.
   */
  loaderMethod = input<LoaderMethod>();

  clickMode = input<"navigate" | "popup" | "popup-details" | "none">(
    "navigate",
  );

  /** initial / default state whether to include archived records in the list */
  showInactive = input<boolean>();

  elementClick = output<T>();
  addNewClick = output<void>();
  selectedRows = model<T[] | undefined>(undefined);

  isDesktop: boolean;

  title = model<string>("");
  columns = input<(FormFieldConfig | string)[]>([]);

  /** Analytics event name for the "Add New" button; `title` comes from config and may not be a string. */
  protected addNewAnalyticsAction = computed(
    () =>
      String(this.title() ?? "")
        .toLowerCase()
        .replace(" ", "_") + "_add_new",
  );

  columnGroups = input<ColumnGroupsConfig>();
  groups: GroupConfig[] = [];
  /** the same groups with their names resolved, for the tab labels */
  displayGroups: { name: string; columns: string[] }[] = [];
  filters = input<FilterConfig[]>([]);

  /**
   * Whether the list's default row coloring should reflect each entity's color.
   */
  showEntityColor = input<boolean>(false);

  columnsToDisplay: string[];

  filterObj = model<DataFilter<T>>({});
  filterString = "";

  get selectedColumnGroupIndex(): number {
    return this.selectedColumnGroupIndex_;
  }

  set selectedColumnGroupIndex(newValue: number) {
    this.selectedColumnGroupIndex_ = newValue;
    this.columnsToDisplay = this.groups[newValue].columns;
  }

  selectedColumnGroupIndex_: number = 0;

  /**
   * defines the bottom margin of the topmost row in the
   * desktop version. This has to be bigger when there are
   * several column groups since there are
   * tabs with zero top-padding in this case
   */
  get offsetFilterStyle(): object {
    const bottomMargin = this.groups.length > 1 ? 29 : 14;
    return {
      "margin-bottom": `${bottomMargin}px`,
    };
  }

  constructor() {
    effect(() => {
      this.entityType();
      this.columns();
      this.columnGroups();
      this.loaderMethod();
      // untracked: internal signals set during build (title, allEntities) must not re-trigger this effect
      void untracked(() => this.buildComponentFromConfig());
    });
    effect(() => {
      this.recordsDataSource().loadRecordConfig.set({
        entityCtr: this.entityConstructor(),
        loaderMethod: this.loaderMethod(),
      });
    });

    this.screenWidthObserver
      .platform()
      .pipe(untilDestroyed(this))
      .subscribe((isDesktop) => {
        // groups are not set yet on the initial emit during construction
        if (this.groups.length > 0) {
          const mobileIndex = indexOfGroupWithFewestColumns(this.groups);
          if (!isDesktop) {
            this.selectedColumnGroupIndex = mobileIndex;
          } else if (this.selectedColumnGroupIndex === mobileIndex) {
            this.selectedColumnGroupIndex = 0;
          }
        }

        this.isDesktop = isDesktop;
        this.cdr.markForCheck();
      });
  }

  async ngOnInit() {
    await this.loadPublicFormConfig();
  }

  private async loadPublicFormConfig() {
    const allForms = await this.publicFormsService.getAllPublicFormConfigs();
    this.publicForms = allForms
      .filter(
        (config) =>
          config.entity &&
          config.entity.toLowerCase() ===
            this.entityConstructor()?.ENTITY_TYPE?.toLowerCase(),
      )
      .map((config) => ({
        config,
        title: resolveLocaleText(config.title) ?? "",
      }));
    this.cdr.markForCheck();
  }

  /** Review and edit which user roles have access to this entity type. */
  openPermissionsDialog() {
    const entityConstructor = this.entityConstructor();
    if (!entityConstructor) {
      return;
    }

    this.dialog.open(FeaturePermissionDialogComponent, {
      data: {
        entityType: entityConstructor.ENTITY_TYPE,
        entityLabel: entityConstructor.labelPlural,
      } as FeaturePermissionDialogData,
      maxWidth: "720px",
    });
  }

  async copyPublicFormLinkForEntityType(config: PublicFormConfig) {
    await this.publicFormsService.copyPublicFormLinkFromConfig(config);
  }

  private async buildComponentFromConfig() {
    const entityType = this.entityType();
    if (entityType) {
      this.entityConstructor.set(
        this.entities.get(entityType) as EntityConstructor<T>,
      );
    }

    this.initColumnGroups(this.columnGroups());
    this.selectedColumnGroupIndex = this.screenWidthObserver.isDesktop()
      ? 0
      : indexOfGroupWithFewestColumns(this.groups);

    if (!this.title()) {
      this.title.set(this.entityConstructor()?.labelPlural);
    }
  }

  private initColumnGroups(columnGroup?: ColumnGroupsConfig) {
    if (columnGroup && columnGroup.groups.length > 0) {
      this.groups = columnGroup.groups;
    } else {
      this.groups = [
        {
          name: "default",
          columns: this.columns().map((c) =>
            typeof c === "string" ? c : c.id,
          ),
        },
      ];
    }

    this.displayGroups = this.groups.map((group) => ({
      ...group,
      name: resolveLocaleText(group.name) ?? "",
    }));
  }

  applyFilter(filterValue: string) {
    this.recordsDataSource().filter = filterValue.trim().toLowerCase();
  }

  /**
   * Calling this function will display the filters in a popup
   */
  openFilterOverlay() {
    this.dialog.open(FilterOverlayComponent, {
      data: {
        filterConfig: this.filters(),
        entityType: this.entityConstructor(),
        entities: this.recordsDataSource().allRecords(),
        useUrlQueryParams: true,
        filterObjChange: (filter: DataFilter<T>) => this.filterObj.set(filter),
      },
    });
  }

  addNew(newEntity?: T) {
    const entityConstructor = this.entityConstructor();
    if (!entityConstructor) {
      return;
    }
    if (!newEntity) {
      newEntity = new entityConstructor();
    }

    switch (this.clickMode()) {
      case "navigate":
        this.router.navigate(["new"], { relativeTo: this.activatedRoute });
        break;
      case "popup":
        this.formDialog.openFormPopup(newEntity, this.columns());
        break;
      case "popup-details":
        this.formDialog.openView(newEntity);
        break;
    }

    this.addNewClick.emit();
  }

  onRowClick(row: T) {
    this.elementClick.emit(row);
  }

  openExportDialog() {
    const cols = this.columnsToDisplay ?? [];
    const availableColumns = this.columns() ?? [];
    const schema = this.entityConstructor()?.schema;

    const { allAvailableColumns, preselectedExportConfig } =
      this.exportColumnsService.buildExportColumns({
        schema,
        visibleColIds: cols,
        availableColumns,
      });

    this.dialog.open(ExportDialogComponent, {
      data: {
        allEntities: () => this.recordsDataSource().getAllData(false),
        filteredData: () => this.recordsDataSource().getAllData(true),
        exportConfig: allAvailableColumns,
        preselectedExportConfig,
        columnGroups: this.columnGroups(),
        filename: String(this.title() ?? "").replaceAll(" ", ""),
      },
    });
  }
}
<!-- Desktop version -->
@if (isDesktop) {
  <div>
    <!-- Header bar; contains the title on the left and controls on the right -->
    <app-view-title [ngStyle]="offsetFilterStyle">
      {{ title() }}
    </app-view-title>

    <app-view-actions>
      @if (selectedRows()) {
        <!-- Bulk actions -->
        <ng-container *ngTemplateOutlet="bulkActions"></ng-container>
      } @else {
        <div class="flex-row gap-regular">
          @if (canManagePermissions() && isFeatureType()) {
            <button
              mat-stroked-button
              color="accent"
              class="header-action-button"
              (click)="openPermissionsDialog()"
              matTooltip="Access to this feature is controlled by user roles"
              i18n-matTooltip="Configure feature permissions tooltip"
            >
              <fa-icon
                class="standard-icon-with-text"
                aria-hidden="true"
                icon="user-lock"
              ></fa-icon>
              <span i18n="Configure feature permissions button"
                >Configure Permissions</span
              >
            </button>
          }
          <app-entity-create-button
            [entityType]="entityConstructor()!"
            (entityCreate)="addNew()"
          ></app-entity-create-button>
          <button
            mat-icon-button
            color="primary"
            [matMenuTriggerFor]="additional"
          >
            <fa-icon icon="ellipsis-v"></fa-icon>
          </button>
        </div>
      }
    </app-view-actions>

    <!-- Filters -->
    <div class="flex-row gap-regular flex-wrap">
      <div *ngTemplateOutlet="filterDialog"></div>
      @if (entityConstructor()) {
        <app-filter
          class="flex-row gap-regular flex-wrap"
          [filterConfig]="filters()"
          [entityType]="entityConstructor()!"
          [entities]="recordsDataSource().allRecords()"
          [useUrlQueryParams]="true"
          [(filterObj)]="filterObj"
          [filterString]="filterString"
          (filterStringChange)="filterString = $event; applyFilter($event)"
        ></app-filter>
      }
    </div>

    <!-- Tab Groups-->
    <div class="mat-elevation-z1">
      <ng-container *ngTemplateOutlet="columnGroupTabs"></ng-container>
      <ng-container *ngTemplateOutlet="subrecord"></ng-container>
    </div>
  </div>
} @else {
  <!-- Mobile version -->
  <div>
    <app-view-title [disableBackButton]="true">
      <h2>{{ title() }}</h2>
    </app-view-title>
    <app-view-actions>
      <div class="flex-row full-width">
        <div *ngTemplateOutlet="filterDialog"></div>
        <button
          mat-icon-button
          color="primary"
          [matMenuTriggerFor]="additional"
        >
          <fa-icon icon="ellipsis-v"></fa-icon>
        </button>
      </div>
    </app-view-actions>

    @if (selectedRows()) {
      <div class="bulk-action-spacing">
        <ng-container *ngTemplateOutlet="bulkActions"></ng-container>
      </div>
    }

    <ng-container *ngTemplateOutlet="columnGroupTabs"></ng-container>
    <ng-container *ngTemplateOutlet="subrecord"></ng-container>
  </div>
}

<!-- Templates and menus for both mobile and desktop -->

<ng-template #columnGroupTabs>
  @if (groups.length > 1) {
    <div>
      <mat-tab-group
        [(selectedIndex)]="selectedColumnGroupIndex"
        appTabStateMemo
      >
        @for (item of displayGroups; track item.name) {
          <mat-tab
            [label]="item.name"
            angulartics2On="click"
            [angularticsCategory]="entityConstructor()?.ENTITY_TYPE"
            angularticsAction="list_column_view"
            [angularticsLabel]="item.name"
          ></mat-tab>
        }
      </mat-tab-group>
    </div>
  }
</ng-template>

<ng-template #filterDialog>
  @if (showFreetextFilter()) {
    <mat-form-field class="full-width filter-field">
      <mat-label
        i18n="Filter placeholder|Allows the user to filter through entities"
        >Filter
      </mat-label>
      <input
        class="full-width"
        matInput
        i18n-placeholder="Examples of things to filter"
        placeholder="e.g. name, age"
        (ngModelChange)="applyFilter($event)"
        [(ngModel)]="filterString"
      />
      @if (filterString) {
        <button
          mat-icon-button
          matIconSuffix
          i18n-aria-label
          aria-label="Clear"
          (click)="filterString = ''; applyFilter('')"
        >
          <fa-icon icon="times"></fa-icon>
        </button>
      }
    </mat-form-field>
  }
</ng-template>

<ng-template #subrecord>
  <app-entities-table
    [entityType]="entityConstructor()!"
    [recordsDataSource]="recordsDataSource()"
    [customColumns]="columns()"
    [editable]="false"
    [clickMode]="clickMode()"
    (entityClick)="onRowClick($event)"
    [columnsToDisplay]="columnsToDisplay"
    [filter]="filterObj()"
    [sortBy]="defaultSort()"
    [(selectedRecords)]="selectedRows"
    [selectable]="!!selectedRows()"
    [showInactive]="showInactive()"
    [showEntityColor]="showEntityColor()"
  ></app-entities-table>
</ng-template>

<mat-menu #additional>
  <div class="hide-desktop">
    <button
      mat-menu-item
      (click)="addNew()"
      angulartics2On="click"
      angularticsCategory="UserAction"
      [angularticsAction]="addNewAnalyticsAction()"
      *appDisabledEntityOperation="{
        entity: entityConstructor()!,
        operation: 'create',
      }"
    >
      <fa-icon
        class="color-accent standard-icon-with-text"
        aria-hidden="true"
        icon="plus-circle"
      ></fa-icon>
      <span i18n="Add a new entity to a list of multiple entities">
        Add New
      </span>
    </button>

    <button mat-menu-item (click)="openFilterOverlay()">
      <fa-icon
        aria-hidden="true"
        class="color-accent standard-icon-with-text"
        icon="filter"
      >
      </fa-icon>
      <span i18n="Show filter options popup for list"> Filter options </span>
    </button>

    @if (canManagePermissions() && isFeatureType()) {
      <button mat-menu-item (click)="openPermissionsDialog()">
        <fa-icon
          class="standard-icon-with-text color-accent"
          aria-hidden="true"
          icon="user-lock"
        ></fa-icon>
        <span i18n="Configure feature permissions button">
          Configure Permissions
        </span>
      </button>
    }
  </div>

  <button
    mat-menu-item
    (click)="openExportDialog()"
    angulartics2On="click"
    [angularticsCategory]="entityConstructor()?.ENTITY_TYPE"
    angularticsAction="list_export"
    matTooltip="Download records as CSV or XLSX"
    i18n-matTooltip
    matTooltipPosition="before"
  >
    <fa-icon
      class="color-accent standard-icon-with-text"
      aria-hidden="true"
      icon="download"
    ></fa-icon>
    <span i18n="Download list contents"> Download </span>
  </button>

  @if (canImport()) {
    <button
      mat-menu-item
      angulartics2On="click"
      [angularticsCategory]="entityConstructor()?.ENTITY_TYPE"
      angularticsAction="import_file"
      [routerLink]="['/import']"
      [queryParams]="{ entityType: entityConstructor()?.ENTITY_TYPE }"
    >
      <fa-icon
        class="color-accent standard-icon-with-text"
        aria-hidden="true"
        icon="file-import"
      ></fa-icon>
      <span i18n> Import from file </span>
    </button>
  }

  @if ("read" | able: auditRecordType) {
    <button
      mat-menu-item
      [routerLink]="['/change-history']"
      [queryParams]="{ entityType: entityConstructor()?.ENTITY_TYPE }"
      matTooltip="See all recorded changes to records of this type: when, by whom and which fields"
      i18n-matTooltip
      matTooltipPosition="before"
    >
      <fa-icon
        class="color-accent standard-icon-with-text"
        aria-hidden="true"
        icon="clock-rotate-left"
      ></fa-icon>
      <span i18n="Open the change log filtered to this record type">
        Change Log
      </span>
    </button>
  }

  <button
    mat-menu-item
    (click)="selectedRows.set([])"
    matTooltip="Select multiple records for bulk actions like duplicating or deleting"
    i18n-matTooltip
    matTooltipPosition="before"
  >
    <fa-icon
      class="color-accent standard-icon-with-text"
      aria-hidden="true"
      icon="list-check"
    ></fa-icon>
    <span i18n> Bulk Actions </span>
  </button>

  @for (form of publicForms; track form.config.route) {
    <button
      mat-menu-item
      (click)="copyPublicFormLinkForEntityType(form.config)"
      [matTooltip]="form.title"
      i18n-matTooltip
      matTooltipPosition="before"
    >
      <fa-icon
        class="color-accent standard-icon-with-text"
        icon="link"
      ></fa-icon>
      <span i18n>Copy Public Form Link</span> ({{ form.title }})
    </button>
  }

  @if (!entityConstructor()?.isInternalEntity) {
    <button
      mat-menu-item
      [routerLink]="['/deduplication/review-duplicates']"
      [queryParams]="{ entityType: entityConstructor()?.ENTITY_TYPE }"
    >
      <fa-icon
        class="standard-icon-with-text color-accent"
        icon="copy"
      ></fa-icon>
      <span i18n>Review Possible Duplicates</span>
    </button>
  }

  @if (("update" | able: "Config") && !entityConstructor()?.isInternalEntity) {
    <button
      mat-menu-item
      [routerLink]="['/admin/entity', entityConstructor()?.ENTITY_TYPE]"
      [queryParams]="{ mode: 'list' }"
      queryParamsHandling="merge"
    >
      <fa-icon
        class="standard-icon-with-text color-accent"
        icon="tools"
      ></fa-icon>
      <span i18n>Configure Data Structure</span>
    </button>
  }

  <ng-content select="[mat-menu-item]"></ng-content>
</mat-menu>

<ng-template #bulkActions>
  @if (!!selectedRows()) {
    <app-entity-bulk-actions
      [entities]="selectedRows()"
      (resetBulkActionMode)="selectedRows.set(undefined)"
    >
    </app-entity-bulk-actions>
  }
</ng-template>

./entity-list.component.scss

@use "../../../../styles/variables/breakpoints";
@use "variables/sizes";
@use "variables/colors";

/**
 * Aligns the baseline of the filter-field with the baseline
 * of the other controls.
 * This only has to be done on the desktop
 */
.filter-field {
  @media screen and (min-width: breakpoints.$md) {
    /* restricts the width so that the field does not feel too big */
    max-width: 412px;
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""