src/app/core/entity-list/entity-list/entity-list.component.ts
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.
EntityListConfig
OnChanges
OnInit
providers |
DuplicateRecordService
|
selector | app-entity-list |
imports |
NgStyle
MatButtonModule
Angulartics2OnModule
FontAwesomeModule
MatMenuModule
NgTemplateOutlet
MatTabsModule
MatFormFieldModule
MatInputModule
EntitiesTableComponent
FormsModule
FilterComponent
TabStateModule
ViewTitleComponent
ExportDataDirective
DisableEntityOperationDirective
RouterLink
MatTooltipModule
EntityCreateButtonComponent
AsyncPipe
AblePurePipe
ViewActionsComponent
EntityLoadPipe
|
styleUrls | ./entity-list.component.scss |
templateUrl | ./entity-list.component.html |
Properties |
Methods |
|
Inputs |
Outputs |
Accessors |
constructor()
|
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 : ""
|
|
addNewClick | |
Type : EventEmitter
|
|
elementClick | |
Type : EventEmitter
|
|
addNew | ||||||
addNew(newEntity?: T)
|
||||||
Parameters :
Returns :
void
|
Async anonymizeRecords |
anonymizeRecords()
|
Returns :
any
|
applyFilter | ||||||
applyFilter(filterValue: string)
|
||||||
Parameters :
Returns :
void
|
Async archiveRecords |
archiveRecords()
|
Returns :
any
|
Async copyPublicFormLinkForEntityType | ||||||
copyPublicFormLinkForEntityType(config: PublicFormConfig)
|
||||||
Parameters :
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 :
Returns :
void
|
openFilterOverlay |
openFilterOverlay()
|
Calling this function will display the filters in a popup
Returns :
void
|
columnsToDisplay |
Type : string[]
|
defaultColumnGroup |
Type : string
|
Default value : ""
|
Protected entityMapperService |
Default value : inject(EntityMapperService)
|
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 : ""
|
Public publicFormConfigs |
Type : PublicFormConfig[]
|
Default value : []
|
selectedColumnGroupIndex_ |
Type : number
|
Default value : 0
|
selectedRows |
Type : T[]
|
selectedColumnGroupIndex | ||||||
getselectedColumnGroupIndex()
|
||||||
setselectedColumnGroupIndex(newValue: number)
|
||||||
Parameters :
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,
Input,
OnChanges,
Output,
SimpleChanges,
inject,
OnInit,
} 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, 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";
import { EntityLoadPipe } from "../../common-components/entity-load/entity-load.pipe";
import { PublicFormConfig } from "#src/app/features/public-form/public-form-config";
import { PublicFormsService } from "#src/app/features/public-form/public-forms.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: [
NgStyle,
MatButtonModule,
Angulartics2OnModule,
FontAwesomeModule,
MatMenuModule,
NgTemplateOutlet,
MatTabsModule,
MatFormFieldModule,
MatInputModule,
EntitiesTableComponent,
FormsModule,
FilterComponent,
TabStateModule,
ViewTitleComponent,
ExportDataDirective,
DisableEntityOperationDirective,
RouterLink,
MatTooltipModule,
EntityCreateButtonComponent,
AsyncPipe,
AblePurePipe,
ViewActionsComponent,
EntityLoadPipe,
],
})
@UntilDestroy()
export class EntityListComponent<T extends Entity>
implements EntityListConfig, OnChanges, OnInit
{
private screenWidthObserver = inject(ScreenWidthObserver);
private router = inject(Router);
private activatedRoute = inject(ActivatedRoute);
protected entityMapperService = inject(EntityMapperService);
private entities = inject(EntityRegistry);
private dialog = inject(MatDialog);
private duplicateRecord = inject(DuplicateRecordService);
private entityActionsService = inject(EntityActionsService);
private entityEditService = inject(EntityEditService);
private bulkMergeService = inject(BulkMergeService);
private entitySpecialLoader = inject(EntitySpecialLoaderService, {
optional: true,
});
private readonly formDialog = inject(FormDialogService);
private readonly publicFormsService = inject(PublicFormsService);
public publicFormConfigs: PublicFormConfig[] = [];
@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() {
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;
});
}
async ngOnInit() {
await this.loadPublicFormConfig();
}
private async loadPublicFormConfig() {
const allForms = await this.publicFormsService.getAllPublicFormConfigs();
this.publicFormConfigs = allForms.filter(
(config) =>
config.entity &&
config.entity.toLowerCase() ===
this.entityConstructor?.ENTITY_TYPE?.toLowerCase(),
);
}
async copyPublicFormLinkForEntityType(config: PublicFormConfig) {
await this.publicFormsService.copyPublicFormLinkFromConfig(config);
}
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 -->
@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>
<div class="flex-row gap-regular">
<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 (!!allEntities) {
<app-filter
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">
@if (groups.length > 1) {
<div>
<mat-tab-group
[(selectedIndex)]="selectedColumnGroupIndex"
appTabStateMemo
>
@for (item of groups; track item) {
<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-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="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"
/>
@if (filterString) {
<button
mat-icon-button
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>
@for (formConfig of publicFormConfigs; track formConfig.route) {
<button
mat-menu-item
(click)="copyPublicFormLinkForEntityType(formConfig)"
[matTooltip]="formConfig.title"
i18n-matTooltip
matTooltipPosition="before"
>
<fa-icon
class="color-accent standard-icon-with-text"
icon="link"
></fa-icon>
<span>Copy Public Form Link ({{ formConfig.title }})</span>
</button>
}
@if (
("update"
| ablePure: ("CONFIG_ENTITY" | entityLoad: "Config" | async)
| async) && !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>Edit Data Structure</span>
</button>
}
<ng-content select="[mat-menu-item]"></ng-content>
</mat-menu>
<ng-template #bulkActions>
@if (!!selectedRows) {
<div 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;
}