diff --git a/src/app/applications/application-detail/application-detail.component.html b/src/app/applications/application-detail/application-detail.component.html index 1bff90fd..cee8771f 100644 --- a/src/app/applications/application-detail/application-detail.component.html +++ b/src/app/applications/application-detail/application-detail.component.html @@ -41,6 +41,7 @@

{{ "APPLICATION.DETAILS" | translate }}

[personalData]="application.personalData" [startDate]="application.startDate" [status]="application.status" + [metadata]="application.metadata" > diff --git a/src/app/applications/application.model.ts b/src/app/applications/application.model.ts index 63fd9a97..4bbc90cb 100644 --- a/src/app/applications/application.model.ts +++ b/src/app/applications/application.model.ts @@ -7,6 +7,8 @@ import { Organisation } from "../admin/organisation/organisation.model"; import { ApplicationStatus } from "./enums/status.enum"; import { IotDevice } from "./iot-devices/iot-device.model"; import { ApplicationDeviceType } from "./models/application-device-type.model"; +import { KeyValue } from "@shared/types/tuple.type"; +import { Metadata } from "@shared/types/metadata"; export type ApplicationWithStatus = Application & { statusCheck: "stable" | "alert" }; @@ -37,6 +39,7 @@ export class Application { public permissions: PermissionResponse[]; public permissionIds: number[]; public dataTargets: Datatarget[]; + public metadata?: Metadata; } export class ApplicationRequest { @@ -56,6 +59,7 @@ export class ApplicationRequest { public controlledProperties?: ControlledPropertyTypes[]; public deviceTypes?: ApplicationDeviceTypeUnion[]; public permissionIds: number[]; + public metadata?: Metadata; } export interface ApplicationData { diff --git a/src/app/shared/components/forms/form-body-application/form-body-application.component.html b/src/app/shared/components/forms/form-body-application/form-body-application.component.html index 016b9c63..b2b8bcde 100644 --- a/src/app/shared/components/forms/form-body-application/form-body-application.component.html +++ b/src/app/shared/components/forms/form-body-application/form-body-application.component.html @@ -10,6 +10,7 @@ } +
@@ -313,6 +314,11 @@
+
+

{{ "QUESTION.METADATA" | translate }}

+ +
+
diff --git a/src/app/shared/components/forms/form-body-application/form-body-application.component.ts b/src/app/shared/components/forms/form-body-application/form-body-application.component.ts index 6e9d2fc5..047567bc 100644 --- a/src/app/shared/components/forms/form-body-application/form-body-application.component.ts +++ b/src/app/shared/components/forms/form-body-application/form-body-application.component.ts @@ -16,6 +16,9 @@ import { SharedVariableService } from "@shared/shared-variable/shared-variable.s import { isPhoneNumberValid } from "@shared/validators/phone-number.validator"; import { ReplaySubject, Subject, Subscription } from "rxjs"; import { takeUntil } from "rxjs/operators"; +import { keyValueListToMetadata, metadataToKeyValueList } from "@shared/helpers/metadata.helper"; +import { TranslatableError } from "@shared/translatable-error"; +import { addWarning } from "@angular-devkit/build-angular/src/utils/webpack-diagnostics"; export class User { public name: string; @@ -58,6 +61,8 @@ export class FormBodyApplicationComponent implements OnInit, OnDestroy { public filteredPermissionsMulti: ReplaySubject = new ReplaySubject(1); private id: number; private _onDestroy = new Subject(); + metadataTags: { key?: string; value?: string }[] = []; + errorMetadataFieldId: string | undefined; constructor( private restService: RestService, @@ -157,6 +162,9 @@ export class FormBodyApplicationComponent implements OnInit, OnDestroy { this.application.deviceTypes = application.deviceTypes.map(deviceType => deviceType.type); this.application.permissionIds = application.permissionIds; this.permissionMultiCtrl.setValue(this.application.permissionIds); + if (application.metadata) { + this.metadataTags = metadataToKeyValueList(application.metadata) ?? []; + } this.fillDefaultMetadata(); }); @@ -177,6 +185,18 @@ export class FormBodyApplicationComponent implements OnInit, OnDestroy { this.application.endDate = this.serializedEndDate.value?.toISOString(); this.application.contactPhone = this.phoneCtrl.value ? this.phoneCtrl.value : null; + try { + this.application.metadata = keyValueListToMetadata(this.metadataTags); + } catch (error) { + let message = error.message; + if (error instanceof TranslatableError) { + // @todo Can we safely use `instant` here? + message = this.translate.instant(message, error.context); + } + this.handleError(this.buildErrorMessage(message), "application.metadata"); + return; + } + if (this.id) { this.updateApplication(this.id); } else { diff --git a/src/app/shared/components/metadata-details/metadata-details.component.html b/src/app/shared/components/metadata-details/metadata-details.component.html index 6b22ee26..e979d9fe 100644 --- a/src/app/shared/components/metadata-details/metadata-details.component.html +++ b/src/app/shared/components/metadata-details/metadata-details.component.html @@ -69,3 +69,16 @@ >{{ deviceTypeText }}

} + +@if (metadata) { +

+ {{ "APPLICATION.METADATA-FIELD.METADATA" | translate }} +

+ @for (key of Object.keys(metadata); track key; let i = $index) { +

+ {{ i + 1 }}: {{ key }} + {{ metadata[key] }} +

+ } +} diff --git a/src/app/shared/components/metadata-details/metadata-details.component.ts b/src/app/shared/components/metadata-details/metadata-details.component.ts index 67ff019c..b330eddd 100644 --- a/src/app/shared/components/metadata-details/metadata-details.component.ts +++ b/src/app/shared/components/metadata-details/metadata-details.component.ts @@ -5,6 +5,7 @@ import { ApplicationDeviceType } from "@applications/models/application-device-t import { TranslateService } from "@ngx-translate/core"; import { toPascalKebabCase } from "@shared/helpers/string.helper"; import { ControlledProperty } from "@shared/models/controlled-property.model"; +import { Metadata } from "@shared/types/metadata"; @Component({ selector: "app-metadata-details", @@ -26,6 +27,7 @@ export class MetadataDetailsComponent implements OnInit { @Input() hardware?: string; @Input() controlledProperties?: ControlledProperty[]; @Input() deviceTypes?: ApplicationDeviceType[]; + @Input() metadata?: Metadata; controlledPropertyText: string; deviceTypeText: string; @@ -48,4 +50,6 @@ export class MetadataDetailsComponent implements OnInit { .join(", "); }); } + + protected readonly Object = Object; } diff --git a/src/app/shared/helpers/metadata.helper.ts b/src/app/shared/helpers/metadata.helper.ts new file mode 100644 index 00000000..8b038448 --- /dev/null +++ b/src/app/shared/helpers/metadata.helper.ts @@ -0,0 +1,39 @@ +import { KeyValue } from "@shared/types/tuple.type"; +import { Metadata } from "@shared/types/metadata"; +import { TranslatableError } from "@shared/translatable-error"; + +export const metadataToKeyValueList = (metadata: object, shouldThrowOnError = false): KeyValue[] => { + try { + const tags: KeyValue[] = []; + + for (const key of Object.keys(metadata)) { + tags.push({ key, value: metadata[key] }); + } + + return tags; + } catch (error) { + if (shouldThrowOnError) { + throw error; + } + } +}; + +export const keyValueListToMetadata = (pairs: KeyValue[]): Metadata => { + const metadata = {} + + for (const pair of pairs) { + const key = pair.key?.trim(); + const value = pair.value?.trim(); + if (!key) { + throw new TranslatableError("ERROR.METADATA-EMPTY-KEY"); + } else if (key in metadata) { + throw new TranslatableError("ERROR.METADATA-DUPLICATE-KEY", { key }); + } + if (!value) { + throw new TranslatableError("ERROR.METADATA-EMPTY-VALUE", { key }); + } + metadata[key] = value + } + + return metadata; +}; diff --git a/src/app/shared/translatable-error.ts b/src/app/shared/translatable-error.ts new file mode 100644 index 00000000..4e9c3258 --- /dev/null +++ b/src/app/shared/translatable-error.ts @@ -0,0 +1,12 @@ +import { InterpolationParameters } from "@ngx-translate/core"; + +export class TranslatableError extends Error { + constructor( + msg: string, + public readonly context?: InterpolationParameters + ) { + super(msg); + // Set the prototype explicitly. + Object.setPrototypeOf(this, TranslatableError.prototype); + } +} diff --git a/src/app/shared/types/metadata.ts b/src/app/shared/types/metadata.ts new file mode 100644 index 00000000..bd55cad8 --- /dev/null +++ b/src/app/shared/types/metadata.ts @@ -0,0 +1 @@ +export type Metadata = Record diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json index c2249658..9b49cc81 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -157,7 +157,8 @@ "PERSONAL-DATA": "Persondata", "HARDWARE": "Hardware", "CONTROLLED-PROPERTY": "Data", - "DEVICE-TYPE": "Forbindelsesteknologi" + "DEVICE-TYPE": "Forbindelsesteknologi", + "METADATA": "Metadata" }, "CHANGE-ORGANIZATION": { "TITLE": "Skift organisation", @@ -1290,7 +1291,10 @@ "USER-INACTIVE": "Login successfuldt, men din bruger er deaktiveret i OS2IoT. Kontakt din administrator for at få tildelt rettigheder" }, "ERROR": { - "SEMANTIC": "Semantisk fejl, verificer at navn og værdier er på det rigtige format" + "SEMANTIC": "Semantisk fejl, verificer at navn og værdier er på det rigtige format", + "METADATA-EMPTY-KEY": "Metadatanavnet må ikke være tomt", + "METADATA-EMPTY-VALUE": "Metadataværdien for \"{{key}}\" må ikke være tom", + "METADATA-DUPLICATE-KEY": "Metadatanavnet \"{{key}}\" er allerede brugt" }, "DIALOG": { "SAVE.TITLE": "Slet",