File

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

EntityListConfig OnChanges

Metadata

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(screenWidthObserver: ScreenWidthObserver, router: Router, activatedRoute: ActivatedRoute, entityMapperService: EntityMapperService, entities: EntityRegistry, dialog: MatDialog, duplicateRecord: DuplicateRecordService, entityActionsService: EntityActionsService, entityEditService: EntityEditService, bulkMergeService: BulkMergeService, entitySpecialLoader: EntitySpecialLoaderService)
Parameters :
Name Type Optional
screenWidthObserver ScreenWidthObserver No
router Router No
activatedRoute ActivatedRoute No
entityMapperService EntityMapperService No
entities EntityRegistry No
dialog MatDialog No
duplicateRecord DuplicateRecordService No
entityActionsService EntityActionsService No
entityEditService EntityEditService No
bulkMergeService BulkMergeService No
entitySpecialLoader EntitySpecialLoaderService No

Inputs

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

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

showInactive
Type : boolean

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

title
Type : string
Default value : ""

Outputs

addNewClick
Type : EventEmitter
elementClick
Type : EventEmitter

Methods

addNew
addNew(newEntity?: T)
Parameters :
Name Type Optional
newEntity T Yes
Returns : void
Async anonymizeRecords
anonymizeRecords()
Returns : any
applyFilter
applyFilter(filterValue: string)
Parameters :
Name Type Optional
filterValue string No
Returns : void
Async archiveRecords
archiveRecords()
Returns : any
Async deleteRecords
deleteRecords()
Returns : any
duplicateRecords
duplicateRecords()
Returns : void
Async editRecords
editRecords()
Returns : any
Protected getEntities
getEntities()

Template method that can be overwritten to change the loading logic.

Returns : Promise<T[]>
linkExternalProfiles
linkExternalProfiles()
Returns : void
Protected Async loadEntities
loadEntities()
Returns : any
Async mergeRecords
mergeRecords()
Returns : any
onRowClick
onRowClick(row: T)
Parameters :
Name Type Optional
row T No
Returns : void
openFilterOverlay
openFilterOverlay()

Calling this function will display the filters in a popup

Returns : void

Properties

columnsToDisplay
Type : string[]
defaultColumnGroup
Type : string
Default value : ""
filteredData
Type : []
Default value : []
filterFreetext
Type : string
filterObj
Type : DataFilter<T>
filterString
Type : string
Default value : ""
groups
Type : GroupConfig[]
Default value : []
isDesktop
Type : boolean
mobileColumnGroup
Type : string
Default value : ""
selectedColumnGroupIndex_
Type : number
Default value : 0
selectedRows
Type : T[]

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 {
  Component,
  EventEmitter,
  inject,
  Input,
  OnChanges,
  Optional,
  Output,
  SimpleChanges,
} from "@angular/core";
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
import {
  ColumnGroupsConfig,
  EntityListConfig,
  FilterConfig,
  GroupConfig,
} from "../EntityListConfig";
import { Entity, EntityConstructor } from "../../entity/model/entity";
import { FormFieldConfig } from "../../common-components/entity-form/FormConfig";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
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 {
  AsyncPipe,
  NgForOf,
  NgIf,
  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 { ExportDataDirective } from "../../export/export-data-directive/export-data.directive";
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 { ExportColumnConfig } from "../../export/data-transformation-service/export-column-config";
import { RouteTarget } from "../../../route-target";
import { EntityActionsService } from "app/core/entity/entity-actions/entity-actions.service";
import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component";
import { applyUpdate } from "../../entity/model/entity-update";
import { Subscription } from "rxjs";
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 {
  EntitySpecialLoaderService,
  LoaderMethod,
} from "../../entity/entity-special-loader/entity-special-loader.service";
import { EntityEditService } from "app/core/entity/entity-actions/entity-edit.service";
import {
  DialogViewComponent,
  DialogViewData,
} from "../../ui/dialog-view/dialog-view.component";
import { AblePurePipe } from "@casl/angular";
import { BulkMergeService } from "app/features/de-duplication/bulk-merge-service";
import { FormDialogService } from "../../form-dialog/form-dialog.service";

/**
 * 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({
  selector: "app-entity-list",
  templateUrl: "./entity-list.component.html",
  styleUrls: ["./entity-list.component.scss"],
  providers: [DuplicateRecordService],
  imports: [
    NgIf,
    NgStyle,
    MatButtonModule,
    Angulartics2OnModule,
    FontAwesomeModule,
    MatMenuModule,
    NgTemplateOutlet,
    MatTabsModule,
    NgForOf,
    MatFormFieldModule,
    MatInputModule,
    EntitiesTableComponent,
    FormsModule,
    FilterComponent,
    TabStateModule,
    ViewTitleComponent,
    ExportDataDirective,
    DisableEntityOperationDirective,
    RouterLink,
    MatTooltipModule,
    EntityCreateButtonComponent,
    AsyncPipe,
    AblePurePipe,
    ViewActionsComponent,
    // WARNING: all imports here also need to be set for components extending EntityList, like ChildrenListComponent
  ],
})
@UntilDestroy()
export class EntityListComponent<T extends Entity>
  implements EntityListConfig, OnChanges
{
  private readonly formDialog = inject(FormDialogService);

  @Input() allEntities: T[];

  @Input() entityType: string;
  @Input() entityConstructor: EntityConstructor<T>;
  @Input() defaultSort: Sort;
  @Input() exportConfig: ExportColumnConfig[];

  /**
   * The special service or method to load data via an index or other special method.
   */
  @Input() loaderMethod: LoaderMethod;

  @Input() clickMode: "navigate" | "popup" | "popup-details" | "none" =
    "navigate";

  /** initial / default state whether to include archived records in the list */
  @Input() showInactive: boolean;

  @Output() elementClick = new EventEmitter<T>();
  @Output() addNewClick = new EventEmitter();
  selectedRows: T[];

  isDesktop: boolean;

  @Input() title = "";
  @Input() columns: (FormFieldConfig | string)[] = [];
  @Input() columnGroups: ColumnGroupsConfig;
  groups: GroupConfig[] = [];
  defaultColumnGroup = "";
  mobileColumnGroup = "";
  @Input() filters: FilterConfig[] = [];

  columnsToDisplay: string[];

  filterObj: DataFilter<T>;
  filterString = "";
  filteredData = [];
  filterFreetext: string;

  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(
    private screenWidthObserver: ScreenWidthObserver,
    private router: Router,
    private activatedRoute: ActivatedRoute,
    protected entityMapperService: EntityMapperService,
    private entities: EntityRegistry,
    private dialog: MatDialog,
    private duplicateRecord: DuplicateRecordService,
    private entityActionsService: EntityActionsService,
    private entityEditService: EntityEditService,
    private bulkMergeService: BulkMergeService,
    @Optional() private entitySpecialLoader: EntitySpecialLoaderService,
  ) {
    this.screenWidthObserver
      .platform()
      .pipe(untilDestroyed(this))
      .subscribe((isDesktop) => {
        if (!isDesktop) {
          this.displayColumnGroupByName(this.mobileColumnGroup);
        } else if (
          this.selectedColumnGroupIndex ===
          this.getSelectedColumnIndexByName(this.mobileColumnGroup)
        ) {
          this.displayColumnGroupByName(this.defaultColumnGroup);
        }

        this.isDesktop = isDesktop;
      });
  }

  ngOnChanges(changes: SimpleChanges) {
    return this.buildComponentFromConfig();
  }

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

    if (!this.allEntities) {
      // if no entities are passed as input, by default load all entities of the type
      await this.loadEntities();
    }

    this.title = this.title || this.entityConstructor?.labelPlural;

    this.initColumnGroups(this.columnGroups);

    this.displayColumnGroupByName(
      this.screenWidthObserver.isDesktop()
        ? this.defaultColumnGroup
        : this.mobileColumnGroup,
    );
  }

  protected async loadEntities() {
    this.allEntities = await this.getEntities();
    this.listenToEntityUpdates();
  }

  /**
   * Template method that can be overwritten to change the loading logic.
   * @protected
   */
  protected getEntities(): Promise<T[]> {
    if (this.loaderMethod && this.entitySpecialLoader) {
      return this.entitySpecialLoader.loadData(this.loaderMethod);
    }

    return this.entityMapperService.loadType(this.entityConstructor);
  }

  private updateSubscription: Subscription;

  private listenToEntityUpdates() {
    if (this.updateSubscription || !this.entityConstructor) {
      return;
    }

    this.updateSubscription = this.entityMapperService
      .receiveUpdates(this.entityConstructor)
      .pipe(untilDestroyed(this))
      .subscribe(async (updatedEntity) => {
        // get specially enhanced entity if necessary
        if (this.loaderMethod && this.entitySpecialLoader) {
          updatedEntity = await this.entitySpecialLoader.extendUpdatedEntity(
            this.loaderMethod,
            updatedEntity,
          );
        }

        this.allEntities = applyUpdate(this.allEntities, updatedEntity);
      });
  }

  private initColumnGroups(columnGroup?: ColumnGroupsConfig) {
    if (columnGroup && columnGroup.groups.length > 0) {
      this.groups = columnGroup.groups;
      this.defaultColumnGroup =
        columnGroup.default && this.configuredTabExists(columnGroup.default)
          ? columnGroup.default
          : columnGroup.groups[0].name;

      this.mobileColumnGroup =
        columnGroup.mobile && this.configuredTabExists(columnGroup.mobile)
          ? columnGroup.mobile
          : columnGroup.groups[0].name;
    } else {
      this.groups = [
        {
          name: "default",
          columns: this.columns.map((c) => (typeof c === "string" ? c : c.id)),
        },
      ];
      this.defaultColumnGroup = "default";
      this.mobileColumnGroup = "default";
    }
  }

  private configuredTabExists(groupName: string): boolean {
    return this.groups.some((group) => group.name === groupName);
  }

  applyFilter(filterValue: string) {
    // TODO: turn this into one of our filter types, so that all filtering happens the same way (and we avoid accessing internal datasource of sub-component here)
    this.filterFreetext = filterValue.trim().toLowerCase();
  }

  private displayColumnGroupByName(columnGroupName: string) {
    const selectedColumnIndex =
      this.getSelectedColumnIndexByName(columnGroupName);
    if (selectedColumnIndex !== -1) {
      this.selectedColumnGroupIndex = selectedColumnIndex;
    }
  }

  private getSelectedColumnIndexByName(columnGroupName: string) {
    return this.groups.findIndex((c) => c.name === columnGroupName);
  }

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

  addNew(newEntity?: T) {
    if (!newEntity) {
      newEntity = new this.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();
  }

  duplicateRecords() {
    this.duplicateRecord.duplicateRecord(this.selectedRows);
    this.selectedRows = undefined;
  }

  async editRecords() {
    await this.entityEditService.edit(
      this.selectedRows,
      this.entityConstructor,
    );
    this.selectedRows = undefined;
  }

  async mergeRecords() {
    await this.bulkMergeService.showMergeDialog(
      this.selectedRows,
      this.entityConstructor,
    );
    this.selectedRows = undefined;
  }

  async deleteRecords() {
    await this.entityActionsService.delete(this.selectedRows);
    this.selectedRows = undefined;
  }

  async archiveRecords() {
    await this.entityActionsService.archive(this.selectedRows);
    this.selectedRows = undefined;
  }

  async anonymizeRecords() {
    await this.entityActionsService.anonymize(this.selectedRows);
    this.selectedRows = undefined;
  }

  linkExternalProfiles() {
    this.dialog.open(DialogViewComponent, {
      width: "98vw",
      maxWidth: "100vw",
      height: "98vh",
      maxHeight: "100vh",

      data: {
        component: "BulkLinkExternalProfiles",
        config: {
          entities: this.selectedRows,
        },
      } as DialogViewData,
    });

    this.selectedRows = undefined;
  }

  onRowClick(row: T) {
    this.elementClick.emit(row);
  }
}
<!-- Desktop version -->
<div *ngIf="isDesktop">
  <!-- 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>
    <div class="flex-row gap-regular">
      <app-entity-create-button
        [entityType]="entityConstructor"
        (entityCreate)="addNew($event)"
      ></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>
    <app-filter
      *ngIf="!!allEntities"
      class="flex-row gap-regular flex-wrap"
      [filterConfig]="filters"
      [entityType]="entityConstructor"
      [entities]="allEntities"
      [useUrlQueryParams]="true"
      [(filterObj)]="filterObj"
      [filterString]="filterString"
      (filterStringChange)="filterString = $event; applyFilter($event)"
    ></app-filter>
  </div>

  <!-- Bulk Actions -->

  <ng-container *ngTemplateOutlet="bulkActions"></ng-container>

  <!-- Tab Groups-->

  <div class="mat-elevation-z1">
    <div *ngIf="groups.length > 1">
      <mat-tab-group
        [(selectedIndex)]="selectedColumnGroupIndex"
        appTabStateMemo
      >
        <mat-tab
          *ngFor="let item of groups"
          [label]="item.name"
          angulartics2On="click"
          [angularticsCategory]="entityConstructor?.ENTITY_TYPE"
          angularticsAction="list_column_view"
          [angularticsLabel]="item.name"
        ></mat-tab>
      </mat-tab-group>
    </div>
    <ng-container *ngTemplateOutlet="subrecord"></ng-container>
  </div>
</div>

<!-- Mobile Version -->
<div *ngIf="!isDesktop">
  <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>

  <div *ngIf="selectedRows" class="bulk-action-spacing">
    <ng-container *ngTemplateOutlet="bulkActions"></ng-container>
  </div>

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

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

<ng-template #filterDialog>
  <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"
    />
    <button
      mat-icon-button
      *ngIf="filterString"
      matIconSuffix
      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"
    [records]="allEntities"
    [customColumns]="columns"
    [editable]="false"
    [clickMode]="clickMode"
    (entityClick)="onRowClick($event)"
    [columnsToDisplay]="columnsToDisplay"
    [filter]="filterObj"
    [sortBy]="defaultSort"
    [(selectedRecords)]="selectedRows"
    [selectable]="!!selectedRows"
    [showInactive]="showInactive"
    (filteredRecordsChange)="filteredData = $event"
    [filterFreetext]="filterFreetext"
  ></app-entities-table>
</ng-template>

<mat-menu #additional>
  <div class="hide-desktop">
    <button
      mat-menu-item
      (click)="addNew()"
      angulartics2On="click"
      angularticsCategory="UserAction"
      [angularticsAction]="title.toLowerCase().replace(' ', '_') + '_add_new'"
      *appDisabledEntityOperation="{
        entity: entityConstructor,
        operation: 'create',
      }"
    >
      <fa-icon
        class="color-accent standard-icon-with-text"
        aria-label="add element"
        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-label="filter"
        class="color-accent standard-icon-with-text"
        icon="filter"
      >
      </fa-icon>
      <span i18n="Show filter options popup for list"> Filter options </span>
    </button>
  </div>

  <button
    mat-menu-item
    [appExportData]="allEntities"
    format="csv"
    [exportConfig]="exportConfig"
    [filename]="title.replace(' ', '')"
    angulartics2On="click"
    [angularticsCategory]="entityConstructor?.ENTITY_TYPE"
    angularticsAction="list_csv_export"
  >
    <fa-icon
      class="color-accent standard-icon-with-text"
      aria-label="download csv"
      icon="download"
    ></fa-icon>
    <span i18n="Download list contents as CSV"> Download all data (.csv) </span>
  </button>

  <button
    mat-menu-item
    [appExportData]="filteredData"
    format="csv"
    [exportConfig]="exportConfig"
    [filename]="title.replace(' ', '')"
    angulartics2On="click"
    [angularticsCategory]="entityConstructor?.ENTITY_TYPE"
    angularticsAction="list_csv_export"
  >
    <fa-icon
      class="color-accent standard-icon-with-text"
      aria-label="download csv"
      icon="download"
    ></fa-icon>
    <span i18n="Download list contents as CSV"> Download current (.csv) </span>
  </button>

  <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-label="import file"
      icon="file-import"
    ></fa-icon>
    <span i18n> Import from file </span>
  </button>

  <button
    mat-menu-item
    (click)="selectedRows = []"
    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-label="bulk actions"
      icon="list-check"
    ></fa-icon>
    <span i18n> Bulk Actions </span>
  </button>

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

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

<ng-template #bulkActions>
  <div *ngIf="!!selectedRows" class="bulk-action-button">
    <div i18n>
      Actions on <b>{{ selectedRows.length }}</b> selected records:
    </div>

    <div
      class="flex-row gap-small bulk-action-button"
      matTooltip="Select rows for an action on multiple records"
      i18n-matTooltip
    >
      <button
        mat-raised-button
        (click)="editRecords()"
        [disabled]="selectedRows.length === 0"
        color="accent"
        i18n="bulk action button"
      >
        Bulk Edit
      </button>
      <button
        mat-raised-button
        (click)="archiveRecords()"
        [disabled]="selectedRows.length === 0"
        color="accent"
        i18n="bulk action button"
      >
        Archive
      </button>
      @if (entityConstructor.hasPII) {
        <button
          mat-raised-button
          (click)="anonymizeRecords()"
          [disabled]="selectedRows.length === 0"
          color="accent"
          i18n="bulk action button"
        >
          Anonymize
        </button>
      }
      <button
        mat-raised-button
        (click)="deleteRecords()"
        [disabled]="selectedRows.length === 0"
        color="accent"
        i18n="bulk action button"
      >
        Delete
      </button>
      <button
        mat-raised-button
        (click)="duplicateRecords()"
        [disabled]="selectedRows.length === 0"
        color="accent"
        i18n="bulk action button"
      >
        Duplicate
      </button>

      <button
        mat-raised-button
        (click)="linkExternalProfiles()"
        [disabled]="selectedRows.length === 0"
        color="accent"
        i18n="bulk action button"
      >
        Link External Profile
      </button>

      <button
        mat-raised-button
        (click)="mergeRecords()"
        [disabled]="selectedRows.length !== 2"
        color="accent"
        i18n="bulk action button"
      >
        Merge
      </button>

      <button mat-raised-button (click)="selectedRows = undefined" i18n>
        Cancel
      </button>
    </div>
  </div>
</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;
  }
}

.bulk-action-button {
  right: sizes.$large;

  padding: sizes.$regular;
  background-color: colors.$background;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""