@yuuvis/client-framework 3.23.0 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/yuuvis-client-framework-forms.mjs +311 -17
- package/fesm2022/yuuvis-client-framework-forms.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-metadata-form.mjs +8 -6
- package/fesm2022/yuuvis-client-framework-metadata-form.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-object-form.mjs +27 -4
- package/fesm2022/yuuvis-client-framework-object-form.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-query-list.mjs +144 -69
- package/fesm2022/yuuvis-client-framework-query-list.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-smart-search.mjs +35 -3
- package/fesm2022/yuuvis-client-framework-smart-search.mjs.map +1 -1
- package/lib/assets/i18n/de.json +1 -0
- package/lib/assets/i18n/en.json +1 -0
- package/package.json +5 -5
- package/types/yuuvis-client-framework-forms.d.ts +104 -4
- package/types/yuuvis-client-framework-query-list.d.ts +20 -12
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, DestroyRef, input, signal, effect, untracked, Component, forwardRef, ChangeDetectionStrategy,
|
|
2
|
+
import { inject, DestroyRef, input, signal, effect, untracked, Component, ChangeDetectorRef, forwardRef, ChangeDetectionStrategy, ElementRef, computed, linkedSignal, viewChild, output, Input, NgModule } from '@angular/core';
|
|
3
3
|
import { takeUntilDestroyed, rxResource } from '@angular/core/rxjs-interop';
|
|
4
4
|
import * as i1$1 from '@angular/forms';
|
|
5
5
|
import { FormControl, Validators, ReactiveFormsModule, FormBuilder, NG_VALUE_ACCESSOR, NG_VALIDATORS, FormGroup, FormsModule } from '@angular/forms';
|
|
6
6
|
import { MatFormFieldControl, MatFormFieldModule } from '@angular/material/form-field';
|
|
7
7
|
import * as i1 from '@angular/material/select';
|
|
8
8
|
import { MatSelectModule } from '@angular/material/select';
|
|
9
|
-
import { SystemService, TranslateService, Classification, TranslatePipe, Situation, Utils, Operator, OperatorLabel, CatalogService, LocaleNumberPipe, FileSizePipe, IdmService, UserService, AppCacheService, FREE_TEXT_OPTION, USER_ID_REGEX, STORAGE_ROLES_KEY, SearchUtils, LocaleDatePipe, SearchService, DmsService, ObjectConfigService, DmsObject, BaseObjectTypeField, ClassificationPrefix } from '@yuuvis/client-core';
|
|
9
|
+
import { SystemService, TranslateService, Classification, TranslatePipe, Situation, ObjectFormControl, Utils, Operator, OperatorLabel, CatalogService, LocaleNumberPipe, FileSizePipe, IdmService, UserService, AppCacheService, FREE_TEXT_OPTION, USER_ID_REGEX, STORAGE_ROLES_KEY, SearchUtils, LocaleDatePipe, SearchService, DmsService, ObjectConfigService, DmsObject, BaseObjectTypeField, ClassificationPrefix } from '@yuuvis/client-core';
|
|
10
10
|
import { AbstractMatFormField, injectNgControl, FormTranslateService, DialogComponent, ScrollButtonsComponent } from '@yuuvis/client-framework/common';
|
|
11
11
|
import * as i1$3 from '@angular/common';
|
|
12
12
|
import { CommonModule, NgClass } from '@angular/common';
|
|
@@ -42,6 +42,15 @@ class FormUtils {
|
|
|
42
42
|
static getErrorState(control) {
|
|
43
43
|
return control.invalid && (control.dirty || control.touched);
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Writes the errors of a form element's inner control onto the hosting control without
|
|
47
|
+
* dropping the host's own validation result (e.g. `required` set up by the surrounding form).
|
|
48
|
+
* `host.setErrors(inner.errors)` alone would wipe those whenever the inner control is valid.
|
|
49
|
+
*/
|
|
50
|
+
static mergeInnerErrors(host, inner) {
|
|
51
|
+
const ownErrors = host.validator ? host.validator(host) : null;
|
|
52
|
+
host.setErrors(ownErrors || inner.errors ? { ...ownErrors, ...inner.errors } : null);
|
|
53
|
+
}
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
class CatalogComponent extends AbstractMatFormField {
|
|
@@ -242,30 +251,164 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
242
251
|
args: [{ selector: 'yuv-color', standalone: true, imports: [CommonModule, ReactiveFormsModule], providers: [{ provide: MatFormFieldControl, useExisting: ColorComponent }], template: "<span class=\"yuv-color\">\n <label class=\"yuv-color__swatch\" [style.background-color]=\"colorFc.value\">\n <input type=\"color\" [formControl]=\"colorFc\" aria-hidden=\"true\" tabindex=\"-1\" />\n </label>\n <input class=\"yuv-color__hex\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\" [formControl]=\"textFc\" />\n</span>\n", styles: [".yuv-color{display:inline-flex;align-items:center;gap:var(--ymt-spacing-xs);width:100%;margin-block-start:var(--ymt-spacing-xs)}.yuv-color__swatch{position:relative;flex:0 0 auto;inline-size:var(--ymt-sizing-m);block-size:var(--ymt-sizing-m);border-radius:var(--ymt-corner-xs);border:var(--ymt-outline-width) solid var(--ymt-outline-variant);overflow:hidden;cursor:pointer;outline:2px solid currentColor;outline-offset:-1px}.yuv-color__swatch input[type=color]{position:absolute;inset:0;inline-size:100%;block-size:100%;padding:0;border:none;opacity:0;cursor:pointer}.yuv-color__hex{flex:1 1 auto;min-inline-size:0;border:none;outline:none;background:transparent;color:var(--ymt-text-color);font:var(--ymt-font-body)}\n"] }]
|
|
243
252
|
}] });
|
|
244
253
|
|
|
254
|
+
/**
|
|
255
|
+
* @ignore
|
|
256
|
+
* Scripting element for one cell of the table row that is being edited. Mirrors the
|
|
257
|
+
* properties form scripts know from the elements of `scope.model`, but applies changes
|
|
258
|
+
* to the controls of the row editing dialog instead of the object form.
|
|
259
|
+
*/
|
|
260
|
+
class TableRowScopeElement {
|
|
261
|
+
constructor(element, onChange) {
|
|
262
|
+
this.element = element;
|
|
263
|
+
this.onChange = onChange;
|
|
264
|
+
if (this.element['value'] === undefined) {
|
|
265
|
+
this.element['value'] = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Called when the user changed the cell value. Triggers the elements `onchange` callback
|
|
270
|
+
* with the element itself and the rows model (siblings), like form scripts get it for table rows.
|
|
271
|
+
*/
|
|
272
|
+
update(value, rowModel) {
|
|
273
|
+
const newValue = value === undefined || value === '' ? null : value;
|
|
274
|
+
if (JSON.stringify(this.element['value']) === JSON.stringify(newValue))
|
|
275
|
+
return;
|
|
276
|
+
this.element['value'] = newValue;
|
|
277
|
+
const onchange = this.onchange;
|
|
278
|
+
if (typeof onchange === 'function') {
|
|
279
|
+
try {
|
|
280
|
+
onchange(this, rowModel);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
console.error(`Error in form script onchange of table column '${this.name}'`, err);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
set value(value) {
|
|
288
|
+
if (JSON.stringify(this.element['value']) === JSON.stringify(value))
|
|
289
|
+
return;
|
|
290
|
+
this.element['value'] = value;
|
|
291
|
+
this.onChange(this.name, { name: 'value', newValue: value });
|
|
292
|
+
}
|
|
293
|
+
get value() {
|
|
294
|
+
return this.element['value'];
|
|
295
|
+
}
|
|
296
|
+
set required(required) {
|
|
297
|
+
this.element['required'] = required;
|
|
298
|
+
this.onChange(this.name, { name: 'required', newValue: required });
|
|
299
|
+
}
|
|
300
|
+
get required() {
|
|
301
|
+
return !!this.element['required'];
|
|
302
|
+
}
|
|
303
|
+
set readonly(readonly) {
|
|
304
|
+
this.element['readonly'] = readonly;
|
|
305
|
+
this.onChange(this.name, { name: 'readonly', newValue: readonly });
|
|
306
|
+
}
|
|
307
|
+
get readonly() {
|
|
308
|
+
return !!this.element['readonly'];
|
|
309
|
+
}
|
|
310
|
+
set error(error) {
|
|
311
|
+
this.element['error'] = error;
|
|
312
|
+
this.onChange(this.name, { name: 'error', newValue: error });
|
|
313
|
+
}
|
|
314
|
+
get error() {
|
|
315
|
+
return this.element['error'] || null;
|
|
316
|
+
}
|
|
317
|
+
set onchange(onchange) {
|
|
318
|
+
this.element['onchange'] = onchange;
|
|
319
|
+
}
|
|
320
|
+
get onchange() {
|
|
321
|
+
return this.element['onchange'] || null;
|
|
322
|
+
}
|
|
323
|
+
get name() {
|
|
324
|
+
return this.element.name;
|
|
325
|
+
}
|
|
326
|
+
get label() {
|
|
327
|
+
return this.element['label'];
|
|
328
|
+
}
|
|
329
|
+
get description() {
|
|
330
|
+
return this.element['description'];
|
|
331
|
+
}
|
|
332
|
+
get type() {
|
|
333
|
+
return this.element['type'];
|
|
334
|
+
}
|
|
335
|
+
get classifications() {
|
|
336
|
+
return this.element['classifications'];
|
|
337
|
+
}
|
|
338
|
+
get scale() {
|
|
339
|
+
return this.element['scale'];
|
|
340
|
+
}
|
|
341
|
+
get precision() {
|
|
342
|
+
return this.element['precision'];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
245
346
|
class EditTableDataComponent {
|
|
246
347
|
constructor() {
|
|
247
348
|
this.#fb = inject(FormBuilder);
|
|
248
349
|
this.#system = inject(SystemService);
|
|
249
350
|
this.#errorMessage = inject(FormTranslateService);
|
|
351
|
+
this.#destroyRef = inject(DestroyRef);
|
|
352
|
+
this.#cdRef = inject(ChangeDetectorRef);
|
|
250
353
|
this.#dialogData = inject(MAT_DIALOG_DATA);
|
|
251
354
|
this.#dialogRef = inject((MatDialogRef));
|
|
252
355
|
this.errorMessage = signal(null, ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
|
|
253
356
|
this.tableForm = this.#fb.group({});
|
|
254
357
|
this.isInnerTableForm = input(true, ...(ngDevMode ? [{ debugName: "isInnerTableForm" }] : /* istanbul ignore next */ []));
|
|
255
358
|
this.formElement = this.#dialogData.formElement || {};
|
|
256
|
-
this.columns = (this.formElement['elements'] || []).map((c) => ({
|
|
359
|
+
this.columns = signal((this.formElement['elements'] || []).map((c) => ({
|
|
257
360
|
...c,
|
|
258
361
|
propertyType: c.type,
|
|
259
362
|
_internalType: this.#system.getInternalFormElementType(c.type, c.classifications, c.catalog)
|
|
260
|
-
}));
|
|
363
|
+
})), ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
261
364
|
this.#elementData = this.#dialogData.elementData || {};
|
|
262
365
|
this.situation = this.#dialogData.situation || Situation.EDIT;
|
|
263
366
|
this.header = this.#dialogData.label || '';
|
|
264
367
|
this.adding = this.#dialogData.adding;
|
|
368
|
+
// may be disabled by the tables 'onrowedit' form script callback
|
|
369
|
+
this.saveEnabled = signal(true, ...(ngDevMode ? [{ debugName: "saveEnabled" }] : /* istanbul ignore next */ []));
|
|
370
|
+
this.#onRowScriptingChange = (name, change) => {
|
|
371
|
+
const control = this.tableForm.get(name);
|
|
372
|
+
if (!control)
|
|
373
|
+
return;
|
|
374
|
+
switch (change.name) {
|
|
375
|
+
case 'value': {
|
|
376
|
+
control.setValue(change.newValue);
|
|
377
|
+
control.markAsDirty();
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case 'required': {
|
|
381
|
+
this.#updateColumn(control, { required: !!change.newValue });
|
|
382
|
+
control.setValidators(this.#getValidators(control._eoFormElement));
|
|
383
|
+
control.markAsTouched();
|
|
384
|
+
control.updateValueAndValidity();
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case 'readonly': {
|
|
388
|
+
this.#updateColumn(control, { readonly: !!change.newValue });
|
|
389
|
+
if (change.newValue) {
|
|
390
|
+
control.disable();
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
control.enable();
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case 'error': {
|
|
398
|
+
control._eoFormElement.error = change.newValue;
|
|
399
|
+
control.markAsTouched();
|
|
400
|
+
control.updateValueAndValidity();
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
this.#cdRef.markForCheck();
|
|
405
|
+
};
|
|
265
406
|
}
|
|
266
407
|
#fb;
|
|
267
408
|
#system;
|
|
268
409
|
#errorMessage;
|
|
410
|
+
#destroyRef;
|
|
411
|
+
#cdRef;
|
|
269
412
|
#dialogData;
|
|
270
413
|
#dialogRef;
|
|
271
414
|
#elementData;
|
|
@@ -275,16 +418,32 @@ class EditTableDataComponent {
|
|
|
275
418
|
submit() {
|
|
276
419
|
const data = this.tableForm.getRawValue();
|
|
277
420
|
Object.keys(data).forEach((key) => (data[key] === '' || data[key] === undefined) && (data[key] = null));
|
|
278
|
-
if (this.tableForm.valid && this.#dialogRef)
|
|
421
|
+
if (this.tableForm.valid && this.saveEnabled() && this.#dialogRef)
|
|
279
422
|
this.#dialogRef.close(data);
|
|
280
423
|
}
|
|
281
424
|
ngOnInit() {
|
|
282
|
-
|
|
283
|
-
|
|
425
|
+
this.#initFormControls();
|
|
426
|
+
this.#initErrorMessages();
|
|
427
|
+
this.#initRowScripting();
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Adds one control per table column to the row form and fills it with the data of the row being edited.
|
|
431
|
+
*/
|
|
432
|
+
#initFormControls() {
|
|
433
|
+
this.columns().forEach((column) => {
|
|
434
|
+
const control = new ObjectFormControl({ value: '', disabled: column.readonly });
|
|
435
|
+
control._eoFormElement = column;
|
|
436
|
+
control.setValidators(this.#getValidators(column));
|
|
437
|
+
this.tableForm.addControl(column.name, control);
|
|
284
438
|
});
|
|
285
439
|
this.tableForm.setValidators(this.#globalErrorValidator());
|
|
286
440
|
this.#elementData && this.tableForm.patchValue(this.#elementData);
|
|
287
|
-
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Keeps the error messages shown by the dialog in sync with the errors of the row form.
|
|
444
|
+
*/
|
|
445
|
+
#initErrorMessages() {
|
|
446
|
+
this.tableForm.valueChanges.subscribe(() => {
|
|
288
447
|
this.errorMessage.set(this.tableForm.errors
|
|
289
448
|
? Object.keys(this.tableForm.errors).map((key) => this.tableForm.errors && this.tableForm.errors[key]
|
|
290
449
|
? Object.keys(this.tableForm.errors[key]).map((k) => ({
|
|
@@ -294,6 +453,57 @@ class EditTableDataComponent {
|
|
|
294
453
|
: null);
|
|
295
454
|
});
|
|
296
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Runs the 'onrowedit' callback the table form element provides (see `TableRowEditSupport`),
|
|
458
|
+
* e.g. registered by a form script. The callback gets the table element and a 'row' object
|
|
459
|
+
* whose model provides access to the cells of the row being edited. Changes applied to the
|
|
460
|
+
* row model are reflected by the dialog immediately and are discarded once it is closed.
|
|
461
|
+
*/
|
|
462
|
+
#initRowScripting() {
|
|
463
|
+
const rowEdit = this.#dialogData.rowEdit;
|
|
464
|
+
if (!rowEdit)
|
|
465
|
+
return;
|
|
466
|
+
const model = {};
|
|
467
|
+
this.columns().forEach((column) => {
|
|
468
|
+
const value = this.tableForm.get(column.name)?.value;
|
|
469
|
+
const element = { ...column, value: value === '' ? null : value };
|
|
470
|
+
model[column.name] = new TableRowScopeElement(element, this.#onRowScriptingChange);
|
|
471
|
+
});
|
|
472
|
+
const row = {
|
|
473
|
+
index: rowEdit.index,
|
|
474
|
+
persisted: rowEdit.persisted,
|
|
475
|
+
saveEnabled: true,
|
|
476
|
+
model
|
|
477
|
+
};
|
|
478
|
+
try {
|
|
479
|
+
rowEdit.callback(rowEdit.table, row);
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
console.error(`Error in form script onrowedit of table '${this.formElement['name']}'`, err);
|
|
483
|
+
}
|
|
484
|
+
// row properties are only evaluated synchronously
|
|
485
|
+
this.saveEnabled.set(row.saveEnabled !== false);
|
|
486
|
+
// subscribe after running the callback, so values set by onrowedit won't trigger onchange handlers
|
|
487
|
+
Object.keys(model).forEach((name) => this.tableForm
|
|
488
|
+
.get(name)
|
|
489
|
+
?.valueChanges.pipe(takeUntilDestroyed(this.#destroyRef))
|
|
490
|
+
.subscribe((value) => model[name].update(value, model)));
|
|
491
|
+
}
|
|
492
|
+
#onRowScriptingChange;
|
|
493
|
+
// replace the column so the form field picks up the changed properties
|
|
494
|
+
#updateColumn(control, patch) {
|
|
495
|
+
const column = { ...control._eoFormElement, ...patch };
|
|
496
|
+
control._eoFormElement = column;
|
|
497
|
+
this.columns.update((columns) => columns.map((col) => (col.name === column.name ? column : col)));
|
|
498
|
+
}
|
|
499
|
+
#getValidators(column) {
|
|
500
|
+
// errors set by form scripts (rendered by the form field using the 'eoformScript' key)
|
|
501
|
+
const scriptError = (control) => control._eoFormElement?.error ? { eoformScript: { valid: false } } : null;
|
|
502
|
+
const validators = [scriptError];
|
|
503
|
+
if (column.required)
|
|
504
|
+
validators.push(Validators.required);
|
|
505
|
+
return validators;
|
|
506
|
+
}
|
|
297
507
|
#globalErrorValidator() {
|
|
298
508
|
return (control) => {
|
|
299
509
|
const formGroup = control;
|
|
@@ -324,7 +534,7 @@ class EditTableDataComponent {
|
|
|
324
534
|
useExisting: forwardRef(() => EditTableDataComponent),
|
|
325
535
|
multi: true
|
|
326
536
|
}
|
|
327
|
-
], ngImport: i0, template: "<yuv-dialog [headertitle]=\"(adding ? 'yuv.form.element.data.grid.add.headline' : 'yuv.form.element.data.grid.edit.headline') | translate: { headline: header }\">\n <main>\n <div [formGroup]=\"tableForm\">\n <div class=\"fields\">\n @for (element of columns; track
|
|
537
|
+
], ngImport: i0, template: "<yuv-dialog [headertitle]=\"(adding ? 'yuv.form.element.data.grid.add.headline' : 'yuv.form.element.data.grid.edit.headline') | translate: { headline: header }\">\n <main>\n <div [formGroup]=\"tableForm\">\n <div class=\"fields\">\n @for (element of columns(); track element.name) {\n <div [attr.data-name]=\"element.name\" class=\"form-field\">\n <yuv-metadata-form-field [field]=\"element\" [formControlName]=\"element.name\" [situation]=\"situation\"></yuv-metadata-form-field>\n </div>\n }\n </div>\n </div>\n </main>\n\n <footer>\n <button ymtButton=\"secondary\" (click)=\"cancel()\">{{ 'yuv.form.element.data.grid.edit.cancel' | translate }}</button>\n <button ymtButton=\"primary\" (click)=\"submit()\" [disabled]=\"!tableForm.dirty || !tableForm.valid || !saveEnabled()\">\n @if (adding) {\n {{ 'yuv.form.element.data.grid.edit.add' | translate }}\n } @else {\n {{ 'yuv.object-metadata.button.save' | translate }}\n }\n </button>\n </footer>\n</yuv-dialog>\n", styles: [":host .fields{padding:var(--ymt-spacing-m)}:host .fields .form-field:not(:last-child){margin-block-end:var(--ymt-spacing-s)}:host .err-msg{font:var(--ymt-font-body-subtle);color:var(--ymt-danger);padding:var(--ymt-spacing-2xs) 0;border:0}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: DialogComponent, selector: "yuv-dialog", inputs: ["headertitle", "headertitel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: MetadataFormFieldComponent, selector: "yuv-metadata-form-field", inputs: ["formChangedSubject", "field", "variant", "situation"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
328
538
|
}
|
|
329
539
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: EditTableDataComponent, decorators: [{
|
|
330
540
|
type: Component,
|
|
@@ -343,7 +553,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
343
553
|
useExisting: forwardRef(() => EditTableDataComponent),
|
|
344
554
|
multi: true
|
|
345
555
|
}
|
|
346
|
-
], template: "<yuv-dialog [headertitle]=\"(adding ? 'yuv.form.element.data.grid.add.headline' : 'yuv.form.element.data.grid.edit.headline') | translate: { headline: header }\">\n <main>\n <div [formGroup]=\"tableForm\">\n <div class=\"fields\">\n @for (element of columns; track
|
|
556
|
+
], template: "<yuv-dialog [headertitle]=\"(adding ? 'yuv.form.element.data.grid.add.headline' : 'yuv.form.element.data.grid.edit.headline') | translate: { headline: header }\">\n <main>\n <div [formGroup]=\"tableForm\">\n <div class=\"fields\">\n @for (element of columns(); track element.name) {\n <div [attr.data-name]=\"element.name\" class=\"form-field\">\n <yuv-metadata-form-field [field]=\"element\" [formControlName]=\"element.name\" [situation]=\"situation\"></yuv-metadata-form-field>\n </div>\n }\n </div>\n </div>\n </main>\n\n <footer>\n <button ymtButton=\"secondary\" (click)=\"cancel()\">{{ 'yuv.form.element.data.grid.edit.cancel' | translate }}</button>\n <button ymtButton=\"primary\" (click)=\"submit()\" [disabled]=\"!tableForm.dirty || !tableForm.valid || !saveEnabled()\">\n @if (adding) {\n {{ 'yuv.form.element.data.grid.edit.add' | translate }}\n } @else {\n {{ 'yuv.object-metadata.button.save' | translate }}\n }\n </button>\n </footer>\n</yuv-dialog>\n", styles: [":host .fields{padding:var(--ymt-spacing-m)}:host .fields .form-field:not(:last-child){margin-block-end:var(--ymt-spacing-s)}:host .err-msg{font:var(--ymt-font-body-subtle);color:var(--ymt-danger);padding:var(--ymt-spacing-2xs) 0;border:0}\n"] }]
|
|
347
557
|
}], propDecorators: { isInnerTableForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "isInnerTableForm", required: false }] }] } });
|
|
348
558
|
|
|
349
559
|
var DataGridSizeType;
|
|
@@ -355,6 +565,7 @@ var DataGridSizeType;
|
|
|
355
565
|
DataGridSizeType["AUTO"] = "auto";
|
|
356
566
|
})(DataGridSizeType || (DataGridSizeType = {}));
|
|
357
567
|
|
|
568
|
+
const isRowObject = (row) => typeof row === 'object' && row !== null;
|
|
358
569
|
const MATERIAL = [MatMenuModule, MatMenuTrigger, MatIconModule, MatButtonModule, MatTableModule, MatTooltipModule];
|
|
359
570
|
class DataGridComponent {
|
|
360
571
|
constructor() {
|
|
@@ -380,6 +591,12 @@ class DataGridComponent {
|
|
|
380
591
|
: formElement && formElement['label'];
|
|
381
592
|
}, ...(ngDevMode ? [{ debugName: "tableLabel" }] : /* istanbul ignore next */ []));
|
|
382
593
|
this.selectedRow = signal(null, ...(ngDevMode ? [{ debugName: "selectedRow" }] : /* istanbul ignore next */ []));
|
|
594
|
+
// Rows that are persisted, i.e. not added while the current form is being edited ('row.persisted'
|
|
595
|
+
// of the 'onrowedit' callback). Rows are tracked by identity and carried over by value whenever
|
|
596
|
+
// they get recreated (e.g. by writeValue), so identical rows can't be told apart.
|
|
597
|
+
this.#persistedRows = new WeakSet();
|
|
598
|
+
// serialized rows the current form element has been loaded with
|
|
599
|
+
this.#loadedRows = [];
|
|
383
600
|
this.isRequired = false;
|
|
384
601
|
this.isInvalid = false;
|
|
385
602
|
this.icons = {
|
|
@@ -404,7 +621,12 @@ class DataGridComponent {
|
|
|
404
621
|
const data = this.#toRowObjects(formElement ? formElement['value'] || [] : [], elements);
|
|
405
622
|
// rendering only — the value already lives in the form model, so a (re)bind
|
|
406
623
|
// of the formElement input must never propagate back or dirty the control
|
|
407
|
-
untracked(() =>
|
|
624
|
+
untracked(() => {
|
|
625
|
+
if (!formElement)
|
|
626
|
+
return;
|
|
627
|
+
this.#resetPersistedRows(data);
|
|
628
|
+
this.#updateTable(elements, data);
|
|
629
|
+
});
|
|
408
630
|
}, ...(ngDevMode ? [{ debugName: "#loadData" }] : /* istanbul ignore next */ []));
|
|
409
631
|
this.propagateChange = () => { };
|
|
410
632
|
this.onTouched = () => { };
|
|
@@ -412,6 +634,12 @@ class DataGridComponent {
|
|
|
412
634
|
#dialog;
|
|
413
635
|
#systemService;
|
|
414
636
|
#cdRef;
|
|
637
|
+
// Rows that are persisted, i.e. not added while the current form is being edited ('row.persisted'
|
|
638
|
+
// of the 'onrowedit' callback). Rows are tracked by identity and carried over by value whenever
|
|
639
|
+
// they get recreated (e.g. by writeValue), so identical rows can't be told apart.
|
|
640
|
+
#persistedRows;
|
|
641
|
+
// serialized rows the current form element has been loaded with
|
|
642
|
+
#loadedRows;
|
|
415
643
|
#loadData;
|
|
416
644
|
// if the form element is created from the ObjectTypeField it contains 'columnDefinitions' instead of 'elements'
|
|
417
645
|
// therefore we need to map them here for proper table rendering
|
|
@@ -443,7 +671,7 @@ class DataGridComponent {
|
|
|
443
671
|
return row;
|
|
444
672
|
});
|
|
445
673
|
}
|
|
446
|
-
#openEditOverlay(elementData, adding = false) {
|
|
674
|
+
#openEditOverlay(elementData, adding = false, rowIndex = -1, persisted = false) {
|
|
447
675
|
return this.#dialog
|
|
448
676
|
.open(EditTableDataComponent, {
|
|
449
677
|
minWidth: '25%',
|
|
@@ -454,7 +682,8 @@ class DataGridComponent {
|
|
|
454
682
|
elementData,
|
|
455
683
|
situation: this.situation(),
|
|
456
684
|
adding,
|
|
457
|
-
label: this.tableLabel()
|
|
685
|
+
label: this.tableLabel(),
|
|
686
|
+
rowEdit: this.#rowEditContext(rowIndex, persisted)
|
|
458
687
|
}
|
|
459
688
|
})
|
|
460
689
|
.afterClosed()
|
|
@@ -467,8 +696,13 @@ class DataGridComponent {
|
|
|
467
696
|
if (this.readonly())
|
|
468
697
|
return;
|
|
469
698
|
this.selectedRow.set(element);
|
|
470
|
-
this.#
|
|
699
|
+
const rowIndex = this.#rowIndex(element);
|
|
700
|
+
const persisted = isRowObject(element) && this.#persistedRows.has(element);
|
|
701
|
+
this.#openEditOverlay(element, false, rowIndex, persisted).subscribe((result) => {
|
|
471
702
|
if (result) {
|
|
703
|
+
// an edited row keeps its persisted state
|
|
704
|
+
if (persisted)
|
|
705
|
+
this.#persistedRows.add(result);
|
|
472
706
|
const updatedData = this.dataSource().map((item) => JSON.stringify(item) === JSON.stringify(element) ? result : item);
|
|
473
707
|
const formElement = this.mappedFormElement();
|
|
474
708
|
if (formElement) {
|
|
@@ -493,6 +727,43 @@ class DataGridComponent {
|
|
|
493
727
|
}
|
|
494
728
|
});
|
|
495
729
|
}
|
|
730
|
+
// context for the row editing dialog, only if the table form element provides an 'onrowedit' callback
|
|
731
|
+
#rowEditContext(index, persisted) {
|
|
732
|
+
const formElement = this.mappedFormElement();
|
|
733
|
+
const callback = formElement?.onrowedit;
|
|
734
|
+
if (typeof callback !== 'function')
|
|
735
|
+
return undefined;
|
|
736
|
+
const resolve = formElement?.resolveRowEditTable;
|
|
737
|
+
const table = typeof resolve === 'function' ? resolve() : undefined;
|
|
738
|
+
return { callback, table: table ?? formElement, index, persisted };
|
|
739
|
+
}
|
|
740
|
+
// rows the form element is loaded with are persisted, unless the form is about to create a new object
|
|
741
|
+
#resetPersistedRows(rows) {
|
|
742
|
+
const persisted = this.situation() === Situation.CREATE ? [] : rows.filter(isRowObject);
|
|
743
|
+
this.#persistedRows = new WeakSet(persisted);
|
|
744
|
+
this.#loadedRows = persisted.map((row) => JSON.stringify(row));
|
|
745
|
+
}
|
|
746
|
+
// rows got recreated: carry over the persisted state of the previous (or the loaded) rows by value
|
|
747
|
+
#carryOverPersistedRows(previous, rows) {
|
|
748
|
+
const candidates = [
|
|
749
|
+
...previous.filter((row) => isRowObject(row) && this.#persistedRows.has(row)).map((row) => JSON.stringify(row)),
|
|
750
|
+
...this.#loadedRows
|
|
751
|
+
];
|
|
752
|
+
rows.forEach((row) => {
|
|
753
|
+
if (!isRowObject(row) || this.#persistedRows.has(row))
|
|
754
|
+
return;
|
|
755
|
+
const idx = candidates.indexOf(JSON.stringify(row));
|
|
756
|
+
if (idx > -1) {
|
|
757
|
+
candidates.splice(idx, 1);
|
|
758
|
+
this.#persistedRows.add(row);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
#rowIndex(element) {
|
|
763
|
+
const rows = this.dataSource();
|
|
764
|
+
const index = rows.indexOf(element);
|
|
765
|
+
return index > -1 ? index : rows.findIndex((item) => JSON.stringify(item) === JSON.stringify(element));
|
|
766
|
+
}
|
|
496
767
|
removeRow(element) {
|
|
497
768
|
const updatedData = this.dataSource().filter((item) => JSON.stringify(item) !== JSON.stringify(element));
|
|
498
769
|
const formElement = this.mappedFormElement();
|
|
@@ -544,7 +815,9 @@ class DataGridComponent {
|
|
|
544
815
|
*/
|
|
545
816
|
writeValue(obj) {
|
|
546
817
|
const elements = this.#resolveElements(this.formElement());
|
|
547
|
-
|
|
818
|
+
const rows = this.#toRowObjects(Array.isArray(obj) ? obj : [], elements);
|
|
819
|
+
this.#carryOverPersistedRows(this.dataSource(), rows);
|
|
820
|
+
this.#updateTable(elements, rows);
|
|
548
821
|
}
|
|
549
822
|
registerOnChange(fn) {
|
|
550
823
|
this.propagateChange = fn;
|
|
@@ -625,6 +898,7 @@ class DatetimeComponent extends AbstractMatFormField {
|
|
|
625
898
|
this.pickerCmp = viewChild.required(DatepickerComponent);
|
|
626
899
|
this.ngControl = injectNgControl(this);
|
|
627
900
|
this.fc = new FormControl(null);
|
|
901
|
+
this.#hostControl = null;
|
|
628
902
|
this._locale = signal(this.translate.getCurrentLang(), ...(ngDevMode ? [{ debugName: "_locale" }] : /* istanbul ignore next */ []));
|
|
629
903
|
this.locale = input(this.translate.getCurrentLang(), ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
630
904
|
this.#localeEffect = effect(() => {
|
|
@@ -650,6 +924,8 @@ class DatetimeComponent extends AbstractMatFormField {
|
|
|
650
924
|
this.propagateChange = (_) => { };
|
|
651
925
|
}
|
|
652
926
|
#dRef;
|
|
927
|
+
#hostControl;
|
|
928
|
+
#hostControlSub;
|
|
653
929
|
#localeEffect;
|
|
654
930
|
openCalendar() {
|
|
655
931
|
this.pickerCmp().openCalendar();
|
|
@@ -693,11 +969,29 @@ class DatetimeComponent extends AbstractMatFormField {
|
|
|
693
969
|
this.translate.onLangChange.subscribe((evt) => this._locale.set(evt.lang));
|
|
694
970
|
this.fc.valueChanges.pipe(takeUntilDestroyed(this.#dRef)).subscribe((value) => this.onValueChange(value));
|
|
695
971
|
this.fc.statusChanges.pipe(takeUntilDestroyed(this.#dRef)).subscribe(() => {
|
|
696
|
-
this.errorState = FormUtils.getErrorState(this.fc);
|
|
697
972
|
if (this.ngControl?.control) {
|
|
698
|
-
this.ngControl.control
|
|
973
|
+
FormUtils.mergeInnerErrors(this.ngControl.control, this.fc);
|
|
699
974
|
}
|
|
975
|
+
this.#updateErrorState();
|
|
700
976
|
});
|
|
977
|
+
this.#dRef.onDestroy(() => this.#hostControlSub?.unsubscribe());
|
|
978
|
+
}
|
|
979
|
+
ngDoCheck() {
|
|
980
|
+
// With `formControlName` the host control is only resolved after this component's `ngOnInit`,
|
|
981
|
+
// and a `[formControl]` binding may swap it later, so (re)attach whenever it changes.
|
|
982
|
+
const host = this.ngControl?.control ?? null;
|
|
983
|
+
if (host && host !== this.#hostControl) {
|
|
984
|
+
this.#hostControl = host;
|
|
985
|
+
this.#hostControlSub?.unsubscribe();
|
|
986
|
+
// The host carries the validation of the surrounding form (e.g. `required`) and may be
|
|
987
|
+
// touched from outside (form scripts, submit handling), so reflect it in the error state.
|
|
988
|
+
this.#hostControlSub = host.events.subscribe(() => this.#updateErrorState());
|
|
989
|
+
this.#updateErrorState();
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
#updateErrorState() {
|
|
993
|
+
const host = this.ngControl?.control;
|
|
994
|
+
this.errorState = FormUtils.getErrorState(this.fc) || (!!host && FormUtils.getErrorState(host));
|
|
701
995
|
}
|
|
702
996
|
#setLabels() {
|
|
703
997
|
this.labels = {
|