Description
Component for Admin UI to edit table columns or fields in other contexts like filters.
Example
|
additionalFields
|
Type : ColumnConfig[]
|
Default value : []
|
|
|
custom fields that will be added in addition to schema fields for users to select from
|
|
fieldLabel
|
Type : string
|
Default value : undefined
|
|
|
|
templateType
|
Type : "default" | "filter"
|
Default value : "default"
|
|
|
Outputs
|
idsChange
|
Type : string[]
|
|
|
emits changes to the selected fields only as field IDs (custom field configs are mapped to their ID only)
|
|
itemsChange
|
Type : ColumnConfig[]
|
|
|
emits changes to the selected fields as field config objects or IDs
|
|
availableItems
|
Type : unknown
|
Default value : computed(() => {
this.schemaUpdated(); // track schema updates
if (!this.entityType()) return [];
const targetEntitySchemaFields = Array.from(
this.entityType().schema.keys(),
);
return Array.from(
new Set([
...(this.activeFields() ?? []),
...targetEntitySchemaFields,
...(this.additionalFields() ?? []),
]),
);
})
|
|
|
Accessors
|
itemsAsStrings
|
getitemsAsStrings()
|
|
|
import { EntityFieldsMenuComponent } from "#src/app/core/common-components/entity-fields-menu/entity-fields-menu.component";
import { EntityFieldLabelComponent } from "#src/app/core/entity/entity-field-label/entity-field-label.component";
import { EntityConstructor } from "#src/app/core/entity/model/entity";
import {
CdkDrag,
CdkDragDrop,
CdkDropList,
moveItemInArray,
} from "@angular/cdk/drag-drop";
import {
Component,
input,
computed,
inject,
ChangeDetectionStrategy,
output,
} from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatSelectModule } from "@angular/material/select";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { ColumnConfig } from "app/core/common-components/entity-form/FormConfig";
import { AdminEntityService } from "../admin-entity.service";
/**
* Component for Admin UI to edit table columns or fields in other contexts like filters.
*/
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-admin-list-manager",
imports: [
CdkDropList,
CdkDrag,
EntityFieldsMenuComponent,
EntityFieldLabelComponent,
FontAwesomeModule,
MatFormFieldModule,
MatSelectModule,
],
templateUrl: "./admin-list-manager.component.html",
styleUrl: "./admin-list-manager.component.scss",
})
export class AdminListManagerComponent {
private readonly adminEntityService = inject(AdminEntityService);
private readonly schemaUpdated = toSignal(
this.adminEntityService.entitySchemaUpdated,
);
items = input<ColumnConfig[]>([]);
entityType = input<EntityConstructor>();
fieldLabel = input<string>(undefined);
templateType = input<"default" | "filter">("default");
activeFields = input<ColumnConfig[]>([]);
/** custom fields that will be added in addition to schema fields for users to select from */
additionalFields = input<ColumnConfig[]>([]);
/** emits changes to the selected fields as field config objects or IDs */
itemsChange = output<ColumnConfig[]>();
/** emits changes to the selected fields only as field IDs (custom field configs are mapped to their ID only) */
idsChange = output<string[]>();
availableItems = computed(() => {
this.schemaUpdated(); // track schema updates
if (!this.entityType()) return [];
const targetEntitySchemaFields = Array.from(
this.entityType().schema.keys(),
);
return Array.from(
new Set([
...(this.activeFields() ?? []),
...targetEntitySchemaFields,
...(this.additionalFields() ?? []),
]),
);
});
drop(event: CdkDragDrop<ColumnConfig[]>) {
const newItems = [...(this.items() ?? [])];
moveItemInArray(newItems, event.previousIndex, event.currentIndex);
this.itemsChange.emit(newItems);
this.idsChange.emit(newItems.map(this.getFieldId));
}
remove(item: ColumnConfig) {
const newItems = (this.items() ?? []).filter((i) => i !== item);
this.itemsChange.emit(newItems);
this.idsChange.emit(newItems.map(this.getFieldId));
}
updateItems(updatedItems: (string | ColumnConfig)[]) {
this.itemsChange.emit(updatedItems as ColumnConfig[]);
this.idsChange.emit((updatedItems as ColumnConfig[]).map(this.getFieldId));
}
/**
* Emits the current items and their IDs to the parent component.
* `itemsChange` provides the full `ColumnConfig` objects,
* `idsChanges`, provides a simplified array of just the IDs
*/
private emitUpdatedConfig() {
const current = this.items() ?? [];
this.itemsChange.emit(current);
this.idsChange.emit(current.map(this.getFieldId));
}
getFieldId(field: ColumnConfig): string {
return typeof field === "string" ? field : field.id;
}
get itemsAsStrings(): string[] {
return (this.items() ?? []).map(this.getFieldId);
}
}
<div
cdkDropList
cdkDropListOrientation="mixed"
(cdkDropListDropped)="drop($event)"
>
<div
class="flex-row gap-regular flex-wrap gap-large"
[class.column-header]="templateType() !== 'filter'"
>
@for (item of items(); track getFieldId(item)) {
<div cdkDrag class="drop-item">
@if (templateType() === "filter") {
<div class="flex-row gap-small filter-field">
<fa-icon
icon="grip-vertical"
size="xl"
class="drag-handle"
></fa-icon>
<mat-form-field appearance="fill">
<mat-label>
<app-entity-field-label
[additionalFields]="additionalFields()"
[field]="item"
[entityType]="entityType()"
></app-entity-field-label>
</mat-label>
<mat-select disabled></mat-select>
</mat-form-field>
<fa-icon
icon="times"
class="remove-icon"
(click)="remove(item)"
></fa-icon>
</div>
} @else {
<div class="default-item flex-row gap-small align-center">
<fa-icon icon="grip-vertical" class="drag-handle"></fa-icon>
<app-entity-field-label
[additionalFields]="additionalFields()"
[field]="item"
[entityType]="entityType()"
></app-entity-field-label>
<fa-icon
icon="times"
class="remove-icon"
(click)="remove(item)"
></fa-icon>
</div>
}
</div>
}
</div>
</div>
@if (availableItems().length > 0) {
<div class="table-content-preview">
<span>{{ fieldLabel() }}</span>
<app-entity-fields-menu
class="full-width-field"
[entityType]="entityType()"
[availableFields]="availableItems()"
[activeFields]="itemsAsStrings"
(activeFieldsChange)="updateItems($event)"
></app-entity-fields-menu>
</div>
}
@use "variables/colors";
.column-header {
padding: 8px 16px;
border-bottom: 1px solid #0000001f;
}
.table-content-preview {
font-style: italic;
padding: 16px;
color: #5f5f5f;
}
.filter-field {
padding: 0 16px;
::ng-deep .mat-mdc-form-field-subscript-wrapper {
height: 0;
}
}
.drag-handle {
cursor: move;
margin: auto;
color: colors.$accent;
}
app-entity-fields-menu {
color: colors.$accent;
}
.default-item {
margin: auto;
padding: 8px 16px;
}
.remove-icon {
cursor: pointer;
margin: auto;
}
.drop-item:has(.remove-icon:hover) {
color: rgb(255, 0, 0);
background-color: rgba(255, 0, 0, 0.1);
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow:
0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.drop-list.cdk-drop-list-dragging .drop-item:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.full-width-field {
width: 100%;
display: inline-block;
::ng-deep mat-form-field {
width: 100% !important;
}
}
Legend
Html element with directive