@fuentis/phoenix-ui 0.0.9-alpha.550 → 0.0.9-alpha.552
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.
- package/fesm2022/fuentis-phoenix-ui.mjs +844 -134
- package/fesm2022/fuentis-phoenix-ui.mjs.map +1 -1
- package/index.d.ts +170 -16
- package/package.json +1 -1
|
@@ -8079,29 +8079,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8079
8079
|
}] });
|
|
8080
8080
|
|
|
8081
8081
|
class ReadOnlyInputV2Component {
|
|
8082
|
+
/** Field metadata (type, key, options, labels, etc.) */
|
|
8082
8083
|
field;
|
|
8084
|
+
/** Parent FormGroup that contains the control referenced by field.configuration.key */
|
|
8083
8085
|
form;
|
|
8086
|
+
/** Used to automatically unsubscribe from valueChanges on destroy */
|
|
8084
8087
|
dr = inject(DestroyRef);
|
|
8088
|
+
/**
|
|
8089
|
+
* Internal reactive signal holding the current control value.
|
|
8090
|
+
* We keep a local signal to optimize OnPush change detection and computed selectors.
|
|
8091
|
+
*/
|
|
8085
8092
|
_v = signal(null, ...(ngDevMode ? [{ debugName: "_v" }] : []));
|
|
8086
8093
|
ngOnInit() {
|
|
8094
|
+
// Initial sync of the control value into the local signal
|
|
8087
8095
|
this.sync();
|
|
8096
|
+
// Keep local signal in sync with the form control value
|
|
8088
8097
|
this.ctrl()?.valueChanges
|
|
8089
8098
|
.pipe(takeUntilDestroyed(this.dr))
|
|
8090
8099
|
.subscribe(() => this.sync());
|
|
8091
8100
|
}
|
|
8101
|
+
/** Control key resolved from MetaFieldConfig */
|
|
8092
8102
|
get key() {
|
|
8093
8103
|
return this.field?.configuration?.key ?? '';
|
|
8094
8104
|
}
|
|
8105
|
+
/** Meta field type (TEXT, DATE, ASSIGN, UPLOAD, etc.) */
|
|
8095
8106
|
get type() {
|
|
8096
8107
|
return this.field?.configuration?.type ?? 'TEXT';
|
|
8097
8108
|
}
|
|
8109
|
+
/** Shortcut to the underlying FormControl */
|
|
8098
8110
|
ctrl() {
|
|
8099
8111
|
return this.form.get(this.key);
|
|
8100
8112
|
}
|
|
8113
|
+
/**
|
|
8114
|
+
* Synchronizes the FormControl value into the local signal.
|
|
8115
|
+
* This keeps computed properties reactive and OnPush-friendly.
|
|
8116
|
+
*/
|
|
8101
8117
|
sync() {
|
|
8102
8118
|
this._v.set(this.ctrl()?.value ?? null);
|
|
8103
8119
|
}
|
|
8104
8120
|
// ---------- helpers ----------
|
|
8121
|
+
/**
|
|
8122
|
+
* Resolves language suffix used by backend DTOs (labelKeyValEn/De/Sr).
|
|
8123
|
+
* This is used to pick the correct localized label for option objects.
|
|
8124
|
+
*/
|
|
8105
8125
|
getLangSuffix() {
|
|
8106
8126
|
const lang = (localStorage.getItem('language') ?? 'en').toLowerCase();
|
|
8107
8127
|
if (lang.startsWith('de'))
|
|
@@ -8110,12 +8130,19 @@ class ReadOnlyInputV2Component {
|
|
|
8110
8130
|
return 'Sr';
|
|
8111
8131
|
return 'En';
|
|
8112
8132
|
}
|
|
8113
|
-
/**
|
|
8133
|
+
/**
|
|
8134
|
+
* Boolean renderer (for SWITCH / CHECKBOX).
|
|
8135
|
+
* Returns boolean value or null if the value is not a boolean.
|
|
8136
|
+
*/
|
|
8114
8137
|
bool = computed(() => {
|
|
8115
8138
|
const v = this._v();
|
|
8116
8139
|
return typeof v === 'boolean' ? v : null;
|
|
8117
8140
|
}, ...(ngDevMode ? [{ debugName: "bool" }] : []));
|
|
8118
|
-
/**
|
|
8141
|
+
/**
|
|
8142
|
+
* DATE renderer.
|
|
8143
|
+
* Supports both Date instances and ISO-like string values.
|
|
8144
|
+
* Returns a valid Date object or null if parsing fails.
|
|
8145
|
+
*/
|
|
8119
8146
|
dateValue = computed(() => {
|
|
8120
8147
|
const v = this._v();
|
|
8121
8148
|
if (!v)
|
|
@@ -8125,7 +8152,11 @@ class ReadOnlyInputV2Component {
|
|
|
8125
8152
|
const d = new Date(String(v));
|
|
8126
8153
|
return isNaN(d.getTime()) ? null : d;
|
|
8127
8154
|
}, ...(ngDevMode ? [{ debugName: "dateValue" }] : []));
|
|
8128
|
-
/**
|
|
8155
|
+
/**
|
|
8156
|
+
* START_DUE_DATE renderer.
|
|
8157
|
+
* Expects shape: { startDate, endDate } (Date or string).
|
|
8158
|
+
* Returns normalized Date objects or null if invalid.
|
|
8159
|
+
*/
|
|
8129
8160
|
startDue = computed(() => {
|
|
8130
8161
|
const v = this._v();
|
|
8131
8162
|
if (!v || typeof v !== 'object')
|
|
@@ -8141,7 +8172,10 @@ class ReadOnlyInputV2Component {
|
|
|
8141
8172
|
endDate: ed && !isNaN(ed.getTime()) ? ed : null,
|
|
8142
8173
|
};
|
|
8143
8174
|
}, ...(ngDevMode ? [{ debugName: "startDue" }] : []));
|
|
8144
|
-
/**
|
|
8175
|
+
/**
|
|
8176
|
+
* ASSIGN renderer.
|
|
8177
|
+
* Normalizes various backend DTO shapes into a unified view model.
|
|
8178
|
+
*/
|
|
8145
8179
|
assign = computed(() => {
|
|
8146
8180
|
const v = this._v();
|
|
8147
8181
|
if (!v || typeof v !== 'object')
|
|
@@ -8153,7 +8187,10 @@ class ReadOnlyInputV2Component {
|
|
|
8153
8187
|
phone: v?.phone ?? null,
|
|
8154
8188
|
};
|
|
8155
8189
|
}, ...(ngDevMode ? [{ debugName: "assign" }] : []));
|
|
8156
|
-
/**
|
|
8190
|
+
/**
|
|
8191
|
+
* UPLOAD renderer.
|
|
8192
|
+
* Supports common upload DTO shapes (fileName/name, size, type).
|
|
8193
|
+
*/
|
|
8157
8194
|
upload = computed(() => {
|
|
8158
8195
|
const v = this._v();
|
|
8159
8196
|
if (!v || typeof v !== 'object')
|
|
@@ -8164,16 +8201,21 @@ class ReadOnlyInputV2Component {
|
|
|
8164
8201
|
type: v?.type ?? null,
|
|
8165
8202
|
};
|
|
8166
8203
|
}, ...(ngDevMode ? [{ debugName: "upload" }] : []));
|
|
8167
|
-
/**
|
|
8204
|
+
/**
|
|
8205
|
+
* Single-select option label resolver (SS_OPTION / SS_OPTION_OBJECT_BASED).
|
|
8206
|
+
* - If value is primitive -> find matching option in configuration.options
|
|
8207
|
+
* - If value is object -> resolve best display label using common DTO fields
|
|
8208
|
+
*/
|
|
8168
8209
|
ssLabel = computed(() => {
|
|
8169
8210
|
const v = this._v();
|
|
8170
8211
|
if (v === null || v === undefined || v === '')
|
|
8171
8212
|
return null;
|
|
8172
|
-
//
|
|
8213
|
+
// Primitive value: resolve label from options list
|
|
8173
8214
|
if (typeof v !== 'object') {
|
|
8174
8215
|
const opt = (this.field.configuration.options ?? []).find((x) => x?.value === v);
|
|
8175
8216
|
return opt?.label ?? String(v);
|
|
8176
8217
|
}
|
|
8218
|
+
// Object value: resolve localized label from DTO
|
|
8177
8219
|
const suffix = this.getLangSuffix();
|
|
8178
8220
|
const key = `labelKeyVal${suffix}`;
|
|
8179
8221
|
return (v?.label ??
|
|
@@ -8183,7 +8225,10 @@ class ReadOnlyInputV2Component {
|
|
|
8183
8225
|
v?.value ??
|
|
8184
8226
|
null);
|
|
8185
8227
|
}, ...(ngDevMode ? [{ debugName: "ssLabel" }] : []));
|
|
8186
|
-
/**
|
|
8228
|
+
/**
|
|
8229
|
+
* Multi-select option label resolver (MS_OPTION).
|
|
8230
|
+
* Joins multiple labels into a single string with truncation after maxItems.
|
|
8231
|
+
*/
|
|
8187
8232
|
msLabel = computed(() => {
|
|
8188
8233
|
const v = this._v();
|
|
8189
8234
|
if (!Array.isArray(v) || v.length === 0)
|
|
@@ -8201,7 +8246,10 @@ class ReadOnlyInputV2Component {
|
|
|
8201
8246
|
? labels.join(', ')
|
|
8202
8247
|
: `${labels.slice(0, maxItems).join(', ')} ...`;
|
|
8203
8248
|
}, ...(ngDevMode ? [{ debugName: "msLabel" }] : []));
|
|
8204
|
-
/**
|
|
8249
|
+
/**
|
|
8250
|
+
* Generic fallback display value.
|
|
8251
|
+
* Ensures that every field type has a safe string representation.
|
|
8252
|
+
*/
|
|
8205
8253
|
displayText = computed(() => {
|
|
8206
8254
|
const v = this._v();
|
|
8207
8255
|
if (v === null || v === undefined || v === '')
|
|
@@ -8217,7 +8265,7 @@ class ReadOnlyInputV2Component {
|
|
|
8217
8265
|
if (this.type === 'ASSIGN' || this.type === 'ASSIGN_ASSET') {
|
|
8218
8266
|
return this.assign()?.name ?? null;
|
|
8219
8267
|
}
|
|
8220
|
-
//
|
|
8268
|
+
// Text areas and editors often contain HTML
|
|
8221
8269
|
if (this.type === 'TEXT_AREA' || this.type === 'TEXT_EDITOR') {
|
|
8222
8270
|
return typeof v === 'string' ? v : String(v);
|
|
8223
8271
|
}
|
|
@@ -8226,7 +8274,7 @@ class ReadOnlyInputV2Component {
|
|
|
8226
8274
|
return String(v);
|
|
8227
8275
|
}, ...(ngDevMode ? [{ debugName: "displayText" }] : []));
|
|
8228
8276
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ReadOnlyInputV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8229
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: ReadOnlyInputV2Component, isStandalone: true, selector: "phoenix-read-only-input-v2", inputs: { field: "field", form: "form" }, ngImport: i0, template: "@switch (type) {\n\n @case ('TEXT_EDITOR') {\n @if (displayText()) {\n <div class=\"read-only-editor-content\" [innerHTML]=\"displayText()\"></div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('TEXT_AREA') {\n @if (displayText()) {\n <div class=\"w-full mt-
|
|
8277
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: ReadOnlyInputV2Component, isStandalone: true, selector: "phoenix-read-only-input-v2", inputs: { field: "field", form: "form" }, ngImport: i0, template: "@switch (type) {\n\n @case ('TEXT_EDITOR') {\n @if (displayText()) {\n <div class=\"read-only-editor-content\" [innerHTML]=\"displayText()\"></div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('TEXT_AREA') {\n @if (displayText()) {\n <div class=\"w-full mt-2 mb-2 font-semibold text-500 whitespace-pre-line\">\n {{ displayText() | stripHtmlSafe }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n <!-- \u2705 SWITCH bez ngModel -->\n @case ('SWITCH') {\n @if (bool() !== null) {\n <div class=\"mt-2 mb-2 font-semibold text-500\">\n {{ bool() ? ('Yes' | translate) : ('No' | translate) }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n <!-- \u2705 CHECKBOX bez ngModel -->\n @case ('CHECKBOX') {\n @if (bool() !== null) {\n <div class=\"mt-2 mb-2 font-semibold text-500\">\n {{ bool() ? ('Yes' | translate) : ('No' | translate) }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('DATE') {\n @if (dateValue()) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ dateValue() | date }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('START_DUE_DATE') {\n @if (startDue()) {\n <div class=\"flex font-semibold text-500 mt-2 mb-2\">\n <span style=\"padding-top: 3px\">{{ startDue()?.startDate | date }}</span>\n <div class=\"flex align-items-center p-2\">\n <i class=\"pi pi-arrow-right text-sm\"></i>\n </div>\n <span style=\"padding-top: 3px\">{{ startDue()?.endDate | date }}</span>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('ASSIGN') {\n @if (assign()?.name) {\n <div>\n <p-button [rounded]=\"true\" [text]=\"true\" (onClick)=\"op.toggle($event)\">\n <div class=\"person-wrap\">\n <div class=\"person-avatar\">\n {{ (assign()?.name ?? '')!.toUpperCase().charAt(0) }}\n </div>\n <div>\n <p class=\"white-space-nowrap overflow-hidden text-overflow-ellipsis\">\n {{ assign()?.name }}\n </p>\n <p class=\"white-space-nowrap overflow-hidden text-overflow-ellipsis\">\n {{ assign()?.function ?? '--' }}\n </p>\n </div>\n </div>\n </p-button>\n\n <p-popover #op [dismissable]=\"true\">\n <div>\n <span class=\"block mb-2\">\n <i class=\"pi pi-envelope mr-1 text-500\"></i>\n {{ assign()?.email ?? '--' }}\n </span>\n <p>\n <i class=\"pi pi-phone mr-1 text-500\"></i>\n {{ assign()?.phone ?? '--' }}\n </p>\n </div>\n </p-popover>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('UPLOAD') {\n @if (upload()?.fileName) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ upload()?.fileName }}\n @if (upload()?.size) {\n <span class=\"text-600 font-normal\"> \u2022 {{ upload()?.size | fileSize }}</span>\n }\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('UPLOAD_DRAG_DROP') {\n @if (upload()?.fileName) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ upload()?.fileName }}\n @if (upload()?.size) {\n <span class=\"text-600 font-normal\"> \u2022 {{ upload()?.size | fileSize }}</span>\n }\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('SS_OPTION') {\n @if (ssLabel()) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ ssLabel() | translate }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('SS_OPTION_OBJECT_BASED') {\n @if (ssLabel()) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ ssLabel() | translate }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('MS_OPTION') {\n @if (msLabel()) {\n <div class=\"font-semibold text-500 mt-2 mb-2 white-space-nowrap overflow-hidden\">\n {{ msLabel() }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @default {\n @if (displayText()) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ displayText() | stripHtmlSafe }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n}\n\n<ng-template #fallBack>\n <div class=\"font-semibold text-500 mt-2 mb-2\">--</div>\n</ng-template>", styles: [".person-wrap{display:flex;align-items:center;margin-left:-6px}.person-wrap p{margin:0}.person-wrap .person-avatar{display:flex;justify-content:center;align-items:center;width:28px;height:28px;min-width:28px;min-height:28px;margin-right:5px;background-color:#e94260;color:#fff;border-radius:50%;font-size:1rem}.person-wrap .person-name :first-child{font-size:1rem}.person-details{border-top:1px solid #e0e0e0;padding:5px;margin-left:-6px;width:150px}.person-details p{margin:0;display:flex;align-items:center}.person-details i{color:#d75063;padding-right:5px}.person-details .p-editor-container{padding-left:0!important}:host ::ng-deep .p-password .p-password-input{border:none!important}:host ::ng-deep .read-only-editor-content{margin-top:.75rem;margin-bottom:.5rem;font-weight:600;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i2$1.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }, { kind: "pipe", type: i3$2.TranslatePipe, name: "translate" }, { kind: "pipe", type: StripHtmlSafePipe, name: "stripHtmlSafe" }, { kind: "pipe", type: FileSizePipe, name: "fileSize" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8230
8278
|
}
|
|
8231
8279
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ReadOnlyInputV2Component, decorators: [{
|
|
8232
8280
|
type: Component,
|
|
@@ -8238,7 +8286,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8238
8286
|
PopoverModule,
|
|
8239
8287
|
StripHtmlSafePipe,
|
|
8240
8288
|
FileSizePipe,
|
|
8241
|
-
], template: "@switch (type) {\n\n @case ('TEXT_EDITOR') {\n @if (displayText()) {\n <div class=\"read-only-editor-content\" [innerHTML]=\"displayText()\"></div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('TEXT_AREA') {\n @if (displayText()) {\n <div class=\"w-full mt-
|
|
8289
|
+
], template: "@switch (type) {\n\n @case ('TEXT_EDITOR') {\n @if (displayText()) {\n <div class=\"read-only-editor-content\" [innerHTML]=\"displayText()\"></div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('TEXT_AREA') {\n @if (displayText()) {\n <div class=\"w-full mt-2 mb-2 font-semibold text-500 whitespace-pre-line\">\n {{ displayText() | stripHtmlSafe }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n <!-- \u2705 SWITCH bez ngModel -->\n @case ('SWITCH') {\n @if (bool() !== null) {\n <div class=\"mt-2 mb-2 font-semibold text-500\">\n {{ bool() ? ('Yes' | translate) : ('No' | translate) }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n <!-- \u2705 CHECKBOX bez ngModel -->\n @case ('CHECKBOX') {\n @if (bool() !== null) {\n <div class=\"mt-2 mb-2 font-semibold text-500\">\n {{ bool() ? ('Yes' | translate) : ('No' | translate) }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('DATE') {\n @if (dateValue()) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ dateValue() | date }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('START_DUE_DATE') {\n @if (startDue()) {\n <div class=\"flex font-semibold text-500 mt-2 mb-2\">\n <span style=\"padding-top: 3px\">{{ startDue()?.startDate | date }}</span>\n <div class=\"flex align-items-center p-2\">\n <i class=\"pi pi-arrow-right text-sm\"></i>\n </div>\n <span style=\"padding-top: 3px\">{{ startDue()?.endDate | date }}</span>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('ASSIGN') {\n @if (assign()?.name) {\n <div>\n <p-button [rounded]=\"true\" [text]=\"true\" (onClick)=\"op.toggle($event)\">\n <div class=\"person-wrap\">\n <div class=\"person-avatar\">\n {{ (assign()?.name ?? '')!.toUpperCase().charAt(0) }}\n </div>\n <div>\n <p class=\"white-space-nowrap overflow-hidden text-overflow-ellipsis\">\n {{ assign()?.name }}\n </p>\n <p class=\"white-space-nowrap overflow-hidden text-overflow-ellipsis\">\n {{ assign()?.function ?? '--' }}\n </p>\n </div>\n </div>\n </p-button>\n\n <p-popover #op [dismissable]=\"true\">\n <div>\n <span class=\"block mb-2\">\n <i class=\"pi pi-envelope mr-1 text-500\"></i>\n {{ assign()?.email ?? '--' }}\n </span>\n <p>\n <i class=\"pi pi-phone mr-1 text-500\"></i>\n {{ assign()?.phone ?? '--' }}\n </p>\n </div>\n </p-popover>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('UPLOAD') {\n @if (upload()?.fileName) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ upload()?.fileName }}\n @if (upload()?.size) {\n <span class=\"text-600 font-normal\"> \u2022 {{ upload()?.size | fileSize }}</span>\n }\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('UPLOAD_DRAG_DROP') {\n @if (upload()?.fileName) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ upload()?.fileName }}\n @if (upload()?.size) {\n <span class=\"text-600 font-normal\"> \u2022 {{ upload()?.size | fileSize }}</span>\n }\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('SS_OPTION') {\n @if (ssLabel()) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ ssLabel() | translate }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('SS_OPTION_OBJECT_BASED') {\n @if (ssLabel()) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ ssLabel() | translate }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @case ('MS_OPTION') {\n @if (msLabel()) {\n <div class=\"font-semibold text-500 mt-2 mb-2 white-space-nowrap overflow-hidden\">\n {{ msLabel() }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n\n @default {\n @if (displayText()) {\n <div class=\"font-semibold text-500 mt-2 mb-2\">\n {{ displayText() | stripHtmlSafe }}\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"fallBack\"></ng-container>\n }\n }\n}\n\n<ng-template #fallBack>\n <div class=\"font-semibold text-500 mt-2 mb-2\">--</div>\n</ng-template>", styles: [".person-wrap{display:flex;align-items:center;margin-left:-6px}.person-wrap p{margin:0}.person-wrap .person-avatar{display:flex;justify-content:center;align-items:center;width:28px;height:28px;min-width:28px;min-height:28px;margin-right:5px;background-color:#e94260;color:#fff;border-radius:50%;font-size:1rem}.person-wrap .person-name :first-child{font-size:1rem}.person-details{border-top:1px solid #e0e0e0;padding:5px;margin-left:-6px;width:150px}.person-details p{margin:0;display:flex;align-items:center}.person-details i{color:#d75063;padding-right:5px}.person-details .p-editor-container{padding-left:0!important}:host ::ng-deep .p-password .p-password-input{border:none!important}:host ::ng-deep .read-only-editor-content{margin-top:.75rem;margin-bottom:.5rem;font-weight:600;color:var(--text-color-secondary)}\n"] }]
|
|
8242
8290
|
}], propDecorators: { field: [{
|
|
8243
8291
|
type: Input,
|
|
8244
8292
|
args: [{ required: true }]
|
|
@@ -8248,37 +8296,92 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8248
8296
|
}] } });
|
|
8249
8297
|
|
|
8250
8298
|
class MetaAssignResponsibleV2Component {
|
|
8251
|
-
/**
|
|
8252
|
-
|
|
8299
|
+
/**
|
|
8300
|
+
* List of available assignees used as table data inside the selection dialog.
|
|
8301
|
+
* This replaces legacy `control.configuration.items` access and keeps the CVA independent
|
|
8302
|
+
* from meta-form internals.
|
|
8303
|
+
*/
|
|
8304
|
+
items = [];
|
|
8305
|
+
/**
|
|
8306
|
+
* Translation key for the dialog header title.
|
|
8307
|
+
* Kept as an input so different contexts can reuse this field with a custom title.
|
|
8308
|
+
*/
|
|
8253
8309
|
dialogHeaderKey = 'LABELS.ASSIGN_RESPONSIBLE';
|
|
8254
8310
|
translate = inject(TranslateService);
|
|
8255
8311
|
dialog = inject(DialogService);
|
|
8312
|
+
/**
|
|
8313
|
+
* Currently selected assignee value bound to the parent form control.
|
|
8314
|
+
* We store the full object (row) because downstream fields often need more than uuid.
|
|
8315
|
+
*/
|
|
8256
8316
|
value = null;
|
|
8317
|
+
/**
|
|
8318
|
+
* Disabled state propagated from Angular forms via `setDisabledState`.
|
|
8319
|
+
* (If you later add a field-level `@Input() disable`, combine them like in other CVAs.)
|
|
8320
|
+
*/
|
|
8257
8321
|
disabled = false;
|
|
8322
|
+
/**
|
|
8323
|
+
* CVA callback invoked when the value changes (selection/clear).
|
|
8324
|
+
*/
|
|
8258
8325
|
onChange = () => { };
|
|
8326
|
+
/**
|
|
8327
|
+
* CVA callback invoked when the control is marked as touched (user interaction).
|
|
8328
|
+
* Standard: call on meaningful interactions (open dialog, select, clear).
|
|
8329
|
+
*/
|
|
8259
8330
|
onTouched = () => { };
|
|
8331
|
+
/**
|
|
8332
|
+
* Called by Angular forms when the model value changes programmatically.
|
|
8333
|
+
* Keep it side-effect free: do not call `onChange`/`onTouched` from here.
|
|
8334
|
+
*/
|
|
8260
8335
|
writeValue(value) {
|
|
8261
8336
|
this.value = value ?? null;
|
|
8262
8337
|
}
|
|
8338
|
+
/**
|
|
8339
|
+
* Registers the callback that should be called when the component updates the value.
|
|
8340
|
+
*/
|
|
8263
8341
|
registerOnChange(fn) {
|
|
8264
8342
|
this.onChange = fn;
|
|
8265
8343
|
}
|
|
8344
|
+
/**
|
|
8345
|
+
* Registers the callback that should be called when the control becomes "touched".
|
|
8346
|
+
*/
|
|
8266
8347
|
registerOnTouched(fn) {
|
|
8267
8348
|
this.onTouched = fn;
|
|
8268
8349
|
}
|
|
8350
|
+
/**
|
|
8351
|
+
* Receives disabled state from Angular forms and updates local state.
|
|
8352
|
+
*/
|
|
8269
8353
|
setDisabledState(isDisabled) {
|
|
8270
8354
|
this.disabled = isDisabled;
|
|
8271
8355
|
}
|
|
8356
|
+
/**
|
|
8357
|
+
* Clears current assignee value.
|
|
8358
|
+
* - emits `null`
|
|
8359
|
+
* - marks as touched (user action)
|
|
8360
|
+
*/
|
|
8272
8361
|
clear() {
|
|
8273
8362
|
if (this.disabled)
|
|
8274
8363
|
return;
|
|
8364
|
+
// prevent redundant emits
|
|
8365
|
+
if (this.value === null) {
|
|
8366
|
+
this.onTouched();
|
|
8367
|
+
return;
|
|
8368
|
+
}
|
|
8275
8369
|
this.value = null;
|
|
8276
8370
|
this.onChange(null);
|
|
8277
8371
|
this.onTouched();
|
|
8278
8372
|
}
|
|
8373
|
+
/**
|
|
8374
|
+
* Opens object selection dialog for choosing a new assignee.
|
|
8375
|
+
* The dialog renders a generic table and returns the selected row on close.
|
|
8376
|
+
*
|
|
8377
|
+
* Standard for CVA:
|
|
8378
|
+
* - mark as touched when user opens the picker (interaction started)
|
|
8379
|
+
* - emit onChange only when a row is actually selected
|
|
8380
|
+
*/
|
|
8279
8381
|
openDialog() {
|
|
8280
8382
|
if (this.disabled)
|
|
8281
8383
|
return;
|
|
8384
|
+
this.onTouched();
|
|
8282
8385
|
const ref = this.dialog.open(ObjectItemDialogComponent, {
|
|
8283
8386
|
header: this.translate.instant(this.dialogHeaderKey),
|
|
8284
8387
|
width: '700px',
|
|
@@ -8299,8 +8402,13 @@ class MetaAssignResponsibleV2Component {
|
|
|
8299
8402
|
ref?.onClose.subscribe((response) => {
|
|
8300
8403
|
if (!response)
|
|
8301
8404
|
return;
|
|
8405
|
+
// prevent redundant emits (same selection)
|
|
8406
|
+
const same = (this.value?.uuid ?? null) === (response?.uuid ?? null) &&
|
|
8407
|
+
JSON.stringify(this.value ?? null) === JSON.stringify(response ?? null);
|
|
8302
8408
|
this.value = response;
|
|
8303
|
-
|
|
8409
|
+
if (!same) {
|
|
8410
|
+
this.onChange(response);
|
|
8411
|
+
}
|
|
8304
8412
|
this.onTouched();
|
|
8305
8413
|
});
|
|
8306
8414
|
}
|
|
@@ -8461,32 +8569,107 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8461
8569
|
}] } });
|
|
8462
8570
|
|
|
8463
8571
|
class MetaColorPickerV2Component {
|
|
8464
|
-
/**
|
|
8572
|
+
/**
|
|
8573
|
+
* Optional external disable flag (field-level disable coming from meta config).
|
|
8574
|
+
* This is combined with the disabled state coming from Angular Forms (CVA).
|
|
8575
|
+
*/
|
|
8465
8576
|
disable = false;
|
|
8466
|
-
|
|
8577
|
+
cdr = inject(ChangeDetectorRef);
|
|
8578
|
+
/**
|
|
8579
|
+
* Currently selected color value.
|
|
8580
|
+
* Expected format: HEX string (e.g. "#ff00aa") or null.
|
|
8581
|
+
*/
|
|
8467
8582
|
value = null;
|
|
8583
|
+
/**
|
|
8584
|
+
* CVA callback invoked when the value changes.
|
|
8585
|
+
*/
|
|
8468
8586
|
onChange = () => { };
|
|
8587
|
+
/**
|
|
8588
|
+
* CVA callback invoked when the control is marked as touched.
|
|
8589
|
+
* Standard: call on blur / close, not on every value change.
|
|
8590
|
+
*/
|
|
8469
8591
|
onTouched = () => { };
|
|
8592
|
+
/**
|
|
8593
|
+
* Disabled state coming from Angular Forms (ControlValueAccessor).
|
|
8594
|
+
*/
|
|
8470
8595
|
isDisabled = false;
|
|
8596
|
+
/**
|
|
8597
|
+
* Final disabled state combining:
|
|
8598
|
+
* - form-level disabled state (CVA)
|
|
8599
|
+
* - field-level disable flag (input)
|
|
8600
|
+
*/
|
|
8601
|
+
get disabled() {
|
|
8602
|
+
return this.disable || this.isDisabled;
|
|
8603
|
+
}
|
|
8604
|
+
/**
|
|
8605
|
+
* Writes a new value from the parent form control into the component.
|
|
8606
|
+
* Keep it idempotent and UI-safe.
|
|
8607
|
+
*/
|
|
8471
8608
|
writeValue(v) {
|
|
8472
|
-
this.value = v
|
|
8609
|
+
this.value = this.normalizeHex(v);
|
|
8610
|
+
this.cdr.markForCheck();
|
|
8473
8611
|
}
|
|
8612
|
+
/**
|
|
8613
|
+
* Registers callback that is triggered when the value changes.
|
|
8614
|
+
*/
|
|
8474
8615
|
registerOnChange(fn) {
|
|
8475
8616
|
this.onChange = fn;
|
|
8476
8617
|
}
|
|
8618
|
+
/**
|
|
8619
|
+
* Registers callback that is triggered when the control is touched.
|
|
8620
|
+
*/
|
|
8477
8621
|
registerOnTouched(fn) {
|
|
8478
8622
|
this.onTouched = fn;
|
|
8479
8623
|
}
|
|
8624
|
+
/**
|
|
8625
|
+
* Receives disabled state from Angular Forms and updates local state.
|
|
8626
|
+
*/
|
|
8480
8627
|
setDisabledState(isDisabled) {
|
|
8481
8628
|
this.isDisabled = isDisabled;
|
|
8629
|
+
this.cdr.markForCheck();
|
|
8482
8630
|
}
|
|
8483
|
-
|
|
8631
|
+
/**
|
|
8632
|
+
* Handler for PrimeNG color picker change event.
|
|
8633
|
+
* Propagates the currently selected color value to the parent form control.
|
|
8634
|
+
*
|
|
8635
|
+
* Note: we intentionally do NOT call onTouched here (our standard is: touched on blur).
|
|
8636
|
+
*/
|
|
8484
8637
|
onPickerChange() {
|
|
8485
|
-
|
|
8638
|
+
if (this.disabled)
|
|
8639
|
+
return;
|
|
8640
|
+
const next = this.normalizeHex(this.value);
|
|
8641
|
+
// prevent redundant emits (useful when PrimeNG fires multiple times)
|
|
8642
|
+
if (next === this.value) {
|
|
8643
|
+
this.cdr.markForCheck();
|
|
8644
|
+
return;
|
|
8645
|
+
}
|
|
8646
|
+
this.value = next;
|
|
8647
|
+
this.onChange(next);
|
|
8648
|
+
this.cdr.markForCheck();
|
|
8649
|
+
}
|
|
8650
|
+
/**
|
|
8651
|
+
* Marks control as touched when user leaves the component.
|
|
8652
|
+
* (Matches "touched on blur" CVA guideline.)
|
|
8653
|
+
*/
|
|
8654
|
+
handleBlur() {
|
|
8655
|
+
if (this.disabled)
|
|
8656
|
+
return;
|
|
8486
8657
|
this.onTouched();
|
|
8487
8658
|
}
|
|
8488
|
-
|
|
8489
|
-
|
|
8659
|
+
/**
|
|
8660
|
+
* Normalizes incoming values to a safe HEX string or null.
|
|
8661
|
+
* - accepts "#RRGGBB" / "#RGB" / "RRGGBB"
|
|
8662
|
+
* - returns null for empty/invalid inputs
|
|
8663
|
+
*/
|
|
8664
|
+
normalizeHex(v) {
|
|
8665
|
+
if (v === null || v === undefined)
|
|
8666
|
+
return null;
|
|
8667
|
+
const s = String(v).trim();
|
|
8668
|
+
if (!s)
|
|
8669
|
+
return null;
|
|
8670
|
+
const withHash = s.startsWith('#') ? s : `#${s}`;
|
|
8671
|
+
const ok = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(withHash);
|
|
8672
|
+
return ok ? withHash.toLowerCase() : null;
|
|
8490
8673
|
}
|
|
8491
8674
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaColorPickerV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8492
8675
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: MetaColorPickerV2Component, isStandalone: true, selector: "phoenix-meta-color-picker-v2", inputs: { disable: "disable" }, providers: [
|
|
@@ -8496,28 +8679,26 @@ class MetaColorPickerV2Component {
|
|
|
8496
8679
|
multi: true,
|
|
8497
8680
|
},
|
|
8498
8681
|
], ngImport: i0, template: `
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
[appendTo]="'body'"
|
|
8682
|
+
<p-colorPicker
|
|
8683
|
+
class="color-swatch"
|
|
8684
|
+
[(ngModel)]="value"
|
|
8685
|
+
(onChange)="onPickerChange()"
|
|
8686
|
+
(onBlur)="handleBlur()"
|
|
8687
|
+
[disabled]="disabled"
|
|
8688
|
+
[appendTo]="'body'"
|
|
8507
8689
|
></p-colorPicker>
|
|
8508
8690
|
`, isInline: true, styles: [":host ::ng-deep .color-swatch.p-colorpicker{width:35px;height:35px;padding:0!important;border:none!important;background:transparent!important;box-shadow:none!important}:host ::ng-deep .color-swatch .p-colorpicker-preview{width:35px!important;height:35px!important;border:none!important;border-radius:6px!important;box-shadow:none!important}:host ::ng-deep .color-swatch .p-colorpicker-preview:focus,:host ::ng-deep .color-swatch .p-colorpicker-preview:focus-visible{outline:none!important;box-shadow:none!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ColorPickerModule }, { kind: "component", type: i2$8.ColorPicker, selector: "p-colorPicker, p-colorpicker, p-color-picker", inputs: ["styleClass", "inline", "format", "tabindex", "inputId", "autoZIndex", "showTransitionOptions", "hideTransitionOptions", "autofocus", "defaultColor", "appendTo"], outputs: ["onChange", "onShow", "onHide"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8509
8691
|
}
|
|
8510
8692
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaColorPickerV2Component, decorators: [{
|
|
8511
8693
|
type: Component,
|
|
8512
8694
|
args: [{ selector: 'phoenix-meta-color-picker-v2', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, ColorPickerModule], template: `
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
[appendTo]="'body'"
|
|
8695
|
+
<p-colorPicker
|
|
8696
|
+
class="color-swatch"
|
|
8697
|
+
[(ngModel)]="value"
|
|
8698
|
+
(onChange)="onPickerChange()"
|
|
8699
|
+
(onBlur)="handleBlur()"
|
|
8700
|
+
[disabled]="disabled"
|
|
8701
|
+
[appendTo]="'body'"
|
|
8521
8702
|
></p-colorPicker>
|
|
8522
8703
|
`, providers: [
|
|
8523
8704
|
{
|
|
@@ -8531,50 +8712,137 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8531
8712
|
}] } });
|
|
8532
8713
|
|
|
8533
8714
|
class MetaCheckboxColorPickerV2Component {
|
|
8715
|
+
/**
|
|
8716
|
+
* 2D array of color values used to render the color grid in the popover.
|
|
8717
|
+
* Example:
|
|
8718
|
+
* [
|
|
8719
|
+
* ['#ff0000', '#00ff00', '#0000ff'],
|
|
8720
|
+
* ['#facc15', '#22c55e', '#0ea5e9']
|
|
8721
|
+
* ]
|
|
8722
|
+
*/
|
|
8534
8723
|
options = [];
|
|
8724
|
+
/**
|
|
8725
|
+
* External disable flag (e.g. meta-form field-level disable).
|
|
8726
|
+
* This is combined with CVA disabled state.
|
|
8727
|
+
*/
|
|
8535
8728
|
disable = false;
|
|
8536
8729
|
cdr = inject(ChangeDetectorRef);
|
|
8730
|
+
/**
|
|
8731
|
+
* Currently selected color value bound to the parent form control.
|
|
8732
|
+
*/
|
|
8537
8733
|
value = null;
|
|
8734
|
+
/**
|
|
8735
|
+
* Color currently focused/hovered in the UI.
|
|
8736
|
+
* Used only for visual outline highlight in the picker grid.
|
|
8737
|
+
*/
|
|
8538
8738
|
focusedColor = null;
|
|
8739
|
+
/**
|
|
8740
|
+
* Disabled state coming from Angular Forms (ControlValueAccessor).
|
|
8741
|
+
*/
|
|
8539
8742
|
isDisabled = false;
|
|
8743
|
+
/**
|
|
8744
|
+
* CVA callback invoked when the value changes.
|
|
8745
|
+
*/
|
|
8540
8746
|
onChange = () => { };
|
|
8747
|
+
/**
|
|
8748
|
+
* CVA callback invoked when the control is marked as touched.
|
|
8749
|
+
* Standard: touched on "open/blur/close" actions, not necessarily on hover.
|
|
8750
|
+
*/
|
|
8541
8751
|
onTouched = () => { };
|
|
8542
|
-
|
|
8752
|
+
/**
|
|
8753
|
+
* Final disabled state combining:
|
|
8754
|
+
* - form-level disabled state (CVA)
|
|
8755
|
+
* - field-level disable flag (input)
|
|
8756
|
+
*/
|
|
8757
|
+
get disabled() {
|
|
8758
|
+
return this.disable || this.isDisabled;
|
|
8759
|
+
}
|
|
8760
|
+
// ----- ControlValueAccessor implementation -----
|
|
8761
|
+
/**
|
|
8762
|
+
* Writes a new value from the parent form into the component.
|
|
8763
|
+
* Keeps internal value and focusedColor in sync for correct UI outline.
|
|
8764
|
+
*/
|
|
8543
8765
|
writeValue(v) {
|
|
8544
|
-
this.value = v
|
|
8766
|
+
this.value = this.normalizeColor(v);
|
|
8545
8767
|
this.focusedColor = this.value;
|
|
8546
8768
|
this.cdr.markForCheck();
|
|
8547
8769
|
}
|
|
8770
|
+
/**
|
|
8771
|
+
* Registers callback that is triggered when the value changes.
|
|
8772
|
+
*/
|
|
8548
8773
|
registerOnChange(fn) {
|
|
8549
8774
|
this.onChange = fn;
|
|
8550
8775
|
}
|
|
8776
|
+
/**
|
|
8777
|
+
* Registers callback that is triggered when the control is touched.
|
|
8778
|
+
*/
|
|
8551
8779
|
registerOnTouched(fn) {
|
|
8552
8780
|
this.onTouched = fn;
|
|
8553
8781
|
}
|
|
8782
|
+
/**
|
|
8783
|
+
* Receives disabled state from Angular Forms and updates local state.
|
|
8784
|
+
*/
|
|
8554
8785
|
setDisabledState(isDisabled) {
|
|
8555
8786
|
this.isDisabled = isDisabled;
|
|
8556
8787
|
this.cdr.markForCheck();
|
|
8557
8788
|
}
|
|
8558
|
-
get disabled() {
|
|
8559
|
-
return this.disable || this.isDisabled;
|
|
8560
|
-
}
|
|
8561
8789
|
// ----- UI handlers -----
|
|
8790
|
+
/**
|
|
8791
|
+
* Toggles the popover visibility when the selected-color button is clicked.
|
|
8792
|
+
* We mark as touched because user interacted with the control.
|
|
8793
|
+
*/
|
|
8562
8794
|
toggle(popover, ev) {
|
|
8563
8795
|
if (this.disabled)
|
|
8564
8796
|
return;
|
|
8565
8797
|
this.onTouched();
|
|
8566
8798
|
popover.toggle(ev);
|
|
8567
8799
|
}
|
|
8800
|
+
/**
|
|
8801
|
+
* Handles color selection from the grid:
|
|
8802
|
+
* - updates internal value
|
|
8803
|
+
* - propagates value to parent form (onChange)
|
|
8804
|
+
* - marks control as touched (selection is a meaningful interaction)
|
|
8805
|
+
* - closes the popover
|
|
8806
|
+
*/
|
|
8568
8807
|
select(color, popover) {
|
|
8569
8808
|
if (this.disabled)
|
|
8570
8809
|
return;
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
this.
|
|
8810
|
+
const next = this.normalizeColor(color);
|
|
8811
|
+
// prevent redundant emits
|
|
8812
|
+
if (next === this.value) {
|
|
8813
|
+
this.focusedColor = next;
|
|
8814
|
+
this.onTouched();
|
|
8815
|
+
this.cdr.markForCheck();
|
|
8816
|
+
popover.hide();
|
|
8817
|
+
return;
|
|
8818
|
+
}
|
|
8819
|
+
this.focusedColor = next;
|
|
8820
|
+
this.value = next;
|
|
8821
|
+
this.onChange(next);
|
|
8574
8822
|
this.onTouched();
|
|
8575
8823
|
this.cdr.markForCheck();
|
|
8576
8824
|
popover.hide();
|
|
8577
8825
|
}
|
|
8826
|
+
/**
|
|
8827
|
+
* Normalizes incoming values to a safe CSS color string or null.
|
|
8828
|
+
* For this component we keep it permissive:
|
|
8829
|
+
* - accepts "#RRGGBB" / "#RGB"
|
|
8830
|
+
* - also allows any non-empty string (in case someone passes "red" or "var(--x)")
|
|
8831
|
+
* - returns null for empty values
|
|
8832
|
+
*/
|
|
8833
|
+
normalizeColor(v) {
|
|
8834
|
+
if (v === null || v === undefined)
|
|
8835
|
+
return null;
|
|
8836
|
+
const s = String(v).trim();
|
|
8837
|
+
if (!s)
|
|
8838
|
+
return null;
|
|
8839
|
+
// normalize common hex values (with/without '#')
|
|
8840
|
+
const withHash = s.startsWith('#') ? s : `#${s}`;
|
|
8841
|
+
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(withHash))
|
|
8842
|
+
return withHash.toLowerCase();
|
|
8843
|
+
// fallback: allow CSS colors (e.g. "red") or CSS vars
|
|
8844
|
+
return s;
|
|
8845
|
+
}
|
|
8578
8846
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaCheckboxColorPickerV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8579
8847
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: MetaCheckboxColorPickerV2Component, isStandalone: true, selector: "phoenix-meta-checkbox-color-picker-v2", inputs: { options: "options", disable: "disable" }, providers: [
|
|
8580
8848
|
{
|
|
@@ -8660,50 +8928,154 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8660
8928
|
}] } });
|
|
8661
8929
|
|
|
8662
8930
|
class MetaStartDueDateV2Component {
|
|
8663
|
-
/**
|
|
8931
|
+
/**
|
|
8932
|
+
* Optional data-cy attribute prefix for e2e testing.
|
|
8933
|
+
* Can be used by parent components to uniquely identify this field in tests.
|
|
8934
|
+
*/
|
|
8664
8935
|
dataCy;
|
|
8665
|
-
|
|
8936
|
+
/**
|
|
8937
|
+
* Optional parent-level disable flag (in addition to reactive-form disable).
|
|
8938
|
+
* Use this when the field must be disabled due to page/business logic.
|
|
8939
|
+
*/
|
|
8940
|
+
disable = false;
|
|
8941
|
+
cdr = inject(ChangeDetectorRef);
|
|
8942
|
+
/**
|
|
8943
|
+
* Disabled state coming from Angular Forms (ControlValueAccessor).
|
|
8944
|
+
* When true, both date pickers are non-interactive.
|
|
8945
|
+
*/
|
|
8946
|
+
isDisabled = false;
|
|
8947
|
+
/**
|
|
8948
|
+
* Local UI state for the selected start date.
|
|
8949
|
+
* Normalized to Date or null for PrimeNG DatePicker compatibility.
|
|
8950
|
+
*/
|
|
8666
8951
|
startDate = null;
|
|
8952
|
+
/**
|
|
8953
|
+
* Local UI state for the selected end date.
|
|
8954
|
+
* Normalized to Date or null for PrimeNG DatePicker compatibility.
|
|
8955
|
+
*/
|
|
8667
8956
|
endDate = null;
|
|
8957
|
+
/**
|
|
8958
|
+
* CVA callback invoked when the composite value changes.
|
|
8959
|
+
*/
|
|
8668
8960
|
onChange = () => { };
|
|
8961
|
+
/**
|
|
8962
|
+
* CVA callback invoked when the control is marked as touched.
|
|
8963
|
+
* IMPORTANT: We call this on blur (not on every change) to match standard CVA behavior.
|
|
8964
|
+
*/
|
|
8669
8965
|
onTouched = () => { };
|
|
8966
|
+
/**
|
|
8967
|
+
* Effective disabled state used by the template.
|
|
8968
|
+
* Combines reactive form disable + parent-level disable.
|
|
8969
|
+
*/
|
|
8970
|
+
get disabled() {
|
|
8971
|
+
return this.disable || this.isDisabled;
|
|
8972
|
+
}
|
|
8973
|
+
/**
|
|
8974
|
+
* Writes a new value from the parent form control into the component.
|
|
8975
|
+
* Incoming values are normalized to Date instances for the UI layer.
|
|
8976
|
+
*
|
|
8977
|
+
* NOTE: "YYYY-MM-DD" strings are parsed as local dates to avoid timezone day-shifts.
|
|
8978
|
+
*/
|
|
8670
8979
|
writeValue(v) {
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
this.
|
|
8980
|
+
this.startDate = this.parseToDate(v?.startDate ?? null);
|
|
8981
|
+
this.endDate = this.parseToDate(v?.endDate ?? null);
|
|
8982
|
+
// OnPush: ensure UI reflects external value writes (patchValue/setValue)
|
|
8983
|
+
this.cdr.markForCheck();
|
|
8675
8984
|
}
|
|
8985
|
+
/**
|
|
8986
|
+
* Registers the callback that should be called when the value changes.
|
|
8987
|
+
*/
|
|
8676
8988
|
registerOnChange(fn) {
|
|
8677
8989
|
this.onChange = fn;
|
|
8678
8990
|
}
|
|
8991
|
+
/**
|
|
8992
|
+
* Registers the callback that should be called when the control is touched.
|
|
8993
|
+
*/
|
|
8679
8994
|
registerOnTouched(fn) {
|
|
8680
8995
|
this.onTouched = fn;
|
|
8681
8996
|
}
|
|
8997
|
+
/**
|
|
8998
|
+
* Receives disabled state from Angular Forms and updates local state.
|
|
8999
|
+
*/
|
|
8682
9000
|
setDisabledState(isDisabled) {
|
|
8683
|
-
this.
|
|
9001
|
+
this.isDisabled = isDisabled;
|
|
9002
|
+
// OnPush: reflect disabled state changes immediately
|
|
9003
|
+
this.cdr.markForCheck();
|
|
8684
9004
|
}
|
|
9005
|
+
/**
|
|
9006
|
+
* Handler used by PrimeNG DatePicker blur events.
|
|
9007
|
+
* Marks the control as touched without emitting a value change.
|
|
9008
|
+
*/
|
|
9009
|
+
handleBlur() {
|
|
9010
|
+
if (this.disabled)
|
|
9011
|
+
return;
|
|
9012
|
+
this.onTouched();
|
|
9013
|
+
}
|
|
9014
|
+
/**
|
|
9015
|
+
* Handler for start date change coming from the DatePicker.
|
|
9016
|
+
* Updates local state and propagates the composite value.
|
|
9017
|
+
*/
|
|
8685
9018
|
onStartChange(d) {
|
|
9019
|
+
if (this.disabled)
|
|
9020
|
+
return;
|
|
8686
9021
|
this.startDate = d ?? null;
|
|
8687
|
-
this.
|
|
9022
|
+
this.emitChange();
|
|
8688
9023
|
}
|
|
9024
|
+
/**
|
|
9025
|
+
* Handler for end date change coming from the DatePicker.
|
|
9026
|
+
* Updates local state and propagates the composite value.
|
|
9027
|
+
*/
|
|
8689
9028
|
onEndChange(d) {
|
|
9029
|
+
if (this.disabled)
|
|
9030
|
+
return;
|
|
8690
9031
|
this.endDate = d ?? null;
|
|
8691
|
-
this.
|
|
9032
|
+
this.emitChange();
|
|
8692
9033
|
}
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
9034
|
+
/**
|
|
9035
|
+
* Emits the composite value to the parent form control.
|
|
9036
|
+
* - Emits `null` when both dates are empty (cleaner semantics for required/bothDates validators).
|
|
9037
|
+
* - Otherwise emits the `{ startDate, endDate }` object (partial values allowed; validator decides).
|
|
9038
|
+
*/
|
|
9039
|
+
emitChange() {
|
|
8696
9040
|
if (!this.startDate && !this.endDate) {
|
|
8697
9041
|
this.onChange(null);
|
|
9042
|
+
this.cdr.markForCheck();
|
|
8698
9043
|
return;
|
|
8699
9044
|
}
|
|
8700
9045
|
this.onChange({
|
|
8701
9046
|
startDate: this.startDate,
|
|
8702
9047
|
endDate: this.endDate,
|
|
8703
9048
|
});
|
|
9049
|
+
this.cdr.markForCheck();
|
|
9050
|
+
}
|
|
9051
|
+
/**
|
|
9052
|
+
* Parses Date | string safely into a Date instance for the UI.
|
|
9053
|
+
* Important: "YYYY-MM-DD" is treated as a local date (prevents timezone day-shift).
|
|
9054
|
+
*/
|
|
9055
|
+
parseToDate(v) {
|
|
9056
|
+
if (!v)
|
|
9057
|
+
return null;
|
|
9058
|
+
if (v instanceof Date) {
|
|
9059
|
+
return isNaN(v.getTime()) ? null : v;
|
|
9060
|
+
}
|
|
9061
|
+
const s = String(v).trim();
|
|
9062
|
+
if (!s)
|
|
9063
|
+
return null;
|
|
9064
|
+
// Date-only string -> create LOCAL date (avoid UTC shifting)
|
|
9065
|
+
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
9066
|
+
if (m) {
|
|
9067
|
+
const y = Number(m[1]);
|
|
9068
|
+
const mo = Number(m[2]) - 1;
|
|
9069
|
+
const d = Number(m[3]);
|
|
9070
|
+
const local = new Date(y, mo, d);
|
|
9071
|
+
return isNaN(local.getTime()) ? null : local;
|
|
9072
|
+
}
|
|
9073
|
+
// ISO / other -> fallback
|
|
9074
|
+
const dt = new Date(s);
|
|
9075
|
+
return isNaN(dt.getTime()) ? null : dt;
|
|
8704
9076
|
}
|
|
8705
9077
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaStartDueDateV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8706
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: MetaStartDueDateV2Component, isStandalone: true, selector: "phoenix-meta-start-due-date-v2", inputs: { dataCy: "dataCy" }, providers: [
|
|
9078
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: MetaStartDueDateV2Component, isStandalone: true, selector: "phoenix-meta-start-due-date-v2", inputs: { dataCy: "dataCy", disable: "disable" }, providers: [
|
|
8707
9079
|
{
|
|
8708
9080
|
provide: NG_VALUE_ACCESSOR,
|
|
8709
9081
|
useExisting: forwardRef(() => MetaStartDueDateV2Component),
|
|
@@ -8722,6 +9094,7 @@ class MetaStartDueDateV2Component {
|
|
|
8722
9094
|
[disabled]="disabled"
|
|
8723
9095
|
[ngModel]="startDate"
|
|
8724
9096
|
(ngModelChange)="onStartChange($event)"
|
|
9097
|
+
(onBlur)="handleBlur()"
|
|
8725
9098
|
appendTo="body"
|
|
8726
9099
|
></p-datepicker>
|
|
8727
9100
|
</div>
|
|
@@ -8740,16 +9113,17 @@ class MetaStartDueDateV2Component {
|
|
|
8740
9113
|
[disabled]="disabled"
|
|
8741
9114
|
[ngModel]="endDate"
|
|
8742
9115
|
(ngModelChange)="onEndChange($event)"
|
|
9116
|
+
(onBlur)="handleBlur()"
|
|
8743
9117
|
appendTo="body"
|
|
8744
9118
|
></p-datepicker>
|
|
8745
9119
|
</div>
|
|
8746
9120
|
</div>
|
|
8747
9121
|
</div>
|
|
8748
|
-
`, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i2$7.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "pipe", type: i3$2.TranslatePipe, name: "translate" }] });
|
|
9122
|
+
`, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i2$7.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "pipe", type: i3$2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8749
9123
|
}
|
|
8750
9124
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaStartDueDateV2Component, decorators: [{
|
|
8751
9125
|
type: Component,
|
|
8752
|
-
args: [{ selector: 'phoenix-meta-start-due-date-v2', standalone: true, imports: [CommonModule, FormsModule, TranslateModule, DatePickerModule], providers: [
|
|
9126
|
+
args: [{ selector: 'phoenix-meta-start-due-date-v2', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, TranslateModule, DatePickerModule], providers: [
|
|
8753
9127
|
{
|
|
8754
9128
|
provide: NG_VALUE_ACCESSOR,
|
|
8755
9129
|
useExisting: forwardRef(() => MetaStartDueDateV2Component),
|
|
@@ -8768,6 +9142,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8768
9142
|
[disabled]="disabled"
|
|
8769
9143
|
[ngModel]="startDate"
|
|
8770
9144
|
(ngModelChange)="onStartChange($event)"
|
|
9145
|
+
(onBlur)="handleBlur()"
|
|
8771
9146
|
appendTo="body"
|
|
8772
9147
|
></p-datepicker>
|
|
8773
9148
|
</div>
|
|
@@ -8786,6 +9161,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8786
9161
|
[disabled]="disabled"
|
|
8787
9162
|
[ngModel]="endDate"
|
|
8788
9163
|
(ngModelChange)="onEndChange($event)"
|
|
9164
|
+
(onBlur)="handleBlur()"
|
|
8789
9165
|
appendTo="body"
|
|
8790
9166
|
></p-datepicker>
|
|
8791
9167
|
</div>
|
|
@@ -8794,18 +9170,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8794
9170
|
`, styles: [":host{display:block}\n"] }]
|
|
8795
9171
|
}], propDecorators: { dataCy: [{
|
|
8796
9172
|
type: Input
|
|
9173
|
+
}], disable: [{
|
|
9174
|
+
type: Input
|
|
8797
9175
|
}] } });
|
|
8798
9176
|
|
|
8799
9177
|
class MetaFormFieldV2Component {
|
|
9178
|
+
/** Metadata definition of the field (type, key, options, styles, flags, etc.) */
|
|
8800
9179
|
field;
|
|
9180
|
+
/** Parent FormGroup that contains the FormControl for this field */
|
|
8801
9181
|
form;
|
|
8802
|
-
/**
|
|
9182
|
+
/**
|
|
9183
|
+
* Page-level read-only flag.
|
|
9184
|
+
* When true, the component renders ReadOnlyInputV2Component instead of editable controls.
|
|
9185
|
+
*/
|
|
8803
9186
|
readOnly = false;
|
|
8804
|
-
|
|
9187
|
+
/**
|
|
9188
|
+
* Global disable flag (e.g. parent dialog toggles entire form disabled).
|
|
9189
|
+
* This is merged with field-level disable configuration.
|
|
9190
|
+
*/
|
|
8805
9191
|
disableForm = false;
|
|
9192
|
+
/** Used to manually trigger change detection for OnPush strategy */
|
|
8806
9193
|
cdr = inject(ChangeDetectorRef);
|
|
9194
|
+
/** Used to automatically unsubscribe from value/status streams on destroy */
|
|
8807
9195
|
dr = inject(DestroyRef);
|
|
9196
|
+
/** Translation service for validation and display labels */
|
|
8808
9197
|
translate = inject(TranslateService);
|
|
9198
|
+
/**
|
|
9199
|
+
* Exposed enum-like mapping of MetaFieldType for template usage.
|
|
9200
|
+
* Keeps templates readable and avoids magic strings.
|
|
9201
|
+
*/
|
|
8809
9202
|
MetaFieldType = Object.freeze({
|
|
8810
9203
|
TEXT: 'TEXT',
|
|
8811
9204
|
NUMBER: 'NUMBER',
|
|
@@ -8831,61 +9224,95 @@ class MetaFormFieldV2Component {
|
|
|
8831
9224
|
LINKS_DATA: 'LINKS_DATA',
|
|
8832
9225
|
SLOT: 'SLOT',
|
|
8833
9226
|
});
|
|
9227
|
+
/** Control key resolved from MetaFieldConfig */
|
|
8834
9228
|
get key() {
|
|
8835
9229
|
return this.field?.configuration?.key ?? '';
|
|
8836
9230
|
}
|
|
9231
|
+
/** Field type resolved from MetaFieldConfig */
|
|
8837
9232
|
get type() {
|
|
8838
9233
|
return this.field?.configuration?.type ?? 'TEXT';
|
|
8839
9234
|
}
|
|
9235
|
+
/** Column width class for grid layout (falls back to default if not provided) */
|
|
8840
9236
|
get colClass() {
|
|
8841
|
-
return this.field?.hidden
|
|
9237
|
+
return this.field?.hidden
|
|
9238
|
+
? 'p-0'
|
|
9239
|
+
: (this.field?.style.colWidth ?? 'col-12 md:col-6');
|
|
8842
9240
|
}
|
|
8843
9241
|
ngOnInit() {
|
|
8844
9242
|
const ctrl = this.ctrl();
|
|
8845
9243
|
if (!ctrl)
|
|
8846
9244
|
return;
|
|
9245
|
+
/**
|
|
9246
|
+
* Subscribe to both valueChanges and statusChanges so the component:
|
|
9247
|
+
* - re-renders when user changes the value
|
|
9248
|
+
* - re-renders when validation state changes (touched/dirty/errors)
|
|
9249
|
+
*/
|
|
8847
9250
|
merge(ctrl.valueChanges, ctrl.statusChanges)
|
|
8848
9251
|
.pipe(takeUntilDestroyed(this.dr))
|
|
8849
9252
|
.subscribe(() => this.cdr.markForCheck());
|
|
8850
9253
|
}
|
|
9254
|
+
/** Human-friendly label defined in metadata (already localized key) */
|
|
8851
9255
|
userFriendlyMessage() {
|
|
8852
9256
|
return this.field?.userFriendlyMessage ?? null;
|
|
8853
9257
|
}
|
|
9258
|
+
/** Optional placeholder i18n key defined in metadata */
|
|
8854
9259
|
placeholderKey() {
|
|
8855
9260
|
return this.field?.configuration?.placeholderKey ?? null;
|
|
8856
9261
|
}
|
|
9262
|
+
/**
|
|
9263
|
+
* Resolves final read-only state for this field:
|
|
9264
|
+
* - page-level readOnly OR field-level readOnly
|
|
9265
|
+
*/
|
|
8857
9266
|
isReadOnly() {
|
|
8858
9267
|
return !!this.readOnly || !!this.field?.readOnly;
|
|
8859
9268
|
}
|
|
9269
|
+
/**
|
|
9270
|
+
* Resolves final disabled state for this field:
|
|
9271
|
+
* - page-level disable OR field-level disable
|
|
9272
|
+
*/
|
|
8860
9273
|
isDisabled() {
|
|
8861
9274
|
return !!this.disableForm || !!this.field?.disable;
|
|
8862
9275
|
}
|
|
9276
|
+
/** Shortcut to underlying FormControl */
|
|
8863
9277
|
ctrl() {
|
|
8864
9278
|
return this.form.get(this.key);
|
|
8865
9279
|
}
|
|
8866
|
-
/**
|
|
9280
|
+
/**
|
|
9281
|
+
* Minimal value formatter for legacy read-only rendering.
|
|
9282
|
+
* Kept intentionally simple to match V1 behavior.
|
|
9283
|
+
*/
|
|
8867
9284
|
displayValue() {
|
|
8868
9285
|
const v = this.ctrl()?.value;
|
|
8869
9286
|
if (v === null || v === undefined)
|
|
8870
9287
|
return '';
|
|
8871
9288
|
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
|
|
8872
9289
|
return v;
|
|
8873
|
-
//
|
|
9290
|
+
// Common DTO shapes (assign, option objects, uploads, etc.)
|
|
8874
9291
|
if (typeof v === 'object') {
|
|
8875
9292
|
return v.label ?? v.name ?? v.fileName ?? JSON.stringify(v);
|
|
8876
9293
|
}
|
|
8877
9294
|
return String(v);
|
|
8878
9295
|
}
|
|
9296
|
+
/**
|
|
9297
|
+
* Determines whether validation error should be displayed.
|
|
9298
|
+
* Errors are shown only after user interaction (touched or dirty).
|
|
9299
|
+
*/
|
|
8879
9300
|
showError() {
|
|
8880
9301
|
const c = this.ctrl();
|
|
8881
9302
|
return !!c && (c.touched || c.dirty) && !!c.errors;
|
|
8882
9303
|
}
|
|
8883
|
-
/**
|
|
9304
|
+
/**
|
|
9305
|
+
* Maps control error object to a normalized error key.
|
|
9306
|
+
* Supports:
|
|
9307
|
+
* - Angular built-in validators
|
|
9308
|
+
* - Phoenix custom validators
|
|
9309
|
+
* - Submit-only async validators
|
|
9310
|
+
*/
|
|
8884
9311
|
errorKey() {
|
|
8885
9312
|
const c = this.ctrl();
|
|
8886
9313
|
if (!c?.errors)
|
|
8887
9314
|
return null;
|
|
8888
|
-
//
|
|
9315
|
+
// Angular built-in validators
|
|
8889
9316
|
if (c.errors['required'])
|
|
8890
9317
|
return 'required';
|
|
8891
9318
|
if (c.errors['minlength'])
|
|
@@ -8900,27 +9327,31 @@ class MetaFormFieldV2Component {
|
|
|
8900
9327
|
return 'min';
|
|
8901
9328
|
if (c.errors['max'])
|
|
8902
9329
|
return 'max';
|
|
8903
|
-
// custom validators
|
|
9330
|
+
// Phoenix custom validators
|
|
8904
9331
|
if (c.errors['dangerousChars'])
|
|
8905
9332
|
return 'dangerousChars';
|
|
8906
9333
|
if (c.errors['timeperiod'])
|
|
8907
9334
|
return 'timeperiod';
|
|
8908
9335
|
if (c.errors['invalidDate'])
|
|
8909
|
-
return 'invalidDate';
|
|
9336
|
+
return 'invalidDate';
|
|
8910
9337
|
if (c.errors['dueDate'])
|
|
8911
9338
|
return 'dueDate';
|
|
8912
9339
|
if (c.errors['bothDates'])
|
|
8913
9340
|
return 'bothDates';
|
|
8914
|
-
//
|
|
9341
|
+
// Submit-only async validators
|
|
8915
9342
|
if (c.errors['unique'])
|
|
8916
9343
|
return 'unique';
|
|
8917
9344
|
if (c.errors['uniqueEntry'])
|
|
8918
9345
|
return 'uniqueEntry';
|
|
8919
9346
|
if (c.errors['custom'])
|
|
8920
9347
|
return 'custom';
|
|
9348
|
+
// Fallback: return first error key
|
|
8921
9349
|
return Object.keys(c.errors)[0] ?? null;
|
|
8922
9350
|
}
|
|
8923
|
-
/**
|
|
9351
|
+
/**
|
|
9352
|
+
* Resolves translated error message based on errorKey().
|
|
9353
|
+
* This is the single place responsible for validation message UX.
|
|
9354
|
+
*/
|
|
8924
9355
|
errorText() {
|
|
8925
9356
|
const c = this.ctrl();
|
|
8926
9357
|
const k = this.errorKey();
|
|
@@ -8942,11 +9373,12 @@ class MetaFormFieldV2Component {
|
|
|
8942
9373
|
case 'dangerousChars':
|
|
8943
9374
|
return this.translate.instant('VALIDATION_MESSAGE.NO_SPECIAL_CHARS_ALLOWED');
|
|
8944
9375
|
case 'custom':
|
|
8945
|
-
//
|
|
9376
|
+
// Legacy behavior: custom error can already be a translation key
|
|
8946
9377
|
return this.translate.instant(c.errors?.['custom']);
|
|
8947
9378
|
case 'uniqueEntry':
|
|
8948
|
-
//
|
|
8949
|
-
return c.errors?.['uniqueEntry'] ??
|
|
9379
|
+
// Legacy behavior: uniqueEntry may already be a translated string
|
|
9380
|
+
return (c.errors?.['uniqueEntry'] ??
|
|
9381
|
+
this.translate.instant('VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE'));
|
|
8950
9382
|
case 'unique':
|
|
8951
9383
|
return this.translate.instant('VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE');
|
|
8952
9384
|
case 'timeperiod':
|
|
@@ -8961,7 +9393,7 @@ class MetaFormFieldV2Component {
|
|
|
8961
9393
|
upperValue: c.errors?.['max']?.max,
|
|
8962
9394
|
});
|
|
8963
9395
|
case 'pattern': {
|
|
8964
|
-
//
|
|
9396
|
+
// Special-case URL pattern handling (legacy InlineFieldError behavior)
|
|
8965
9397
|
const re = '^(https?://)?([\\da-z.-]+)\\.([a-z.]{2,6})[/\\w .-]*/?$';
|
|
8966
9398
|
const requiredPattern = c.errors?.['pattern']?.requiredPattern;
|
|
8967
9399
|
if (requiredPattern === re) {
|
|
@@ -8973,42 +9405,47 @@ class MetaFormFieldV2Component {
|
|
|
8973
9405
|
return this.translate.instant('VALIDATION_MESSAGE.INVALID_VALUE');
|
|
8974
9406
|
}
|
|
8975
9407
|
}
|
|
9408
|
+
/**
|
|
9409
|
+
* Lightweight text formatter for simple read-only display use cases.
|
|
9410
|
+
* This is used mainly for inline displays and summary UIs.
|
|
9411
|
+
*/
|
|
8976
9412
|
valueText() {
|
|
8977
9413
|
const c = this.ctrl();
|
|
8978
9414
|
const v = c?.value;
|
|
8979
9415
|
if (v === null || v === undefined || v === '')
|
|
8980
9416
|
return '--';
|
|
8981
|
-
//
|
|
9417
|
+
// Single-select option: resolve label from options
|
|
8982
9418
|
if (this.type === 'SS_OPTION') {
|
|
8983
9419
|
const opts = this.field?.configuration?.options ?? [];
|
|
8984
9420
|
if (typeof v !== 'object') {
|
|
8985
9421
|
const hit = opts.find((o) => o?.value === v);
|
|
8986
9422
|
const label = hit?.label ?? v;
|
|
8987
|
-
// ako je label i18n key
|
|
8988
9423
|
return this.translate.instant(label);
|
|
8989
9424
|
}
|
|
8990
|
-
//
|
|
9425
|
+
// Object value fallback
|
|
8991
9426
|
const label = v.label ?? v.value;
|
|
8992
9427
|
return this.translate.instant(label);
|
|
8993
9428
|
}
|
|
8994
|
-
//
|
|
9429
|
+
// Date formatting
|
|
8995
9430
|
if (this.type === 'DATE' && v instanceof Date) {
|
|
8996
9431
|
return v.toLocaleDateString();
|
|
8997
9432
|
}
|
|
8998
|
-
//
|
|
9433
|
+
// Text editor / textarea: strip basic HTML tags for compact display
|
|
8999
9434
|
if (this.type === 'TEXT_EDITOR' || this.type === 'TEXT_AREA') {
|
|
9000
9435
|
return String(v).replace(/<[^>]*>/g, '').trim() || '--';
|
|
9001
9436
|
}
|
|
9002
9437
|
return String(v);
|
|
9003
9438
|
}
|
|
9004
9439
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaFormFieldV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9005
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: MetaFormFieldV2Component, isStandalone: true, selector: "phoenix-meta-form-field-v2", inputs: { field: "field", form: "form", readOnly: "readOnly", disableForm: "disableForm" }, ngImport: i0, template: "<div [formGroup]=\"form\">\n @if (!field.hidden) {\n <div class=\"meta-field flex flex-column gap-2\" [style.order]=\"field.order ?? null\"\n [attr.data-cy]=\"'meta-field-' + key\">\n\n @if (userFriendlyMessage()) {\n <label class=\"meta-label\" [attr.for]=\"key\">\n {{ userFriendlyMessage()! | translate }}\n @if (field.mandatory) { <span class=\"meta-required\">*</span> }\n </label>\n }\n\n <!-- READ ONLY (page-level ili field-level) -->\n @if (isReadOnly()) {\n <phoenix-read-only-input-v2 [field]=\"field\" [form]=\"form\"></phoenix-read-only-input-v2>\n } @else {\n\n @switch (type) {\n\n @case (MetaFieldType.TEXT) {\n <input pInputText [id]=\"key\" [formControlName]=\"key\"\n [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\" [readonly]=\"isReadOnly()\">\n }\n\n @case (MetaFieldType.PASSWORD) {\n
|
|
9006
|
-
// PrimeNG 20
|
|
9440
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: MetaFormFieldV2Component, isStandalone: true, selector: "phoenix-meta-form-field-v2", inputs: { field: "field", form: "form", readOnly: "readOnly", disableForm: "disableForm" }, ngImport: i0, template: "<div [formGroup]=\"form\">\n @if (!field.hidden) {\n <div class=\"meta-field flex flex-column gap-2\" [style.order]=\"field.order ?? null\"\n [attr.data-cy]=\"'meta-field-' + key\">\n\n @if (userFriendlyMessage()) {\n <label class=\"meta-label\" [attr.for]=\"key\">\n {{ userFriendlyMessage()! | translate }}\n @if (field.mandatory) { <span class=\"meta-required\">*</span> }\n </label>\n }\n\n <!-- READ ONLY (page-level ili field-level) -->\n @if (isReadOnly()) {\n <phoenix-read-only-input-v2 [field]=\"field\" [form]=\"form\"></phoenix-read-only-input-v2>\n } @else {\n\n @switch (type) {\n\n @case (MetaFieldType.TEXT) {\n <input pInputText [id]=\"key\" [formControlName]=\"key\"\n [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\" [readonly]=\"isReadOnly()\">\n }\n\n @case (MetaFieldType.PASSWORD) {\n <phoenix-meta-password-feild [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\">\n </phoenix-meta-password-feild>\n }\n\n @case (MetaFieldType.TEXT_AREA) {\n <textarea pTextarea class=\"meta-textarea\" [id]=\"key\" [formControlName]=\"key\" fluid [autoResize]=\"false\" rows=\"5\"\n [readonly]=\"isReadOnly()\" [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\">\n </textarea>\n }\n\n @case (MetaFieldType.NUMBER) {\n <p-inputNumber [inputId]=\"key\" [formControlName]=\"key\">\n </p-inputNumber>\n }\n\n @case (MetaFieldType.DATE) {\n <p-datepicker [inputId]=\"key\" [formControlName]=\"key\" [showIcon]=\"true\">\n </p-datepicker>\n }\n\n @case (MetaFieldType.SS_OPTION) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\">\n </p-select>\n }\n\n @case (MetaFieldType.SS_OPTION_OBJECT_BASED) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" [formControlName]=\"key\"\n [showClear]=\"true\">\n </p-select>\n }\n\n @case (MetaFieldType.MS_OPTION) {\n <p-multiselect [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\" display=\"chip\">\n </p-multiselect>\n }\n\n @case (MetaFieldType.CHECKBOX) {\n <div class=\"flex align-items-center gap-2\">\n <p-checkbox [inputId]=\"key\" [binary]=\"true\" [formControlName]=\"key\">\n </p-checkbox>\n\n @if (placeholderKey()) {\n <label [attr.for]=\"key\" class=\"meta-inline-label\">\n {{ placeholderKey()! | translate }}\n </label>\n }\n </div>\n }\n\n <!-- advanced: preko postoje\u0107ih komponenti -->\n @case (MetaFieldType.TIMEPERIOD) {\n <phoenix-meta-timeperiod [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\"></phoenix-meta-timeperiod>\n }\n\n @case (MetaFieldType.CURRENCY) {\n <phoenix-meta-currency [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-currency>\n }\n\n @case (MetaFieldType.START_DUE_DATE) {\n <phoenix-meta-start-due-date-v2\n [formControlName]=\"key\"\n [attr.data-cy]=\"'start-due-' + key\">\n </phoenix-meta-start-due-date-v2>\n }\n\n @case (MetaFieldType.TEXT_EDITOR) {\n <phoenix-meta-text-editor [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-text-editor>\n }\n\n @case (MetaFieldType.CHECKBOX_COLOR) {\n <phoenix-meta-checkbox-color-picker-v2\n [formControlName]=\"key\"\n [options]=\"(field.configuration.extra?.['colorGrid'] ?? [])\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-checkbox-color-picker-v2>\n }\n\n @case (MetaFieldType.SWITCH) {\n <phoenix-meta-switch [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-switch>\n }\n\n @case (MetaFieldType.SELECT_BUTTON) {\n <phoenix-meta-select-button [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-select-button>\n }\n\n @case (MetaFieldType.ASSIGN) {\n <phoenix-meta-assign-responsible-v2\n [formControlName]=\"key\"\n [items]=\"(field.configuration.extra?.['items'] ?? [])\"\n [dialogHeaderKey]=\"(field.configuration.extra?.['dialogHeaderKey'] ?? 'LABELS.ASSIGN_RESPONSIBLE')\"\n ></phoenix-meta-assign-responsible-v2>\n }\n\n <!-- @case (MetaFieldType.ASSIGN_ASSET) {\n <phoenix-meta-assign-asset [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-assign-asset>\n } -->\n\n @case (MetaFieldType.COLOR) {\n <phoenix-meta-color-picker-v2\n [formControlName]=\"key\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-color-picker-v2>\n }\n\n @case (MetaFieldType.UPLOAD) {\n <phoenix-meta-upload [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload>\n }\n\n @case (MetaFieldType.UPLOAD_DRAG_DROP) {\n <phoenix-meta-upload-dragdrop [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload-dragdrop>\n }\n\n @case (MetaFieldType.LINKS_DATA) {\n <!-- <input pInputText [id]=\"key\" [formControlName]=\"key\" [readonly]=\"true\"> -->\n }\n\n @case (MetaFieldType.SLOT) { }\n\n @default {\n <input pInputText [id]=\"key\" [formControlName]=\"key\">\n }\n }\n }\n\n\n @if (!readOnly && showError()) {\n <small class=\"p-error block mt-1\">\n <i class=\"pi pi-info-circle mr-1\"></i>{{ errorText() }}\n </small>\n }\n </div>\n }\n</div>", styles: [".meta-field{width:100%}.meta-required{margin-left:4px;color:#ef4444}.meta-textarea{resize:none!important}.meta-inline-label{opacity:.9}.p-inputtext.ng-invalid.ng-dirty{border-color:var(--p-inputtext-border-color)!important}.p-select.ng-invalid.ng-dirty{border-color:var(--p-select-border-color)!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type:
|
|
9441
|
+
// PrimeNG 20 base inputs
|
|
9007
9442
|
InputTextModule }, { kind: "directive", type: i3$4.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i3$8.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "ngmodule", type: InputNumberModule }, { kind: "component", type: i3$6.InputNumber, selector: "p-inputNumber, p-inputnumber, p-input-number", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "placeholder", "tabindex", "title", "ariaLabelledBy", "ariaDescribedBy", "ariaLabel", "ariaRequired", "autocomplete", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i10.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "styleClass", "panelStyle", "panelStyleClass", "inputId", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "dataKey", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "placeholder", "options", "filterValue", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect", "size", "variant", "fluid", "appendTo"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3$5.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i2$7.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "ngmodule", type: MessageModule }, { kind: "component", type:
|
|
9008
|
-
//
|
|
9009
|
-
MetaTimeperiodComponent, selector: "phoenix-meta-timeperiod", inputs: ["control", "parentForm"] }, { kind: "component", type: MetaCurrencyComponent, selector: "phoenix-meta-currency" }, { kind: "component", type: MetaStartDueDateV2Component, selector: "phoenix-meta-start-due-date-v2", inputs: ["dataCy"] }, { kind: "component", type: MetaTextEditorComponent, selector: "phoenix-meta-text-editor", inputs: ["previewMode", "hideLabel"] }, { kind: "component", type: MetaCheckboxColorPickerV2Component, selector: "phoenix-meta-checkbox-color-picker-v2", inputs: ["options", "disable"] }, { kind: "component", type: MetaSwitchComponent, selector: "phoenix-meta-switch" }, { kind: "component", type: MetaSelectButtonComponent, selector: "phoenix-meta-select-button" }, { kind: "component", type: MetaAssignResponsibleV2Component, selector: "phoenix-meta-assign-responsible-v2", inputs: ["items", "dialogHeaderKey"] }, { kind: "component", type:
|
|
9443
|
+
// Advanced / custom Phoenix fields
|
|
9444
|
+
MetaTimeperiodComponent, selector: "phoenix-meta-timeperiod", inputs: ["control", "parentForm"] }, { kind: "component", type: MetaCurrencyComponent, selector: "phoenix-meta-currency" }, { kind: "component", type: MetaStartDueDateV2Component, selector: "phoenix-meta-start-due-date-v2", inputs: ["dataCy", "disable"] }, { kind: "component", type: MetaTextEditorComponent, selector: "phoenix-meta-text-editor", inputs: ["previewMode", "hideLabel"] }, { kind: "component", type: MetaCheckboxColorPickerV2Component, selector: "phoenix-meta-checkbox-color-picker-v2", inputs: ["options", "disable"] }, { kind: "component", type: MetaSwitchComponent, selector: "phoenix-meta-switch" }, { kind: "component", type: MetaSelectButtonComponent, selector: "phoenix-meta-select-button" }, { kind: "component", type: MetaAssignResponsibleV2Component, selector: "phoenix-meta-assign-responsible-v2", inputs: ["items", "dialogHeaderKey"] }, { kind: "component", type:
|
|
9010
9445
|
// MetaAssignAssetComponent,
|
|
9011
|
-
MetaPasswordFeildComponent, selector: "phoenix-meta-password-feild" }, { kind: "component", type: MetaColorPickerV2Component, selector: "phoenix-meta-color-picker-v2", inputs: ["disable"] }, { kind: "component", type: MetaUploadComponent, selector: "phoenix-meta-upload" }, { kind: "component", type: MetaUploadComponentDragDrop, selector: "phoenix-meta-upload-dragdrop" }, { kind: "component", type:
|
|
9446
|
+
MetaPasswordFeildComponent, selector: "phoenix-meta-password-feild" }, { kind: "component", type: MetaColorPickerV2Component, selector: "phoenix-meta-color-picker-v2", inputs: ["disable"] }, { kind: "component", type: MetaUploadComponent, selector: "phoenix-meta-upload" }, { kind: "component", type: MetaUploadComponentDragDrop, selector: "phoenix-meta-upload-dragdrop" }, { kind: "component", type:
|
|
9447
|
+
// Read-only renderer used when page or field is in read-only mode
|
|
9448
|
+
ReadOnlyInputV2Component, selector: "phoenix-read-only-input-v2", inputs: ["field", "form"] }, { kind: "pipe", type: i3$2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9012
9449
|
}
|
|
9013
9450
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaFormFieldV2Component, decorators: [{
|
|
9014
9451
|
type: Component,
|
|
@@ -9016,7 +9453,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
9016
9453
|
CommonModule,
|
|
9017
9454
|
ReactiveFormsModule,
|
|
9018
9455
|
TranslateModule,
|
|
9019
|
-
// PrimeNG 20
|
|
9456
|
+
// PrimeNG 20 base inputs
|
|
9020
9457
|
InputTextModule,
|
|
9021
9458
|
TextareaModule,
|
|
9022
9459
|
InputNumberModule,
|
|
@@ -9025,7 +9462,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
9025
9462
|
SelectModule,
|
|
9026
9463
|
DatePickerModule,
|
|
9027
9464
|
MessageModule,
|
|
9028
|
-
//
|
|
9465
|
+
// Advanced / custom Phoenix fields
|
|
9029
9466
|
MetaTimeperiodComponent,
|
|
9030
9467
|
MetaCurrencyComponent,
|
|
9031
9468
|
MetaStartDueDateV2Component,
|
|
@@ -9039,8 +9476,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
9039
9476
|
MetaColorPickerV2Component,
|
|
9040
9477
|
MetaUploadComponent,
|
|
9041
9478
|
MetaUploadComponentDragDrop,
|
|
9479
|
+
// Read-only renderer used when page or field is in read-only mode
|
|
9042
9480
|
ReadOnlyInputV2Component,
|
|
9043
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [formGroup]=\"form\">\n @if (!field.hidden) {\n <div class=\"meta-field flex flex-column gap-2\" [style.order]=\"field.order ?? null\"\n [attr.data-cy]=\"'meta-field-' + key\">\n\n @if (userFriendlyMessage()) {\n <label class=\"meta-label\" [attr.for]=\"key\">\n {{ userFriendlyMessage()! | translate }}\n @if (field.mandatory) { <span class=\"meta-required\">*</span> }\n </label>\n }\n\n <!-- READ ONLY (page-level ili field-level) -->\n @if (isReadOnly()) {\n <phoenix-read-only-input-v2 [field]=\"field\" [form]=\"form\"></phoenix-read-only-input-v2>\n } @else {\n\n @switch (type) {\n\n @case (MetaFieldType.TEXT) {\n <input pInputText [id]=\"key\" [formControlName]=\"key\"\n [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\" [readonly]=\"isReadOnly()\">\n }\n\n @case (MetaFieldType.PASSWORD) {\n
|
|
9481
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [formGroup]=\"form\">\n @if (!field.hidden) {\n <div class=\"meta-field flex flex-column gap-2\" [style.order]=\"field.order ?? null\"\n [attr.data-cy]=\"'meta-field-' + key\">\n\n @if (userFriendlyMessage()) {\n <label class=\"meta-label\" [attr.for]=\"key\">\n {{ userFriendlyMessage()! | translate }}\n @if (field.mandatory) { <span class=\"meta-required\">*</span> }\n </label>\n }\n\n <!-- READ ONLY (page-level ili field-level) -->\n @if (isReadOnly()) {\n <phoenix-read-only-input-v2 [field]=\"field\" [form]=\"form\"></phoenix-read-only-input-v2>\n } @else {\n\n @switch (type) {\n\n @case (MetaFieldType.TEXT) {\n <input pInputText [id]=\"key\" [formControlName]=\"key\"\n [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\" [readonly]=\"isReadOnly()\">\n }\n\n @case (MetaFieldType.PASSWORD) {\n <phoenix-meta-password-feild [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\">\n </phoenix-meta-password-feild>\n }\n\n @case (MetaFieldType.TEXT_AREA) {\n <textarea pTextarea class=\"meta-textarea\" [id]=\"key\" [formControlName]=\"key\" fluid [autoResize]=\"false\" rows=\"5\"\n [readonly]=\"isReadOnly()\" [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\">\n </textarea>\n }\n\n @case (MetaFieldType.NUMBER) {\n <p-inputNumber [inputId]=\"key\" [formControlName]=\"key\">\n </p-inputNumber>\n }\n\n @case (MetaFieldType.DATE) {\n <p-datepicker [inputId]=\"key\" [formControlName]=\"key\" [showIcon]=\"true\">\n </p-datepicker>\n }\n\n @case (MetaFieldType.SS_OPTION) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\">\n </p-select>\n }\n\n @case (MetaFieldType.SS_OPTION_OBJECT_BASED) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" [formControlName]=\"key\"\n [showClear]=\"true\">\n </p-select>\n }\n\n @case (MetaFieldType.MS_OPTION) {\n <p-multiselect [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\" display=\"chip\">\n </p-multiselect>\n }\n\n @case (MetaFieldType.CHECKBOX) {\n <div class=\"flex align-items-center gap-2\">\n <p-checkbox [inputId]=\"key\" [binary]=\"true\" [formControlName]=\"key\">\n </p-checkbox>\n\n @if (placeholderKey()) {\n <label [attr.for]=\"key\" class=\"meta-inline-label\">\n {{ placeholderKey()! | translate }}\n </label>\n }\n </div>\n }\n\n <!-- advanced: preko postoje\u0107ih komponenti -->\n @case (MetaFieldType.TIMEPERIOD) {\n <phoenix-meta-timeperiod [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\"></phoenix-meta-timeperiod>\n }\n\n @case (MetaFieldType.CURRENCY) {\n <phoenix-meta-currency [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-currency>\n }\n\n @case (MetaFieldType.START_DUE_DATE) {\n <phoenix-meta-start-due-date-v2\n [formControlName]=\"key\"\n [attr.data-cy]=\"'start-due-' + key\">\n </phoenix-meta-start-due-date-v2>\n }\n\n @case (MetaFieldType.TEXT_EDITOR) {\n <phoenix-meta-text-editor [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-text-editor>\n }\n\n @case (MetaFieldType.CHECKBOX_COLOR) {\n <phoenix-meta-checkbox-color-picker-v2\n [formControlName]=\"key\"\n [options]=\"(field.configuration.extra?.['colorGrid'] ?? [])\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-checkbox-color-picker-v2>\n }\n\n @case (MetaFieldType.SWITCH) {\n <phoenix-meta-switch [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-switch>\n }\n\n @case (MetaFieldType.SELECT_BUTTON) {\n <phoenix-meta-select-button [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-select-button>\n }\n\n @case (MetaFieldType.ASSIGN) {\n <phoenix-meta-assign-responsible-v2\n [formControlName]=\"key\"\n [items]=\"(field.configuration.extra?.['items'] ?? [])\"\n [dialogHeaderKey]=\"(field.configuration.extra?.['dialogHeaderKey'] ?? 'LABELS.ASSIGN_RESPONSIBLE')\"\n ></phoenix-meta-assign-responsible-v2>\n }\n\n <!-- @case (MetaFieldType.ASSIGN_ASSET) {\n <phoenix-meta-assign-asset [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-assign-asset>\n } -->\n\n @case (MetaFieldType.COLOR) {\n <phoenix-meta-color-picker-v2\n [formControlName]=\"key\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-color-picker-v2>\n }\n\n @case (MetaFieldType.UPLOAD) {\n <phoenix-meta-upload [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload>\n }\n\n @case (MetaFieldType.UPLOAD_DRAG_DROP) {\n <phoenix-meta-upload-dragdrop [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload-dragdrop>\n }\n\n @case (MetaFieldType.LINKS_DATA) {\n <!-- <input pInputText [id]=\"key\" [formControlName]=\"key\" [readonly]=\"true\"> -->\n }\n\n @case (MetaFieldType.SLOT) { }\n\n @default {\n <input pInputText [id]=\"key\" [formControlName]=\"key\">\n }\n }\n }\n\n\n @if (!readOnly && showError()) {\n <small class=\"p-error block mt-1\">\n <i class=\"pi pi-info-circle mr-1\"></i>{{ errorText() }}\n </small>\n }\n </div>\n }\n</div>", styles: [".meta-field{width:100%}.meta-required{margin-left:4px;color:#ef4444}.meta-textarea{resize:none!important}.meta-inline-label{opacity:.9}.p-inputtext.ng-invalid.ng-dirty{border-color:var(--p-inputtext-border-color)!important}.p-select.ng-invalid.ng-dirty{border-color:var(--p-select-border-color)!important}\n"] }]
|
|
9044
9482
|
}], propDecorators: { field: [{
|
|
9045
9483
|
type: Input,
|
|
9046
9484
|
args: [{ required: true }]
|
|
@@ -9079,22 +9517,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
9079
9517
|
*/
|
|
9080
9518
|
const META_FORM_ASYNC_EXECUTOR = new InjectionToken('META_FORM_ASYNC_EXECUTOR');
|
|
9081
9519
|
|
|
9520
|
+
// Symbols used to attach submit-only validators and cleanup subscriptions
|
|
9521
|
+
// directly onto the FormGroup instance without polluting its public API.
|
|
9082
9522
|
const SUBMIT_VALIDATORS = Symbol('META_FORM_V2_SUBMIT_VALIDATORS');
|
|
9083
9523
|
const SUBMIT_CLEAR_SUBS = Symbol('META_FORM_V2_SUBMIT_CLEAR_SUBS');
|
|
9084
9524
|
class MetaSubmitValidatorService {
|
|
9085
9525
|
executor;
|
|
9526
|
+
/** Used to automatically clean up subscriptions when the service is destroyed */
|
|
9086
9527
|
dr = inject(DestroyRef);
|
|
9087
|
-
constructor(
|
|
9528
|
+
constructor(
|
|
9529
|
+
/**
|
|
9530
|
+
* Optional async executor abstraction used to perform server-side validation
|
|
9531
|
+
* (e.g. uniqueness checks).
|
|
9532
|
+
*
|
|
9533
|
+
* If not provided, submit-only validators will be skipped gracefully.
|
|
9534
|
+
*/
|
|
9535
|
+
executor) {
|
|
9088
9536
|
this.executor = executor;
|
|
9089
9537
|
}
|
|
9538
|
+
/**
|
|
9539
|
+
* Registers submit-only validators on the given form.
|
|
9540
|
+
*
|
|
9541
|
+
* - Attaches validator metadata to the form instance (via Symbols)
|
|
9542
|
+
* - Cleans up any previous subscriptions
|
|
9543
|
+
* - Subscribes to valueChanges in order to auto-clear submit-only errors
|
|
9544
|
+
* when the user modifies the field after a failed submit
|
|
9545
|
+
*/
|
|
9090
9546
|
register(form, validators) {
|
|
9091
9547
|
const list = validators ?? [];
|
|
9092
9548
|
form[SUBMIT_VALIDATORS] = list;
|
|
9093
|
-
//
|
|
9549
|
+
// Cleanup old auto-clear subscriptions
|
|
9094
9550
|
const old = form[SUBMIT_CLEAR_SUBS] ?? [];
|
|
9095
9551
|
old.forEach((s) => s.unsubscribe());
|
|
9096
9552
|
form[SUBMIT_CLEAR_SUBS] = [];
|
|
9097
|
-
//
|
|
9553
|
+
// Subscribe to valueChanges in order to remove submit-only errors
|
|
9554
|
+
// as soon as the user changes the input.
|
|
9098
9555
|
const subs = [];
|
|
9099
9556
|
for (const v of list) {
|
|
9100
9557
|
const ctrl = form.get(v.controlKey);
|
|
@@ -9104,29 +9561,45 @@ class MetaSubmitValidatorService {
|
|
|
9104
9561
|
subs.push(ctrl.valueChanges
|
|
9105
9562
|
.pipe(takeUntilDestroyed(this.dr))
|
|
9106
9563
|
.subscribe(() => {
|
|
9107
|
-
// ✅
|
|
9564
|
+
// ✅ Only remove submit-only error.
|
|
9565
|
+
// ❌ Do NOT trigger updateValueAndValidity here to avoid noisy re-validation.
|
|
9108
9566
|
this.removeError(ctrl, errorKey);
|
|
9109
9567
|
}));
|
|
9110
9568
|
}
|
|
9111
9569
|
form[SUBMIT_CLEAR_SUBS] = subs;
|
|
9112
9570
|
}
|
|
9571
|
+
/**
|
|
9572
|
+
* Executes submit-only validators.
|
|
9573
|
+
*
|
|
9574
|
+
* Flow:
|
|
9575
|
+
* 1) Mark all controls as touched/dirty so sync validation messages become visible
|
|
9576
|
+
* 2) If form is sync-invalid, skip async validation
|
|
9577
|
+
* 3) Clear previous submit-only errors
|
|
9578
|
+
* 4) Execute async validators sequentially
|
|
9579
|
+
* 5) Apply mapped errors back to controls
|
|
9580
|
+
*
|
|
9581
|
+
* Returns:
|
|
9582
|
+
* - true → form is valid (sync + submit-only async)
|
|
9583
|
+
* - false → at least one submit-only validator failed
|
|
9584
|
+
*/
|
|
9113
9585
|
async run(form) {
|
|
9114
9586
|
const validators = form[SUBMIT_VALIDATORS] ?? [];
|
|
9115
|
-
//
|
|
9587
|
+
// Make sure sync validation errors are visible on submit
|
|
9116
9588
|
this.markAllTouchedOnSubmit(form);
|
|
9117
|
-
//
|
|
9589
|
+
// Do not emit valueChanges/statusChanges events here
|
|
9118
9590
|
form.updateValueAndValidity({ emitEvent: false });
|
|
9119
9591
|
if (!validators.length)
|
|
9120
9592
|
return form.valid;
|
|
9121
|
-
//
|
|
9593
|
+
// If no async executor is provided, do not block submit
|
|
9122
9594
|
if (!this.executor)
|
|
9123
9595
|
return form.valid;
|
|
9124
|
-
//
|
|
9596
|
+
// Clear previous submit-only errors before re-running validation
|
|
9125
9597
|
this.clearSubmitErrors(form, validators);
|
|
9126
|
-
//
|
|
9598
|
+
// If sync validation fails, skip async calls
|
|
9127
9599
|
form.updateValueAndValidity({ emitEvent: false });
|
|
9128
9600
|
if (form.invalid)
|
|
9129
9601
|
return false;
|
|
9602
|
+
// Execute submit-only validators sequentially
|
|
9130
9603
|
for (const v of validators) {
|
|
9131
9604
|
const req = v.buildRequest(form);
|
|
9132
9605
|
if (!req)
|
|
@@ -9141,7 +9614,9 @@ class MetaSubmitValidatorService {
|
|
|
9141
9614
|
const errorKey = v.errorKey ?? 'unique';
|
|
9142
9615
|
const msg = mapped?.message ??
|
|
9143
9616
|
'VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE';
|
|
9617
|
+
// Attach submit-only error to control
|
|
9144
9618
|
ctrl.setErrors({ ...(ctrl.errors ?? {}), [errorKey]: msg });
|
|
9619
|
+
// Force visibility of the error in UI
|
|
9145
9620
|
ctrl.markAsTouched();
|
|
9146
9621
|
ctrl.markAsDirty();
|
|
9147
9622
|
return false;
|
|
@@ -9149,6 +9624,10 @@ class MetaSubmitValidatorService {
|
|
|
9149
9624
|
}
|
|
9150
9625
|
return form.valid;
|
|
9151
9626
|
}
|
|
9627
|
+
/**
|
|
9628
|
+
* Removes a specific submit-only error from the control without
|
|
9629
|
+
* touching other validation errors.
|
|
9630
|
+
*/
|
|
9152
9631
|
removeError(ctrl, errorKey) {
|
|
9153
9632
|
if (!ctrl.errors?.[errorKey])
|
|
9154
9633
|
return;
|
|
@@ -9156,6 +9635,10 @@ class MetaSubmitValidatorService {
|
|
|
9156
9635
|
delete next[errorKey];
|
|
9157
9636
|
ctrl.setErrors(Object.keys(next).length ? next : null);
|
|
9158
9637
|
}
|
|
9638
|
+
/**
|
|
9639
|
+
* Clears submit-only errors for all controls involved in submit validators.
|
|
9640
|
+
* This does NOT trigger updateValueAndValidity to avoid unnecessary re-validation.
|
|
9641
|
+
*/
|
|
9159
9642
|
clearSubmitErrors(form, validators) {
|
|
9160
9643
|
const keys = new Set();
|
|
9161
9644
|
validators.forEach((v) => keys.add(v.controlKey));
|
|
@@ -9166,10 +9649,14 @@ class MetaSubmitValidatorService {
|
|
|
9166
9649
|
return;
|
|
9167
9650
|
const nextErrors = { ...(ctrl.errors ?? {}) };
|
|
9168
9651
|
errorKeys.forEach((ek) => delete nextErrors[ek]);
|
|
9169
|
-
// ✅
|
|
9652
|
+
// ✅ Only update errors object, no re-validation side effects
|
|
9170
9653
|
ctrl.setErrors(Object.keys(nextErrors).length ? nextErrors : null);
|
|
9171
9654
|
});
|
|
9172
9655
|
}
|
|
9656
|
+
/**
|
|
9657
|
+
* Marks all form controls as touched and dirty.
|
|
9658
|
+
* This is used on submit to make validation errors visible to the user.
|
|
9659
|
+
*/
|
|
9173
9660
|
markAllTouchedOnSubmit(form) {
|
|
9174
9661
|
Object.values(form.controls).forEach((c) => {
|
|
9175
9662
|
c.markAsTouched();
|
|
@@ -9189,71 +9676,129 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
9189
9676
|
args: [META_FORM_ASYNC_EXECUTOR]
|
|
9190
9677
|
}] }] });
|
|
9191
9678
|
|
|
9679
|
+
/**
|
|
9680
|
+
* Validator for START_DUE_DATE composite field.
|
|
9681
|
+
*
|
|
9682
|
+
* Responsibilities:
|
|
9683
|
+
* - Optionally enforce that BOTH startDate and endDate are provided (requireBoth)
|
|
9684
|
+
* - Validate that provided dates are valid Date values
|
|
9685
|
+
* - Ensure startDate is not after endDate
|
|
9686
|
+
*
|
|
9687
|
+
* This is a synchronous validator and should be attached to the single FormControl
|
|
9688
|
+
* that represents the composite START_DUE_DATE value.
|
|
9689
|
+
*/
|
|
9192
9690
|
function startDueDateV2Validator(opts) {
|
|
9193
9691
|
const requireBoth = !!opts?.requireBoth;
|
|
9194
9692
|
return (control) => {
|
|
9195
9693
|
const v = control.value;
|
|
9196
9694
|
const sdRaw = v?.startDate ?? null;
|
|
9197
9695
|
const edRaw = v?.endDate ?? null;
|
|
9198
|
-
//
|
|
9696
|
+
// Case 1: both dates are empty
|
|
9697
|
+
// - If both are required -> invalid
|
|
9698
|
+
// - Otherwise -> valid (optional date range)
|
|
9199
9699
|
if (!sdRaw && !edRaw) {
|
|
9200
9700
|
return requireBoth ? { bothDates: true } : null;
|
|
9201
9701
|
}
|
|
9202
|
-
//
|
|
9702
|
+
// Case 2: only one date is provided but both are required
|
|
9203
9703
|
if (requireBoth && (!sdRaw || !edRaw)) {
|
|
9204
9704
|
return { bothDates: true };
|
|
9205
9705
|
}
|
|
9206
|
-
//
|
|
9706
|
+
// Case 3: both are optional and one is missing -> valid
|
|
9207
9707
|
if (!sdRaw || !edRaw)
|
|
9208
9708
|
return null;
|
|
9709
|
+
// Parse raw values into Date instances
|
|
9209
9710
|
const sd = new Date(sdRaw);
|
|
9210
9711
|
const ed = new Date(edRaw);
|
|
9712
|
+
// Invalid date values (e.g. unparsable strings)
|
|
9211
9713
|
if (isNaN(sd.getTime()) || isNaN(ed.getTime()))
|
|
9212
9714
|
return { dueDate: true };
|
|
9715
|
+
// Normalize both dates to midnight for date-only comparison (ignore time)
|
|
9213
9716
|
const s = new Date(sd);
|
|
9214
9717
|
s.setHours(0, 0, 0, 0);
|
|
9215
9718
|
const e = new Date(ed);
|
|
9216
9719
|
e.setHours(0, 0, 0, 0);
|
|
9720
|
+
// Start date must not be after end date
|
|
9217
9721
|
return s.getTime() > e.getTime() ? { dueDate: true } : null;
|
|
9218
9722
|
};
|
|
9219
9723
|
}
|
|
9220
9724
|
|
|
9725
|
+
/**
|
|
9726
|
+
* Builds synchronous Angular validators for a single meta-field.
|
|
9727
|
+
*
|
|
9728
|
+
* This function only returns *sync* validators:
|
|
9729
|
+
* - Required, min/max, length
|
|
9730
|
+
* - Email
|
|
9731
|
+
* - Regex / phone
|
|
9732
|
+
* - Timeperiod
|
|
9733
|
+
* - Start/Due date cross-field-ish validator (but still sync)
|
|
9734
|
+
* - Whitespace rules
|
|
9735
|
+
* - Dangerous characters
|
|
9736
|
+
*
|
|
9737
|
+
* Submit-only async validators (e.g. uniqueness) are handled elsewhere (MetaSubmitValidatorService).
|
|
9738
|
+
*/
|
|
9221
9739
|
function buildSyncValidators(field, ctx) {
|
|
9222
9740
|
const out = [];
|
|
9741
|
+
// `field.validators` is a metadata-driven bag of validation rules
|
|
9223
9742
|
const v = field.validators ?? {};
|
|
9743
|
+
// Field type drives some special-case validators
|
|
9224
9744
|
const type = field.configuration.type;
|
|
9745
|
+
// ---- numeric constraints ----
|
|
9225
9746
|
if (v.min != null)
|
|
9226
9747
|
out.push(Validators.min(v.min));
|
|
9227
9748
|
if (v.max != null)
|
|
9228
9749
|
out.push(Validators.max(v.max));
|
|
9750
|
+
// ---- length constraints ----
|
|
9229
9751
|
if (v.minLength != null)
|
|
9230
9752
|
out.push(Validators.minLength(v.minLength));
|
|
9231
9753
|
if (v.maxLength != null)
|
|
9232
9754
|
out.push(Validators.maxLength(v.maxLength));
|
|
9755
|
+
// ---- email ----
|
|
9233
9756
|
if (v.email)
|
|
9234
9757
|
out.push(Validators.email);
|
|
9235
|
-
|
|
9758
|
+
// ---- regex / pattern ----
|
|
9759
|
+
// Supports either:
|
|
9760
|
+
// - plain pattern string
|
|
9761
|
+
// - "/.../flags" format which we try to compile into RegExp
|
|
9762
|
+
if (v.regex?.regexType) {
|
|
9236
9763
|
out.push(Validators.pattern(resolvePattern(v.regex.regexType) ?? v.regex.regexType));
|
|
9764
|
+
}
|
|
9765
|
+
// ---- phone ----
|
|
9766
|
+
// if (v.phone) out.push(Validators.pattern(/^\+?[1-9]\d{7,14}$/));
|
|
9237
9767
|
if (v.phone)
|
|
9238
|
-
out.push(
|
|
9768
|
+
out.push(phoneHumanValidator({ minDigits: 8, maxDigits: 15 }));
|
|
9769
|
+
// ---- time period ----
|
|
9770
|
+
// Localized validator; tpMin can enforce minimal value/length depending on your implementation.
|
|
9239
9771
|
if (v.timeperiod)
|
|
9240
9772
|
out.push(timePeriod(ctx?.lang ?? 'en', v.tpMin));
|
|
9773
|
+
// ---- start/due date ----
|
|
9774
|
+
// Apply the validator if field is of START_DUE_DATE type or if metadata explicitly enables dueDate.
|
|
9775
|
+
// requireBoth is driven by field.mandatory
|
|
9241
9776
|
if (type === 'START_DUE_DATE' || v.dueDate) {
|
|
9242
9777
|
out.push(startDueDateV2Validator({ requireBoth: !!field.mandatory }));
|
|
9243
9778
|
}
|
|
9779
|
+
// ---- required ----
|
|
9244
9780
|
if (field.mandatory)
|
|
9245
9781
|
out.push(Validators.required);
|
|
9782
|
+
// ---- whitespace guard for required text fields ----
|
|
9783
|
+
// Prevent " " from passing required validation.
|
|
9246
9784
|
if (field.mandatory && (type === 'TEXT' || type === 'TEXT_AREA')) {
|
|
9247
9785
|
out.push(WhiteSpaceValidator.noWhiteSpaceValidator);
|
|
9248
9786
|
}
|
|
9787
|
+
// ---- dangerous characters ----
|
|
9788
|
+
// Applied to text and textarea only.
|
|
9249
9789
|
if (type === 'TEXT' || type === 'TEXT_AREA') {
|
|
9250
9790
|
out.push(noDangerousCharsValidator());
|
|
9251
9791
|
}
|
|
9252
9792
|
return out;
|
|
9253
9793
|
}
|
|
9794
|
+
/**
|
|
9795
|
+
* Tries to interpret a regex string in "/pattern/flags" format.
|
|
9796
|
+
* If parsing fails, returns null and the caller falls back to using the raw string.
|
|
9797
|
+
*/
|
|
9254
9798
|
function resolvePattern(regexType) {
|
|
9255
9799
|
if (!regexType)
|
|
9256
9800
|
return null;
|
|
9801
|
+
// Support format like: "/^[0-9]+$/g"
|
|
9257
9802
|
const m = regexType.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
9258
9803
|
if (m) {
|
|
9259
9804
|
try {
|
|
@@ -9263,26 +9808,94 @@ function resolvePattern(regexType) {
|
|
|
9263
9808
|
return null;
|
|
9264
9809
|
}
|
|
9265
9810
|
}
|
|
9811
|
+
// Otherwise treat it as a plain string pattern
|
|
9266
9812
|
return regexType;
|
|
9267
9813
|
}
|
|
9814
|
+
/**
|
|
9815
|
+
* Phone validator that accepts common human formats:
|
|
9816
|
+
* - spaces, hyphens, parentheses
|
|
9817
|
+
* - local numbers starting with 0 (e.g. 060...)
|
|
9818
|
+
* - international numbers with +
|
|
9819
|
+
*
|
|
9820
|
+
* Strategy:
|
|
9821
|
+
* 1) Strip formatting chars
|
|
9822
|
+
* 2) Validate the normalized digits
|
|
9823
|
+
*/
|
|
9824
|
+
function phoneHumanValidator(opts) {
|
|
9825
|
+
const min = opts?.minDigits ?? 8;
|
|
9826
|
+
const max = opts?.maxDigits ?? 15;
|
|
9827
|
+
return (control) => {
|
|
9828
|
+
const raw = (control.value ?? '').toString().trim();
|
|
9829
|
+
if (!raw)
|
|
9830
|
+
return null; // let required handle empties
|
|
9831
|
+
// Keep leading "+" if present, remove common separators
|
|
9832
|
+
const normalized = raw
|
|
9833
|
+
.replace(/[\s\-().]/g, '') // remove spaces, dashes, parentheses, dots
|
|
9834
|
+
.replace(/(?!^\+)\+/g, ''); // remove any "+" not at the start
|
|
9835
|
+
// Allow optional leading "+"
|
|
9836
|
+
const hasPlus = normalized.startsWith('+');
|
|
9837
|
+
const digits = hasPlus ? normalized.slice(1) : normalized;
|
|
9838
|
+
// Digits only after normalization
|
|
9839
|
+
if (!/^\d+$/.test(digits))
|
|
9840
|
+
return { phone: true };
|
|
9841
|
+
// Length constraints (digits only)
|
|
9842
|
+
if (digits.length < min || digits.length > max)
|
|
9843
|
+
return { phone: true };
|
|
9844
|
+
// If it has "+", require country code style (first digit not 0)
|
|
9845
|
+
if (hasPlus && digits.startsWith('0'))
|
|
9846
|
+
return { phone: true };
|
|
9847
|
+
// If local, allow leading 0 (e.g. 060...)
|
|
9848
|
+
return null;
|
|
9849
|
+
};
|
|
9850
|
+
}
|
|
9268
9851
|
|
|
9852
|
+
/**
|
|
9853
|
+
* Ensures that the FormGroup structure matches the provided metadata (flat field list).
|
|
9854
|
+
*
|
|
9855
|
+
* Responsibilities:
|
|
9856
|
+
* - Create missing FormControls based on MetaFieldConfig
|
|
9857
|
+
* - Apply (and re-apply) synchronous validators derived from metadata
|
|
9858
|
+
* - Sync disabled/enabled state with metadata
|
|
9859
|
+
* - Remove obsolete controls that are no longer present in metadata
|
|
9860
|
+
*
|
|
9861
|
+
* This function is intentionally idempotent:
|
|
9862
|
+
* Calling it multiple times with the same metadata should not break form state.
|
|
9863
|
+
*/
|
|
9269
9864
|
function ensureControlsV2(fb, form, flat, initialValues, ctx) {
|
|
9865
|
+
// Tracks which control keys are allowed by current metadata
|
|
9270
9866
|
const allowed = new Set();
|
|
9271
9867
|
for (const field of flat) {
|
|
9272
9868
|
const key = field?.configuration?.key;
|
|
9273
9869
|
if (!key)
|
|
9274
9870
|
continue;
|
|
9275
9871
|
allowed.add(key);
|
|
9872
|
+
/**
|
|
9873
|
+
* Create control if it does not exist yet.
|
|
9874
|
+
* Initial value is taken from initialValues map if provided,
|
|
9875
|
+
* otherwise defaults to null.
|
|
9876
|
+
*/
|
|
9276
9877
|
if (!form.contains(key)) {
|
|
9277
9878
|
form.addControl(key, new FormControl(initialValues?.[key] ?? null));
|
|
9278
9879
|
}
|
|
9279
9880
|
const ctrl = form.get(key);
|
|
9280
9881
|
if (!ctrl)
|
|
9281
9882
|
continue;
|
|
9282
|
-
|
|
9883
|
+
/**
|
|
9884
|
+
* (1) Sync validators:
|
|
9885
|
+
* Rebuild and apply synchronous validators based on current field metadata.
|
|
9886
|
+
* This ensures that changes in metadata (e.g. required, min/max, patterns)
|
|
9887
|
+
* are reflected on the form control.
|
|
9888
|
+
*/
|
|
9283
9889
|
ctrl.setValidators(buildSyncValidators(field, ctx));
|
|
9890
|
+
// Recalculate validity silently (do not emit events to avoid UI side-effects)
|
|
9284
9891
|
ctrl.updateValueAndValidity({ emitEvent: false });
|
|
9285
|
-
|
|
9892
|
+
/**
|
|
9893
|
+
* (2) Sync disabled/enabled state:
|
|
9894
|
+
* The control's enabled state must reflect the field metadata.
|
|
9895
|
+
* This is symmetric:
|
|
9896
|
+
* - If metadata says "disable" → disable control
|
|
9897
|
+
* - If metadata says "enable" → enable control
|
|
9898
|
+
*/
|
|
9286
9899
|
const shouldBeDisabled = !!field.disable;
|
|
9287
9900
|
if (shouldBeDisabled && ctrl.enabled) {
|
|
9288
9901
|
ctrl.disable({ emitEvent: false });
|
|
@@ -9291,6 +9904,13 @@ function ensureControlsV2(fb, form, flat, initialValues, ctx) {
|
|
|
9291
9904
|
ctrl.enable({ emitEvent: false });
|
|
9292
9905
|
}
|
|
9293
9906
|
}
|
|
9907
|
+
/**
|
|
9908
|
+
* Remove any controls that are present in the FormGroup
|
|
9909
|
+
* but no longer exist in the metadata.
|
|
9910
|
+
*
|
|
9911
|
+
* This keeps the form structure in sync when metadata changes dynamically
|
|
9912
|
+
* (e.g. conditional fields, stage-based forms, etc.).
|
|
9913
|
+
*/
|
|
9294
9914
|
Object.keys(form.controls).forEach((k) => {
|
|
9295
9915
|
if (!allowed.has(k))
|
|
9296
9916
|
form.removeControl(k);
|
|
@@ -9311,48 +9931,101 @@ function flattenControls(input) {
|
|
|
9311
9931
|
}
|
|
9312
9932
|
|
|
9313
9933
|
class MetaFormV2Component {
|
|
9934
|
+
/** Form instance created/owned by the parent (dialog/page) */
|
|
9314
9935
|
form;
|
|
9936
|
+
/**
|
|
9937
|
+
* V2 metadata/config:
|
|
9938
|
+
* - controls (grouped or flat)
|
|
9939
|
+
* - initialValues
|
|
9940
|
+
* - submitValidators (run on submit only)
|
|
9941
|
+
* - setupDependencies (optional runtime bindings)
|
|
9942
|
+
*/
|
|
9315
9943
|
config;
|
|
9316
|
-
/**
|
|
9944
|
+
/**
|
|
9945
|
+
* Page-level readOnly state controlled by parent.
|
|
9946
|
+
* Used both for rendering and for "enter edit mode" behavior.
|
|
9947
|
+
*/
|
|
9317
9948
|
readOnly = false;
|
|
9318
|
-
/** Optional layout
|
|
9949
|
+
/** Optional layout customization for inner content wrapper */
|
|
9319
9950
|
contentStyle = null;
|
|
9951
|
+
/** Optional class name(s) applied to inner content wrapper */
|
|
9320
9952
|
contentClass = null;
|
|
9953
|
+
/** Builds/ensures controls exist (ensures validators & default values are wired) */
|
|
9321
9954
|
fb = inject(FormBuilder);
|
|
9955
|
+
/** Registers and executes submit-only validators (async validation on submit) */
|
|
9322
9956
|
submitValidator = inject(MetaSubmitValidatorService);
|
|
9957
|
+
/** Used for validator localization (lang-dependent validators / messages) */
|
|
9323
9958
|
translate = inject(TranslateService);
|
|
9959
|
+
/**
|
|
9960
|
+
* A lightweight signature of the current metadata structure.
|
|
9961
|
+
* Used to detect when the form schema changes (and avoid unnecessary resets).
|
|
9962
|
+
*/
|
|
9324
9963
|
lastSignature = '';
|
|
9964
|
+
/**
|
|
9965
|
+
* Cleanup function returned by setupDependencies (if any).
|
|
9966
|
+
* Called only when metadata structure changes or on destroy.
|
|
9967
|
+
*/
|
|
9325
9968
|
depCleanup;
|
|
9326
|
-
/**
|
|
9969
|
+
/**
|
|
9970
|
+
* PrimeNG Accordion "value" for opened panels.
|
|
9971
|
+
* For multiple panels, PrimeNG expects an array of ids.
|
|
9972
|
+
*/
|
|
9327
9973
|
expandedGroupIds = [];
|
|
9328
9974
|
ngOnChanges(changes) {
|
|
9329
9975
|
if (!this.form || !this.config)
|
|
9330
9976
|
return;
|
|
9977
|
+
/**
|
|
9978
|
+
* Flatten metadata controls to a single list for:
|
|
9979
|
+
* - building/enforcing FormControls
|
|
9980
|
+
* - dependency binding
|
|
9981
|
+
* - signature calculation
|
|
9982
|
+
*/
|
|
9331
9983
|
const flat = flattenControls(this.config.controls);
|
|
9984
|
+
/**
|
|
9985
|
+
* Signature is used to detect real schema changes.
|
|
9986
|
+
* We intentionally ignore labels/props and track key+type only.
|
|
9987
|
+
*/
|
|
9332
9988
|
const signature = flat.map((f) => `${f.configuration.key}:${f.configuration.type}`).join('|');
|
|
9989
|
+
/** "config changed" includes initialValues, validators, dependencies, etc. */
|
|
9333
9990
|
const configChanged = !!changes['config'];
|
|
9991
|
+
/** "form changed" means parent passed a different FormGroup instance */
|
|
9334
9992
|
const formChanged = !!changes['form'];
|
|
9993
|
+
/** schema changed if the signature differs from the last one */
|
|
9335
9994
|
const metaChanged = signature !== this.lastSignature;
|
|
9995
|
+
/**
|
|
9996
|
+
* Special case: entering edit mode (readOnly true -> false).
|
|
9997
|
+
* Used to selectively show validation for already-filled values.
|
|
9998
|
+
*/
|
|
9336
9999
|
const enteringEdit = !!changes['readOnly'] &&
|
|
9337
10000
|
changes['readOnly'].previousValue === true &&
|
|
9338
10001
|
changes['readOnly'].currentValue === false;
|
|
9339
|
-
// If nothing relevant changed, exit
|
|
10002
|
+
// If nothing relevant changed, exit early to avoid unnecessary work.
|
|
9340
10003
|
if (!configChanged && !formChanged && !metaChanged && !enteringEdit)
|
|
9341
10004
|
return;
|
|
9342
|
-
|
|
10005
|
+
/**
|
|
10006
|
+
* Dependencies are tied to the metadata structure.
|
|
10007
|
+
* If schema changed, cleanup old subscriptions/bindings first.
|
|
10008
|
+
*/
|
|
9343
10009
|
if (configChanged || metaChanged) {
|
|
9344
10010
|
this.depCleanup?.();
|
|
9345
10011
|
this.depCleanup = undefined;
|
|
9346
10012
|
}
|
|
9347
|
-
|
|
10013
|
+
/**
|
|
10014
|
+
* Ensure controls exist on the passed FormGroup and sync validators.
|
|
10015
|
+
* This is where missing controls are added and validator wiring is applied.
|
|
10016
|
+
*/
|
|
9348
10017
|
const initial = this.config.initialValues ?? {};
|
|
9349
10018
|
ensureControlsV2(this.fb, this.form, flat, initial, { lang: this.translate.currentLang });
|
|
9350
|
-
|
|
10019
|
+
/**
|
|
10020
|
+
* Patch initial values without emitting changes:
|
|
10021
|
+
* - prevents loops
|
|
10022
|
+
* - keeps create/edit initialization silent
|
|
10023
|
+
*/
|
|
9351
10024
|
this.form.patchValue(initial, { emitEvent: false });
|
|
9352
10025
|
this.form.updateValueAndValidity({ emitEvent: false });
|
|
9353
10026
|
/**
|
|
9354
|
-
* Initialize accordion
|
|
9355
|
-
*
|
|
10027
|
+
* Initialize accordion open panels ONLY when schema changes.
|
|
10028
|
+
* IMPORTANT: do NOT do this on readOnly toggle, otherwise user-collapsed state resets.
|
|
9356
10029
|
*/
|
|
9357
10030
|
if (metaChanged) {
|
|
9358
10031
|
if (this.isGrouped) {
|
|
@@ -9366,9 +10039,16 @@ class MetaFormV2Component {
|
|
|
9366
10039
|
this.expandedGroupIds = [];
|
|
9367
10040
|
}
|
|
9368
10041
|
}
|
|
9369
|
-
|
|
10042
|
+
/**
|
|
10043
|
+
* Register submit-only validators.
|
|
10044
|
+
* Safe to call even if empty; service will attach necessary structures internally.
|
|
10045
|
+
*/
|
|
9370
10046
|
this.submitValidator.register(this.form, this.config.submitValidators);
|
|
9371
|
-
|
|
10047
|
+
/**
|
|
10048
|
+
* Bind dependencies ONLY when schema changes.
|
|
10049
|
+
* setupDependencies can subscribe to valueChanges, set options, reset fields, etc.
|
|
10050
|
+
* If it returns a function, we store it for cleanup.
|
|
10051
|
+
*/
|
|
9372
10052
|
if ((configChanged || metaChanged) && this.config.setupDependencies) {
|
|
9373
10053
|
const maybeCleanup = this.config.setupDependencies({
|
|
9374
10054
|
form: this.form,
|
|
@@ -9381,10 +10061,10 @@ class MetaFormV2Component {
|
|
|
9381
10061
|
this.depCleanup = maybeCleanup;
|
|
9382
10062
|
}
|
|
9383
10063
|
/**
|
|
9384
|
-
* Entering edit:
|
|
9385
|
-
*
|
|
9386
|
-
*
|
|
9387
|
-
*
|
|
10064
|
+
* Entering edit mode:
|
|
10065
|
+
* - mark controls as touched ONLY if they already have a meaningful value
|
|
10066
|
+
* - expand groups that contain "visible invalid" controls (invalid + touched/dirty)
|
|
10067
|
+
* This prevents CREATE dialogs from showing "required" errors immediately.
|
|
9388
10068
|
*/
|
|
9389
10069
|
if (enteringEdit) {
|
|
9390
10070
|
queueMicrotask(() => {
|
|
@@ -9392,12 +10072,19 @@ class MetaFormV2Component {
|
|
|
9392
10072
|
this.expandVisibleInvalidGroupsUnion();
|
|
9393
10073
|
});
|
|
9394
10074
|
}
|
|
10075
|
+
// Store signature for the next change detection pass
|
|
9395
10076
|
this.lastSignature = signature;
|
|
9396
10077
|
}
|
|
9397
|
-
|
|
10078
|
+
/**
|
|
10079
|
+
* PrimeNG Accordion emits value as:
|
|
10080
|
+
* - single id (string/number)
|
|
10081
|
+
* - array of ids
|
|
10082
|
+
* We normalize everything into string[] for stable internal state.
|
|
10083
|
+
*/
|
|
9398
10084
|
onAccordionValueChange(v) {
|
|
9399
10085
|
this.expandedGroupIds = this.normalizeAccordionValue(v);
|
|
9400
10086
|
}
|
|
10087
|
+
/** Normalizes Accordion value into a stable string[] representation */
|
|
9401
10088
|
normalizeAccordionValue(v) {
|
|
9402
10089
|
if (Array.isArray(v))
|
|
9403
10090
|
return v.map((x) => `${x}`);
|
|
@@ -9406,31 +10093,44 @@ class MetaFormV2Component {
|
|
|
9406
10093
|
return [`${v}`];
|
|
9407
10094
|
}
|
|
9408
10095
|
// ---------------- template helpers ----------------
|
|
10096
|
+
/** True when metadata contains at least one control definition */
|
|
9409
10097
|
get hasControls() {
|
|
9410
10098
|
return Array.isArray(this.config?.controls) && this.config.controls.length > 0;
|
|
9411
10099
|
}
|
|
10100
|
+
/**
|
|
10101
|
+
* Heuristic: grouped config has "ctrl" on first element.
|
|
10102
|
+
* (Keeps template simple and avoids extra schema fields.)
|
|
10103
|
+
*/
|
|
9412
10104
|
get isGrouped() {
|
|
9413
10105
|
const c = this.config?.controls ?? [];
|
|
9414
10106
|
return !!c[0]?.ctrl;
|
|
9415
10107
|
}
|
|
10108
|
+
/** Returns grouped schema structure (accordion groups) */
|
|
9416
10109
|
get groupedControls() {
|
|
9417
10110
|
return this.config?.controls ?? [];
|
|
9418
10111
|
}
|
|
10112
|
+
/** Returns flat schema structure (grid mode) */
|
|
9419
10113
|
get flatControls() {
|
|
9420
10114
|
return this.config?.controls ?? [];
|
|
9421
10115
|
}
|
|
10116
|
+
/** TrackBy for group rendering */
|
|
9422
10117
|
groupTrack(g, idx) {
|
|
9423
10118
|
return g?.id ?? idx;
|
|
9424
10119
|
}
|
|
9425
|
-
/**
|
|
10120
|
+
/**
|
|
10121
|
+
* PrimeNG accordion panel `value` must match accordion `value` type.
|
|
10122
|
+
* We always convert group id to string for consistent behavior.
|
|
10123
|
+
*/
|
|
9426
10124
|
panelValue(g) {
|
|
9427
10125
|
const id = g?.id;
|
|
9428
10126
|
return id === null || id === undefined ? '' : `${id}`;
|
|
9429
10127
|
}
|
|
9430
10128
|
// ---------------- core behavior ----------------
|
|
9431
10129
|
/**
|
|
9432
|
-
*
|
|
9433
|
-
* This
|
|
10130
|
+
* Marks & validates ONLY controls that already have meaningful values.
|
|
10131
|
+
* This is used when switching from readOnly -> edit mode to avoid:
|
|
10132
|
+
* - triggering "required" errors for empty fields
|
|
10133
|
+
* - expanding groups based on empty mandatory fields on CREATE dialogs
|
|
9434
10134
|
*/
|
|
9435
10135
|
touchAndValidateOnlyFilledControls() {
|
|
9436
10136
|
const controls = this.form?.controls ?? {};
|
|
@@ -9438,21 +10138,21 @@ class MetaFormV2Component {
|
|
|
9438
10138
|
if (!ctrl)
|
|
9439
10139
|
continue;
|
|
9440
10140
|
const value = ctrl.value;
|
|
9441
|
-
// Touch only if
|
|
10141
|
+
// Touch only if this field is already populated (edit case) or prefilled.
|
|
9442
10142
|
if (this.hasMeaningfulValue(value)) {
|
|
9443
10143
|
ctrl.markAsTouched();
|
|
9444
10144
|
ctrl.updateValueAndValidity({ emitEvent: true });
|
|
9445
10145
|
}
|
|
9446
10146
|
}
|
|
9447
|
-
//
|
|
10147
|
+
// Optional: keep form status consistent after selective updates
|
|
9448
10148
|
this.form.updateValueAndValidity({ emitEvent: true });
|
|
9449
10149
|
}
|
|
9450
10150
|
/**
|
|
9451
|
-
*
|
|
10151
|
+
* Defines what counts as a "meaningful" value:
|
|
9452
10152
|
* - non-empty strings
|
|
9453
10153
|
* - numbers / booleans
|
|
9454
10154
|
* - non-empty arrays
|
|
9455
|
-
* - objects with
|
|
10155
|
+
* - objects with common identifiers (key/uuid/id) or any own keys
|
|
9456
10156
|
*/
|
|
9457
10157
|
hasMeaningfulValue(v) {
|
|
9458
10158
|
if (v === null || v === undefined)
|
|
@@ -9466,34 +10166,43 @@ class MetaFormV2Component {
|
|
|
9466
10166
|
if (Array.isArray(v))
|
|
9467
10167
|
return v.length > 0;
|
|
9468
10168
|
if (typeof v === 'object') {
|
|
9469
|
-
//
|
|
10169
|
+
// Common selection shapes: { key }, { uuid }, { id }, etc.
|
|
9470
10170
|
if ('key' in v && v.key != null && `${v.key}`.trim() !== '')
|
|
9471
10171
|
return true;
|
|
9472
10172
|
if ('uuid' in v && v.uuid != null && `${v.uuid}`.trim() !== '')
|
|
9473
10173
|
return true;
|
|
9474
10174
|
if ('id' in v && v.id != null && `${v.id}`.trim() !== '')
|
|
9475
10175
|
return true;
|
|
9476
|
-
//
|
|
10176
|
+
// Fallback: any own keys
|
|
9477
10177
|
return Object.keys(v).length > 0;
|
|
9478
10178
|
}
|
|
9479
10179
|
return false;
|
|
9480
10180
|
}
|
|
9481
10181
|
/**
|
|
9482
|
-
* Visible invalid
|
|
9483
|
-
*
|
|
10182
|
+
* "Visible invalid" means:
|
|
10183
|
+
* - invalid
|
|
10184
|
+
* - AND user has interacted with it (touched or dirty)
|
|
10185
|
+
* This matches typical UI behavior: show errors only after interaction.
|
|
9484
10186
|
*/
|
|
9485
10187
|
isVisibleInvalid(ctrl) {
|
|
9486
10188
|
if (!ctrl)
|
|
9487
10189
|
return false;
|
|
9488
10190
|
return ctrl.invalid && (ctrl.touched || ctrl.dirty);
|
|
9489
10191
|
}
|
|
10192
|
+
/**
|
|
10193
|
+
* Checks if a group contains at least one visible invalid control.
|
|
10194
|
+
* Used to auto-expand groups when entering edit mode.
|
|
10195
|
+
*/
|
|
9490
10196
|
groupHasVisibleInvalid(g) {
|
|
9491
10197
|
const keys = (g?.ctrl ?? [])
|
|
9492
10198
|
.map((f) => f?.configuration?.key)
|
|
9493
10199
|
.filter(Boolean);
|
|
9494
10200
|
return keys.some((k) => this.isVisibleInvalid(this.form.get(k)));
|
|
9495
10201
|
}
|
|
9496
|
-
/**
|
|
10202
|
+
/**
|
|
10203
|
+
* Expands all groups that contain visible invalid controls,
|
|
10204
|
+
* while preserving any groups already expanded by the user.
|
|
10205
|
+
*/
|
|
9497
10206
|
expandVisibleInvalidGroupsUnion() {
|
|
9498
10207
|
if (!this.isGrouped)
|
|
9499
10208
|
return;
|
|
@@ -9506,6 +10215,7 @@ class MetaFormV2Component {
|
|
|
9506
10215
|
return;
|
|
9507
10216
|
this.expandedGroupIds = Array.from(new Set([...this.expandedGroupIds, ...invalidIds]));
|
|
9508
10217
|
}
|
|
10218
|
+
/** Cleanup dependency subscriptions when component is destroyed */
|
|
9509
10219
|
ngOnDestroy() {
|
|
9510
10220
|
this.depCleanup?.();
|
|
9511
10221
|
}
|