Skip to content

Commit

Permalink
feat: public and embeddable forms (#1618)
Browse files Browse the repository at this point in the history
closes #1607 

This functionality has been developed for the project “QualitätsMENTOR”.
QualitätsMENTOR 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/qualitaetsmentor

“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 QualitätsMENTOR.
Der QualitätsMENTOR 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/qualitaetsmentor

“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: QualitaetsMENTOR <117934638+QualitaetsMENTOR@users.noreply.github.com>
Co-authored-by: Sebastian Leidig <sebastian.leidig@gmail.com>
  • Loading branch information
3 people authored Jan 11, 2023
1 parent 5ba6dfb commit 45c90f5
Show file tree
Hide file tree
Showing 35 changed files with 561 additions and 128 deletions.
2 changes: 1 addition & 1 deletion e2e/integration/RecordingAttendanceOfChild.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("Scenario: Recording attendance of a child - E2E test", function () {
});

it("THEN in the details page of this child under 'attendance' for the specific class I should see a green background for the current day", function () {
cy.get("#mat-input-2")
cy.get('[placeholder="Search"]')
.focus()
.type(this.childName)
.wait(500)
Expand Down
25 changes: 23 additions & 2 deletions src/app/app.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { ApplicationLoadingComponent } from "./core/view/dynamic-routing/empty/a
import { NotFoundComponent } from "./core/view/dynamic-routing/not-found/not-found.component";
import { UserAccountComponent } from "./core/user/user-account/user-account.component";
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.
Expand All @@ -42,9 +44,28 @@ export const allRoutes: Routes = [
import("./core/coming-soon/coming-soon/coming-soon.component").then(
(c) => c.ComingSoonComponent
),
canActivate: [AuthGuard],
},
{
path: "user-account",
component: UserAccountComponent,
canActivate: [AuthGuard],
},
{ path: "user-account", component: UserAccountComponent },
{ path: "support", component: SupportComponent },
// this can't be configured in config as the config is only loaded on login
{
path: "public-form/:id",
loadComponent: () =>
import("./features/public-form/public-form.component").then(
(c) => c.PublicFormComponent
),
},
{ path: "login", component: LoginComponent },
{ path: "404", component: NotFoundComponent },
{ path: "**", pathMatch: "full", component: ApplicationLoadingComponent },
{
path: "**",
pathMatch: "full",
component: ApplicationLoadingComponent,
canActivate: [AuthGuard],
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,10 @@ import { EntityMapperService } from "../../../../core/entity/entity-mapper.servi
describe("ChildrenBmiDashboardComponent", () => {
let component: ChildrenBmiDashboardComponent;
let fixture: ComponentFixture<ChildrenBmiDashboardComponent>;
let mockEntityMapper: jasmine.SpyObj<EntityMapperService>;

beforeEach(() => {
mockEntityMapper = jasmine.createSpyObj("mockEntityMapper", ["loadType"]);
mockEntityMapper.loadType.and.resolveTo([]);
TestBed.configureTestingModule({
imports: [ChildrenBmiDashboardComponent, MockedTestingModule.withState()],
providers: [{ provide: EntityMapperService, useValue: mockEntityMapper }],
}).compileComponents();
});

Expand Down Expand Up @@ -52,15 +48,12 @@ describe("ChildrenBmiDashboardComponent", () => {
height: 115,
weight: 30,
});
mockEntityMapper.loadType.and.resolveTo([
healthCheck1,
healthCheck2,
healthCheck3,
]);
const loadTypeSpy = spyOn(TestBed.inject(EntityMapperService), "loadType");
loadTypeSpy.and.resolveTo([healthCheck1, healthCheck2, healthCheck3]);

component.onInitFromDynamicConfig();

expect(mockEntityMapper.loadType).toHaveBeenCalledWith(HealthCheck);
expect(loadTypeSpy).toHaveBeenCalledWith(HealthCheck);
tick();
expect(component.bmiDataSource.data).toEqual([
{ childId: "testID", bmi: healthCheck2.bmi },
Expand Down
58 changes: 49 additions & 9 deletions src/app/core/database/pouch-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { PerformanceAnalysisLogging } from "../../utils/performance-analysis-log
import { Injectable } from "@angular/core";
import { firstValueFrom, Observable, Subject } from "rxjs";
import { filter } from "rxjs/operators";
import { AppSettings } from "../app-config/app-settings";
import { HttpStatusCode } from "@angular/common/http";

/**
* Wrapper for a PouchDB instance to decouple the code from
Expand Down Expand Up @@ -96,6 +98,34 @@ export class PouchDatabase extends Database {
return this;
}

/**
* Initializes the PouchDB with the http adapter to directly access a remote CouchDB without replication
* See {@link https://pouchdb.com/adapters.html#pouchdb_over_http}
* @param dbName (relative) path to the remote database
* @param fetch a overwrite for the default fetch handler
*/
initRemoteDB(
dbName = `${AppSettings.DB_PROXY_PREFIX}/${AppSettings.DB_NAME}`,
fetch = this.defaultFetch
): PouchDatabase {
const options = {
adapter: "http",
skip_setup: true,
fetch,
};
this.pouchDB = new PouchDB(dbName, options);
this.databaseInitialized.complete();
return this;
}

private defaultFetch(url, opts: any) {
if (typeof url === "string") {
const remoteUrl =
AppSettings.DB_PROXY_PREFIX + url.split(AppSettings.DB_PROXY_PREFIX)[1];
return PouchDB.fetch(remoteUrl, opts);
}
}

async getPouchDBOnceReady(): Promise<PouchDB.Database> {
await firstValueFrom(this.databaseInitialized, {
defaultValue: this.pouchDB,
Expand Down Expand Up @@ -237,15 +267,25 @@ export class PouchDatabase extends Database {
changes(prefix: string): Observable<any> {
if (!this.changesFeed) {
this.changesFeed = new Subject();
this.getPouchDBOnceReady().then((pouchDB) =>
pouchDB
.changes({
live: true,
since: "now",
include_docs: true,
})
.addListener("change", (change) => this.changesFeed.next(change.doc))
);
this.getPouchDBOnceReady()
.then((pouchDB) =>
pouchDB
.changes({
live: true,
since: "now",
include_docs: true,
})
.addListener("change", (change) =>
this.changesFeed.next(change.doc)
)
)
.catch((err) => {
if (err.statusCode === HttpStatusCode.Unauthorized) {
this.loggingService.warn(err);
} else {
throw err;
}
});
}
return this.changesFeed.pipe(filter((doc) => doc._id.startsWith(prefix)));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<form class="entity-form-columns">
<form [ngClass]="{ 'grid-layout': gridLayout }">
<div *ngFor="let col of columns; let i=index" class="entity-form-cell">
<h2 *ngIf="columnHeaders?.[i]">{{columnHeaders[i]}}</h2>
<div *ngFor="let row of col">
Expand All @@ -8,7 +8,7 @@ <h2 *ngIf="columnHeaders?.[i]">{{columnHeaders[i]}}</h2>
config: {
formFieldConfig: row,
propertySchema: entity.getSchema().get(row.id),
formControl: _form.get(row.id),
formControl: form.get(row.id),
entity: entity
}
}">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@use "src/styles/mixins/grid-layout";

.entity-form-columns {
.grid-layout {
@include grid-layout.adaptive($min-block-width: 250px, $max-screen-width: 414px);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ describe("EntityFormComponent", () => {
beforeEach(waitForAsync(() => {
mockConfirmation = jasmine.createSpyObj(["getConfirmation"]);
TestBed.configureTestingModule({
imports: [
MockedTestingModule.withState(),
EntityFormComponent
],
imports: [MockedTestingModule.withState(), EntityFormComponent],
providers: [
{ provide: ConfirmationDialogService, useValue: mockConfirmation },
],
Expand All @@ -37,6 +34,7 @@ describe("EntityFormComponent", () => {
component.columns[0],
component.entity
);
component.ngOnChanges({ entity: true, form: true } as any);
fixture.detectChanges();
});

Expand Down Expand Up @@ -88,8 +86,8 @@ describe("EntityFormComponent", () => {
) {
mockConfirmation.getConfirmation.and.resolveTo(popupAction === "yes");
for (const c in formChanges) {
component._form.get(c).setValue(formChanges[c]);
component._form.get(c).markAsDirty();
component.form.get(c).setValue(formChanges[c]);
component.form.get(c).markAsDirty();
}
const updatedChild = new Child(component.entity.getId());
for (const c in remoteChanges) {
Expand All @@ -100,7 +98,7 @@ describe("EntityFormComponent", () => {
await entityMapper.save(updatedChild);

for (const v in expectedFormValues) {
const form = component._form.get(v);
const form = component.form.get(v);
if (form) {
expect(form).toHaveValue(expectedFormValues[v]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { Component, Input, OnInit, ViewEncapsulation } from "@angular/core";
import {
Component,
Input,
OnChanges,
SimpleChanges,
ViewEncapsulation,
} from "@angular/core";
import { Entity } from "../../../entity/model/entity";
import { FormFieldConfig } from "./FormConfig";
import { EntityForm } from "../entity-form.service";
import { EntityMapperService } from "../../../entity/entity-mapper.service";
import { filter } from "rxjs/operators";
import { ConfirmationDialogService } from "../../../confirmation-dialog/confirmation-dialog.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { NgForOf, NgIf } from "@angular/common";
import { NgClass, NgForOf, NgIf } from "@angular/common";
import { DynamicComponentDirective } from "../../../view/dynamic-components/dynamic-component.directive";
import { MatButtonModule } from "@angular/material/button";
import { MatTooltipModule } from "@angular/material/tooltip";
Expand Down Expand Up @@ -35,11 +41,14 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
NgIf,
MatButtonModule,
MatTooltipModule,
FontAwesomeModule
FontAwesomeModule,
NgClass,
],
standalone: true
standalone: true,
})
export class EntityFormComponent<T extends Entity = Entity> implements OnInit {
export class EntityFormComponent<T extends Entity = Entity>
implements OnChanges
{
/**
* The entity which should be displayed and edited
*/
Expand All @@ -49,27 +58,33 @@ export class EntityFormComponent<T extends Entity = Entity> implements OnInit {

@Input() columnHeaders?: (string | null)[];

@Input() set form(form: EntityForm<T>) {
this._form = form;
this.initialFormValues = form.getRawValue();
}
@Input() form: EntityForm<T>;

/**
* Whether the component should use a grid layout or just rows
*/
@Input() gridLayout = true;

_form: EntityForm<T>;
initialFormValues: any;

constructor(
private entityMapper: EntityMapperService,
private confirmationDialog: ConfirmationDialogService
) {}

ngOnInit() {
this.entityMapper
.receiveUpdates(this.entity.getConstructor())
.pipe(
filter(({ entity }) => entity.getId() === this.entity.getId()),
untilDestroyed(this)
)
.subscribe(({ entity }) => this.applyChanges(entity));
ngOnChanges(changes: SimpleChanges) {
if (changes.entity && this.entity) {
this.entityMapper
.receiveUpdates(this.entity.getConstructor())
.pipe(
filter(({ entity }) => entity.getId() === this.entity.getId()),
untilDestroyed(this)
)
.subscribe(({ entity }) => this.applyChanges(entity));
}
if (changes.form && this.form) {
this.initialFormValues = this.form.getRawValue();
}
}

private async applyChanges(entity: T) {
Expand All @@ -85,16 +100,16 @@ export class EntityFormComponent<T extends Entity = Entity> implements OnInit {
))
) {
Object.assign(this.initialFormValues, entity);
this._form.patchValue(entity as any);
this.form.patchValue(entity as any);
}
}

private changesOnlyAffectPristineFields(updatedEntity: T) {
if (this._form.pristine) {
if (this.form.pristine) {
return true;
}

const dirtyFields = Object.entries(this._form.controls).filter(
const dirtyFields = Object.entries(this.form.controls).filter(
([_, form]) => form.dirty
);
for (const [key] of dirtyFields) {
Expand All @@ -116,7 +131,7 @@ export class EntityFormComponent<T extends Entity = Entity> implements OnInit {
}

private formIsUpToDate(entity: T): boolean {
return Object.entries(this._form.getRawValue()).every(([key, value]) => {
return Object.entries(this.form.getRawValue()).every(([key, value]) => {
return this.entityEqualsFormValue(entity[key], value);
});
}
Expand Down
6 changes: 0 additions & 6 deletions src/app/core/navigation/navigation/navigation.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { NavigationMenuConfig } from "../navigation-menu-config.interface";
import { ConfigService } from "../../config/config.service";
import { UserRoleGuard } from "../../permissions/permission-guard/user-role.guard";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { SessionService } from "../../session/session-service/session.service";
import { NavigationEnd, Router, RouterLink } from "@angular/router";
import { filter, startWith } from "rxjs/operators";
import { MatListModule } from "@angular/material/list";
Expand Down Expand Up @@ -57,7 +56,6 @@ export class NavigationComponent {
constructor(
private userRoleGuard: UserRoleGuard,
private configService: ConfigService,
private session: SessionService,
private router: Router
) {
this.configService.configUpdates
Expand Down Expand Up @@ -126,8 +124,4 @@ export class NavigationComponent {
}
}
}

logout() {
this.session.logout();
}
}
Loading

0 comments on commit 45c90f5

Please sign in to comment.