From d64f9372b7c0089ce6bd244de8e5276436ace560 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 12 Apr 2024 14:52:28 +0200 Subject: [PATCH] feat(core): components can be used both as dialog & full-screen routed views (#2304) * migrated special config appConfig:note-details to a standard view:note/:id * adapted building blocks to be more flexible (app-view-title, app-dialog-buttons) * extracted basics from EntityDetailsComponent into a new AbstractEntityDetailsComponent to be reused * implemented DialogViewComponent similar to RoutedViewComponent * generalized NoteDetailsComponent (and to some extend EntityDetailsComponent) to be usable in both dialogs and routed views * ChildrenList & NotesManager take actual inputs instead of the full config object * add an app-view-actions wrapper to align DialogButtons and Actions placed on top in routed views closes #2069 --- .../how-to-guides/create-custom-view.md | 64 +++++++++ .../create-entity-details-panel.md | 16 +-- doc/compodoc_sources/summary.json | 4 + doc/images/routed-views.png | Bin 0 -> 79709 bytes .../children-list.component.spec.ts | 5 +- .../children-list/children-list.component.ts | 117 ++++++++++++---- .../note-details/note-details.component.html | 82 +++++------ .../note-details/note-details.component.ts | 71 +++++++--- .../notes/notes-components.ts | 7 + .../notes-manager.component.html | 8 +- .../notes-manager.component.spec.ts | 37 ++--- .../notes-manager/notes-manager.component.ts | 35 ++--- .../admin-entity.component.spec.ts | 1 + .../view-actions/view-actions.component.html | 7 + .../view-actions.component.spec.ts | 24 ++++ .../view-actions/view-actions.component.ts | 31 +++++ .../view-title/view-title.component.html | 32 +++-- .../view-title/view-title.component.scss | 9 +- .../view-title/view-title.component.ts | 54 ++++---- src/app/core/config/config-fix.ts | 12 +- .../dynamic-component.pipe.ts | 27 ++++ .../abstract-entity-details.component.spec.ts | 129 ++++++++++++++++++ .../abstract-entity-details.component.ts | 79 +++++++++++ .../entity-actions-menu.component.html | 99 +++++++++----- .../entity-actions-menu.component.scss | 4 + .../entity-actions-menu.component.ts | 22 ++- .../entity-details.component.html | 61 +++------ .../entity-details.component.spec.ts | 56 +------- .../entity-details.component.ts | 105 ++------------ .../entity-details/form/form.component.ts | 6 +- .../entity-list/entity-list.component.html | 11 +- .../entity-list/entity-list.component.spec.ts | 46 ++----- .../entity-list/entity-list.component.ts | 31 ++--- src/app/core/entity/entity-config.service.ts | 22 ++- .../dialog-buttons.component.html | 5 +- .../dialog-buttons.component.ts | 37 ++++- .../core/form-dialog/form-dialog.service.ts | 20 ++- .../abstract-view/abstract-view.component.ts | 34 +++++ .../ui/dialog-view/dialog-view.component.html | 21 +++ .../ui/dialog-view/dialog-view.component.scss | 23 ++++ .../dialog-view/dialog-view.component.spec.ts | 85 ++++++++++++ .../ui/dialog-view/dialog-view.component.ts | 87 ++++++++++++ .../primary-action.component.ts | 7 +- .../ui/routed-view/routed-view.component.html | 19 +++ .../ui/routed-view/routed-view.component.ts | 25 ++-- .../todos/todo-list/todo-list.component.ts | 7 +- .../tab-state/tab-state-memo.directive.ts | 14 +- 47 files changed, 1190 insertions(+), 508 deletions(-) create mode 100644 doc/compodoc_sources/how-to-guides/create-custom-view.md create mode 100644 doc/images/routed-views.png create mode 100644 src/app/core/common-components/view-actions/view-actions.component.html create mode 100644 src/app/core/common-components/view-actions/view-actions.component.spec.ts create mode 100644 src/app/core/common-components/view-actions/view-actions.component.ts create mode 100644 src/app/core/config/dynamic-components/dynamic-component.pipe.ts create mode 100644 src/app/core/entity-details/abstract-entity-details/abstract-entity-details.component.spec.ts create mode 100644 src/app/core/entity-details/abstract-entity-details/abstract-entity-details.component.ts create mode 100644 src/app/core/ui/abstract-view/abstract-view.component.ts create mode 100644 src/app/core/ui/dialog-view/dialog-view.component.html create mode 100644 src/app/core/ui/dialog-view/dialog-view.component.scss create mode 100644 src/app/core/ui/dialog-view/dialog-view.component.spec.ts create mode 100644 src/app/core/ui/dialog-view/dialog-view.component.ts create mode 100644 src/app/core/ui/routed-view/routed-view.component.html diff --git a/doc/compodoc_sources/how-to-guides/create-custom-view.md b/doc/compodoc_sources/how-to-guides/create-custom-view.md new file mode 100644 index 0000000000..51ccb5214d --- /dev/null +++ b/doc/compodoc_sources/how-to-guides/create-custom-view.md @@ -0,0 +1,64 @@ +# How to create a custom View Component +We aim to build flexible, reusable components. +If you implement a custom component using the building blocks of Aam Digital's platform, this can seamlessly be displayed both in modal forms and as a fullscreen view. + +## Architecture & Generic wrapper components +The following architecture allows you to implement components that only have `@Input` properties +and do not access either Route or Dialog data directly. +Instead, the platform always uses `RoutedViewComponent` or `DialogViewComponent` to parse such context and pass it into your component as simple Angular @Inputs. + +![](../../images/routed-views.png) + +If you implement a special view to display a single entities' details, you should also extend `AbstractEntityDetailsComponent` with your component. +This takes care of loading the entity from the database, in case it is passed in as an id from the URL. + +## Implementing a custom view Component + +1. Create a new component class +2. Add any `@Input()` properties for values that are provided from the config. +3. For EntityDetails views, you get access to an `@Input() entity` and `@Input() entityConstructor` via the `AbstractEntityDetailsComponent` automatically. Otherwise, you do not have to extend from this. +4. Use `` and `` in your template to wrap the elements (if any) that you want to display as a header and action buttons. +These parts are automatically placed differently in the layout depending on whether your component is display as a fullscreen, routed view (actions displayed top right) or as a dialog/modal (actions displayed fixed at bottom). +5. Register your component under a name (string) with the `ComponentRegistry` (usually we do this in one of the modules), so that it can be referenced under this string form the config. +6. You can then use it in config, as shown below. + +Example template for a custom view component: +```html + + + My Entity {{ entity.name }} + + + +
+ My Custom View Content +
+ + + + + +``` + +An example config for the above: +```json +{ + "component": "MyView", + "config": { "showDescription": true } +} +``` + +Use the `ComponentRegistry` to register your component, +e.g. in its Module: +```javascript +export class MyModule { + constructor(components: ComponentRegistry) { + components.addAll([ + [ + "MyView", // this is the name to use in the config document + () => import("./my-view/my-view.component").then((c) => c.MyViewComponent), + ], + ]); + } +} +``` diff --git a/doc/compodoc_sources/how-to-guides/create-entity-details-panel.md b/doc/compodoc_sources/how-to-guides/create-entity-details-panel.md index c288856047..0868d4ce13 100644 --- a/doc/compodoc_sources/how-to-guides/create-entity-details-panel.md +++ b/doc/compodoc_sources/how-to-guides/create-entity-details-panel.md @@ -21,6 +21,7 @@ Those background details aside, what that means for your implementation is: (e.g. `@Input() showDescription: boolean;`, which you can use in your template or code to adapt the component.) These values are automatically set to whatever value is specified in the config object for your component at runtime in the database. 4. Register the new component in its parent module, so that it can be loaded under its name through the config. + (for details see [Create a custom View Component](./create-a-custom-view-component.html)) An example config for the above: ```json @@ -29,18 +30,3 @@ An example config for the above: "config": { "showDescription": true } } ``` - -Use the `ComponentRegistry` to register your component, -e.g. in its Module: -```javascript -export class MyModule { - constructor(components: ComponentRegistry) { - components.addAll([ - [ - "MySubView", // this is the name to use in the config document - () => import("./my-sub-view/my-sub-view.component").then((c) => c.MySubViewComponent), - ], - ]); - } -} -``` diff --git a/doc/compodoc_sources/summary.json b/doc/compodoc_sources/summary.json index 244192ddaa..52b0cf86d3 100644 --- a/doc/compodoc_sources/summary.json +++ b/doc/compodoc_sources/summary.json @@ -119,6 +119,10 @@ "title": "Create a New Entity Type", "file": "how-to-guides/create-new-entity-type.md" }, + { + "title": "Create a custom View Component", + "file": "how-to-guides/create-custom-view.md" + }, { "title": "Create an Entity Details Panel", "file": "how-to-guides/create-entity-details-panel.md" diff --git a/doc/images/routed-views.png b/doc/images/routed-views.png new file mode 100644 index 0000000000000000000000000000000000000000..37beeea660423abda71a722ffe44223c56bb7a81 GIT binary patch literal 79709 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 z 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 a-?yxzn7fvK{q+u~2@`{h=gRf2h5r{jWFf`? literal 0 HcmV?d00001 diff --git a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts index e080a43b13..ca6d940619 100644 --- a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts +++ b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts @@ -8,7 +8,6 @@ import { BooleanFilterConfig, EntityListConfig, } from "../../../core/entity-list/EntityListConfig"; -import { School } from "../../schools/model/school"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; import { DownloadService } from "../../../core/export/download-service/download.service"; @@ -101,9 +100,9 @@ describe("ChildrenListComponent", () => { const child1 = new Child("c1"); const child2 = new Child("c2"); mockChildrenService.getChildren.and.resolveTo([child1, child2]); - await component.ngOnInit(); + await component.ngOnChanges({}); expect(mockChildrenService.getChildren).toHaveBeenCalled(); - expect(component.childrenList).toEqual([child1, child2]); + expect(component.allEntities).toEqual([child1, child2]); }); }); diff --git a/src/app/child-dev-project/children/children-list/children-list.component.ts b/src/app/child-dev-project/children/children-list/children-list.component.ts index bd1c7ed4ff..e61c947d10 100644 --- a/src/app/child-dev-project/children/children-list/children-list.component.ts +++ b/src/app/child-dev-project/children/children-list/children-list.component.ts @@ -1,42 +1,107 @@ -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { Child } from "../model/child"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { ChildrenService } from "../children.service"; -import { EntityListConfig } from "../../../core/entity-list/EntityListConfig"; -import { DynamicComponentConfig } from "../../../core/config/dynamic-components/dynamic-component-config.interface"; import { EntityListComponent } from "../../../core/entity-list/entity-list/entity-list.component"; import { RouteTarget } from "../../../route-target"; +import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service"; +import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; +import { EntityRegistry } from "../../../core/entity/database-entity.decorator"; +import { MatDialog } from "@angular/material/dialog"; +import { DuplicateRecordService } from "../../../core/entity-list/duplicate-records/duplicate-records.service"; +import { EntityActionsService } from "../../../core/entity/entity-actions/entity-actions.service"; +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 { EntitiesTableComponent } from "../../../core/common-components/entities-table/entities-table.component"; +import { FormsModule } from "@angular/forms"; +import { FilterComponent } from "../../../core/filter/filter/filter.component"; +import { TabStateModule } from "../../../utils/tab-state/tab-state.module"; +import { ViewTitleComponent } from "../../../core/common-components/view-title/view-title.component"; +import { ExportDataDirective } from "../../../core/export/export-data-directive/export-data.directive"; +import { DisableEntityOperationDirective } from "../../../core/permissions/permission-directive/disable-entity-operation.directive"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { EntityCreateButtonComponent } from "../../../core/common-components/entity-create-button/entity-create-button.component"; +import { AbilityModule } from "@casl/angular"; +import { EntityActionsMenuComponent } from "../../../core/entity-details/entity-actions-menu/entity-actions-menu.component"; +import { ViewActionsComponent } from "../../../core/common-components/view-actions/view-actions.component"; @RouteTarget("ChildrenList") @Component({ selector: "app-children-list", - template: ` - - `, + templateUrl: + "../../../core/entity-list/entity-list/entity-list.component.html", + styleUrls: [ + "../../../core/entity-list/entity-list/entity-list.component.scss", + ], standalone: true, - imports: [EntityListComponent], + + imports: [ + NgIf, + NgStyle, + MatButtonModule, + Angulartics2OnModule, + FontAwesomeModule, + MatMenuModule, + NgTemplateOutlet, + MatTabsModule, + NgForOf, + MatFormFieldModule, + MatInputModule, + EntitiesTableComponent, + FormsModule, + FilterComponent, + TabStateModule, + ViewTitleComponent, + ExportDataDirective, + DisableEntityOperationDirective, + RouterLink, + MatTooltipModule, + EntityCreateButtonComponent, + AbilityModule, + AsyncPipe, + EntityActionsMenuComponent, + ViewActionsComponent, + ], }) -export class ChildrenListComponent implements OnInit { - childrenList: Child[]; - listConfig: EntityListConfig; - childConstructor = Child; +export class ChildrenListComponent extends EntityListComponent { + override entityConstructor = Child; constructor( + screenWidthObserver: ScreenWidthObserver, + router: Router, + activatedRoute: ActivatedRoute, + entityMapperService: EntityMapperService, + entities: EntityRegistry, + dialog: MatDialog, + duplicateRecord: DuplicateRecordService, + entityActionsService: EntityActionsService, private childrenService: ChildrenService, - private route: ActivatedRoute, - ) {} - - async ngOnInit() { - this.route.data.subscribe( - // TODO replace this use of route and rely on the RoutedViewComponent instead - // see that flattens the config option, assigning individual properties as inputs however, so we can't easily pass on - (data: DynamicComponentConfig) => - (this.listConfig = data.config), + ) { + super( + screenWidthObserver, + router, + activatedRoute, + entityMapperService, + entities, + dialog, + duplicateRecord, + entityActionsService, ); - this.childrenList = await this.childrenService.getChildren(); + } + + override async getEntities() { + return this.childrenService.getChildren(); } } diff --git a/src/app/child-dev-project/notes/note-details/note-details.component.html b/src/app/child-dev-project/notes/note-details/note-details.component.html index ce73122f35..9256917b1a 100644 --- a/src/app/child-dev-project/notes/note-details/note-details.component.html +++ b/src/app/child-dev-project/notes/note-details/note-details.component.html @@ -1,24 +1,12 @@ - - -

{{ tmpEntity.date | date }}: {{ tmpEntity.subject }}

- +@if (isLoading || !tmpEntity) { +
+ +
+} @else { + + {{ tmpEntity.date | date }}: {{ tmpEntity.subject }} + -
@@ -57,31 +45,31 @@

{{ tmpEntity.date | date }}: {{ tmpEntity.subject }}

style="margin-top: 10px" >
-
- - - - - + + + + + +} diff --git a/src/app/child-dev-project/notes/note-details/note-details.component.ts b/src/app/child-dev-project/notes/note-details/note-details.component.ts index 6dfa813e7b..878cf0218e 100644 --- a/src/app/child-dev-project/notes/note-details/note-details.component.ts +++ b/src/app/child-dev-project/notes/note-details/note-details.component.ts @@ -1,9 +1,8 @@ import { Component, - Inject, Input, - OnInit, - Optional, + OnChanges, + SimpleChanges, ViewEncapsulation, } from "@angular/core"; import { Note } from "../model/note"; @@ -22,17 +21,29 @@ import { } from "../../../core/common-components/entity-form/entity-form.service"; import { EntityFormComponent } from "../../../core/common-components/entity-form/entity-form/entity-form.component"; import { DynamicComponentDirective } from "../../../core/config/dynamic-components/dynamic-component.directive"; -import { MAT_DIALOG_DATA, MatDialogModule } from "@angular/material/dialog"; +import { MatDialogModule } from "@angular/material/dialog"; import { DialogButtonsComponent } from "../../../core/form-dialog/dialog-buttons/dialog-buttons.component"; import { DialogCloseComponent } from "../../../core/common-components/dialog-close/dialog-close.component"; import { EntityArchivedInfoComponent } from "../../../core/entity-details/entity-archived-info/entity-archived-info.component"; import { EntityFieldEditComponent } from "../../../core/common-components/entity-field-edit/entity-field-edit.component"; import { FieldGroup } from "../../../core/entity-details/form/field-group"; +import { DynamicComponent } from "../../../core/config/dynamic-components/dynamic-component.decorator"; +import { ViewTitleComponent } from "../../../core/common-components/view-title/view-title.component"; +import { AbstractEntityDetailsComponent } from "../../../core/entity-details/abstract-entity-details/abstract-entity-details.component"; +import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service"; +import { EntityRegistry } from "../../../core/entity/database-entity.decorator"; +import { EntityAbility } from "../../../core/permissions/ability/entity-ability"; +import { Router } from "@angular/router"; +import { LoggingService } from "../../../core/logging/logging.service"; +import { UnsavedChangesService } from "../../../core/entity-details/form/unsaved-changes.service"; +import { MatProgressBar } from "@angular/material/progress-bar"; +import { ViewActionsComponent } from "../../../core/common-components/view-actions/view-actions.component"; /** * Component responsible for displaying the Note creation/view window */ @UntilDestroy() +@DynamicComponent("NoteDetails") @Component({ selector: "app-note-details", templateUrl: "./note-details.component.html", @@ -50,19 +61,33 @@ import { FieldGroup } from "../../../core/entity-details/form/field-group"; DialogCloseComponent, EntityArchivedInfoComponent, EntityFieldEditComponent, + ViewTitleComponent, + MatProgressBar, + ViewActionsComponent, ], standalone: true, encapsulation: ViewEncapsulation.None, }) -export class NoteDetailsComponent implements OnInit { +export class NoteDetailsComponent + extends AbstractEntityDetailsComponent + implements OnChanges +{ @Input() entity: Note; + entityConstructor = Note; /** export format for notes to be used for downloading the individual details */ exportConfig: ExportColumnConfig[]; - topForm = ["date", "warningLevel", "category", "authors", "attachment"]; - middleForm = ["subject", "text"]; - bottomForm = ["children", "schools"]; + @Input() topForm = [ + "date", + "warningLevel", + "category", + "authors", + "attachment", + ]; + @Input() middleForm = ["subject", "text"]; + @Input() bottomForm = ["children", "schools"]; + topFieldGroups: FieldGroup[]; bottomFieldGroups: FieldGroup[]; @@ -70,26 +95,32 @@ export class NoteDetailsComponent implements OnInit { tmpEntity: Note; constructor( + entityMapperService: EntityMapperService, + entities: EntityRegistry, + ability: EntityAbility, + router: Router, + logger: LoggingService, + unsavedChanges: UnsavedChangesService, private configService: ConfigService, private entityFormService: EntityFormService, - @Optional() @Inject(MAT_DIALOG_DATA) data: { entity: Note }, ) { - if (data) { - this.entity = data.entity; - } + super( + entityMapperService, + entities, + ability, + router, + logger, + unsavedChanges, + ); + this.exportConfig = this.configService.getConfig<{ config: EntityListConfig; }>("view:note")?.config.exportConfig; - - const formConfig = this.configService.getConfig( - "appConfig:note-details", - ); - this.topForm = formConfig?.topForm ?? this.topForm; - this.middleForm = formConfig?.middleForm ?? this.middleForm; - this.bottomForm = formConfig?.bottomForm ?? this.bottomForm; } - ngOnInit() { + async ngOnChanges(changes: SimpleChanges) { + await super.ngOnChanges(changes); + this.topFieldGroups = this.topForm.map((f) => ({ fields: [f] })); this.bottomFieldGroups = [{ fields: this.bottomForm }]; diff --git a/src/app/child-dev-project/notes/notes-components.ts b/src/app/child-dev-project/notes/notes-components.ts index 5afc78891d..7d4af46b86 100644 --- a/src/app/child-dev-project/notes/notes-components.ts +++ b/src/app/child-dev-project/notes/notes-components.ts @@ -51,4 +51,11 @@ export const notesComponents: ComponentTuple[] = [ "./dashboard-widgets/important-notes-dashboard/important-notes-dashboard.component" ).then((c) => c.ImportantNotesDashboardComponent), ], + [ + "NoteDetails", + () => + import("./note-details/note-details.component").then( + (c) => c.NoteDetailsComponent, + ), + ], ]; diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html index cad8702d47..8ee604e28d 100644 --- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html +++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html @@ -1,10 +1,16 @@ + +
+ -

- -

+

+ +

+
+
+ +@if (!viewContext) { + +} diff --git a/src/app/core/common-components/view-title/view-title.component.scss b/src/app/core/common-components/view-title/view-title.component.scss index f03d963386..a0e96c01ad 100644 --- a/src/app/core/common-components/view-title/view-title.component.scss +++ b/src/app/core/common-components/view-title/view-title.component.scss @@ -1,8 +1,9 @@ -:host { - display: flex; - flex-direction: row; +.container { align-items: center; margin-bottom: 0 !important; +} - max-width: 100%; +.back-button { + position: relative; + left: -12px; } diff --git a/src/app/core/common-components/view-title/view-title.component.ts b/src/app/core/common-components/view-title/view-title.component.ts index 2e53ddd4bb..d49a3fd0a5 100644 --- a/src/app/core/common-components/view-title/view-title.component.ts +++ b/src/app/core/common-components/view-title/view-title.component.ts @@ -1,25 +1,40 @@ import { + AfterViewInit, Component, HostBinding, Input, - OnChanges, - SimpleChanges, + Optional, + TemplateRef, + ViewChild, } from "@angular/core"; import { getUrlWithoutParams } from "../../../utils/utils"; import { Router } from "@angular/router"; -import { Location, NgIf } from "@angular/common"; +import { Location, NgIf, NgTemplateOutlet } from "@angular/common"; import { MatButtonModule } from "@angular/material/button"; import { MatTooltipModule } from "@angular/material/tooltip"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { ViewComponentContext } from "../../ui/abstract-view/abstract-view.component"; +/** + * Building block for views, providing a consistent layout to a title section + * for both dialog and routed views. + */ @Component({ selector: "app-view-title", templateUrl: "./view-title.component.html", styleUrls: ["./view-title.component.scss"], - imports: [NgIf, MatButtonModule, MatTooltipModule, FontAwesomeModule], + imports: [ + NgIf, + MatButtonModule, + MatTooltipModule, + FontAwesomeModule, + NgTemplateOutlet, + ], standalone: true, }) -export class ViewTitleComponent implements OnChanges { +export class ViewTitleComponent implements AfterViewInit { + @ViewChild("template") template: TemplateRef; + /** The page title to be displayed */ @Input() title: string; @@ -36,8 +51,19 @@ export class ViewTitleComponent implements OnChanges { constructor( private router: Router, private location: Location, + @Optional() protected viewContext: ViewComponentContext, ) { this.parentUrl = this.findParentUrl(); + + if (this.viewContext?.isDialog) { + this.disableBackButton = true; + } + } + + ngAfterViewInit(): void { + if (this.viewContext) { + setTimeout(() => (this.viewContext.title = this)); + } } private findParentUrl(): string { @@ -59,23 +85,5 @@ export class ViewTitleComponent implements OnChanges { } } - ngOnChanges(changes: SimpleChanges) { - if (changes.hasOwnProperty("disableBackButton")) { - this.extraStyles = this.buildExtraStyles(); - } - } - - private buildExtraStyles() { - /* Moves the whole title component 12 pixels to the left so that - * the "go back" button is aligned with the left border. This class - * is applied conditionally when the "back" button is shown - */ - return { - position: "relative", - left: this.disableBackButton ? "unset" : "-12px", - }; - } - @HostBinding("class") extraClasses = "mat-title"; - @HostBinding("style") extraStyles = this.buildExtraStyles(); } diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index e3a1b19b84..99f72c9c75 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -161,7 +161,6 @@ export const defaultJsonConfig = { "view:note": { "component": "NotesManager", "config": { - "entityType": "Note", "title": $localize`:Title for notes overview:Notes & Reports`, "includeEventNotes": false, "showEventNotesToggle": true, @@ -237,6 +236,12 @@ export const defaultJsonConfig = { ] } }, + "view:note/:id": { + "component": "NoteDetails", + "config": { + "topForm": ["date", "warningLevel", "category", "authors", "attachment"] + } + }, "view:import": { "component": "Import", }, @@ -714,6 +719,11 @@ export const defaultJsonConfig = { "title", "type", "assignedTo" + ], + "exportConfig": [ + { "label": "Title", "query": "title" }, + { "label": "Type", "query": "type" }, + { "label": "Assigned users", "query": "assignedTo" } ] } }, diff --git a/src/app/core/config/dynamic-components/dynamic-component.pipe.ts b/src/app/core/config/dynamic-components/dynamic-component.pipe.ts new file mode 100644 index 0000000000..ab582e35e5 --- /dev/null +++ b/src/app/core/config/dynamic-components/dynamic-component.pipe.ts @@ -0,0 +1,27 @@ +import { Pipe, PipeTransform, Type } from "@angular/core"; +import { ComponentRegistry } from "../../../dynamic-components"; + +/** + * Transform a string "component name" and load the referenced component. + * + * This is async and needs an additional async pipe. Use with *ngComponentOutlet +``` + +``` + */ +@Pipe({ + name: "dynamicComponent", + standalone: true, +}) +export class DynamicComponentPipe implements PipeTransform { + constructor(private componentRegistry: ComponentRegistry) {} + + async transform(value: string): Promise> { + return await this.componentRegistry.get(value)(); + } +} diff --git a/src/app/core/entity-details/abstract-entity-details/abstract-entity-details.component.spec.ts b/src/app/core/entity-details/abstract-entity-details/abstract-entity-details.component.spec.ts new file mode 100644 index 0000000000..ed402ad710 --- /dev/null +++ b/src/app/core/entity-details/abstract-entity-details/abstract-entity-details.component.spec.ts @@ -0,0 +1,129 @@ +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from "@angular/core/testing"; +import { AbstractEntityDetailsComponent } from "./abstract-entity-details.component"; +import { Router } from "@angular/router"; +import { EntityDetailsConfig } from "../EntityDetailsConfig"; +import { Child } from "../../../child-dev-project/children/model/child"; +import { MockedTestingModule } from "../../../utils/mocked-testing.module"; +import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service"; +import { EntityAbility } from "../../permissions/ability/entity-ability"; +import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; +import { Component, SimpleChange } from "@angular/core"; +import { mockEntityMapper } from "../../entity/entity-mapper/mock-entity-mapper-service"; + +@Component({ + template: ``, + standalone: true, +}) +class TestEntityDetailsComponent extends AbstractEntityDetailsComponent {} + +describe("AbstractEntityDetailsComponent", () => { + let component: TestEntityDetailsComponent; + let fixture: ComponentFixture; + + const routeConfig: EntityDetailsConfig = { + entityType: "Child", + panels: [], + }; + + let mockEntityRemoveService: jasmine.SpyObj; + let mockAbility: jasmine.SpyObj; + + beforeEach(waitForAsync(() => { + mockEntityRemoveService = jasmine.createSpyObj(["remove"]); + mockAbility = jasmine.createSpyObj(["cannot", "update", "on"]); + mockAbility.cannot.and.returnValue(false); + mockAbility.on.and.returnValue(() => true); + + TestBed.configureTestingModule({ + imports: [TestEntityDetailsComponent, MockedTestingModule.withState()], + providers: [ + { provide: EntityMapperService, useValue: mockEntityMapper() }, + { provide: EntityActionsService, useValue: mockEntityRemoveService }, + { provide: EntityAbility, useValue: mockAbility }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TestEntityDetailsComponent); + component = fixture.componentInstance; + + Object.assign(component, routeConfig); + component.ngOnChanges( + simpleChangesFor(component, ...Object.keys(routeConfig)), + ); + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should load the correct entity on init", fakeAsync(() => { + component.isLoading = true; + const testChild = new Child("Test-Child"); + const entityMapper = TestBed.inject(EntityMapperService); + entityMapper.save(testChild); + tick(); + spyOn(entityMapper, "load").and.callThrough(); + + component.id = testChild.getId(true); + component.ngOnChanges(simpleChangesFor(component, "id")); + expect(component.isLoading).toBeTrue(); + tick(); + + expect(entityMapper.load).toHaveBeenCalledWith( + Child, + testChild.getId(true), + ); + expect(component.entity).toBe(testChild); + expect(component.isLoading).toBeFalse(); + })); + + it("should also support the long ID format", fakeAsync(() => { + const child = new Child(); + const entityMapper = TestBed.inject(EntityMapperService); + entityMapper.save(child); + tick(); + spyOn(entityMapper, "load").and.callThrough(); + + component.id = child.getId(); + component.ngOnChanges(simpleChangesFor(component, "id")); + tick(); + + expect(entityMapper.load).toHaveBeenCalledWith(Child, child.getId()); + expect(component.entity).toEqual(child); + + // entity is updated + const childUpdate = child.copy(); + childUpdate.name = "update"; + entityMapper.save(childUpdate); + tick(); + + expect(component.entity).toEqual(childUpdate); + })); + + it("should call router when user is not permitted to create entities", () => { + mockAbility.cannot.and.returnValue(true); + const router = fixture.debugElement.injector.get(Router); + spyOn(router, "navigate"); + component.id = "new"; + component.ngOnChanges(simpleChangesFor(component, "id")); + expect(router.navigate).toHaveBeenCalled(); + }); +}); + +function simpleChangesFor(component, ...properties: string[]) { + const changes = {}; + for (const p of properties) { + changes[p] = new SimpleChange(null, component[p], true); + } + return changes; +} diff --git a/src/app/core/entity-details/abstract-entity-details/abstract-entity-details.component.ts b/src/app/core/entity-details/abstract-entity-details/abstract-entity-details.component.ts new file mode 100644 index 0000000000..97edf9f480 --- /dev/null +++ b/src/app/core/entity-details/abstract-entity-details/abstract-entity-details.component.ts @@ -0,0 +1,79 @@ +import { Directive, Input, OnChanges, SimpleChanges } from "@angular/core"; +import { Router } from "@angular/router"; +import { Entity, EntityConstructor } from "../../entity/model/entity"; +import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; +import { EntityAbility } from "../../permissions/ability/entity-ability"; +import { EntityRegistry } from "../../entity/database-entity.decorator"; +import { filter } from "rxjs/operators"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { Subscription } from "rxjs"; +import { LoggingService } from "../../logging/logging.service"; +import { UnsavedChangesService } from "../form/unsaved-changes.service"; + +/** + * This component can be used to display an entity in more detail. + * As an abstract base component, this provides functionality to load an entity + * and leaves the UI and special functionality to components that extend this class, like EntityDetailsComponent. + */ +@UntilDestroy() +@Directive() +export abstract class AbstractEntityDetailsComponent implements OnChanges { + isLoading: boolean; + private changesSubscription: Subscription; + + @Input() entityType: string; + entityConstructor: EntityConstructor; + + @Input() id: string; + @Input() entity: Entity; + + constructor( + private entityMapperService: EntityMapperService, + private entities: EntityRegistry, + private ability: EntityAbility, + private router: Router, + protected logger: LoggingService, + protected unsavedChanges: UnsavedChangesService, + ) {} + + async ngOnChanges(changes: SimpleChanges) { + if (changes.entityType) { + this.entityConstructor = this.entities.get(this.entityType); + } + + if (changes.id) { + await this.loadEntity(); + this.subscribeToEntityChanges(); + } + } + + protected subscribeToEntityChanges() { + const fullId = Entity.createPrefixedId(this.entityType, this.id); + this.changesSubscription?.unsubscribe(); + this.changesSubscription = this.entityMapperService + .receiveUpdates(this.entityConstructor) + .pipe( + filter(({ entity }) => entity.getId() === fullId), + filter(({ type }) => type !== "remove"), + untilDestroyed(this), + ) + .subscribe(({ entity }) => (this.entity = entity)); + } + + protected async loadEntity() { + this.isLoading = true; + if (this.id === "new") { + if (this.ability.cannot("create", this.entityConstructor)) { + this.router.navigate([""]); + return; + } + this.entity = new this.entityConstructor(); + } else { + this.entity = await this.entityMapperService.load( + this.entityConstructor, + this.id, + ); + } + this.isLoading = false; + } +} diff --git a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.html b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.html index dc0ba188be..9526faa07b 100644 --- a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.html +++ b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.html @@ -1,39 +1,66 @@ - +@if (!entity?.isNew) { + + @for (a of actions; track a.action) { + @if (showExpanded && viewContext?.isDialog && a.primaryAction) { + + } + } - - - + + - - - + + + + @for (a of actions; track a.action) { + @if (!a.primaryAction || !showExpanded || !viewContext?.isDialog) { + + } + } - - - + + + +} diff --git a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.scss b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.scss index e69de29bb2..4b374974e7 100644 --- a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.scss +++ b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.scss @@ -0,0 +1,4 @@ +:host { + display: flex; + align-items: center; +} diff --git a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.ts b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.ts index 625821d4af..923fc6db27 100644 --- a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.ts +++ b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.ts @@ -3,6 +3,7 @@ import { EventEmitter, Input, OnChanges, + Optional, Output, SimpleChanges, } from "@angular/core"; @@ -17,6 +18,7 @@ import { DisableEntityOperationDirective } from "../../permissions/permission-di import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { EntityAction } from "../../permissions/permission-types"; import { MatTooltipModule } from "@angular/material/tooltip"; +import { ViewComponentContext } from "../../ui/abstract-view/abstract-view.component"; export type EntityMenuAction = "archive" | "anonymize" | "delete"; type EntityMenuActionItem = { @@ -26,6 +28,9 @@ type EntityMenuActionItem = { icon: IconProp; label: string; tooltip?: string; + + /** important action to be displayed directly, outside context menu in some views */ + primaryAction?: boolean; }; @Component({ @@ -68,6 +73,7 @@ export class EntityActionsMenuComponent implements OnChanges { icon: "box-archive", label: $localize`:entity context menu:Archive`, tooltip: $localize`:entity context menu tooltip:Mark the record as inactive, hiding it from lists by default while keeping the data.`, + primaryAction: true, }, { action: "anonymize", @@ -87,7 +93,15 @@ export class EntityActionsMenuComponent implements OnChanges { }, ]; - constructor(private entityRemoveService: EntityActionsService) {} + /** + * Whether some buttons should be displayed directly, outside the three-dot menu in dialog views. + */ + @Input() showExpanded?: boolean; + + constructor( + private entityRemoveService: EntityActionsService, + @Optional() protected viewContext: ViewComponentContext, + ) {} ngOnChanges(changes: SimpleChanges): void { if (changes.entity) { @@ -111,9 +125,13 @@ export class EntityActionsMenuComponent implements OnChanges { } async executeAction(action: EntityMenuActionItem) { - const result = await action.execute(this.entity, this.navigateOnDelete); + const result = await action.execute( + this.entity, + this.navigateOnDelete && !this.viewContext?.isDialog, + ); if (result) { this.actionTriggered.emit(action.action); } + setTimeout(() => this.filterAvailableActions()); } } diff --git a/src/app/core/entity-details/entity-details/entity-details.component.html b/src/app/core/entity-details/entity-details/entity-details.component.html index bcbc5eded0..fb46b34003 100644 --- a/src/app/core/entity-details/entity-details/entity-details.component.html +++ b/src/app/core/entity-details/entity-details/entity-details.component.html @@ -1,37 +1,23 @@ - + + + @if (!entity?.isNew) { + {{ entity?.toString() }} + } @else { + + Adding new {{ this.entityConstructor?.label }} + + } + -
- - {{ record?.toString() }} - - + - Adding new {{ this.entityConstructor?.label }} - - - -
+
- + - + + { it("sets the panels config with child and creating status", fakeAsync(() => { const testChild = new Child("Test-Child"); + testChild["_rev"] = "1"; // mark as "not new" TestBed.inject(EntityMapperService).save(testChild); tick(); - component.creatingNew = false; component.id = testChild.getId(true); component.ngOnChanges(simpleChangesFor(component, "id")); tick(); @@ -98,59 +97,6 @@ describe("EntityDetailsComponent", () => { }), ); })); - - it("should load the correct child on startup", fakeAsync(() => { - component.isLoading = true; - const testChild = new Child("Test-Child"); - const entityMapper = TestBed.inject(EntityMapperService); - entityMapper.save(testChild); - tick(); - spyOn(entityMapper, "load").and.callThrough(); - - component.id = testChild.getId(true); - component.ngOnChanges(simpleChangesFor(component, "id")); - expect(component.isLoading).toBeTrue(); - tick(); - - expect(entityMapper.load).toHaveBeenCalledWith( - Child, - testChild.getId(true), - ); - expect(component.record).toBe(testChild); - expect(component.isLoading).toBeFalse(); - })); - - it("should also support the long ID format", fakeAsync(() => { - const child = new Child(); - const entityMapper = TestBed.inject(EntityMapperService); - entityMapper.save(child); - tick(); - spyOn(entityMapper, "load").and.callThrough(); - - component.id = child.getId(); - component.ngOnChanges(simpleChangesFor(component, "id")); - tick(); - - expect(entityMapper.load).toHaveBeenCalledWith(Child, child.getId()); - expect(component.record).toEqual(child); - - // entity is updated - const childUpdate = child.copy(); - childUpdate.name = "update"; - entityMapper.save(childUpdate); - tick(); - - expect(component.record).toEqual(childUpdate); - })); - - it("should call router when user is not permitted to create entities", () => { - mockAbility.cannot.and.returnValue(true); - const router = fixture.debugElement.injector.get(Router); - spyOn(router, "navigate"); - component.id = "new"; - component.ngOnChanges(simpleChangesFor(component, "id")); - expect(router.navigate).toHaveBeenCalled(); - }); }); function simpleChangesFor(component, ...properties: string[]) { diff --git a/src/app/core/entity-details/entity-details/entity-details.component.ts b/src/app/core/entity-details/entity-details/entity-details.component.ts index 4e0e6962e0..00124a88ea 100644 --- a/src/app/core/entity-details/entity-details/entity-details.component.ts +++ b/src/app/core/entity-details/entity-details/entity-details.component.ts @@ -1,11 +1,6 @@ import { Component, Input, OnChanges, SimpleChanges } from "@angular/core"; -import { Router, RouterLink } from "@angular/router"; +import { RouterLink } from "@angular/router"; import { Panel, PanelComponent, PanelConfig } from "../EntityDetailsConfig"; -import { Entity, EntityConstructor } from "../../entity/model/entity"; -import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service"; -import { AnalyticsService } from "../../analytics/analytics.service"; -import { EntityAbility } from "../../permissions/ability/entity-ability"; -import { EntityRegistry } from "../../entity/database-entity.decorator"; import { MatButtonModule } from "@angular/material/button"; import { MatMenuModule } from "@angular/material/menu"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; @@ -18,15 +13,13 @@ import { CommonModule, NgForOf, NgIf } from "@angular/common"; import { ViewTitleComponent } from "../../common-components/view-title/view-title.component"; import { DynamicComponentDirective } from "../../config/dynamic-components/dynamic-component.directive"; import { DisableEntityOperationDirective } from "../../permissions/permission-directive/disable-entity-operation.directive"; -import { LoggingService } from "../../logging/logging.service"; -import { UnsavedChangesService } from "../form/unsaved-changes.service"; import { EntityActionsMenuComponent } from "../entity-actions-menu/entity-actions-menu.component"; import { EntityArchivedInfoComponent } from "../entity-archived-info/entity-archived-info.component"; -import { filter } from "rxjs/operators"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { Subscription } from "rxjs"; +import { UntilDestroy } from "@ngneat/until-destroy"; import { AbilityModule } from "@casl/angular"; import { RouteTarget } from "../../../route-target"; +import { AbstractEntityDetailsComponent } from "../abstract-entity-details/abstract-entity-details.component"; +import { ViewActionsComponent } from "../../common-components/view-actions/view-actions.component"; /** * This component can be used to display an entity in more detail. @@ -60,80 +53,26 @@ import { RouteTarget } from "../../../route-target"; RouterLink, AbilityModule, CommonModule, + ViewActionsComponent, ], }) -export class EntityDetailsComponent implements OnChanges { - creatingNew = false; - isLoading = true; - private changesSubscription: Subscription; - - @Input() entityType: string; - entityConstructor: EntityConstructor; - - // TODO: instead use an @Input entity: Entity and let RoutedView handle the entity loading - @Input() id: string; - record: Entity; - +export class EntityDetailsComponent + extends AbstractEntityDetailsComponent + implements OnChanges +{ /** * The configuration for the panels on this details page. */ @Input() panels: Panel[] = []; - constructor( - private entityMapperService: EntityMapperService, - private router: Router, - private analyticsService: AnalyticsService, - private ability: EntityAbility, - private entities: EntityRegistry, - private logger: LoggingService, - public unsavedChanges: UnsavedChangesService, - ) {} + async ngOnChanges(changes: SimpleChanges) { + await super.ngOnChanges(changes); - ngOnChanges(changes: SimpleChanges): void { - if (changes.entity || changes.entityType) { - this.entityConstructor = this.entities.get(this.entityType); - } - if (changes.id) { - this.loadEntity(); - this.subscribeToEntityChanges(); - // `initPanels()` is already called inside `loadEntity()` - } else if (changes.panels) { + if (changes.id || changes.entity || changes.panels) { this.initPanels(); } } - private subscribeToEntityChanges() { - const fullId = Entity.createPrefixedId(this.entityType, this.id); - this.changesSubscription?.unsubscribe(); - this.changesSubscription = this.entityMapperService - .receiveUpdates(this.entityConstructor) - .pipe( - filter(({ entity }) => entity.getId() === fullId), - filter(({ type }) => type !== "remove"), - untilDestroyed(this), - ) - .subscribe(({ entity }) => (this.record = entity)); - } - - private async loadEntity() { - if (this.id === "new") { - if (this.ability.cannot("create", this.entityConstructor)) { - this.router.navigate([""]); - return; - } - this.record = new this.entityConstructor(); - this.creatingNew = true; - } else { - this.creatingNew = false; - this.record = await this.entityMapperService.load( - this.entityConstructor, - this.id, - ); - } - this.initPanels(); - this.isLoading = false; - } - private initPanels() { this.panels = this.panels.map((p) => ({ title: p.title, @@ -147,30 +86,14 @@ export class EntityDetailsComponent implements OnChanges { private getPanelConfig(c: PanelComponent): PanelConfig { let panelConfig: PanelConfig = { - entity: this.record, - creatingNew: this.creatingNew, + entity: this.entity, + creatingNew: this.entity.isNew, }; if (typeof c.config === "object" && !Array.isArray(c.config)) { - if (c.config?.entity) { - this.logger.warn( - `DEPRECATION panel config uses 'entity' keyword: ${JSON.stringify( - c, - )}`, - ); - c.config["entityType"] = c.config.entity; - delete c.config.entity; - } panelConfig = { ...c.config, ...panelConfig }; } else { panelConfig.config = c.config; } return panelConfig; } - - trackTabChanged(index: number) { - this.analyticsService.eventTrack("details_tab_changed", { - category: this.entityType, - label: this.panels[index].title, - }); - } } diff --git a/src/app/core/entity-details/form/form.component.ts b/src/app/core/entity-details/form/form.component.ts index 76aaefcbb3..5aa979621a 100644 --- a/src/app/core/entity-details/form/form.component.ts +++ b/src/app/core/entity-details/form/form.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component, Input, OnInit, Optional } from "@angular/core"; import { Entity } from "../../entity/model/entity"; import { getParentUrl } from "../../../utils/utils"; import { Router } from "@angular/router"; @@ -14,6 +14,7 @@ import { MatButtonModule } from "@angular/material/button"; import { EntityFormComponent } from "../../common-components/entity-form/entity-form/entity-form.component"; import { DisableEntityOperationDirective } from "../../permissions/permission-directive/disable-entity-operation.directive"; import { FieldGroup } from "./field-group"; +import { ViewComponentContext } from "../../ui/abstract-view/abstract-view.component"; /** * A simple wrapper function of the EntityFormComponent which can be used as a dynamic component @@ -45,6 +46,7 @@ export class FormComponent implements FormConfig, OnInit { private location: Location, private entityFormService: EntityFormService, private alertService: AlertService, + @Optional() private viewContext: ViewComponentContext, ) {} ngOnInit() { @@ -63,7 +65,7 @@ export class FormComponent implements FormConfig, OnInit { await this.entityFormService.saveChanges(this.form, this.entity); this.form.markAsPristine(); this.form.disable(); - if (this.creatingNew) { + if (this.creatingNew && !this.viewContext?.isDialog) { await this.router.navigate([ getParentUrl(this.router), this.entity.getId(true), diff --git a/src/app/core/entity-list/entity-list/entity-list.component.html b/src/app/core/entity-list/entity-list/entity-list.component.html index b5895146ea..8f40cd2cbd 100644 --- a/src/app/core/entity-list/entity-list/entity-list.component.html +++ b/src/app/core/entity-list/entity-list/entity-list.component.html @@ -1,12 +1,11 @@
+ + {{ title }} + -
- - {{ title }} - - +
@@ -19,7 +18,7 @@
-
+ diff --git a/src/app/core/entity-list/entity-list/entity-list.component.spec.ts b/src/app/core/entity-list/entity-list/entity-list.component.spec.ts index 76117a9346..25dbe6cd7a 100644 --- a/src/app/core/entity-list/entity-list/entity-list.component.spec.ts +++ b/src/app/core/entity-list/entity-list/entity-list.component.spec.ts @@ -154,18 +154,15 @@ describe("EntityListComponent", () => { } component.entityConstructor = Test; - component.listConfig = { - title: "", - columns: [ - { - id: "anotherColumn", - label: "Predefined Title", - viewComponent: "DisplayDate", - }, - ], - columnGroups: { - groups: [{ name: "Both", columns: ["testProperty", "anotherColumn"] }], + component.columns = [ + { + id: "anotherColumn", + label: "Predefined Title", + viewComponent: "DisplayDate", }, + ]; + component.columnGroups = { + groups: [{ name: "Both", columns: ["testProperty", "anotherColumn"] }], }; component.ngOnChanges({ listConfig: null }); @@ -177,30 +174,6 @@ describe("EntityListComponent", () => { ]); })); - it("should automatically initialize values if directly referenced from config", fakeAsync(() => { - mockActivatedRoute.component = EntityListComponent; - const entityMapper = TestBed.inject(EntityMapperService); - const children = [new Child(), new Child()]; - spyOn(entityMapper, "loadType").and.resolveTo(children); - - createComponent(); - component.listConfig = { - entityType: "Child", - title: "Some title", - columns: ["name", "gender"], - }; - component.ngOnChanges({ listConfig: undefined }); - tick(); - - expect(component.entityConstructor).toBe(Child); - expect(component.allEntities).toEqual(children); - expect(component.title).toBe("Some title"); - - const navigateSpy = spyOn(TestBed.inject(Router), "navigate"); - component.addNew(); - expect(navigateSpy.calls.mostRecent().args[0]).toEqual(["new"]); - })); - it("should not navigate on addNew if clickMode is not 'navigate'", () => { createComponent(); const navigateSpy = spyOn(TestBed.inject(Router), "navigate"); @@ -257,10 +230,9 @@ describe("EntityListComponent", () => { } async function initComponentInputs() { - component.listConfig = testConfig; + Object.assign(component, testConfig); await component.ngOnChanges({ allEntities: undefined, - listConfig: undefined, }); fixture.detectChanges(); } diff --git a/src/app/core/entity-list/entity-list/entity-list.component.ts b/src/app/core/entity-list/entity-list/entity-list.component.ts index 518a638996..8d9231f7f7 100644 --- a/src/app/core/entity-list/entity-list/entity-list.component.ts +++ b/src/app/core/entity-list/entity-list/entity-list.component.ts @@ -15,7 +15,6 @@ import { } from "../EntityListConfig"; import { Entity, EntityConstructor } from "../../entity/model/entity"; import { FormFieldConfig } from "../../common-components/entity-form/FormConfig"; -import { AnalyticsService } from "../../analytics/analytics.service"; 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"; @@ -55,6 +54,7 @@ import { DataFilter } from "../../filter/filters/filters"; import { EntityCreateButtonComponent } from "../../common-components/entity-create-button/entity-create-button.component"; import { AbilityModule } from "@casl/angular"; import { EntityActionsMenuComponent } from "../../entity-details/entity-actions-menu/entity-actions-menu.component"; +import { ViewActionsComponent } from "../../common-components/view-actions/view-actions.component"; /** * This component allows to create a full-blown table with pagination, filtering, searching and grouping. @@ -96,6 +96,8 @@ import { EntityActionsMenuComponent } from "../../entity-details/entity-actions- AbilityModule, AsyncPipe, EntityActionsMenuComponent, + ViewActionsComponent, + // WARNING: all imports here also need to be set for components extending EntityList, like ChildrenListComponent ], standalone: true, }) @@ -105,9 +107,6 @@ export class EntityListComponent { @Input() allEntities: T[]; - /** @deprecated this is often used when this has a wrapper component (e.g. ChildrenList), preferably use individual @Input properties */ - @Input() listConfig: EntityListConfig; - @Input() entityType: string; @Input() entityConstructor: EntityConstructor; @Input() defaultSort: Sort; @@ -167,8 +166,7 @@ export class EntityListComponent private screenWidthObserver: ScreenWidthObserver, private router: Router, private activatedRoute: ActivatedRoute, - private analyticsService: AnalyticsService, - private entityMapperService: EntityMapperService, + protected entityMapperService: EntityMapperService, private entities: EntityRegistry, private dialog: MatDialog, private duplicateRecord: DuplicateRecordService, @@ -192,9 +190,6 @@ export class EntityListComponent } ngOnChanges(changes: SimpleChanges) { - if (changes.hasOwnProperty("listConfig")) { - Object.assign(this, this.listConfig); - } return this.buildComponentFromConfig(); } @@ -221,13 +216,19 @@ export class EntityListComponent ); } - private async loadEntities() { - this.allEntities = await this.entityMapperService.loadType( - this.entityConstructor, - ); + protected async loadEntities() { + this.allEntities = await this.getEntities(); this.listenToEntityUpdates(); } + /** + * Template method that can be overwritten to change the loading logic. + * @protected + */ + protected async getEntities(): Promise { + return this.entityMapperService.loadType(this.entityConstructor); + } + private updateSubscription: Subscription; private listenToEntityUpdates() { @@ -262,10 +263,6 @@ export class EntityListComponent 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(); - - this.analyticsService.eventTrack("list_filter_freetext", { - category: this.entityConstructor?.ENTITY_TYPE, - }); } private displayColumnGroupByName(columnGroupName: string) { diff --git a/src/app/core/entity/entity-config.service.ts b/src/app/core/entity/entity-config.service.ts index a17e7cbc71..a158aaa348 100644 --- a/src/app/core/entity/entity-config.service.ts +++ b/src/app/core/entity/entity-config.service.ts @@ -5,9 +5,14 @@ import { EntityRegistry } from "./database-entity.decorator"; import { IconName } from "@fortawesome/fontawesome-svg-core"; import { EntityConfig } from "./entity-config"; import { addPropertySchema } from "./database-field.decorator"; -import { PREFIX_VIEW_CONFIG } from "../config/dynamic-routing/view-config.interface"; +import { + PREFIX_VIEW_CONFIG, + ViewConfig, +} from "../config/dynamic-routing/view-config.interface"; import { EntitySchemaField } from "./schema/entity-schema-field"; import { EntitySchema } from "./schema/entity-schema"; +import { EntityDetailsConfig } from "../entity-details/EntityDetailsConfig"; +import { EntityListConfig } from "../entity-list/EntityListConfig"; /** * A service that allows to work with configuration-objects @@ -149,4 +154,19 @@ export class EntityConfigService { EntityConfigService.PREFIX_ENTITY_CONFIG + entityType.ENTITY_TYPE; return this.configService.getConfig(configName); } + + getDetailsViewConfig( + entityType: EntityConstructor, + ): ViewConfig { + return this.configService.getConfig>( + EntityConfigService.getDetailsViewId(entityType), + ); + } + getListViewConfig( + entityType: EntityConstructor, + ): ViewConfig { + return this.configService.getConfig>( + EntityConfigService.getListViewId(entityType), + ); + } } diff --git a/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.html b/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.html index 19d8cabcae..61f3fa4c88 100644 --- a/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.html +++ b/src/app/core/form-dialog/dialog-buttons/dialog-buttons.component.html @@ -12,8 +12,9 @@