@openmfp/ngx 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,7 @@ import '@ui5/webcomponents-icons/dist/add.js';
27
27
  import '@ui5/webcomponents-icons/dist/delete.js';
28
28
  import '@ui5/webcomponents-icons/dist/navigation-down-arrow.js';
29
29
  import '@ui5/webcomponents-icons/dist/navigation-right-arrow.js';
30
+ import { InputIcon } from '@fundamental-ngx/ui5-webcomponents/input-icon';
30
31
  import { Label } from '@fundamental-ngx/ui5-webcomponents/label';
31
32
  import { Option } from '@fundamental-ngx/ui5-webcomponents/option';
32
33
  import { Select } from '@fundamental-ngx/ui5-webcomponents/select';
@@ -1832,6 +1833,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
1832
1833
  ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.ShadowDom, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<ui5-dialog\n data-testid=\"generic-table-card-delete-dialog\"\n [accessibleName]=\"config()?.title ?? 'Confirm Delete'\"\n [open]=\"open()\"\n (ui5BeforeClose)=\"cancelled.emit()\"\n>\n <div class=\"dialog__header\" slot=\"header\">\n <ui5-icon class=\"dialog__header-icon\" design=\"Critical\" name=\"alert\" />\n <ui5-title level=\"H5\">\n <span\n [innerHTML]=\"config()?.title ?? 'Confirm Delete' | sanitizeHtml\"\n ></span>\n </ui5-title>\n </div>\n @if (config()?.message || requiresConfirmation()) {\n <div class=\"dialog__body\">\n @if (config()?.message; as message) {\n <p class=\"dialog__message\" [innerHTML]=\"message | sanitizeHtml\"></p>\n }\n @if (requiresConfirmation()) {\n <p class=\"dialog__message\">\n <ui5-input\n class=\"dialog__confirmation-input\"\n data-testid=\"generic-table-card-delete-confirmation-input\"\n [formControl]=\"confirmationControl\"\n [placeholder]=\"\n config()?.confirmationPlaceholder ?? 'Type to confirm'\n \"\n [valueState]=\"\n confirmationControl.invalid && confirmationControl.dirty\n ? 'Negative'\n : 'None'\n \"\n />\n </p>\n }\n </div>\n }\n <div class=\"dialog__footer\" slot=\"footer\">\n <ui5-button\n data-testid=\"generic-table-card-delete-confirm\"\n design=\"Negative\"\n [disabled]=\"confirmDisabled()\"\n (click)=\"onConfirm()\"\n >\n {{ config()?.confirmLabel ?? 'Delete' }}\n </ui5-button>\n <ui5-button\n data-testid=\"generic-table-card-delete-cancel\"\n design=\"Transparent\"\n (click)=\"cancelled.emit()\"\n >\n {{ config()?.cancelLabel ?? 'Cancel' }}\n </ui5-button>\n </div>\n</ui5-dialog>\n", styles: [":host{display:contents}.dialog__header{display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem}.dialog__header-icon{color:var(--sapCriticalColor)}.dialog__body{margin:-1rem;min-width:320px;padding:1rem}.dialog__message{margin:0;font-size:.875rem;max-width:320px}.dialog__message--critical{color:var(--sapCriticalElementColor)}.dialog__confirmation-input{display:block;width:100%;max-width:320px}.dialog__footer{display:flex;justify-content:space-between;align-items:center;gap:.5rem;padding:.5rem 1rem;margin:0 -1rem;width:100%}\n"] }]
1833
1834
  }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], confirmed: [{ type: i0.Output, args: ["confirmed"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } });
1834
1835
 
1836
+ function coerceBoolean(value) {
1837
+ if (typeof value === 'boolean')
1838
+ return value;
1839
+ if (typeof value === 'string') {
1840
+ return value.trim().toLowerCase() === 'true';
1841
+ }
1842
+ return false;
1843
+ }
1844
+
1835
1845
  const FORBIDDEN_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
1836
1846
  function setPropertyByPath(object, path, value) {
1837
1847
  const segments = path.split('.').filter(Boolean);
@@ -1941,6 +1951,8 @@ class DeclarativeForm {
1941
1951
  form;
1942
1952
  collectionSeeds = signal({}, /* @ts-ignore */
1943
1953
  ...(ngDevMode ? [{ debugName: "collectionSeeds" }] : /* istanbul ignore next */ []));
1954
+ passwordVisible = signal({}, /* @ts-ignore */
1955
+ ...(ngDevMode ? [{ debugName: "passwordVisible" }] : /* istanbul ignore next */ []));
1944
1956
  fb = inject(FormBuilder);
1945
1957
  constructor() {
1946
1958
  this.form = this.fb.group({});
@@ -1977,6 +1989,47 @@ class DeclarativeForm {
1977
1989
  });
1978
1990
  }
1979
1991
  }
1992
+ setSwitchValue($event, field) {
1993
+ const target = $event.target;
1994
+ const control = this.form.controls[field.name];
1995
+ const checked = coerceBoolean(target.checked);
1996
+ control.setValue(checked);
1997
+ control.markAsTouched();
1998
+ control.markAsDirty();
1999
+ this.formValueChange.emit(this.form.value);
2000
+ if (field.validation === 'onChange') {
2001
+ this.fieldChange.emit({
2002
+ fieldProperty: field.name,
2003
+ value: checked,
2004
+ });
2005
+ }
2006
+ }
2007
+ coerceChecked = coerceBoolean;
2008
+ isPasswordVisible(field) {
2009
+ return this.passwordVisible()[field.name] ?? false;
2010
+ }
2011
+ passwordInputType(field) {
2012
+ if (field.inputType !== 'Password') {
2013
+ return 'Text';
2014
+ }
2015
+ return this.isPasswordVisible(field) ? 'Text' : 'Password';
2016
+ }
2017
+ passwordToggleIcon(field) {
2018
+ return this.isPasswordVisible(field) ? 'hide' : 'show';
2019
+ }
2020
+ passwordToggleLabel(field) {
2021
+ return this.isPasswordVisible(field) ? 'Hide value' : 'Show value';
2022
+ }
2023
+ shouldShowPasswordToggle(field) {
2024
+ return field.inputType === 'Password' && (field.showPasswordToggle ?? true);
2025
+ }
2026
+ togglePasswordVisibility(field, event) {
2027
+ event.stopPropagation();
2028
+ this.passwordVisible.update((state) => ({
2029
+ ...state,
2030
+ [field.name]: !this.isPasswordVisible(field),
2031
+ }));
2032
+ }
1980
2033
  onCollectionValueChange(field, entries) {
1981
2034
  const array = this.form.controls[field.name];
1982
2035
  if (!array)
@@ -2017,6 +2070,7 @@ class DeclarativeForm {
2017
2070
  }
2018
2071
  clear() {
2019
2072
  this.form.reset();
2073
+ this.passwordVisible.set({});
2020
2074
  }
2021
2075
  collectionEntries(field) {
2022
2076
  return this.collectionSeeds()[field.name] ?? [];
@@ -2039,6 +2093,9 @@ class DeclarativeForm {
2039
2093
  if (wantsCollection) {
2040
2094
  this.form.addControl(field.name, this.fb.array([]));
2041
2095
  }
2096
+ else if (field.inputType === 'Switch') {
2097
+ this.form.addControl(field.name, new FormControl(false));
2098
+ }
2042
2099
  else {
2043
2100
  this.form.addControl(field.name, new FormControl(''));
2044
2101
  }
@@ -2073,7 +2130,13 @@ class DeclarativeForm {
2073
2130
  seeds[field.name] = entries;
2074
2131
  }
2075
2132
  this.collectionSeeds.set(seeds);
2076
- this.form.patchValue(initialValues, { emitEvent: false });
2133
+ const normalizedValues = { ...initialValues };
2134
+ for (const field of this.fields()) {
2135
+ if (field.inputType === 'Switch') {
2136
+ normalizedValues[field.name] = coerceBoolean(normalizedValues[field.name]);
2137
+ }
2138
+ }
2139
+ this.form.patchValue(normalizedValues, { emitEvent: false });
2077
2140
  this.form.markAsPristine({ emitEvent: false });
2078
2141
  this.form.updateValueAndValidity({ emitEvent: false });
2079
2142
  }
@@ -2095,18 +2158,20 @@ class DeclarativeForm {
2095
2158
  });
2096
2159
  }
2097
2160
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: DeclarativeForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
2098
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: DeclarativeForm, isStandalone: true, selector: "mfp-declarative-form", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null }, initialValues: { classPropertyName: "initialValues", publicName: "initialValues", isSignal: true, isRequired: false, transformFunction: null }, fieldErrors: { classPropertyName: "fieldErrors", publicName: "fieldErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fieldChange: "fieldChange", formSubmit: "formSubmit", formValueChange: "formValueChange" }, ngImport: i0, template: "<section class=\"form\" data-testid=\"generic-form\" [formGroup]=\"form\">\n @for (field of fields(); track field.name) {\n @let control = form.controls[field.name];\n <div\n class=\"inputs\"\n [attr.data-testid]=\"'generic-form-field-container-' + field.name\"\n >\n <ui5-label\n show-colon\n [attr.data-testid]=\"'generic-form-field-label-' + field.name\"\n [attr.for]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n >{{ field.label }}</ui5-label\n >\n @if (field.propertyCollection?.length) {\n <mfp-form-collection-field\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [fields]=\"field.propertyCollection!\"\n [initialEntries]=\"collectionEntries(field)\"\n [label]=\"field.label\"\n (valueChange)=\"onCollectionValueChange(field, $event)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"collection-error\" role=\"alert\">{{ error }}</div>\n }\n } @else if (field.values?.length) {\n <ui5-select\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n @for (value of [''].concat(field.values ?? []); track value) {\n <ui5-option\n [attr.data-testid]=\"\n 'generic-form-field-' +\n field.name +\n '-option-' +\n (value || 'empty')\n \"\n [selected]=\"value === control.value\"\n [value]=\"value\"\n >{{ value }}</ui5-option\n >\n }\n </ui5-select>\n } @else {\n <ui5-input\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n </ui5-input>\n }\n </div>\n }\n</section>\n", styles: [".form>div{display:flex;flex-direction:column;justify-content:space-evenly;align-items:flex-start;margin-bottom:.5rem;width:100%}.input{width:100%}.collection-error{color:var(--sapNegativeColor, #b00);font-size:.75rem;margin-top:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: Input, selector: "ui5-input, [ui5-input]", inputs: ["accessibleDescription", "accessibleDescriptionRef", "accessibleName", "accessibleNameRef", "disabled", "filter", "maxlength", "name", "noTypeahead", "open", "placeholder", "readonly", "required", "showClearIcon", "showSuggestions", "type", "value", "valueState"], outputs: ["ui5Change", "ui5Close", "ui5Input", "ui5Open", "ui5Select", "ui5SelectionChange"], exportAs: ["ui5Input"] }, { kind: "component", type: Label, selector: "ui5-label, [ui5-label]", inputs: ["for", "required", "showColon", "wrappingType"], exportAs: ["ui5Label"] }, { kind: "component", type: Select, selector: "ui5-select, [ui5-select]", inputs: ["accessibleDescription", "accessibleDescriptionRef", "accessibleName", "accessibleNameRef", "disabled", "name", "readonly", "required", "textSeparator", "tooltip", "value", "valueState"], outputs: ["ui5Change", "ui5Close", "ui5LiveChange", "ui5Open"], exportAs: ["ui5Select"] }, { kind: "component", type: Option, selector: "ui5-option, [ui5-option]", inputs: ["additionalText", "icon", "selected", "tooltip", "value"], outputs: ["ui5Click"], exportAs: ["ui5Option"] }, { kind: "component", type: FormCollectionField, selector: "mfp-form-collection-field", inputs: ["fields", "label", "initialEntries"], outputs: ["valueChange"] }], encapsulation: i0.ViewEncapsulation.ShadowDom });
2161
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: DeclarativeForm, isStandalone: true, selector: "mfp-declarative-form", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null }, initialValues: { classPropertyName: "initialValues", publicName: "initialValues", isSignal: true, isRequired: false, transformFunction: null }, fieldErrors: { classPropertyName: "fieldErrors", publicName: "fieldErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fieldChange: "fieldChange", formSubmit: "formSubmit", formValueChange: "formValueChange" }, ngImport: i0, template: "<section class=\"form\" data-testid=\"generic-form\" [formGroup]=\"form\">\n @for (field of fields(); track field.name) {\n @let control = form.controls[field.name];\n <div\n class=\"inputs\"\n [attr.data-testid]=\"'generic-form-field-container-' + field.name\"\n [class.inputs--switch]=\"field.inputType === 'Switch'\"\n >\n <ui5-label\n show-colon\n [attr.data-testid]=\"'generic-form-field-label-' + field.name\"\n [attr.for]=\"\n field.inputType === 'Switch'\n ? null\n : 'generic-form-field-input-' + field.name\n \"\n [required]=\"field.required\"\n >{{ field.label }}</ui5-label\n >\n @if (field.propertyCollection?.length) {\n <mfp-form-collection-field\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [fields]=\"field.propertyCollection!\"\n [initialEntries]=\"collectionEntries(field)\"\n [label]=\"field.label\"\n (valueChange)=\"onCollectionValueChange(field, $event)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"collection-error\" role=\"alert\">{{ error }}</div>\n }\n } @else if (field.values?.length) {\n <ui5-select\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n @for (value of [''].concat(field.values ?? []); track value) {\n <ui5-option\n [attr.data-testid]=\"\n 'generic-form-field-' +\n field.name +\n '-option-' +\n (value || 'empty')\n \"\n [selected]=\"value === control.value\"\n [value]=\"value\"\n >{{ value }}</ui5-option\n >\n }\n </ui5-select>\n } @else if (field.inputType === 'Switch') {\n <ui5-switch\n class=\"switch\"\n [accessibleName]=\"field.label\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [checked]=\"coerceChecked(control.value)\"\n [disabled]=\"field.disabled\"\n (blur)=\"onFieldBlur(field)\"\n (ui5Change)=\"setSwitchValue($event, field)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"field-error\" role=\"alert\">{{ error }}</div>\n }\n } @else {\n <ui5-input\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [attr.title]=\"field.placeholder || null\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [placeholder]=\"field.placeholder ?? ''\"\n [required]=\"field.required\"\n [type]=\"passwordInputType(field)\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (shouldShowPasswordToggle(field)) {\n <ui5-input-icon\n slot=\"icon\"\n [accessibleName]=\"passwordToggleLabel(field)\"\n [attr.data-testid]=\"\n 'generic-form-field-' + field.name + '-password-toggle'\n \"\n [name]=\"passwordToggleIcon(field)\"\n (ui5Click)=\"togglePasswordVisibility(field, $event)\"\n />\n }\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n </ui5-input>\n }\n @if (field.hint && !field.propertyCollection?.length) {\n <div\n class=\"field-hint\"\n [attr.data-testid]=\"'generic-form-field-hint-' + field.name\"\n >\n {{ field.hint }}\n </div>\n }\n </div>\n }\n</section>\n", styles: [".form>div{display:flex;flex-direction:column;justify-content:space-evenly;align-items:flex-start;margin-bottom:.5rem;width:100%}.input{width:100%}.inputs--switch{flex-direction:row;align-items:center;gap:.75rem}.switch{flex-shrink:0}.inputs--switch .field-hint{flex-basis:100%}.collection-error,.field-error{color:var(--sapNegativeColor, #b00);font-size:.75rem;margin-top:.25rem}.inputs--switch .field-error{flex-basis:100%}.field-hint{color:var(--sapContent_LabelColor, #6a6d70);font-size:.75rem;line-height:1.4;margin-top:.25rem;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: Input, selector: "ui5-input, [ui5-input]", inputs: ["accessibleDescription", "accessibleDescriptionRef", "accessibleName", "accessibleNameRef", "disabled", "filter", "maxlength", "name", "noTypeahead", "open", "placeholder", "readonly", "required", "showClearIcon", "showSuggestions", "type", "value", "valueState"], outputs: ["ui5Change", "ui5Close", "ui5Input", "ui5Open", "ui5Select", "ui5SelectionChange"], exportAs: ["ui5Input"] }, { kind: "component", type: InputIcon, selector: "ui5-input-icon, [ui5-input-icon]", inputs: ["accessibleName", "name"], outputs: ["ui5Click"], exportAs: ["ui5InputIcon"] }, { kind: "component", type: Label, selector: "ui5-label, [ui5-label]", inputs: ["for", "required", "showColon", "wrappingType"], exportAs: ["ui5Label"] }, { kind: "component", type: Select, selector: "ui5-select, [ui5-select]", inputs: ["accessibleDescription", "accessibleDescriptionRef", "accessibleName", "accessibleNameRef", "disabled", "name", "readonly", "required", "textSeparator", "tooltip", "value", "valueState"], outputs: ["ui5Change", "ui5Close", "ui5LiveChange", "ui5Open"], exportAs: ["ui5Select"] }, { kind: "component", type: Option, selector: "ui5-option, [ui5-option]", inputs: ["additionalText", "icon", "selected", "tooltip", "value"], outputs: ["ui5Click"], exportAs: ["ui5Option"] }, { kind: "component", type: Switch, selector: "ui5-switch, [ui5-switch]", inputs: ["accessibleName", "accessibleNameRef", "checked", "design", "disabled", "name", "readonly", "required", "textOff", "textOn", "tooltip", "value"], outputs: ["ui5Change"], exportAs: ["ui5Switch"] }, { kind: "component", type: FormCollectionField, selector: "mfp-form-collection-field", inputs: ["fields", "label", "initialEntries"], outputs: ["valueChange"] }], encapsulation: i0.ViewEncapsulation.ShadowDom });
2099
2162
  }
2100
2163
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: DeclarativeForm, decorators: [{
2101
2164
  type: Component,
2102
2165
  args: [{ selector: 'mfp-declarative-form', imports: [
2103
2166
  ReactiveFormsModule,
2104
2167
  Input,
2168
+ InputIcon,
2105
2169
  Label,
2106
2170
  Select,
2107
2171
  Option,
2172
+ Switch,
2108
2173
  FormCollectionField,
2109
- ], encapsulation: ViewEncapsulation.ShadowDom, template: "<section class=\"form\" data-testid=\"generic-form\" [formGroup]=\"form\">\n @for (field of fields(); track field.name) {\n @let control = form.controls[field.name];\n <div\n class=\"inputs\"\n [attr.data-testid]=\"'generic-form-field-container-' + field.name\"\n >\n <ui5-label\n show-colon\n [attr.data-testid]=\"'generic-form-field-label-' + field.name\"\n [attr.for]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n >{{ field.label }}</ui5-label\n >\n @if (field.propertyCollection?.length) {\n <mfp-form-collection-field\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [fields]=\"field.propertyCollection!\"\n [initialEntries]=\"collectionEntries(field)\"\n [label]=\"field.label\"\n (valueChange)=\"onCollectionValueChange(field, $event)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"collection-error\" role=\"alert\">{{ error }}</div>\n }\n } @else if (field.values?.length) {\n <ui5-select\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n @for (value of [''].concat(field.values ?? []); track value) {\n <ui5-option\n [attr.data-testid]=\"\n 'generic-form-field-' +\n field.name +\n '-option-' +\n (value || 'empty')\n \"\n [selected]=\"value === control.value\"\n [value]=\"value\"\n >{{ value }}</ui5-option\n >\n }\n </ui5-select>\n } @else {\n <ui5-input\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n </ui5-input>\n }\n </div>\n }\n</section>\n", styles: [".form>div{display:flex;flex-direction:column;justify-content:space-evenly;align-items:flex-start;margin-bottom:.5rem;width:100%}.input{width:100%}.collection-error{color:var(--sapNegativeColor, #b00);font-size:.75rem;margin-top:.25rem}\n"] }]
2174
+ ], encapsulation: ViewEncapsulation.ShadowDom, template: "<section class=\"form\" data-testid=\"generic-form\" [formGroup]=\"form\">\n @for (field of fields(); track field.name) {\n @let control = form.controls[field.name];\n <div\n class=\"inputs\"\n [attr.data-testid]=\"'generic-form-field-container-' + field.name\"\n [class.inputs--switch]=\"field.inputType === 'Switch'\"\n >\n <ui5-label\n show-colon\n [attr.data-testid]=\"'generic-form-field-label-' + field.name\"\n [attr.for]=\"\n field.inputType === 'Switch'\n ? null\n : 'generic-form-field-input-' + field.name\n \"\n [required]=\"field.required\"\n >{{ field.label }}</ui5-label\n >\n @if (field.propertyCollection?.length) {\n <mfp-form-collection-field\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [fields]=\"field.propertyCollection!\"\n [initialEntries]=\"collectionEntries(field)\"\n [label]=\"field.label\"\n (valueChange)=\"onCollectionValueChange(field, $event)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"collection-error\" role=\"alert\">{{ error }}</div>\n }\n } @else if (field.values?.length) {\n <ui5-select\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n @for (value of [''].concat(field.values ?? []); track value) {\n <ui5-option\n [attr.data-testid]=\"\n 'generic-form-field-' +\n field.name +\n '-option-' +\n (value || 'empty')\n \"\n [selected]=\"value === control.value\"\n [value]=\"value\"\n >{{ value }}</ui5-option\n >\n }\n </ui5-select>\n } @else if (field.inputType === 'Switch') {\n <ui5-switch\n class=\"switch\"\n [accessibleName]=\"field.label\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [checked]=\"coerceChecked(control.value)\"\n [disabled]=\"field.disabled\"\n (blur)=\"onFieldBlur(field)\"\n (ui5Change)=\"setSwitchValue($event, field)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"field-error\" role=\"alert\">{{ error }}</div>\n }\n } @else {\n <ui5-input\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [attr.title]=\"field.placeholder || null\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [placeholder]=\"field.placeholder ?? ''\"\n [required]=\"field.required\"\n [type]=\"passwordInputType(field)\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (shouldShowPasswordToggle(field)) {\n <ui5-input-icon\n slot=\"icon\"\n [accessibleName]=\"passwordToggleLabel(field)\"\n [attr.data-testid]=\"\n 'generic-form-field-' + field.name + '-password-toggle'\n \"\n [name]=\"passwordToggleIcon(field)\"\n (ui5Click)=\"togglePasswordVisibility(field, $event)\"\n />\n }\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n </ui5-input>\n }\n @if (field.hint && !field.propertyCollection?.length) {\n <div\n class=\"field-hint\"\n [attr.data-testid]=\"'generic-form-field-hint-' + field.name\"\n >\n {{ field.hint }}\n </div>\n }\n </div>\n }\n</section>\n", styles: [".form>div{display:flex;flex-direction:column;justify-content:space-evenly;align-items:flex-start;margin-bottom:.5rem;width:100%}.input{width:100%}.inputs--switch{flex-direction:row;align-items:center;gap:.75rem}.switch{flex-shrink:0}.inputs--switch .field-hint{flex-basis:100%}.collection-error,.field-error{color:var(--sapNegativeColor, #b00);font-size:.75rem;margin-top:.25rem}.inputs--switch .field-error{flex-basis:100%}.field-hint{color:var(--sapContent_LabelColor, #6a6d70);font-size:.75rem;line-height:1.4;margin-top:.25rem;width:100%}\n"] }]
2110
2175
  }], ctorParameters: () => [], propDecorators: { fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: false }] }], initialValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValues", required: false }] }], fieldErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldErrors", required: false }] }], fieldChange: [{ type: i0.Output, args: ["fieldChange"] }], formSubmit: [{ type: i0.Output, args: ["formSubmit"] }], formValueChange: [{ type: i0.Output, args: ["formValueChange"] }] } });
2111
2176
 
2112
2177
  class ResourceFormDialog {
@@ -2230,6 +2295,48 @@ const decodeBase64 = (base64) => {
2230
2295
  throw new Error('Failed to decode Base64 string');
2231
2296
  }
2232
2297
  };
2298
+ /**
2299
+ * Replaces every `{{path}}` placeholder in `template` with the value resolved
2300
+ * from `resource` via the given path (dot-notation). Unresolved placeholders
2301
+ * become an empty string.
2302
+ *
2303
+ * After substitution, the resulting href is resolved against `baseHref`
2304
+ * (typically the current `window.location.href`):
2305
+ * - An absolute URL (with a protocol, e.g. `https://…`) is returned unchanged.
2306
+ * - A relative path is resolved via the standard URL algorithm:
2307
+ * a leading `/` resolves from the origin root, otherwise it resolves
2308
+ * relative to the current path.
2309
+ */
2310
+ const resolveLinkTemplate = (template, resource, baseHref = window.location.href) => {
2311
+ const substituted = template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, path) => {
2312
+ if (!resource)
2313
+ return '';
2314
+ const value = getResourceValueByJsonPath(resource, { property: path });
2315
+ return value == null ? '' : String(value);
2316
+ });
2317
+ if (!substituted || isAbsoluteUrl(substituted)) {
2318
+ return substituted;
2319
+ }
2320
+ try {
2321
+ return new URL(substituted, baseHref + '/').href;
2322
+ }
2323
+ catch {
2324
+ return substituted;
2325
+ }
2326
+ };
2327
+ /**
2328
+ * True when `value` parses as an absolute URL on its own (has a protocol,
2329
+ * e.g. `https://…`, `mailto:…`). Relative paths return `false`.
2330
+ */
2331
+ const isAbsoluteUrl = (value) => {
2332
+ try {
2333
+ new URL(value);
2334
+ return true;
2335
+ }
2336
+ catch {
2337
+ return false;
2338
+ }
2339
+ };
2233
2340
 
2234
2341
  function getFieldValue(field, resource) {
2235
2342
  if (resource) {
@@ -2331,16 +2438,18 @@ class LinkValue {
2331
2438
  ...(ngDevMode ? [{ debugName: "urlValue" }] : /* istanbul ignore next */ []));
2332
2439
  testId = input('link-value-link', /* @ts-ignore */
2333
2440
  ...(ngDevMode ? [{ debugName: "testId" }] : /* istanbul ignore next */ []));
2441
+ displayValue = input(/* @ts-ignore */
2442
+ ...(ngDevMode ? [undefined, { debugName: "displayValue" }] : /* istanbul ignore next */ []));
2334
2443
  stopPropagation(event) {
2335
2444
  event.stopPropagation();
2336
2445
  }
2337
2446
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: LinkValue, deps: [], target: i0.ɵɵFactoryTarget.Component });
2338
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.2", type: LinkValue, isStandalone: true, selector: "mfp-link-value", inputs: { urlValue: { classPropertyName: "urlValue", publicName: "urlValue", isSignal: true, isRequired: true, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ui5-link\n target=\"_blank\"\n [attr.data-testid]=\"testId()\"\n [href]=\"urlValue()\"\n [tooltip]=\"urlValue()\"\n (click)=\"stopPropagation($event)\"\n >Link</ui5-link\n>\n", styles: [""], dependencies: [{ kind: "component", type: Link, selector: "ui5-link, [ui5-link]", inputs: ["accessibilityAttributes", "accessibleDescription", "accessibleName", "accessibleNameRef", "accessibleRole", "design", "disabled", "endIcon", "href", "icon", "interactiveAreaSize", "target", "tooltip", "wrappingType"], outputs: ["ui5Click"], exportAs: ["ui5Link"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2447
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.2", type: LinkValue, isStandalone: true, selector: "mfp-link-value", inputs: { urlValue: { classPropertyName: "urlValue", publicName: "urlValue", isSignal: true, isRequired: true, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, displayValue: { classPropertyName: "displayValue", publicName: "displayValue", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ui5-link\n target=\"_blank\"\n [attr.data-testid]=\"testId()\"\n [href]=\"urlValue()\"\n [tooltip]=\"urlValue()\"\n (click)=\"stopPropagation($event)\"\n >{{ displayValue() || urlValue() }}</ui5-link\n>\n", styles: [""], dependencies: [{ kind: "component", type: Link, selector: "ui5-link, [ui5-link]", inputs: ["accessibilityAttributes", "accessibleDescription", "accessibleName", "accessibleNameRef", "accessibleRole", "design", "disabled", "endIcon", "href", "icon", "interactiveAreaSize", "target", "tooltip", "wrappingType"], outputs: ["ui5Click"], exportAs: ["ui5Link"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2339
2448
  }
2340
2449
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: LinkValue, decorators: [{
2341
2450
  type: Component,
2342
- args: [{ selector: 'mfp-link-value', imports: [Link], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<ui5-link\n target=\"_blank\"\n [attr.data-testid]=\"testId()\"\n [href]=\"urlValue()\"\n [tooltip]=\"urlValue()\"\n (click)=\"stopPropagation($event)\"\n >Link</ui5-link\n>\n" }]
2343
- }], propDecorators: { urlValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "urlValue", required: true }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }] } });
2451
+ args: [{ selector: 'mfp-link-value', imports: [Link], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<ui5-link\n target=\"_blank\"\n [attr.data-testid]=\"testId()\"\n [href]=\"urlValue()\"\n [tooltip]=\"urlValue()\"\n (click)=\"stopPropagation($event)\"\n >{{ displayValue() || urlValue() }}</ui5-link\n>\n" }]
2452
+ }], propDecorators: { urlValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "urlValue", required: true }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], displayValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayValue", required: false }] }] } });
2344
2453
 
2345
2454
  /**
2346
2455
  * Read-only display for a `propertyCollection` field. Renders the resolved
@@ -2483,8 +2592,17 @@ class ResourceField {
2483
2592
  ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
2484
2593
  isBoolLike = computed(() => this.boolValue() !== undefined, /* @ts-ignore */
2485
2594
  ...(ngDevMode ? [{ debugName: "isBoolLike" }] : /* istanbul ignore next */ []));
2486
- isUrlValue = computed(() => this.checkValidUrl(this.stringValue()), /* @ts-ignore */
2487
- ...(ngDevMode ? [{ debugName: "isUrlValue" }] : /* istanbul ignore next */ []));
2595
+ linkSettings = computed(() => this.uiSettings()?.linkSettings, /* @ts-ignore */
2596
+ ...(ngDevMode ? [{ debugName: "linkSettings" }] : /* istanbul ignore next */ []));
2597
+ linkHref = computed(() => {
2598
+ const template = this.linkSettings()?.link;
2599
+ return template
2600
+ ? resolveLinkTemplate(template, this.resource())
2601
+ : this.stringValue();
2602
+ }, /* @ts-ignore */
2603
+ ...(ngDevMode ? [{ debugName: "linkHref" }] : /* istanbul ignore next */ []));
2604
+ linkText = computed(() => this.linkSettings()?.text ?? this.stringValue(), /* @ts-ignore */
2605
+ ...(ngDevMode ? [{ debugName: "linkText" }] : /* istanbul ignore next */ []));
2488
2606
  testId = computed(() => `resource-field-${this.fieldDefinition().property}`, /* @ts-ignore */
2489
2607
  ...(ngDevMode ? [{ debugName: "testId" }] : /* istanbul ignore next */ []));
2490
2608
  buttonDisabled = computed(() => this.resource()?.isAvailable === false, /* @ts-ignore */
@@ -2552,18 +2670,6 @@ class ResourceField {
2552
2670
  }
2553
2671
  return [];
2554
2672
  }
2555
- checkValidUrl(value) {
2556
- if (!value) {
2557
- return false;
2558
- }
2559
- try {
2560
- new URL(value);
2561
- return true;
2562
- }
2563
- catch {
2564
- return false;
2565
- }
2566
- }
2567
2673
  copyValue(event) {
2568
2674
  event.stopPropagation();
2569
2675
  navigator.clipboard.writeText(this.value() || '').then(() => {
@@ -2585,7 +2691,7 @@ class ResourceField {
2585
2691
  });
2586
2692
  }
2587
2693
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ResourceField, deps: [], target: i0.ɵɵFactoryTarget.Component });
2588
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: ResourceField, isStandalone: true, selector: "mfp-resource-field", inputs: { fieldDefinition: { classPropertyName: "fieldDefinition", publicName: "fieldDefinition", isSignal: true, isRequired: true, transformFunction: null }, resource: { classPropertyName: "resource", publicName: "resource", isSignal: true, isRequired: false, transformFunction: null }, permissions: { classPropertyName: "permissions", publicName: "permissions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { buttonClick: "buttonClick" }, host: { properties: { "class.resource-field--collection": "isCollection()" } }, ngImport: i0, template: "@if (canRenderField()) {\n @if (isCollection()) {\n <mfp-resource-collection-field\n [fieldDefinition]=\"fieldDefinition()\"\n [resource]=\"resource()\"\n />\n } @else {\n @let displayType = displayAs();\n <span [attr.data-testid]=\"testId()\" [style]=\"cssStyles()\">\n @if (displayType === 'secret') {\n <span class=\"secret-value-container\">\n <mfp-secret-value\n [isVisible]=\"isVisible()\"\n [testId]=\"testId() + '-secret'\"\n [value]=\"value()\"\n />\n <ui5-icon\n class=\"toggle-icon\"\n mode=\"Interactive\"\n [accessibleName]=\"isVisible() ? 'Hide value' : 'Show value'\"\n [attr.data-testid]=\"testId() + '-secret-toggle'\"\n [name]=\"isVisible() ? 'hide' : 'show'\"\n (click)=\"toggleVisibility($event)\"\n />\n </span>\n } @else if (displayType === 'boolIcon' && isBoolLike()) {\n <mfp-boolean-value\n [boolValue]=\"boolValue()!\"\n [testId]=\"testId() + '-boolean'\"\n />\n } @else if (displayType === 'link' && isUrlValue()) {\n <mfp-link-value\n [testId]=\"testId() + '-link'\"\n [urlValue]=\"stringValue()!\"\n />\n } @else if (displayType === 'tooltip') {\n <ui5-icon\n [accessibleName]=\"stringValue()!\"\n [attr.data-testid]=\"testId() + '-tooltip'\"\n [name]=\"tooltipIcon() ?? 'hint'\"\n [showTooltip]=\"true\"\n />\n } @else if (displayType === 'alert') {\n @if (!value()) {\n <ui5-icon\n design=\"Critical\"\n name=\"alert\"\n [accessibleName]=\"resource()?.accessibleName ?? ''\"\n [attr.data-testid]=\"testId() + '-icon'\"\n [showTooltip]=\"true\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n } @else if (displayType === 'img' && value()) {\n <img alt=\"Resource image\" class=\"image-cell\" [src]=\"value()\" />\n } @else if (displayType === 'button') {\n @let buttonSettings = uiSettings()?.buttonSettings;\n <ui5-button\n [accessibleName]=\"buttonAccessibleName()\"\n [attr.data-testid]=\"testId() + '-button'\"\n [design]=\"buttonSettings?.design\"\n [disabled]=\"buttonDisabled()\"\n [endIcon]=\"buttonSettings?.endIcon\"\n [icon]=\"buttonSettings?.icon\"\n [tooltip]=\"buttonSettings?.tooltip\"\n (click)=\"buttonClicked($event)\"\n >{{ buttonSettings?.text }}</ui5-button\n >&nbsp;\n } @else if (displayType === 'tag') {\n <mfp-tag-list-value\n [tagSettings]=\"uiSettings()?.tagSettings\"\n [tags]=\"tags()\"\n [testId]=\"testId() + '-tags'\"\n />\n } @else {\n {{ displayValue() }}\n }\n </span>\n\n @if (withCopyButton()) {\n <ui5-icon\n class=\"toggle-icon\"\n mode=\"Interactive\"\n [accessibleName]=\"copySuccess() ? 'Copied' : 'Copy value'\"\n [attr.data-testid]=\"testId() + '-copy'\"\n [design]=\"copySuccess() ? 'Positive' : 'Default'\"\n [name]=\"copySuccess() ? 'accept' : 'copy'\"\n (click)=\"copyValue($event)\"\n />\n }\n }\n}\n", styles: [":host{display:inline-flex;align-items:center}:host(.resource-field--collection){display:block;width:100%}.secret-value-container{display:flex;align-items:center;gap:.25rem}.toggle-icon{cursor:pointer;transition:color .2s ease;padding:.25rem}.toggle-icon:hover{color:var(--sapButton_Hover_TextColor, #0854a0)}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => Icon), selector: "ui5-icon, [ui5-icon]", inputs: ["accessibleName", "design", "mode", "name", "showTooltip"], outputs: ["ui5Click"], exportAs: ["ui5Icon"] }, { kind: "component", type: i0.forwardRef(() => BooleanValue), selector: "mfp-boolean-value", inputs: ["boolValue", "testId"] }, { kind: "component", type: i0.forwardRef(() => LinkValue), selector: "mfp-link-value", inputs: ["urlValue", "testId"] }, { kind: "component", type: i0.forwardRef(() => SecretValue), selector: "mfp-secret-value", inputs: ["value", "isVisible", "testId"] }, { kind: "component", type: i0.forwardRef(() => Button), selector: "ui5-button, [ui5-button]", inputs: ["accessibilityAttributes", "accessibleDescription", "accessibleName", "accessibleNameRef", "accessibleRole", "design", "disabled", "endIcon", "form", "icon", "loading", "loadingDelay", "submits", "tooltip", "type"], outputs: ["ui5Click"], exportAs: ["ui5Button"] }, { kind: "component", type: i0.forwardRef(() => TagListValue), selector: "mfp-tag-list-value", inputs: ["tags", "tagSettings", "testId"] }, { kind: "component", type: i0.forwardRef(() => ResourceCollectionField), selector: "mfp-resource-collection-field", inputs: ["fieldDefinition", "resource"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.ShadowDom });
2694
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: ResourceField, isStandalone: true, selector: "mfp-resource-field", inputs: { fieldDefinition: { classPropertyName: "fieldDefinition", publicName: "fieldDefinition", isSignal: true, isRequired: true, transformFunction: null }, resource: { classPropertyName: "resource", publicName: "resource", isSignal: true, isRequired: false, transformFunction: null }, permissions: { classPropertyName: "permissions", publicName: "permissions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { buttonClick: "buttonClick" }, host: { properties: { "class.resource-field--collection": "isCollection()" } }, ngImport: i0, template: "@if (canRenderField()) {\n @if (isCollection()) {\n <mfp-resource-collection-field\n [fieldDefinition]=\"fieldDefinition()\"\n [resource]=\"resource()\"\n />\n } @else {\n @let displayType = displayAs();\n <span [attr.data-testid]=\"testId()\" [style]=\"cssStyles()\">\n @if (displayType === 'secret') {\n <span class=\"secret-value-container\">\n <mfp-secret-value\n [isVisible]=\"isVisible()\"\n [testId]=\"testId() + '-secret'\"\n [value]=\"value()\"\n />\n <ui5-icon\n class=\"toggle-icon\"\n mode=\"Interactive\"\n [accessibleName]=\"isVisible() ? 'Hide value' : 'Show value'\"\n [attr.data-testid]=\"testId() + '-secret-toggle'\"\n [name]=\"isVisible() ? 'hide' : 'show'\"\n (click)=\"toggleVisibility($event)\"\n />\n </span>\n } @else if (displayType === 'boolIcon' && isBoolLike()) {\n <mfp-boolean-value\n [boolValue]=\"boolValue()!\"\n [testId]=\"testId() + '-boolean'\"\n />\n } @else if (displayType === 'link' && linkHref()) {\n <mfp-link-value\n [displayValue]=\"linkText()\"\n [testId]=\"testId() + '-link'\"\n [urlValue]=\"linkHref()!\"\n />\n } @else if (displayType === 'tooltip') {\n <ui5-icon\n [accessibleName]=\"stringValue()!\"\n [attr.data-testid]=\"testId() + '-tooltip'\"\n [name]=\"tooltipIcon() ?? 'hint'\"\n [showTooltip]=\"true\"\n />\n } @else if (displayType === 'alert') {\n @if (!value()) {\n <ui5-icon\n design=\"Critical\"\n name=\"alert\"\n [accessibleName]=\"resource()?.accessibleName ?? ''\"\n [attr.data-testid]=\"testId() + '-icon'\"\n [showTooltip]=\"true\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n } @else if (displayType === 'img' && value()) {\n <img alt=\"Resource image\" class=\"image-cell\" [src]=\"value()\" />\n } @else if (displayType === 'button') {\n @let buttonSettings = uiSettings()?.buttonSettings;\n <ui5-button\n [accessibleName]=\"buttonAccessibleName()\"\n [attr.data-testid]=\"testId() + '-button'\"\n [design]=\"buttonSettings?.design\"\n [disabled]=\"buttonDisabled()\"\n [endIcon]=\"buttonSettings?.endIcon\"\n [icon]=\"buttonSettings?.icon\"\n [tooltip]=\"buttonSettings?.tooltip\"\n (click)=\"buttonClicked($event)\"\n >{{ buttonSettings?.text }}</ui5-button\n >&nbsp;\n } @else if (displayType === 'tag') {\n <mfp-tag-list-value\n [tagSettings]=\"uiSettings()?.tagSettings\"\n [tags]=\"tags()\"\n [testId]=\"testId() + '-tags'\"\n />\n } @else {\n {{ displayValue() }}\n }\n </span>\n\n @if (withCopyButton()) {\n <ui5-icon\n class=\"toggle-icon\"\n mode=\"Interactive\"\n [accessibleName]=\"copySuccess() ? 'Copied' : 'Copy value'\"\n [attr.data-testid]=\"testId() + '-copy'\"\n [design]=\"copySuccess() ? 'Positive' : 'Default'\"\n [name]=\"copySuccess() ? 'accept' : 'copy'\"\n (click)=\"copyValue($event)\"\n />\n }\n }\n}\n", styles: [":host{display:inline-flex;align-items:center}:host(.resource-field--collection){display:block;width:100%}.secret-value-container{display:flex;align-items:center;gap:.25rem}.toggle-icon{cursor:pointer;transition:color .2s ease;padding:.25rem}.toggle-icon:hover{color:var(--sapButton_Hover_TextColor, #0854a0)}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => Icon), selector: "ui5-icon, [ui5-icon]", inputs: ["accessibleName", "design", "mode", "name", "showTooltip"], outputs: ["ui5Click"], exportAs: ["ui5Icon"] }, { kind: "component", type: i0.forwardRef(() => BooleanValue), selector: "mfp-boolean-value", inputs: ["boolValue", "testId"] }, { kind: "component", type: i0.forwardRef(() => LinkValue), selector: "mfp-link-value", inputs: ["urlValue", "testId", "displayValue"] }, { kind: "component", type: i0.forwardRef(() => SecretValue), selector: "mfp-secret-value", inputs: ["value", "isVisible", "testId"] }, { kind: "component", type: i0.forwardRef(() => Button), selector: "ui5-button, [ui5-button]", inputs: ["accessibilityAttributes", "accessibleDescription", "accessibleName", "accessibleNameRef", "accessibleRole", "design", "disabled", "endIcon", "form", "icon", "loading", "loadingDelay", "submits", "tooltip", "type"], outputs: ["ui5Click"], exportAs: ["ui5Button"] }, { kind: "component", type: i0.forwardRef(() => TagListValue), selector: "mfp-tag-list-value", inputs: ["tags", "tagSettings", "testId"] }, { kind: "component", type: i0.forwardRef(() => ResourceCollectionField), selector: "mfp-resource-collection-field", inputs: ["fieldDefinition", "resource"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.ShadowDom });
2589
2695
  }
2590
2696
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ResourceField, decorators: [{
2591
2697
  type: Component,
@@ -2603,7 +2709,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
2603
2709
  forwardRef(() => ResourceCollectionField),
2604
2710
  ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.ShadowDom, host: {
2605
2711
  '[class.resource-field--collection]': 'isCollection()',
2606
- }, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "@if (canRenderField()) {\n @if (isCollection()) {\n <mfp-resource-collection-field\n [fieldDefinition]=\"fieldDefinition()\"\n [resource]=\"resource()\"\n />\n } @else {\n @let displayType = displayAs();\n <span [attr.data-testid]=\"testId()\" [style]=\"cssStyles()\">\n @if (displayType === 'secret') {\n <span class=\"secret-value-container\">\n <mfp-secret-value\n [isVisible]=\"isVisible()\"\n [testId]=\"testId() + '-secret'\"\n [value]=\"value()\"\n />\n <ui5-icon\n class=\"toggle-icon\"\n mode=\"Interactive\"\n [accessibleName]=\"isVisible() ? 'Hide value' : 'Show value'\"\n [attr.data-testid]=\"testId() + '-secret-toggle'\"\n [name]=\"isVisible() ? 'hide' : 'show'\"\n (click)=\"toggleVisibility($event)\"\n />\n </span>\n } @else if (displayType === 'boolIcon' && isBoolLike()) {\n <mfp-boolean-value\n [boolValue]=\"boolValue()!\"\n [testId]=\"testId() + '-boolean'\"\n />\n } @else if (displayType === 'link' && isUrlValue()) {\n <mfp-link-value\n [testId]=\"testId() + '-link'\"\n [urlValue]=\"stringValue()!\"\n />\n } @else if (displayType === 'tooltip') {\n <ui5-icon\n [accessibleName]=\"stringValue()!\"\n [attr.data-testid]=\"testId() + '-tooltip'\"\n [name]=\"tooltipIcon() ?? 'hint'\"\n [showTooltip]=\"true\"\n />\n } @else if (displayType === 'alert') {\n @if (!value()) {\n <ui5-icon\n design=\"Critical\"\n name=\"alert\"\n [accessibleName]=\"resource()?.accessibleName ?? ''\"\n [attr.data-testid]=\"testId() + '-icon'\"\n [showTooltip]=\"true\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n } @else if (displayType === 'img' && value()) {\n <img alt=\"Resource image\" class=\"image-cell\" [src]=\"value()\" />\n } @else if (displayType === 'button') {\n @let buttonSettings = uiSettings()?.buttonSettings;\n <ui5-button\n [accessibleName]=\"buttonAccessibleName()\"\n [attr.data-testid]=\"testId() + '-button'\"\n [design]=\"buttonSettings?.design\"\n [disabled]=\"buttonDisabled()\"\n [endIcon]=\"buttonSettings?.endIcon\"\n [icon]=\"buttonSettings?.icon\"\n [tooltip]=\"buttonSettings?.tooltip\"\n (click)=\"buttonClicked($event)\"\n >{{ buttonSettings?.text }}</ui5-button\n >&nbsp;\n } @else if (displayType === 'tag') {\n <mfp-tag-list-value\n [tagSettings]=\"uiSettings()?.tagSettings\"\n [tags]=\"tags()\"\n [testId]=\"testId() + '-tags'\"\n />\n } @else {\n {{ displayValue() }}\n }\n </span>\n\n @if (withCopyButton()) {\n <ui5-icon\n class=\"toggle-icon\"\n mode=\"Interactive\"\n [accessibleName]=\"copySuccess() ? 'Copied' : 'Copy value'\"\n [attr.data-testid]=\"testId() + '-copy'\"\n [design]=\"copySuccess() ? 'Positive' : 'Default'\"\n [name]=\"copySuccess() ? 'accept' : 'copy'\"\n (click)=\"copyValue($event)\"\n />\n }\n }\n}\n", styles: [":host{display:inline-flex;align-items:center}:host(.resource-field--collection){display:block;width:100%}.secret-value-container{display:flex;align-items:center;gap:.25rem}.toggle-icon{cursor:pointer;transition:color .2s ease;padding:.25rem}.toggle-icon:hover{color:var(--sapButton_Hover_TextColor, #0854a0)}\n"] }]
2712
+ }, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "@if (canRenderField()) {\n @if (isCollection()) {\n <mfp-resource-collection-field\n [fieldDefinition]=\"fieldDefinition()\"\n [resource]=\"resource()\"\n />\n } @else {\n @let displayType = displayAs();\n <span [attr.data-testid]=\"testId()\" [style]=\"cssStyles()\">\n @if (displayType === 'secret') {\n <span class=\"secret-value-container\">\n <mfp-secret-value\n [isVisible]=\"isVisible()\"\n [testId]=\"testId() + '-secret'\"\n [value]=\"value()\"\n />\n <ui5-icon\n class=\"toggle-icon\"\n mode=\"Interactive\"\n [accessibleName]=\"isVisible() ? 'Hide value' : 'Show value'\"\n [attr.data-testid]=\"testId() + '-secret-toggle'\"\n [name]=\"isVisible() ? 'hide' : 'show'\"\n (click)=\"toggleVisibility($event)\"\n />\n </span>\n } @else if (displayType === 'boolIcon' && isBoolLike()) {\n <mfp-boolean-value\n [boolValue]=\"boolValue()!\"\n [testId]=\"testId() + '-boolean'\"\n />\n } @else if (displayType === 'link' && linkHref()) {\n <mfp-link-value\n [displayValue]=\"linkText()\"\n [testId]=\"testId() + '-link'\"\n [urlValue]=\"linkHref()!\"\n />\n } @else if (displayType === 'tooltip') {\n <ui5-icon\n [accessibleName]=\"stringValue()!\"\n [attr.data-testid]=\"testId() + '-tooltip'\"\n [name]=\"tooltipIcon() ?? 'hint'\"\n [showTooltip]=\"true\"\n />\n } @else if (displayType === 'alert') {\n @if (!value()) {\n <ui5-icon\n design=\"Critical\"\n name=\"alert\"\n [accessibleName]=\"resource()?.accessibleName ?? ''\"\n [attr.data-testid]=\"testId() + '-icon'\"\n [showTooltip]=\"true\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n } @else if (displayType === 'img' && value()) {\n <img alt=\"Resource image\" class=\"image-cell\" [src]=\"value()\" />\n } @else if (displayType === 'button') {\n @let buttonSettings = uiSettings()?.buttonSettings;\n <ui5-button\n [accessibleName]=\"buttonAccessibleName()\"\n [attr.data-testid]=\"testId() + '-button'\"\n [design]=\"buttonSettings?.design\"\n [disabled]=\"buttonDisabled()\"\n [endIcon]=\"buttonSettings?.endIcon\"\n [icon]=\"buttonSettings?.icon\"\n [tooltip]=\"buttonSettings?.tooltip\"\n (click)=\"buttonClicked($event)\"\n >{{ buttonSettings?.text }}</ui5-button\n >&nbsp;\n } @else if (displayType === 'tag') {\n <mfp-tag-list-value\n [tagSettings]=\"uiSettings()?.tagSettings\"\n [tags]=\"tags()\"\n [testId]=\"testId() + '-tags'\"\n />\n } @else {\n {{ displayValue() }}\n }\n </span>\n\n @if (withCopyButton()) {\n <ui5-icon\n class=\"toggle-icon\"\n mode=\"Interactive\"\n [accessibleName]=\"copySuccess() ? 'Copied' : 'Copy value'\"\n [attr.data-testid]=\"testId() + '-copy'\"\n [design]=\"copySuccess() ? 'Positive' : 'Default'\"\n [name]=\"copySuccess() ? 'accept' : 'copy'\"\n (click)=\"copyValue($event)\"\n />\n }\n }\n}\n", styles: [":host{display:inline-flex;align-items:center}:host(.resource-field--collection){display:block;width:100%}.secret-value-container{display:flex;align-items:center;gap:.25rem}.toggle-icon{cursor:pointer;transition:color .2s ease;padding:.25rem}.toggle-icon:hover{color:var(--sapButton_Hover_TextColor, #0854a0)}\n"] }]
2607
2713
  }], propDecorators: { fieldDefinition: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldDefinition", required: true }] }], resource: [{ type: i0.Input, args: [{ isSignal: true, alias: "resource", required: false }] }], permissions: [{ type: i0.Input, args: [{ isSignal: true, alias: "permissions", required: false }] }], buttonClick: [{ type: i0.Output, args: ["buttonClick"] }] } });
2608
2714
 
2609
2715
  const processGroupFields = (fields) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmfp/ngx",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,6 +26,20 @@ interface PropertyField {
26
26
  /** Ordered list of transforms applied to the resolved string. */
27
27
  transform?: TransformType[];
28
28
  }
29
+ /** Configuration for a `displayAs: 'link'` cell. */
30
+ interface LinkSettings {
31
+ /**
32
+ * href template. Supports `{{path}}` placeholders resolved against the
33
+ * resource (e.g. `/{{metadata.name}}/accounts`). Absolute or relative.
34
+ * When omitted, the field value is used as the href.
35
+ */
36
+ link?: string;
37
+ /**
38
+ * Static visible text for the link. When set, overrides the field value as
39
+ * the link label. When omitted, the field value is used as the link text.
40
+ */
41
+ text?: string;
42
+ }
29
43
  /** Appearance settings for tag chip rendering. */
30
44
  interface TagSettings {
31
45
  design?: 'Neutral' | 'Positive' | 'Critical' | 'Negative' | 'Information' | 'Set1' | 'Set2';
@@ -41,6 +55,8 @@ interface UiSettings {
41
55
  buttonSettings?: ButtonSettings;
42
56
  /** Tag chip configuration — only used when `displayAs` is `'tag'`. */
43
57
  tagSettings?: TagSettings;
58
+ /** Link configuration — only used when `displayAs` is `'link'`. */
59
+ linkSettings?: LinkSettings;
44
60
  /** SAP UI5 icon name shown as the tooltip trigger icon. */
45
61
  tooltipIcon?: string;
46
62
  /** When `true`, a copy-to-clipboard button is rendered next to the value. */
@@ -526,6 +542,22 @@ interface FormFieldDefinition {
526
542
  values?: string[];
527
543
  /** When `true`, the field is disabled (not interactive). */
528
544
  disabled?: boolean;
545
+ /** UI5 input type rendered by `mfp-declarative-form`. */
546
+ inputType?: 'Text' | 'Password' | 'Switch';
547
+ /**
548
+ * When `inputType` is `Password`, renders a show/hide icon inside the input.
549
+ * Defaults to `true`; set to `false` to keep the field masked without a toggle.
550
+ */
551
+ showPasswordToggle?: boolean;
552
+ /** UI5 input placeholder (functional, not example values). */
553
+ placeholder?: string;
554
+ /** Persistent help text below the control; never submitted with the form. */
555
+ hint?: string;
556
+ /**
557
+ * When `true`, the host should omit this field from read queries and skip
558
+ * empty values on edit submit (e.g. write-only secrets).
559
+ */
560
+ writeOnly?: boolean;
529
561
  /** Controls when `fieldChange` is emitted for this field. When omitted, `fieldChange` is never emitted. */
530
562
  validation?: 'onBlur' | 'onChange';
531
563
  /**
@@ -553,6 +585,8 @@ interface FormFieldChangeEvent {
553
585
  /** Map of field names to their current validation error messages (`null` = no error). */
554
586
  type FormFieldErrors = Record<string, string | null>;
555
587
 
588
+ declare function coerceBoolean(value: unknown): boolean;
589
+
556
590
  declare class DeclarativeForm<T extends GenericResource> {
557
591
  readonly fields: _angular_core.InputSignal<FormFieldDefinition[]>;
558
592
  readonly initialValues: _angular_core.InputSignal<T>;
@@ -562,9 +596,18 @@ declare class DeclarativeForm<T extends GenericResource> {
562
596
  readonly formValueChange: _angular_core.OutputEmitterRef<Record<string, unknown>>;
563
597
  readonly form: FormGroup;
564
598
  protected readonly collectionSeeds: _angular_core.WritableSignal<Record<string, Record<string, unknown>[]>>;
599
+ private readonly passwordVisible;
565
600
  private readonly fb;
566
601
  constructor();
567
602
  setFormControlValue($event: Event, field: FormFieldDefinition): void;
603
+ setSwitchValue($event: Event, field: FormFieldDefinition): void;
604
+ protected readonly coerceChecked: typeof coerceBoolean;
605
+ isPasswordVisible(field: FormFieldDefinition): boolean;
606
+ passwordInputType(field: FormFieldDefinition): 'Text' | 'Password';
607
+ passwordToggleIcon(field: FormFieldDefinition): 'hide' | 'show';
608
+ passwordToggleLabel(field: FormFieldDefinition): string;
609
+ shouldShowPasswordToggle(field: FormFieldDefinition): boolean;
610
+ togglePasswordVisibility(field: FormFieldDefinition, event: Event): void;
568
611
  onCollectionValueChange(field: FormFieldDefinition, entries: Record<string, unknown>[]): void;
569
612
  getError(name: string): string | null;
570
613
  getValueState(name: string): 'None' | 'Negative';
@@ -1468,7 +1511,9 @@ declare class ResourceField<T extends GenericResource, F extends FieldDefinition
1468
1511
  }>;
1469
1512
  displayValue: _angular_core.Signal<string>;
1470
1513
  isBoolLike: _angular_core.Signal<boolean>;
1471
- isUrlValue: _angular_core.Signal<boolean>;
1514
+ linkSettings: _angular_core.Signal<_openmfp_ngx.LinkSettings | undefined>;
1515
+ linkHref: _angular_core.Signal<string | undefined>;
1516
+ linkText: _angular_core.Signal<string | undefined>;
1472
1517
  testId: _angular_core.Signal<string>;
1473
1518
  buttonDisabled: _angular_core.Signal<boolean>;
1474
1519
  buttonAccessibleName: _angular_core.Signal<string>;
@@ -1483,7 +1528,6 @@ declare class ResourceField<T extends GenericResource, F extends FieldDefinition
1483
1528
  private normalizeBoolean;
1484
1529
  private normalizeString;
1485
1530
  private normalizeTagsArray;
1486
- private checkValidUrl;
1487
1531
  copyValue(event: Event): void;
1488
1532
  protected buttonClicked(event: MouseEvent): void;
1489
1533
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ResourceField<any, any>, never>;
@@ -1506,9 +1550,10 @@ declare class BooleanValue {
1506
1550
  declare class LinkValue {
1507
1551
  urlValue: _angular_core.InputSignal<string>;
1508
1552
  testId: _angular_core.InputSignal<string>;
1553
+ displayValue: _angular_core.InputSignal<string | undefined>;
1509
1554
  stopPropagation(event: Event): void;
1510
1555
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<LinkValue, never>;
1511
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<LinkValue, "mfp-link-value", never, { "urlValue": { "alias": "urlValue"; "required": true; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
1556
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<LinkValue, "mfp-link-value", never, { "urlValue": { "alias": "urlValue"; "required": true; "isSignal": true; }; "testId": { "alias": "testId"; "required": false; "isSignal": true; }; "displayValue": { "alias": "displayValue"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
1512
1557
  }
1513
1558
 
1514
1559
  declare class SecretValue {
@@ -1588,4 +1633,4 @@ declare class MockCard {
1588
1633
  }
1589
1634
 
1590
1635
  export { BooleanValue, CARD_TYPES, DASHBOARD_I18N_KEYS, Dashboard, DashboardI18nService, DeclarativeForm, DeclarativeTable, DeclarativeTableCard, DeleteConfirmationDialog, EN_DEFAULTS, Favorites, LinkValue, MockCard, ResourceField, ResourceFormDialog, SanitizeHtmlPipe, SecretValue, ServiceStatusCard, TagListValue, VisitedServiceCard, WhatsNew, defineDashboardElementMethods };
1591
- export type { ButtonSettings, CardConfig, CardsType, CssRule, DashboardButtonsSettings, DashboardConfig, DashboardI18nKey, DashboardTranslations, DeleteResourceConfirmationConfig, FieldDefinition, FieldFilterDefinition, FormFieldChangeEvent, FormFieldDefinition, FormFieldErrors, GenericResource, IconDesignType, ModalSettings, MountCfg, PropertyField, ResourceFieldButtonClickEvent, ResourceFormConfig, RuleCondition, SectionConfig, ServiceStatusItem, ServiceStatusValue, TableCardButtonSettings, TableCardConfig, TableCardFormState, TableCardSearchConfig, TableConfig, TableFieldDefinition, TagSettings, TransformType, UiSettings, ValueRule };
1636
+ export type { ButtonSettings, CardConfig, CardsType, CssRule, DashboardButtonsSettings, DashboardConfig, DashboardI18nKey, DashboardTranslations, DeleteResourceConfirmationConfig, FieldDefinition, FieldFilterDefinition, FormFieldChangeEvent, FormFieldDefinition, FormFieldErrors, GenericResource, IconDesignType, LinkSettings, ModalSettings, MountCfg, PropertyField, ResourceFieldButtonClickEvent, ResourceFormConfig, RuleCondition, SectionConfig, ServiceStatusItem, ServiceStatusValue, TableCardButtonSettings, TableCardConfig, TableCardFormState, TableCardSearchConfig, TableConfig, TableFieldDefinition, TagSettings, TransformType, UiSettings, ValueRule };