Skip to content

Commit

Permalink
feat(admin): Basic Admin UI to add and edit fields of entity and deta…
Browse files Browse the repository at this point in the history
…ils view (#2057)

closes  #2047 , closes #2048

---------
This functionality has been developed for the project “codo”.
codo is developed under the projects “Landungsbrücken – Patenschaften in Hamburg stärken” and “openTransfer Patenschaften”. It is funded through the program “Menschen stärken Menschen” by the German Federal Ministry of Family Affairs, Senior Citizens, Women and Youth.
More information at https://github.com/codo-mentoring

“Landungsbrücken – Patenschaften in Hamburg stärken” is a project of BürgerStiftung Hamburg in cooperation with the Mentor.Ring Hamburg. With a mix of networking opportunities, capacity building and financial support the project strengthens Hamburg’s scene of mentoring projects since its founding in 2016.

The “Stiftung Bürgermut” foundation since 2007 supports the digital and real exchange of experiences and connections of active citizens. Within the federal program “Menschen stärken Menschen” the foundation as part of its program “openTransfer Patenschaften” offers support services for connecting, spreading and upskilling mentoring organisations across Germany.

Diese Funktion wurde entwickelt für das Projekt codo.
codo wird entwickelt im Rahmen der Projekte Landungsbrücken – Patenschaften in Hamburg stärken und openTransfer Patenschaften. Er ist gefördert durch das Bundesprogramm Menschen stärken Menschen des Bundesministeriums für Familie, Senioren, Frauen und Jugend.
Mehr Informationen unter https://github.com/codo-mentoring

“Landungsbrücken – Patenschaften in Hamburg stärken” ist ein Projekt der BürgerStiftung Hamburg in Kooperation mit dem Mentor.Ring Hamburg. Mit einer Mischung aus Vernetzungsangeboten, Qualifizierungsmaßnahmen und finanzieller Förderung stärkt das Projekt die Hamburger Szene der Patenschaftsprojekte seit der Gründung im Jahr 2016.

Die Stiftung Bürgermut fördert seit 2007 den digitalen und realen Erfahrungsaustausch und die Vernetzung von engagierten Bürger:innen. Innerhalb des Bundesprogramms „Menschen stärken Menschen” bietet die Stiftung im Rahmen ihres Programms openTransfer Patenschaften Unterstützungsleistungen zur Vernetzung, Verbreitung und Qualifizierung von Patenschafts- und Mentoringorganisationen bundesweit.

Co-authored-by: codo-mentoring <117934638+codo-mentoring@users.noreply.github.com>
Co-authored-by: Simon <simon@aam-digital.com>
  • Loading branch information
3 people authored Dec 13, 2023
1 parent 9d650ec commit 3b4168b
Show file tree
Hide file tree
Showing 126 changed files with 2,980 additions and 381 deletions.
8 changes: 2 additions & 6 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ import {
import { AttendanceModule } from "./child-dev-project/attendance/attendance.module";
import { NotesModule } from "./child-dev-project/notes/notes.module";
import { SchoolsModule } from "./child-dev-project/schools/schools.module";
import { ConflictResolutionModule } from "./features/conflict-resolution/conflict-resolution.module";
import { HistoricalDataModule } from "./features/historical-data/historical-data.module";
import { MatchingEntitiesModule } from "./features/matching-entities/matching-entities.module";
import { ProgressDashboardWidgetModule } from "./features/dashboard-widgets/progress-dashboard-widget/progress-dashboard-widget.module";
Expand All @@ -87,10 +86,9 @@ import { ImportModule } from "./core/import/import.module";
import { ShortcutDashboardWidgetModule } from "./features/dashboard-widgets/shortcut-dashboard-widget/shortcut-dashboard-widget.module";
import { EntityCountDashboardWidgetModule } from "./features/dashboard-widgets/entity-count-dashboard-widget/entity-count-dashboard-widget.module";
import { BirthdayDashboardWidgetModule } from "./features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard-widget.module";
import { ConfigSetupModule } from "./features/config-setup/config-setup.module";
import { MarkdownPageModule } from "./features/markdown-page/markdown-page.module";
import { AdminModule } from "./features/admin/admin.module";
import { LoginStateSubject } from "./core/session/session-type";
import { AdminModule } from "./core/admin/admin.module";

/**
* Main entry point of the application.
Expand Down Expand Up @@ -123,10 +121,7 @@ import { LoginStateSubject } from "./core/session/session-type";
NotesModule,
SchoolsModule,
// feature module
ConflictResolutionModule,
AdminModule,
ImportModule,
ConfigSetupModule,
FileModule,
MarkdownPageModule,
HistoricalDataModule,
Expand All @@ -138,6 +133,7 @@ import { LoginStateSubject } from "./core/session/session-type";
BirthdayDashboardWidgetModule,
ReportingModule,
TodosModule,
AdminModule,
// top level component
UiComponent,
// Global Angular Material modules
Expand Down
16 changes: 6 additions & 10 deletions src/app/app.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,7 @@ import { UserAccountComponent } from "./core/user/user-account/user-account.comp
import { SupportComponent } from "./core/support/support/support.component";
import { AuthGuard } from "./core/session/auth.guard";
import { LoginComponent } from "./core/session/login/login.component";

/**
* Marks a class to be the target when routing.
* Use this by adding the annotation `@RouteTarget("...")` to a component.
* The name provided to the annotation can then be used in the configuration.
*
* IMPORTANT:
* The component also needs to be added to the `...Components` list of the respective module.
*/
export const RouteTarget = (_name: string) => (_) => undefined;
import { AdminModule } from "./core/admin/admin.module";

/**
* All routes configured for the main app routing.
Expand Down Expand Up @@ -60,6 +51,11 @@ export const allRoutes: Routes = [
(c) => c.PublicFormComponent,
),
},
{
path: "admin",
// add directly without lazy-loading so that Menu can detect permissions for child routes
children: AdminModule.routes,
},
{ path: "login", component: LoginComponent },
{ path: "404", component: NotFoundComponent },

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { Note } from "../../notes/model/note";
import { ConfirmationDialogService } from "../../../core/common-components/confirmation-dialog/confirmation-dialog.service";
import { ConfirmationDialogButton } from "../../../core/common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component";
import { RollCallComponent } from "./roll-call/roll-call.component";
import { RouteTarget } from "../../../app.routing";
import { NgIf } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MatTooltipModule } from "@angular/material/tooltip";
import { RollCallSetupComponent } from "./roll-call-setup/roll-call-setup.component";
import { ViewTitleComponent } from "../../../core/common-components/view-title/view-title.component";
import { RouteTarget } from "../../../route-target";

@RouteTarget("AddDayAttendance")
@Component({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Component } from "@angular/core";
import { ComingSoonDialogService } from "../../../features/coming-soon/coming-soon-dialog.service";
import { RouteTarget } from "../../../app.routing";
import { MatCardModule } from "@angular/material/card";
import { MatButtonModule } from "@angular/material/button";
import { RouterLink } from "@angular/router";
import { ViewTitleComponent } from "../../../core/common-components/view-title/view-title.component";
import { RouteTarget } from "../../../route-target";

@RouteTarget("AttendanceManager")
@Component({
Expand Down
12 changes: 11 additions & 1 deletion src/app/child-dev-project/attendance/attendance.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,18 @@ import { ComponentRegistry } from "../../dynamic-components";
import { attendanceComponents } from "./attendance-components";
import { RecurringActivity } from "./model/recurring-activity";
import { EventNote } from "./model/event-note";
import { DefaultDatatype } from "../../core/entity/default-datatype/default.datatype";
import { EventAttendanceDatatype } from "./model/event-attendance.datatype";

@NgModule({})
@NgModule({
providers: [
{
provide: DefaultDatatype,
useClass: EventAttendanceDatatype,
multi: true,
},
],
})
export class AttendanceModule {
static databaseEntities = [RecurringActivity, EventNote];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { testDatatype } from "../../../core/entity/schema/entity-schema.service.spec";
import { EventAttendanceDatatype } from "./event-attendance.datatype";
import { EventAttendance } from "./event-attendance";
import { defaultAttendanceStatusTypes } from "../../../core/config/default-config/default-attendance-status-types";
import { DefaultDatatype } from "../../../core/entity/default-datatype/default.datatype";
import { StringDatatype } from "../../../core/basic-datatypes/string/string.datatype";
import { ConfigurableEnumDatatype } from "../../../core/basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype";
import { ConfigurableEnumService } from "../../../core/basic-datatypes/configurable-enum/configurable-enum.service";

describe("Schema data type: event-attendance", () => {
testDatatype(
EventAttendanceDatatype,
new EventAttendance(defaultAttendanceStatusTypes[0], "test remark"),
{
status: defaultAttendanceStatusTypes[0].id,
remarks: "test remark",
},
undefined,
[
{ provide: DefaultDatatype, useClass: StringDatatype, multi: true },
{
provide: DefaultDatatype,
useClass: ConfigurableEnumDatatype,
multi: true,
},
{
provide: ConfigurableEnumService,
useValue: { getEnumValues: () => defaultAttendanceStatusTypes },
},
],
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable } from "@angular/core";
import { SchemaEmbedDatatype } from "../../../core/basic-datatypes/schema-embed/schema-embed.datatype";
import { EntitySchemaService } from "../../../core/entity/schema/entity-schema.service";
import { EntityConstructor } from "../../../core/entity/model/entity";
import { EventAttendance } from "./event-attendance";

@Injectable()
export class EventAttendanceDatatype extends SchemaEmbedDatatype {
static override dataType = EventAttendance.DATA_TYPE;

override embeddedType = EventAttendance as unknown as EntityConstructor;

constructor(schemaService: EntitySchemaService) {
super(schemaService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { DatabaseField } from "../../../core/entity/database-field.decorator";
* TODO overwork this concept to either be a sublass of Entity or not (at the moment it uses a lot of casting, e.g. to be used in the entity subrecord)
*/
export class EventAttendance {
static DATA_TYPE = "event-attendance";

private _status: AttendanceStatusType;
@DatabaseField({
dataType: "configurable-enum",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ActivatedRoute } from "@angular/router";
import { ChildrenService } from "../children.service";
import { EntityListConfig } from "../../../core/entity-list/EntityListConfig";
import { RouteData } from "../../../core/config/dynamic-routing/view-config.interface";
import { RouteTarget } from "../../../app.routing";
import { EntityListComponent } from "../../../core/entity-list/entity-list/entity-list.component";
import { RouteTarget } from "../../../route-target";

@RouteTarget("ChildrenList")
@Component({
Expand Down
3 changes: 1 addition & 2 deletions src/app/child-dev-project/notes/model/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,7 @@ export class Note extends Entity {
* No direct access to change this property. Use the `.getAttendance()` method to have safe access.
*/
@DatabaseField({
innerDataType: "schema-embed",
additional: EventAttendance,
innerDataType: EventAttendance.DATA_TYPE,
anonymize: "retain",
})
private childrenAttendance: Map<string, EventAttendance> = new Map();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import { EventNote } from "../../attendance/model/event-note";
import { WarningLevel } from "../../warning-level";
import { RouteData } from "../../../core/config/dynamic-routing/view-config.interface";
import { merge } from "rxjs";
import { RouteTarget } from "../../../app.routing";
import moment from "moment";
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { NgIf } from "@angular/common";
import { FormsModule } from "@angular/forms";
import { Angulartics2Module } from "angulartics2";
import { MatMenuModule } from "@angular/material/menu";
import { FaDynamicIconComponent } from "../../../core/common-components/fa-dynamic-icon/fa-dynamic-icon.component";
import { RouteTarget } from "../../../route-target";

/**
* additional config specifically for NotesManagerComponent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<h2 mat-dialog-title i18n>Configure Field "{{ entitySchemaField.label }}"</h2>
<app-dialog-close mat-dialog-close></app-dialog-close>

<mat-dialog-content>
<p i18n>
The field settings here apply to the record type overall and affect both the
field here in the current view as well as all other forms and lists where
this field is displayed.
</p>

<form [formGroup]="form">
<mat-tab-group formGroupName="schemaFields">
<mat-tab label="Basics" i18n-label>
<div class="grid-layout margin-top-regular">
<div class="entity-form-cell">
<mat-form-field>
<mat-label>Label</mat-label>
<input formControlName="label" matInput #formLabel />
</mat-form-field>

<mat-form-field floatLabel="always">
<mat-label>
Label (short)
<fa-icon
icon="question-circle"
matTooltip="Optionally you can define an additional shorter label to be displayed in table headers and other places where space is limited."
i18n-matTooltip
></fa-icon>
</mat-label>
<input
formControlName="labelShort"
matInput
[placeholder]="formLabel.value"
/>
</mat-form-field>

<mat-form-field>
<mat-label i18n>
Description
<fa-icon
icon="question-circle"
matTooltip="The description provides additional explanation or context about this field. It is usually displayed as a help icon with tooltip."
i18n-matTooltip
></fa-icon>
</mat-label>
<textarea
formControlName="description"
matInput
rows="3"
></textarea>
</mat-form-field>
</div>

<div class="entity-form-cell">
<mat-form-field>
<mat-label i18n>
Field ID (readonly)
<fa-icon
icon="question-circle"
matTooltip="The internal ID of the field is used at a technical level in the database. The ID cannot be changed after the field has been created."
i18n-matTooltip
></fa-icon>
</mat-label>
<input [formControl]="fieldIdForm" matInput />
<fa-icon
*ngIf="fieldIdForm.disabled"
icon="lock"
matSuffix
></fa-icon>
<mat-error *ngIf="fieldIdForm.hasError('uniqueId')">
{{ fieldIdForm.getError("uniqueId") }}
</mat-error>
</mat-form-field>

<mat-form-field>
<mat-label>Type</mat-label>
<app-basic-autocomplete
formControlName="dataType"
#formDataType
[options]="dataTypes"
[optionToString]="objectToLabel"
[valueMapper]="objectToValue"
></app-basic-autocomplete>
</mat-form-field>

<!-- "additional" for enum datatype -->
<mat-form-field
*ngIf="
typeAdditionalOptions &&
formDataType.value === 'configurable-enum'
"
>
<mat-label i18n>
Type Details (dropdown options set)
<fa-icon
icon="question-circle"
matTooltip="Select an existing set of options to share between multiple fields or create a new, independent list of dropdown options."
i18n-matTooltip
></fa-icon>
</mat-label>

<app-basic-autocomplete
formControlName="additional"
[options]="typeAdditionalOptions ?? []"
[optionToString]="objectToLabel"
[valueMapper]="objectToValue"
[createOption]="createNewAdditionalOption"
></app-basic-autocomplete>

<button
mat-icon-button
matSuffix
(click)="openEnumOptions($event)"
>
<fa-icon icon="wrench"></fa-icon>
</button>
</mat-form-field>

<!-- "additional" for entity ref datatypes -->
<mat-form-field
*ngIf="
(typeAdditionalOptions && formDataType.value === 'entity') ||
formDataType.value === 'entity-array'
"
>
<mat-label i18n>
Type Details (target record type)
<fa-icon
icon="question-circle"
matTooltip="Select from which type of records the user can select and link to with this field."
i18n-matTooltip
></fa-icon>
</mat-label>

<app-basic-autocomplete
formControlName="additional"
[options]="typeAdditionalOptions ?? []"
[optionToString]="objectToLabel"
[valueMapper]="objectToValue"
[createOption]="createNewAdditionalOption"
></app-basic-autocomplete>
</mat-form-field>
</div>
</div>
</mat-tab>

<!--
ADVANCED SETTINGS
-->
<mat-tab
label="Advanced Options & Validation [COMING SOON]"
i18n-label
[disabled]="true"
>
<div class="grid-layout margin-top-regular">
<div class="entity-form-cell">
<mat-form-field>
<mat-label>Default Value</mat-label>
<input formControlName="defaultValue" matInput />
</mat-form-field>

<mat-form-field>
<mat-label>Anonymize</mat-label>
<input formControlName="anonymize" matInput />
</mat-form-field>

<mat-form-field>
<mat-label>Searchable</mat-label>
<input formControlName="searchable" matInput />
</mat-form-field>
</div>

<div class="entity-form-cell">
<mat-form-field>
<mat-label>Field Validation</mat-label>
<input formControlName="validators" matInput />
</mat-form-field>
</div>
</div>
</mat-tab>
</mat-tab-group>
</form>
</mat-dialog-content>

<mat-dialog-actions>
<button mat-button (click)="save()">Save</button>
<button mat-button mat-dialog-close>Cancel</button>
</mat-dialog-actions>
Loading

0 comments on commit 3b4168b

Please sign in to comment.