import {
Component,
effect,
inject,
input,
output,
ChangeDetectionStrategy,
linkedSignal,
} from "@angular/core";
import {
EntityDetailsConfig,
Panel,
PanelComponent,
} from "../../../entity-details/EntityDetailsConfig";
import { EntityConstructor } from "../../../entity/model/entity";
import { DynamicComponent } from "../../../config/dynamic-components/dynamic-component.decorator";
import { MatTabsModule } from "@angular/material/tabs";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { MatButtonModule } from "@angular/material/button";
import { ViewTitleComponent } from "../../../common-components/view-title/view-title.component";
import { AdminSectionHeaderComponent } from "../../building-blocks/admin-section-header/admin-section-header.component";
import { AdminEntityFormComponent } from "../admin-entity-form/admin-entity-form.component";
import { AdminEntityPanelComponentComponent } from "../admin-entity-panel-component/admin-entity-panel-component.component";
import { MatTooltipModule } from "@angular/material/tooltip";
import { AdminTabsComponent } from "../../building-blocks/admin-tabs/admin-tabs.component";
import { AdminTabTemplateDirective } from "../../building-blocks/admin-tabs/admin-tab-template.directive";
import { MatDialog } from "@angular/material/dialog";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatIconModule } from "@angular/material/icon";
import {
CdkDragDrop,
DragDropModule,
moveItemInArray,
} from "@angular/cdk/drag-drop";
import { WidgetComponentSelectComponent } from "#src/app/core/admin/admin-entity-details/widget-component-select/widget-component-select.component";
import { HintBoxComponent } from "#src/app/core/common-components/hint-box/hint-box.component";
@DynamicComponent("AdminEntityDetails")
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-admin-entity-details",
templateUrl: "./admin-entity-details.component.html",
styleUrls: ["./admin-entity-details.component.scss"],
imports: [
MatTabsModule,
FaIconComponent,
MatButtonModule,
ViewTitleComponent,
AdminSectionHeaderComponent,
AdminEntityFormComponent,
AdminEntityPanelComponentComponent,
MatTooltipModule,
AdminTabsComponent,
AdminTabTemplateDirective,
MatExpansionModule,
MatIconModule,
DragDropModule,
HintBoxComponent,
],
})
export class AdminEntityDetailsComponent {
private dialog = inject(MatDialog);
entityConstructor = input.required<EntityConstructor>();
config = input.required<EntityDetailsConfig>();
panels = linkedSignal(() => this.config()?.panels ?? []);
constructor() {
effect(() => {
this.config().panels = this.panels();
});
}
/**
* Event emitted when a related entity's schema has been modified,
* to ensure changes are saved centrally by parent component.
*/
relatedEntityModified = output<EntityConstructor>();
newPanelFactory(): Panel {
return { title: "New Tab", components: [] };
}
addComponent(panel: Panel) {
this.dialog
.open(WidgetComponentSelectComponent, {
data: { entityType: this.entityConstructor().ENTITY_TYPE },
})
.afterClosed()
.subscribe((sectionConfig: PanelComponent) => {
if (!sectionConfig) return;
this.panels.update((panels) =>
panels.map((p) =>
p === panel
? { ...p, components: [...p.components, sectionConfig] }
: p,
),
);
});
}
removeComponent(panel: Panel, index: number) {
this.panels.update((panels) =>
panels.map((p) =>
p === panel
? { ...p, components: p.components.filter((_, i) => i !== index) }
: p,
),
);
}
onSectionDrop(panel: Panel, event: CdkDragDrop<PanelComponent[]>) {
if (
event.previousIndex === event.currentIndex ||
event.currentIndex < 0 ||
event.currentIndex >= panel.components.length
) {
return;
}
const components = [...panel.components];
moveItemInArray(components, event.previousIndex, event.currentIndex);
this.panels.update((panels) =>
panels.map((p) => (p === panel ? { ...p, components } : p)),
);
}
}
<app-hint-box i18n>
You can edit how users will see the details of a single record of this type.
Drag and drop fields and sections in this preview of a profile view and group
them as appropriate. The editor below closely resembles how the form will look
for users later.
<br />
We recommend to keep things simple: Only add fields that you really need for
your work.
</app-hint-box>
<app-view-title [disableBackButton]="true" [displayInPlace]="true">
<span i18n><Name of displayed {{ entityConstructor()?.label }}></span>
</app-view-title>
<app-admin-tabs
[tabs]="panels()"
(tabsChange)="panels.set($event)"
[newTabFactory]="newPanelFactory"
>
<ng-template [appAdminTabTemplate]="panels()" let-item let-tabIndex="index">
<div class="flex-column gap-large padding-top-large">
<mat-accordion
cdkDropList
[cdkDropListData]="item.components"
cdkDropListOrientation="vertical"
(cdkDropListDropped)="onSectionDrop(item, $event)"
>
@for (
componentConfig of item.components;
track componentConfig;
let i = $index
) {
<mat-expansion-panel
class="section-wrapper"
[expanded]="item.components.length <= 1"
cdkDrag
cdkDragLockAxis="y"
>
<mat-expansion-panel-header>
<div class="section-header-row">
<fa-icon
icon="grip-vertical"
size="xl"
class="drag-handle"
></fa-icon>
<div class="section-header-content">
<app-admin-section-header
[(title)]="componentConfig.title"
(remove)="removeComponent(item, i)"
(keydown)="$event.stopPropagation()"
></app-admin-section-header>
</div>
</div>
</mat-expansion-panel-header>
<div class="section-content">
@if (componentConfig.component === "Form") {
<app-admin-entity-form
[config]="componentConfig.config"
(configChange)="componentConfig.config = $event"
[entityType]="entityConstructor()"
uniqueAreaId="{{ tabIndex }}-{{ i }}"
>
</app-admin-entity-form>
} @else {
<app-admin-entity-panel-component
[config]="componentConfig"
[entityType]="entityConstructor()"
(relatedEntityModified)="relatedEntityModified.emit($event)"
></app-admin-entity-panel-component>
}
</div>
</mat-expansion-panel>
}
</mat-accordion>
<button
mat-stroked-button
color="accent"
class="section-add-button"
(click)="addComponent(item)"
i18n
>
<fa-icon
aria-hidden="true"
icon="plus-circle"
class="standard-icon-with-text"
></fa-icon>
Add Section
</button>
</div>
</ng-template>
</app-admin-tabs>
@use "@angular/material/core/style/elevation" as mat-elevation;
@use "variables/sizes";
@use "variables/colors";
.admin-ui-title {
font-style: italic;
}
.save-buttons {
margin: auto 0;
}
.section-wrapper {
@include mat-elevation.elevation(1);
margin: sizes.$small;
padding: sizes.$small;
.mat-expansion-panel-header {
min-height: 70px;
align-items: center;
}
}
/* TODO: making direct child of app-admin-section-header somehow breaks this
although `:has(> app-admin-section-header .group-remove-button` (without :hover) does work ... */
.section-wrapper:has(
.section-header-content app-admin-section-header .group-remove-button:hover
) {
border-color: rgb(255, 0, 0);
background-color: rgba(255, 0, 0, 0.1);
}
.section-header-row {
display: flex;
align-items: center;
gap: sizes.$small;
width: 100%;
}
.section-header-content {
flex: 1;
padding-right: sizes.$large;
}
.section-add-button {
padding: sizes.$large;
margin: sizes.$small;
}
.section-content {
overflow-x: auto;
overflow-y: hidden;
}
.drag-handle {
color: colors.$accent;
cursor: move;
min-width: 2em;
text-align: center;
}