Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dialog & Routed Views #2304

Merged
merged 25 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
098acc0
refactor(config): rename entity to entityType in config
sleidig Mar 15, 2024
db64c33
refactor: simplify view-title layout logic
sleidig Mar 18, 2024
a2c2705
basic generic dialog-view
sleidig Mar 18, 2024
57ee5d6
Merge branch 'master' into view-architecture
sleidig Mar 26, 2024
2c4fd0b
basic generic dialog-view
sleidig Mar 18, 2024
2dd8508
adapt NoteDetails to work in Modal again
sleidig Mar 21, 2024
c70daee
replace special appConfig:note-details with standard view config
sleidig Mar 21, 2024
6fc7c27
rework entity-details components to allow opening in routed-view for …
sleidig Mar 26, 2024
f858e11
adapt special list components to properly map inputs
sleidig Mar 27, 2024
b559b7d
add ViewActionsComponent and generalize button layout for dialog and …
sleidig Mar 27, 2024
aa30d70
add documentation
sleidig Mar 27, 2024
af6b7c2
code cleanup
sleidig Mar 27, 2024
1652677
Merge branch 'master' into view-architecture
sleidig Apr 3, 2024
addd1ef
Merge branch 'master' into view-architecture
sleidig Apr 3, 2024
68b2a00
Merge branch 'master' into view-architecture
sleidig Apr 5, 2024
62d8140
Merge branch 'master' into view-architecture
tomwwinter Apr 8, 2024
78473f7
refactor(config): rename entity to entityType in config
sleidig Mar 15, 2024
e36d7b7
fix(support): avoid error when trying to submit report without being …
sleidig Apr 11, 2024
ceac18a
Merge branch 'refactor/config-entitytype' into view-architecture
sleidig Apr 12, 2024
a326549
refactor(config): rename entity to entityType in config
sleidig Mar 15, 2024
5e7a7fa
Merge remote-tracking branch 'origin/refactor/config-entitytype' into…
sleidig Apr 12, 2024
a2648f7
Merge remote-tracking branch 'origin/master' into view-architecture
sleidig Apr 12, 2024
523d08f
fix height if multi-tab component displayed in popup
sleidig Apr 12, 2024
b86577b
display some buttons in action dialog bar
sleidig Apr 12, 2024
b1251f4
do not interact with route if in dialog mode
sleidig Apr 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions doc/compodoc_sources/how-to-guides/create-custom-view.md
Original file line number Diff line number Diff line change
@@ -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 `<app-view-title>` and `<app-view-actions>` 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
<app-view-title>
<!-- the title is specially fixed and receives a back button or dialog close -->
My Entity {{ entity.name }}
</app-view-title>

<!-- anything in the template not specially marked/wrapped is used as main content -->
<div>
My Custom View Content
</div>

<app-view-actions>
<!-- some action buttons, e.g. using the app-dialog-buttons or anything else -->
<app-dialog-buttons [form]="form" [entity]="entity"></app-dialog-buttons>
</app-view-actions>
```

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),
],
]);
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
],
]);
}
}
```
4 changes: 4 additions & 0 deletions doc/compodoc_sources/summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Binary file added doc/images/routed-views.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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]);
});
});
Original file line number Diff line number Diff line change
@@ -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: `
<app-entity-list
[allEntities]="childrenList"
[listConfig]="listConfig"
[entityConstructor]="childConstructor"
></app-entity-list>
`,
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<Child> {
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<EntityListConfig>) =>
(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();
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
<!--
~ This file is part of ndb-core.
~
~ ndb-core is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ ndb-core is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
-->

<h1 mat-dialog-title>{{ tmpEntity.date | date }}: {{ tmpEntity.subject }}</h1>
<app-dialog-close mat-dialog-close=""></app-dialog-close>
@if (isLoading || !tmpEntity) {
<div>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
} @else {
<app-view-title>
{{ tmpEntity.date | date }}: {{ tmpEntity.subject }}
</app-view-title>

<mat-dialog-content>
<div class="flex-column gap-regular">
<app-entity-archived-info [entity]="entity"></app-entity-archived-info>

Expand Down Expand Up @@ -57,31 +45,31 @@ <h1 mat-dialog-title>{{ tmpEntity.date | date }}: {{ tmpEntity.subject }}</h1>
style="margin-top: 10px"
></app-entity-form>
</div>
</mat-dialog-content>

<mat-dialog-actions>
<app-dialog-buttons [form]="form" [entity]="entity">
<button
mat-menu-item
[appExportData]="[entity]"
format="csv"
[exportConfig]="exportConfig"
[filename]="
'event_' +
entity.toString()?.replace(' ', '-') +
'_' +
(entity.date | date: 'YYYY-MM-dd')
"
>
<fa-icon
class="color-accent standard-icon-with-text"
aria-label="download csv"
icon="download"
angulartics2On="click"
angularticsCategory="Note"
angularticsAction="single_note_csv_export"
></fa-icon>
<span i18n="Download note details as CSV"> Download details </span>
</button>
</app-dialog-buttons>
</mat-dialog-actions>
<app-view-actions>
<app-dialog-buttons [form]="form" [entity]="entity">
<button
mat-menu-item
[appExportData]="[entity]"
format="csv"
[exportConfig]="exportConfig"
[filename]="
'event_' +
entity.toString()?.replace(' ', '-') +
'_' +
(entity.date | date: 'YYYY-MM-dd')
"
>
<fa-icon
class="color-accent standard-icon-with-text"
aria-label="download csv"
icon="download"
angulartics2On="click"
angularticsCategory="Note"
angularticsAction="single_note_csv_export"
></fa-icon>
<span i18n="Download note details as CSV"> Download details </span>
</button>
</app-dialog-buttons>
</app-view-actions>
}
Loading
Loading