src/app/core/admin/admin-list-manager/admin-list-manager.component.ts

Description

Component for Admin UI to edit table columns or fields in other contexts like filters.

Example

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs
Accessors

Inputs

activeFields
Type : ColumnConfig[]
Default value : []
additionalFields
Type : ColumnConfig[]
Default value : []

custom fields that will be added in addition to schema fields for users to select from

entityType
Type : EntityConstructor
fieldLabel
Type : string
Default value : undefined
items
Type : ColumnConfig[]
Default value : []
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

Methods

drop
drop(event: CdkDragDrop<ColumnConfig[]>)
Parameters :
Name Type Optional
event CdkDragDrop<ColumnConfig[]> No
Returns : void
getFieldId
getFieldId(field: ColumnConfig)
Parameters :
Name Type Optional
field ColumnConfig No
Returns : string
remove
remove(item: ColumnConfig)
Parameters :
Name Type Optional
item ColumnConfig No
Returns : void
updateItems
updateItems(updatedItems: (string | ColumnConfig)[])
Parameters :
Name Type Optional
updatedItems (string | ColumnConfig)[] No
Returns : void

Properties

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>
}

./admin-list-manager.component.scss

@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
Component
Html element with directive

results matching ""

    No results matching ""