@mintplayer/ng-spark 22.23.0 → 22.25.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.
Files changed (71) hide show
  1. package/README.md +196 -0
  2. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +100 -47
  3. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +310 -329
  5. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-column-filter.mjs +175 -135
  7. package/fesm2022/mintplayer-ng-spark-column-filter.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-contributions.mjs +721 -0
  9. package/fesm2022/mintplayer-ng-spark-contributions.mjs.map +1 -0
  10. package/fesm2022/mintplayer-ng-spark-grid.mjs +2385 -1118
  11. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  12. package/fesm2022/mintplayer-ng-spark-history.mjs +598 -0
  13. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -0
  14. package/fesm2022/mintplayer-ng-spark-icon.mjs +64 -27
  15. package/fesm2022/mintplayer-ng-spark-icon.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-models.mjs +405 -762
  17. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  18. package/fesm2022/mintplayer-ng-spark-moderation.mjs +1212 -0
  19. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -0
  20. package/fesm2022/mintplayer-ng-spark-panels.mjs +60 -0
  21. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -0
  22. package/fesm2022/mintplayer-ng-spark-pipes.mjs +1158 -765
  23. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-create.mjs +273 -169
  25. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +795 -410
  27. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +1196 -280
  29. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-po-form.mjs +2244 -1224
  31. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-query-list.mjs +571 -303
  33. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-renderers.mjs +44 -55
  35. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +216 -177
  37. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-routes.mjs +24 -28
  39. package/fesm2022/mintplayer-ng-spark-routes.mjs.map +1 -1
  40. package/fesm2022/mintplayer-ng-spark-services.mjs +625 -640
  41. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  42. package/fesm2022/mintplayer-ng-spark-shell.mjs +1031 -397
  43. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  44. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +294 -0
  45. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -0
  46. package/fesm2022/mintplayer-ng-spark.mjs +14 -48
  47. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  48. package/package.json +23 -2
  49. package/types/mintplayer-ng-spark-attribute-description.d.ts +25 -18
  50. package/types/mintplayer-ng-spark-client-operations.d.ts +139 -160
  51. package/types/mintplayer-ng-spark-column-filter.d.ts +73 -55
  52. package/types/mintplayer-ng-spark-contributions.d.ts +231 -0
  53. package/types/mintplayer-ng-spark-grid.d.ts +917 -448
  54. package/types/mintplayer-ng-spark-history.d.ts +216 -0
  55. package/types/mintplayer-ng-spark-icon.d.ts +15 -13
  56. package/types/mintplayer-ng-spark-models.d.ts +681 -573
  57. package/types/mintplayer-ng-spark-moderation.d.ts +402 -0
  58. package/types/mintplayer-ng-spark-panels.d.ts +143 -0
  59. package/types/mintplayer-ng-spark-pipes.d.ts +131 -149
  60. package/types/mintplayer-ng-spark-po-create.d.ts +46 -37
  61. package/types/mintplayer-ng-spark-po-detail.d.ts +180 -117
  62. package/types/mintplayer-ng-spark-po-edit.d.ts +367 -64
  63. package/types/mintplayer-ng-spark-po-form.d.ts +558 -373
  64. package/types/mintplayer-ng-spark-query-list.d.ts +143 -82
  65. package/types/mintplayer-ng-spark-renderers.d.ts +121 -85
  66. package/types/mintplayer-ng-spark-retry-action-modal.d.ts +31 -34
  67. package/types/mintplayer-ng-spark-routes.d.ts +7 -11
  68. package/types/mintplayer-ng-spark-services.d.ts +377 -261
  69. package/types/mintplayer-ng-spark-shell.d.ts +228 -132
  70. package/types/mintplayer-ng-spark-soft-delete.d.ts +90 -0
  71. package/types/mintplayer-ng-spark.d.ts +19 -12
@@ -1,1228 +1,2248 @@
1
- import * as i0 from '@angular/core';
2
- import { inject, input, output, signal, computed, ChangeDetectionStrategy, Component, model, effect } from '@angular/core';
3
- import * as i1$1 from '@angular/common';
4
- import { CommonModule, NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
5
- import * as i1 from '@angular/forms';
6
- import { FormsModule } from '@angular/forms';
7
- import { moveItemInArray, CdkDropList, CdkDrag, CdkDragHandle, CdkDragPreview } from '@angular/cdk/drag-drop';
8
- import { Color } from '@mintplayer/ng-bootstrap';
9
- import { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';
10
- import { BsFormComponent, BsFormControlDirective } from '@mintplayer/ng-bootstrap/form';
11
- import { BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsGridColDirective, BsColFormLabelDirective } from '@mintplayer/ng-bootstrap/grid';
12
- import { BsInputGroupComponent } from '@mintplayer/ng-bootstrap/input-group';
13
- import { BsButtonTypeDirective } from '@mintplayer/ng-bootstrap/button-type';
14
- import { BsSelectComponent, BsSelectOption } from '@mintplayer/ng-bootstrap/select';
15
- import { InMemoryTreeSelectProvider, BsTreeSelectComponent } from '@mintplayer/ng-bootstrap/tree-select';
16
- import { BsModalHostComponent, BsModalDirective, BsModalHeaderDirective, BsModalBodyDirective, BsModalFooterDirective } from '@mintplayer/ng-bootstrap/modal';
17
- import { BsCheckboxComponent } from '@mintplayer/ng-bootstrap/checkbox';
18
- import { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';
19
- import { BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective } from '@mintplayer/ng-bootstrap/tab-control';
20
- import { BsTableComponent } from '@mintplayer/ng-bootstrap/table';
21
- import { SparkService, SparkLanguageService } from '@mintplayer/ng-spark/services';
22
- import { TranslateKeyPipe, ResolveTranslationPipe, ReferenceAttrValuePipe, InputTypePipe, LookupDisplayTypePipe, LookupOptionsPipe, AsDetailDisplayValuePipe, AsDetailTypePipe, AsDetailColumnsPipe, AsDetailCellValuePipe, CanCreateDetailRowPipe, CanDeleteDetailRowPipe, CanEditDetailRowPipe, InlineRefOptionsPipe, ErrorForAttributePipe } from '@mintplayer/ng-spark/pipes';
23
- import { resolveTranslation, ELookupDisplayType, EReferenceDisplayType, hasShowedOnFlag, ShowedOn, applyOverlay, evaluateRules, overlayFromResponse, mergeRefreshValues, nestedPoToDict, AS_DETAIL_ROW_KEY } from '@mintplayer/ng-spark/models';
24
- import { SparkIconComponent } from '@mintplayer/ng-spark/icon';
25
- import { SparkAttributeDescriptionComponent } from '@mintplayer/ng-spark/attribute-description';
26
- import { SPARK_ATTRIBUTE_RENDERERS, withDeclaredInputs } from '@mintplayer/ng-spark/renderers';
27
- import { DatatableSettings, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective } from '@mintplayer/ng-bootstrap/datatable';
28
-
29
- /**
30
- * Reusable Reference value picker: a readonly textbox showing the current selection's
31
- * breadcrumb/name plus a "…" button that opens a searchable modal grid over the candidate
32
- * `options`. Per-instance state (no single top-level slot), so it works for top-level
33
- * reference attributes and for individual inline AsDetail reference cells alike.
34
- *
35
- * The parent pre-loads `options` (the query result) and passes `referenceType` (the target
36
- * CLR type); the component lazily loads that EntityType's metadata on first open to render
37
- * the grid's column headers. Emits the picked id via `valueChange`.
38
- */
39
- class SparkReferencePickerComponent {
40
- sparkService = inject(SparkService);
41
- lang = inject(SparkLanguageService);
42
- /** Currently selected referenced id (or null when unset). */
43
- value = input(null, /* @ts-ignore */
44
- ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
45
- /** Candidate items to pick from (the full query result, pre-loaded by the parent). */
46
- options = input([], /* @ts-ignore */
47
- ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
48
- /** Target entity type's CLR type name; used to load the grid's column headers. */
49
- referenceType = input(undefined, /* @ts-ignore */
50
- ...(ngDevMode ? [{ debugName: "referenceType" }] : /* istanbul ignore next */ []));
51
- /** Renders the field with the Bootstrap is-invalid state. */
52
- isInvalid = input(false, /* @ts-ignore */
53
- ...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
54
- /** Optional id forwarded to the readonly input (for label association). */
55
- inputId = input(undefined, /* @ts-ignore */
56
- ...(ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []));
57
- /** Optional resolved label appended to the modal header ("Select {title}"). */
58
- title = input('', /* @ts-ignore */
59
- ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
60
- valueChange = output();
61
- colors = Color;
62
- showModal = signal(false, /* @ts-ignore */
63
- ...(ngDevMode ? [{ debugName: "showModal" }] : /* istanbul ignore next */ []));
64
- entityType = signal(null, /* @ts-ignore */
65
- ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
66
- pagination = signal(undefined, /* @ts-ignore */
67
- ...(ngDevMode ? [{ debugName: "pagination" }] : /* istanbul ignore next */ []));
68
- settings = signal(new DatatableSettings({
69
- perPage: { values: [10, 25, 50], selected: 10 },
70
- page: { values: [1], selected: 1 },
71
- sortColumns: []
72
- }), /* @ts-ignore */
73
- ...(ngDevMode ? [{ debugName: "settings" }] : /* istanbul ignore next */ []));
74
- searchTerm = '';
75
- visibleAttributes = computed(() => {
76
- return this.entityType()?.attributes
77
- .filter(a => a.isVisible)
78
- .sort((a, b) => a.order - b.order) || [];
79
- }, /* @ts-ignore */
80
- ...(ngDevMode ? [{ debugName: "visibleAttributes" }] : /* istanbul ignore next */ []));
81
- // Typed rows so the datatable generic infers the row type.
82
- rows = computed(() => this.pagination()?.data ?? [], /* @ts-ignore */
83
- ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
84
- displayValue = computed(() => {
85
- const id = this.value();
86
- if (!id)
87
- return this.lang.t('notSelected');
88
- const selected = this.options().find(o => o.id === id);
89
- return selected?.breadcrumb || id;
90
- }, /* @ts-ignore */
91
- ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
92
- async open() {
93
- this.searchTerm = '';
94
- this.settings.set(new DatatableSettings({
95
- perPage: { values: [10, 25, 50], selected: 10 },
96
- page: { values: [1], selected: 1 },
97
- sortColumns: []
98
- }));
99
- // Lazily resolve the target entity type for the grid's column headers.
100
- const refType = this.referenceType();
101
- if (!this.entityType() && refType) {
102
- const types = await this.sparkService.getEntityTypes();
103
- this.entityType.set(types.find(t => t.clrType === refType) || null);
104
- }
105
- this.applyFilter();
106
- this.showModal.set(true);
107
- }
108
- onSearchChange() {
109
- this.settings().page.selected = 1;
110
- this.applyFilter();
111
- }
112
- applyFilter() {
113
- let filteredItems = this.options();
114
- if (this.searchTerm.trim()) {
115
- const term = this.searchTerm.toLowerCase().trim();
116
- filteredItems = this.options().filter(item => {
117
- if (item.breadcrumb?.toLowerCase().includes(term))
118
- return true;
119
- return item.values.some(v => {
120
- const value = v.breadcrumb ?? v.value;
121
- if (value == null)
122
- return false;
123
- return String(value).toLowerCase().includes(term);
124
- });
125
- });
126
- }
127
- const totalPages = Math.ceil(filteredItems.length / this.settings().perPage.selected) || 1;
128
- this.pagination.set({
129
- data: filteredItems,
130
- totalRecords: filteredItems.length,
131
- totalPages: totalPages,
132
- perPage: this.settings().perPage.selected,
133
- page: this.settings().page.selected
134
- });
135
- this.settings().page.values = Array.from({ length: totalPages }, (_, i) => i + 1);
136
- if (this.settings().page.selected > totalPages) {
137
- this.settings().page.selected = 1;
138
- }
139
- }
140
- clearSearch() {
141
- this.searchTerm = '';
142
- this.onSearchChange();
143
- }
144
- select(item) {
145
- this.valueChange.emit(item.id ?? null);
146
- this.close();
147
- }
148
- close() {
149
- this.showModal.set(false);
150
- this.searchTerm = '';
151
- }
152
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkReferencePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
153
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkReferencePickerComponent, isStandalone: true, selector: "spark-reference-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, referenceType: { classPropertyName: "referenceType", publicName: "referenceType", isSignal: true, isRequired: false, transformFunction: null }, isInvalid: { classPropertyName: "isInvalid", publicName: "isInvalid", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<bs-input-group>\n <input\n type=\"text\"\n [id]=\"inputId()\"\n [value]=\"displayValue()\"\n readonly\n [class.is-invalid]=\"isInvalid()\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"open()\">\n ...\n </button>\n</bs-input-group>\n\n<bs-modal [isOpen]=\"showModal()\" (isOpenChange)=\"!$event && close()\">\n <div *bsModal class=\"reference-modal\">\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.select' | t }} {{ title() }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- bs-modal portals its content into a CDK overlay outside the host form, so the\n modal needs its own <bs-form> context for inputs to pick up .form-control styles. -->\n <bs-form>\n @if (entityType()) {\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange()\">\n @if (searchTerm) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n @if (searchTerm && pagination()) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ pagination()!.totalRecords }} {{ pagination()!.totalRecords === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n\n <bs-datatable\n [(settings)]=\"settings\"\n [data]=\"rows()\"\n (rowClick)=\"select($event.row)\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ attr.label | resolveTranslation:attr.name }}\n <spark-attribute-description [description]=\"attr.description\" />\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @for (attr of visibleAttributes(); track attr.id) {\n <td>{{ $any(item) | referenceAttrValue:attr.name }}</td>\n }\n </ng-container>\n </bs-datatable>\n } @else {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n }\n </bs-form>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"close()\">{{ 'common.cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: BsFormComponent, selector: "bs-form", inputs: ["action", "method"], outputs: ["submitted"] }, { kind: "directive", type: BsFormControlDirective, selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "component", type: BsInputGroupComponent, selector: "bs-input-group", inputs: ["size"] }, { kind: "directive", type: BsButtonTypeDirective, selector: "button[color],input[type=\"button\"][color],input[type=\"submit\"][color],a[color]", inputs: ["color"] }, { kind: "component", type: BsModalHostComponent, selector: "bs-modal", inputs: ["isOpen", "closeOnEscape", "scrollable"], outputs: ["isOpenChange"] }, { kind: "directive", type: BsModalDirective, selector: "[bsModal]" }, { kind: "directive", type: BsModalHeaderDirective, selector: "[bsModalHeader]" }, { kind: "directive", type: BsModalBodyDirective, selector: "[bsModalBody]" }, { kind: "directive", type: BsModalFooterDirective, selector: "[bsModalFooter]" }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["columns", "data", "fetch", "distincts", "labels", "settings", "selectionMode", "selectable", "selection", "rowKey", "resizableColumns", "pagination", "virtualScroll", "itemSize", "virtualBuffer", "isResponsive", "compareWith", "tree", "idKey", "childCountKey", "treeIndent", "expandedIds", "selectionStrategy"], outputs: ["settingsChange", "selectionChange", "rowClick", "rowDblClick", "rowContextMenu", "expandedIdsChange", "rowExpand", "rowCollapse", "filterChange"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable", "bsDatatableColumnFilterable", "bsDatatableColumnFilterActive", "bsDatatableColumnFilterSummary", "bsDatatableColumnFilterSelection", "bsDatatableColumnFilterMode", "bsDatatableColumnFilterInputType", "bsDatatableColumnFilterOperators"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "component", type: SparkAttributeDescriptionComponent, selector: "spark-attribute-description", inputs: ["description", "position"] }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: ReferenceAttrValuePipe, name: "referenceAttrValue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1
+ import * as i0 from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, model, output, signal } from "@angular/core";
3
+ import * as i1 from "@angular/common";
4
+ import { CommonModule, NgComponentOutlet, NgTemplateOutlet } from "@angular/common";
5
+ import * as i1$1 from "@angular/forms";
6
+ import { FormsModule } from "@angular/forms";
7
+ import { CdkDrag, CdkDragHandle, CdkDragPreview, CdkDropList, moveItemInArray } from "@angular/cdk/drag-drop";
8
+ import { Color } from "@mintplayer/ng-bootstrap";
9
+ import { BsCardComponent, BsCardHeaderComponent } from "@mintplayer/ng-bootstrap/card";
10
+ import { BsFormComponent, BsFormControlDirective } from "@mintplayer/ng-bootstrap/form";
11
+ import { BsColFormLabelDirective, BsGridColDirective, BsGridColumnDirective, BsGridComponent, BsGridRowDirective } from "@mintplayer/ng-bootstrap/grid";
12
+ import { BsInputGroupComponent } from "@mintplayer/ng-bootstrap/input-group";
13
+ import { BsButtonTypeDirective } from "@mintplayer/ng-bootstrap/button-type";
14
+ import { BsSelectComponent, BsSelectOption } from "@mintplayer/ng-bootstrap/select";
15
+ import { BsTreeSelectComponent, InMemoryTreeSelectProvider } from "@mintplayer/ng-bootstrap/tree-select";
16
+ import { BsModalBodyDirective, BsModalDirective, BsModalFooterDirective, BsModalHeaderDirective, BsModalHostComponent } from "@mintplayer/ng-bootstrap/modal";
17
+ import { BsCheckboxComponent } from "@mintplayer/ng-bootstrap/checkbox";
18
+ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
19
+ import { BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective } from "@mintplayer/ng-bootstrap/tab-control";
20
+ import { BsTableComponent } from "@mintplayer/ng-bootstrap/table";
21
+ import { SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
22
+ import { AsDetailCellValuePipe, AsDetailColumnsPipe, AsDetailDisplayValuePipe, AsDetailRowRendererPipe, AsDetailTypePipe, CanCreateDetailRowPipe, CanDeleteDetailRowPipe, CanEditDetailRowPipe, ErrorForAttributePipe, InlineRefOptionsPipe, InputTypePipe, LookupDisplayTypePipe, LookupOptionsPipe, ReferenceAttrValuePipe, ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
23
+ import { AS_DETAIL_ROW_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, applyOverlay, evaluateRules, hasShowedOnFlag, mergeRefreshValues, nestedPoToDict, overlayFromResponse, resolveTranslation } from "@mintplayer/ng-spark/models";
24
+ import { SparkIconComponent } from "@mintplayer/ng-spark/icon";
25
+ import { SparkAttributeDescriptionComponent } from "@mintplayer/ng-spark/attribute-description";
26
+ import { SPARK_ATTRIBUTE_RENDERERS, rowRendererInputs, withDeclaredInputs } from "@mintplayer/ng-spark/renderers";
27
+ import { BsDatatableColumnDirective, BsDatatableComponent, BsRowTemplateDirective, DatatableSettings } from "@mintplayer/ng-bootstrap/datatable";
28
+ var SparkReferencePickerComponent = class SparkReferencePickerComponent {
29
+ sparkService = inject(SparkService);
30
+ lang = inject(SparkLanguageService);
31
+ value = input(null, ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
32
+ options = input([], ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
33
+ referenceType = input(void 0, ...ngDevMode ? [{ debugName: "referenceType" }] : /* istanbul ignore next */ []);
34
+ isInvalid = input(false, ...ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []);
35
+ inputId = input(void 0, ...ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []);
36
+ title = input("", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
37
+ valueChange = output();
38
+ colors = Color;
39
+ showModal = signal(false, ...ngDevMode ? [{ debugName: "showModal" }] : /* istanbul ignore next */ []);
40
+ entityType = signal(null, ...ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []);
41
+ pagination = signal(void 0, ...ngDevMode ? [{ debugName: "pagination" }] : /* istanbul ignore next */ []);
42
+ settings = signal(new DatatableSettings({
43
+ perPage: {
44
+ values: [
45
+ 10,
46
+ 25,
47
+ 50
48
+ ],
49
+ selected: 10
50
+ },
51
+ page: {
52
+ values: [1],
53
+ selected: 1
54
+ },
55
+ sortColumns: []
56
+ }), ...ngDevMode ? [{ debugName: "settings" }] : /* istanbul ignore next */ []);
57
+ searchTerm = "";
58
+ visibleAttributes = computed(() => {
59
+ return this.entityType()?.attributes.filter((a) => a.isVisible).sort((a, b) => a.order - b.order) || [];
60
+ }, ...ngDevMode ? [{ debugName: "visibleAttributes" }] : /* istanbul ignore next */ []);
61
+ rows = computed(() => this.pagination()?.data ?? [], ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
62
+ displayValue = computed(() => {
63
+ const id = this.value();
64
+ if (!id) return this.lang.t("common.notSelected");
65
+ return this.options().find((o) => o.id === id)?.breadcrumb || id;
66
+ }, ...ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []);
67
+ async open() {
68
+ this.searchTerm = "";
69
+ this.settings.set(new DatatableSettings({
70
+ perPage: {
71
+ values: [
72
+ 10,
73
+ 25,
74
+ 50
75
+ ],
76
+ selected: 10
77
+ },
78
+ page: {
79
+ values: [1],
80
+ selected: 1
81
+ },
82
+ sortColumns: []
83
+ }));
84
+ const refType = this.referenceType();
85
+ if (!this.entityType() && refType) {
86
+ const types = await this.sparkService.getEntityTypes();
87
+ this.entityType.set(types.find((t) => t.clrType === refType) || null);
88
+ }
89
+ this.applyFilter();
90
+ this.showModal.set(true);
91
+ }
92
+ onSearchChange() {
93
+ this.settings().page.selected = 1;
94
+ this.applyFilter();
95
+ }
96
+ applyFilter() {
97
+ let filteredItems = this.options();
98
+ if (this.searchTerm.trim()) {
99
+ const term = this.searchTerm.toLowerCase().trim();
100
+ filteredItems = this.options().filter((item) => {
101
+ if (item.breadcrumb?.toLowerCase().includes(term)) return true;
102
+ return item.values.some((v) => {
103
+ const value = v.breadcrumb ?? v.value;
104
+ if (value == null) return false;
105
+ return String(value).toLowerCase().includes(term);
106
+ });
107
+ });
108
+ }
109
+ const totalPages = Math.ceil(filteredItems.length / this.settings().perPage.selected) || 1;
110
+ this.pagination.set({
111
+ data: filteredItems,
112
+ totalRecords: filteredItems.length,
113
+ totalPages,
114
+ perPage: this.settings().perPage.selected,
115
+ page: this.settings().page.selected
116
+ });
117
+ this.settings().page.values = Array.from({ length: totalPages }, (_, i) => i + 1);
118
+ if (this.settings().page.selected > totalPages) this.settings().page.selected = 1;
119
+ }
120
+ clearSearch() {
121
+ this.searchTerm = "";
122
+ this.onSearchChange();
123
+ }
124
+ select(item) {
125
+ this.valueChange.emit(item.id ?? null);
126
+ this.close();
127
+ }
128
+ close() {
129
+ this.showModal.set(false);
130
+ this.searchTerm = "";
131
+ }
132
+ static ɵfac = i0.ɵɵngDeclareFactory({
133
+ minVersion: "12.0.0",
134
+ version: "22.2.0",
135
+ ngImport: i0,
136
+ type: SparkReferencePickerComponent,
137
+ deps: [],
138
+ target: i0.ɵɵFactoryTarget.Component
139
+ });
140
+ static ɵcmp = i0.ɵɵngDeclareComponent({
141
+ minVersion: "17.0.0",
142
+ version: "22.2.0",
143
+ type: SparkReferencePickerComponent,
144
+ isStandalone: true,
145
+ selector: "spark-reference-picker",
146
+ inputs: {
147
+ value: {
148
+ classPropertyName: "value",
149
+ publicName: "value",
150
+ isSignal: true,
151
+ isRequired: false,
152
+ transformFunction: null
153
+ },
154
+ options: {
155
+ classPropertyName: "options",
156
+ publicName: "options",
157
+ isSignal: true,
158
+ isRequired: false,
159
+ transformFunction: null
160
+ },
161
+ referenceType: {
162
+ classPropertyName: "referenceType",
163
+ publicName: "referenceType",
164
+ isSignal: true,
165
+ isRequired: false,
166
+ transformFunction: null
167
+ },
168
+ isInvalid: {
169
+ classPropertyName: "isInvalid",
170
+ publicName: "isInvalid",
171
+ isSignal: true,
172
+ isRequired: false,
173
+ transformFunction: null
174
+ },
175
+ inputId: {
176
+ classPropertyName: "inputId",
177
+ publicName: "inputId",
178
+ isSignal: true,
179
+ isRequired: false,
180
+ transformFunction: null
181
+ },
182
+ title: {
183
+ classPropertyName: "title",
184
+ publicName: "title",
185
+ isSignal: true,
186
+ isRequired: false,
187
+ transformFunction: null
188
+ }
189
+ },
190
+ outputs: { valueChange: "valueChange" },
191
+ ngImport: i0,
192
+ template: "<bs-input-group>\n <input\n type=\"text\"\n [id]=\"inputId()\"\n [value]=\"displayValue()\"\n readonly\n [class.is-invalid]=\"isInvalid()\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"open()\">\n ...\n </button>\n</bs-input-group>\n\n<bs-modal [isOpen]=\"showModal()\" (isOpenChange)=\"!$event && close()\">\n <div *bsModal class=\"reference-modal\">\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.select' | t }} {{ title() }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- bs-modal portals its content into a CDK overlay outside the host form, so the\n modal needs its own <bs-form> context for inputs to pick up .form-control styles. -->\n <bs-form>\n @if (entityType()) {\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange()\">\n @if (searchTerm) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n @if (searchTerm && pagination()) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ pagination()!.totalRecords }} {{ pagination()!.totalRecords === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n\n <bs-datatable\n [(settings)]=\"settings\"\n [data]=\"rows()\"\n (rowClick)=\"select($event.row)\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ attr.label | resolveTranslation:attr.name }}\n <spark-attribute-description [description]=\"attr.description\" />\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @for (attr of visibleAttributes(); track attr.id) {\n <td>{{ $any(item) | referenceAttrValue:attr.name }}</td>\n }\n </ng-container>\n </bs-datatable>\n } @else {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n }\n </bs-form>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"close()\">{{ 'common.cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n",
193
+ dependencies: [
194
+ {
195
+ kind: "ngmodule",
196
+ type: FormsModule
197
+ },
198
+ {
199
+ kind: "directive",
200
+ type: i1$1.DefaultValueAccessor,
201
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
202
+ },
203
+ {
204
+ kind: "directive",
205
+ type: i1$1.NgControlStatus,
206
+ selector: "[formControlName],[ngModel],[formControl]"
207
+ },
208
+ {
209
+ kind: "directive",
210
+ type: i1$1.NgModel,
211
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
212
+ inputs: [
213
+ "name",
214
+ "disabled",
215
+ "ngModel",
216
+ "ngModelOptions"
217
+ ],
218
+ outputs: ["ngModelChange"],
219
+ exportAs: ["ngModel"]
220
+ },
221
+ {
222
+ kind: "component",
223
+ type: BsFormComponent,
224
+ selector: "bs-form",
225
+ inputs: ["action", "method"],
226
+ outputs: ["submitted"]
227
+ },
228
+ {
229
+ kind: "directive",
230
+ type: BsFormControlDirective,
231
+ selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)"
232
+ },
233
+ {
234
+ kind: "component",
235
+ type: BsGridComponent,
236
+ selector: "bs-grid",
237
+ inputs: ["stopFullWidthAt"]
238
+ },
239
+ {
240
+ kind: "directive",
241
+ type: BsGridRowDirective,
242
+ selector: "[bsRow]"
243
+ },
244
+ {
245
+ kind: "directive",
246
+ type: BsGridColumnDirective,
247
+ selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]",
248
+ inputs: [
249
+ "xxs",
250
+ "xs",
251
+ "sm",
252
+ "md",
253
+ "lg",
254
+ "xl",
255
+ "xxl"
256
+ ]
257
+ },
258
+ {
259
+ kind: "component",
260
+ type: BsInputGroupComponent,
261
+ selector: "bs-input-group",
262
+ inputs: ["size"]
263
+ },
264
+ {
265
+ kind: "directive",
266
+ type: BsButtonTypeDirective,
267
+ selector: "button[color],input[type=\"button\"][color],input[type=\"submit\"][color],a[color]",
268
+ inputs: ["color"]
269
+ },
270
+ {
271
+ kind: "component",
272
+ type: BsModalHostComponent,
273
+ selector: "bs-modal",
274
+ inputs: [
275
+ "isOpen",
276
+ "closeOnEscape",
277
+ "scrollable"
278
+ ],
279
+ outputs: ["isOpenChange"]
280
+ },
281
+ {
282
+ kind: "directive",
283
+ type: BsModalDirective,
284
+ selector: "[bsModal]"
285
+ },
286
+ {
287
+ kind: "directive",
288
+ type: BsModalHeaderDirective,
289
+ selector: "[bsModalHeader]"
290
+ },
291
+ {
292
+ kind: "directive",
293
+ type: BsModalBodyDirective,
294
+ selector: "[bsModalBody]"
295
+ },
296
+ {
297
+ kind: "directive",
298
+ type: BsModalFooterDirective,
299
+ selector: "[bsModalFooter]"
300
+ },
301
+ {
302
+ kind: "component",
303
+ type: BsDatatableComponent,
304
+ selector: "bs-datatable",
305
+ inputs: [
306
+ "columns",
307
+ "data",
308
+ "fetch",
309
+ "distincts",
310
+ "labels",
311
+ "settings",
312
+ "selectionMode",
313
+ "selectable",
314
+ "selection",
315
+ "rowKey",
316
+ "resizableColumns",
317
+ "pagination",
318
+ "virtualScroll",
319
+ "itemSize",
320
+ "virtualBuffer",
321
+ "isResponsive",
322
+ "compareWith",
323
+ "tree",
324
+ "idKey",
325
+ "childCountKey",
326
+ "treeIndent",
327
+ "expandedIds",
328
+ "selectionStrategy"
329
+ ],
330
+ outputs: [
331
+ "settingsChange",
332
+ "selectionChange",
333
+ "rowClick",
334
+ "rowDblClick",
335
+ "rowContextMenu",
336
+ "expandedIdsChange",
337
+ "rowExpand",
338
+ "rowCollapse",
339
+ "filterChange"
340
+ ]
341
+ },
342
+ {
343
+ kind: "directive",
344
+ type: BsDatatableColumnDirective,
345
+ selector: "[bsDatatableColumn]",
346
+ inputs: [
347
+ "bsDatatableColumn",
348
+ "bsDatatableColumnSortable",
349
+ "bsDatatableColumnFilterable",
350
+ "bsDatatableColumnFilterActive",
351
+ "bsDatatableColumnFilterSummary",
352
+ "bsDatatableColumnFilterSelection",
353
+ "bsDatatableColumnFilterMode",
354
+ "bsDatatableColumnFilterInputType",
355
+ "bsDatatableColumnFilterOperators"
356
+ ]
357
+ },
358
+ {
359
+ kind: "directive",
360
+ type: BsRowTemplateDirective,
361
+ selector: "[bsRowTemplate]"
362
+ },
363
+ {
364
+ kind: "component",
365
+ type: BsSpinnerComponent,
366
+ selector: "bs-spinner",
367
+ inputs: ["type", "color"]
368
+ },
369
+ {
370
+ kind: "component",
371
+ type: SparkIconComponent,
372
+ selector: "spark-icon",
373
+ inputs: ["name"]
374
+ },
375
+ {
376
+ kind: "component",
377
+ type: SparkAttributeDescriptionComponent,
378
+ selector: "spark-attribute-description",
379
+ inputs: ["description", "position"]
380
+ },
381
+ {
382
+ kind: "pipe",
383
+ type: TranslateKeyPipe,
384
+ name: "t"
385
+ },
386
+ {
387
+ kind: "pipe",
388
+ type: ResolveTranslationPipe,
389
+ name: "resolveTranslation"
390
+ },
391
+ {
392
+ kind: "pipe",
393
+ type: ReferenceAttrValuePipe,
394
+ name: "referenceAttrValue"
395
+ }
396
+ ],
397
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
398
+ });
399
+ };
400
+ i0.ɵɵngDeclareClassMetadata({
401
+ minVersion: "12.0.0",
402
+ version: "22.2.0",
403
+ ngImport: i0,
404
+ type: SparkReferencePickerComponent,
405
+ decorators: [{
406
+ type: Component,
407
+ args: [{
408
+ selector: "spark-reference-picker",
409
+ imports: [
410
+ FormsModule,
411
+ BsFormComponent,
412
+ BsFormControlDirective,
413
+ BsGridComponent,
414
+ BsGridRowDirective,
415
+ BsGridColumnDirective,
416
+ BsInputGroupComponent,
417
+ BsButtonTypeDirective,
418
+ BsModalHostComponent,
419
+ BsModalDirective,
420
+ BsModalHeaderDirective,
421
+ BsModalBodyDirective,
422
+ BsModalFooterDirective,
423
+ BsDatatableComponent,
424
+ BsDatatableColumnDirective,
425
+ BsRowTemplateDirective,
426
+ BsSpinnerComponent,
427
+ SparkIconComponent,
428
+ TranslateKeyPipe,
429
+ ResolveTranslationPipe,
430
+ ReferenceAttrValuePipe,
431
+ SparkAttributeDescriptionComponent
432
+ ],
433
+ changeDetection: ChangeDetectionStrategy.OnPush,
434
+ template: "<bs-input-group>\n <input\n type=\"text\"\n [id]=\"inputId()\"\n [value]=\"displayValue()\"\n readonly\n [class.is-invalid]=\"isInvalid()\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"open()\">\n ...\n </button>\n</bs-input-group>\n\n<bs-modal [isOpen]=\"showModal()\" (isOpenChange)=\"!$event && close()\">\n <div *bsModal class=\"reference-modal\">\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.select' | t }} {{ title() }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- bs-modal portals its content into a CDK overlay outside the host form, so the\n modal needs its own <bs-form> context for inputs to pick up .form-control styles. -->\n <bs-form>\n @if (entityType()) {\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange()\">\n @if (searchTerm) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n @if (searchTerm && pagination()) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ pagination()!.totalRecords }} {{ pagination()!.totalRecords === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n\n <bs-datatable\n [(settings)]=\"settings\"\n [data]=\"rows()\"\n (rowClick)=\"select($event.row)\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ attr.label | resolveTranslation:attr.name }}\n <spark-attribute-description [description]=\"attr.description\" />\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @for (attr of visibleAttributes(); track attr.id) {\n <td>{{ $any(item) | referenceAttrValue:attr.name }}</td>\n }\n </ng-container>\n </bs-datatable>\n } @else {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n }\n </bs-form>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"close()\">{{ 'common.cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n"
435
+ }]
436
+ }],
437
+ propDecorators: {
438
+ value: [{
439
+ type: i0.Input,
440
+ args: [{
441
+ isSignal: true,
442
+ alias: "value",
443
+ required: false
444
+ }]
445
+ }],
446
+ options: [{
447
+ type: i0.Input,
448
+ args: [{
449
+ isSignal: true,
450
+ alias: "options",
451
+ required: false
452
+ }]
453
+ }],
454
+ referenceType: [{
455
+ type: i0.Input,
456
+ args: [{
457
+ isSignal: true,
458
+ alias: "referenceType",
459
+ required: false
460
+ }]
461
+ }],
462
+ isInvalid: [{
463
+ type: i0.Input,
464
+ args: [{
465
+ isSignal: true,
466
+ alias: "isInvalid",
467
+ required: false
468
+ }]
469
+ }],
470
+ inputId: [{
471
+ type: i0.Input,
472
+ args: [{
473
+ isSignal: true,
474
+ alias: "inputId",
475
+ required: false
476
+ }]
477
+ }],
478
+ title: [{
479
+ type: i0.Input,
480
+ args: [{
481
+ isSignal: true,
482
+ alias: "title",
483
+ required: false
484
+ }]
485
+ }],
486
+ valueChange: [{
487
+ type: i0.Output,
488
+ args: ["valueChange"]
489
+ }]
490
+ }
491
+ });
492
+ var SparkLookupPickerComponent = class SparkLookupPickerComponent {
493
+ value = input(null, ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
494
+ options = input([], ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
495
+ isInvalid = input(false, ...ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []);
496
+ inputId = input(void 0, ...ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []);
497
+ title = input("", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
498
+ valueChange = output();
499
+ colors = Color;
500
+ showModal = signal(false, ...ngDevMode ? [{ debugName: "showModal" }] : /* istanbul ignore next */ []);
501
+ searchTerm = signal("", ...ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []);
502
+ displayValue = computed(() => {
503
+ const key = this.value();
504
+ if (key == null || key === "") return "";
505
+ const selected = this.options().find((o) => o.key === String(key));
506
+ if (!selected) return String(key);
507
+ return resolveTranslation(selected.values) || selected.key;
508
+ }, ...ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []);
509
+ filteredItems = computed(() => {
510
+ if (!this.searchTerm().trim()) return this.options();
511
+ const term = this.searchTerm().toLowerCase().trim();
512
+ return this.options().filter((item) => {
513
+ return resolveTranslation(item.values).toLowerCase().includes(term) || item.key.toLowerCase().includes(term);
514
+ });
515
+ }, ...ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []);
516
+ open() {
517
+ this.searchTerm.set("");
518
+ this.showModal.set(true);
519
+ }
520
+ select(item) {
521
+ this.valueChange.emit(item.key);
522
+ this.close();
523
+ }
524
+ close() {
525
+ this.showModal.set(false);
526
+ this.searchTerm.set("");
527
+ }
528
+ static ɵfac = i0.ɵɵngDeclareFactory({
529
+ minVersion: "12.0.0",
530
+ version: "22.2.0",
531
+ ngImport: i0,
532
+ type: SparkLookupPickerComponent,
533
+ deps: [],
534
+ target: i0.ɵɵFactoryTarget.Component
535
+ });
536
+ static ɵcmp = i0.ɵɵngDeclareComponent({
537
+ minVersion: "17.0.0",
538
+ version: "22.2.0",
539
+ type: SparkLookupPickerComponent,
540
+ isStandalone: true,
541
+ selector: "spark-lookup-picker",
542
+ inputs: {
543
+ value: {
544
+ classPropertyName: "value",
545
+ publicName: "value",
546
+ isSignal: true,
547
+ isRequired: false,
548
+ transformFunction: null
549
+ },
550
+ options: {
551
+ classPropertyName: "options",
552
+ publicName: "options",
553
+ isSignal: true,
554
+ isRequired: false,
555
+ transformFunction: null
556
+ },
557
+ isInvalid: {
558
+ classPropertyName: "isInvalid",
559
+ publicName: "isInvalid",
560
+ isSignal: true,
561
+ isRequired: false,
562
+ transformFunction: null
563
+ },
564
+ inputId: {
565
+ classPropertyName: "inputId",
566
+ publicName: "inputId",
567
+ isSignal: true,
568
+ isRequired: false,
569
+ transformFunction: null
570
+ },
571
+ title: {
572
+ classPropertyName: "title",
573
+ publicName: "title",
574
+ isSignal: true,
575
+ isRequired: false,
576
+ transformFunction: null
577
+ }
578
+ },
579
+ outputs: { valueChange: "valueChange" },
580
+ ngImport: i0,
581
+ template: "<bs-input-group>\n <input\n type=\"text\"\n [id]=\"inputId()\"\n [value]=\"displayValue()\"\n readonly\n [class.is-invalid]=\"isInvalid()\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"open()\">\n ...\n </button>\n</bs-input-group>\n\n<bs-modal [isOpen]=\"showModal()\" (isOpenChange)=\"!$event && close()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.select' | t }} {{ title() }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- bs-modal portals its content into a CDK overlay outside the host form, so the\n modal needs its own <bs-form> context for inputs to pick up .form-control styles. -->\n <bs-form>\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [col]>\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\">\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"searchTerm.set('')\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n </div>\n </bs-grid>\n\n <!-- List of items -->\n <bs-table [striped]=\"true\" [hover]=\"true\">\n <tbody class=\"align-middle\">\n @for (item of filteredItems(); track item.key) {\n <tr\n [class.table-primary]=\"value() === item.key\"\n (click)=\"select(item)\"\n style=\"cursor: pointer;\">\n <td>{{ item.values | resolveTranslation:item.key }}</td>\n </tr>\n } @empty {\n <tr>\n <td class=\"text-center text-muted\">{{ 'common.noItemsFound' | t }}</td>\n </tr>\n }\n </tbody>\n </bs-table>\n </bs-form>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"close()\">{{ 'common.cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n",
582
+ dependencies: [
583
+ {
584
+ kind: "ngmodule",
585
+ type: FormsModule
586
+ },
587
+ {
588
+ kind: "directive",
589
+ type: i1$1.DefaultValueAccessor,
590
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
591
+ },
592
+ {
593
+ kind: "directive",
594
+ type: i1$1.NgControlStatus,
595
+ selector: "[formControlName],[ngModel],[formControl]"
596
+ },
597
+ {
598
+ kind: "directive",
599
+ type: i1$1.NgModel,
600
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
601
+ inputs: [
602
+ "name",
603
+ "disabled",
604
+ "ngModel",
605
+ "ngModelOptions"
606
+ ],
607
+ outputs: ["ngModelChange"],
608
+ exportAs: ["ngModel"]
609
+ },
610
+ {
611
+ kind: "component",
612
+ type: BsFormComponent,
613
+ selector: "bs-form",
614
+ inputs: ["action", "method"],
615
+ outputs: ["submitted"]
616
+ },
617
+ {
618
+ kind: "directive",
619
+ type: BsFormControlDirective,
620
+ selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)"
621
+ },
622
+ {
623
+ kind: "component",
624
+ type: BsGridComponent,
625
+ selector: "bs-grid",
626
+ inputs: ["stopFullWidthAt"]
627
+ },
628
+ {
629
+ kind: "directive",
630
+ type: BsGridRowDirective,
631
+ selector: "[bsRow]"
632
+ },
633
+ {
634
+ kind: "directive",
635
+ type: BsGridColDirective,
636
+ selector: "[col]",
637
+ inputs: ["col"]
638
+ },
639
+ {
640
+ kind: "component",
641
+ type: BsInputGroupComponent,
642
+ selector: "bs-input-group",
643
+ inputs: ["size"]
644
+ },
645
+ {
646
+ kind: "directive",
647
+ type: BsButtonTypeDirective,
648
+ selector: "button[color],input[type=\"button\"][color],input[type=\"submit\"][color],a[color]",
649
+ inputs: ["color"]
650
+ },
651
+ {
652
+ kind: "component",
653
+ type: BsModalHostComponent,
654
+ selector: "bs-modal",
655
+ inputs: [
656
+ "isOpen",
657
+ "closeOnEscape",
658
+ "scrollable"
659
+ ],
660
+ outputs: ["isOpenChange"]
661
+ },
662
+ {
663
+ kind: "directive",
664
+ type: BsModalDirective,
665
+ selector: "[bsModal]"
666
+ },
667
+ {
668
+ kind: "directive",
669
+ type: BsModalHeaderDirective,
670
+ selector: "[bsModalHeader]"
671
+ },
672
+ {
673
+ kind: "directive",
674
+ type: BsModalBodyDirective,
675
+ selector: "[bsModalBody]"
676
+ },
677
+ {
678
+ kind: "directive",
679
+ type: BsModalFooterDirective,
680
+ selector: "[bsModalFooter]"
681
+ },
682
+ {
683
+ kind: "component",
684
+ type: BsTableComponent,
685
+ selector: "bs-table",
686
+ inputs: [
687
+ "isResponsive",
688
+ "striped",
689
+ "hover",
690
+ "border",
691
+ "ariaRowCount"
692
+ ]
693
+ },
694
+ {
695
+ kind: "component",
696
+ type: SparkIconComponent,
697
+ selector: "spark-icon",
698
+ inputs: ["name"]
699
+ },
700
+ {
701
+ kind: "pipe",
702
+ type: TranslateKeyPipe,
703
+ name: "t"
704
+ },
705
+ {
706
+ kind: "pipe",
707
+ type: ResolveTranslationPipe,
708
+ name: "resolveTranslation"
709
+ }
710
+ ],
711
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
712
+ });
713
+ };
714
+ i0.ɵɵngDeclareClassMetadata({
715
+ minVersion: "12.0.0",
716
+ version: "22.2.0",
717
+ ngImport: i0,
718
+ type: SparkLookupPickerComponent,
719
+ decorators: [{
720
+ type: Component,
721
+ args: [{
722
+ selector: "spark-lookup-picker",
723
+ imports: [
724
+ FormsModule,
725
+ BsFormComponent,
726
+ BsFormControlDirective,
727
+ BsGridComponent,
728
+ BsGridRowDirective,
729
+ BsGridColDirective,
730
+ BsInputGroupComponent,
731
+ BsButtonTypeDirective,
732
+ BsModalHostComponent,
733
+ BsModalDirective,
734
+ BsModalHeaderDirective,
735
+ BsModalBodyDirective,
736
+ BsModalFooterDirective,
737
+ BsTableComponent,
738
+ SparkIconComponent,
739
+ TranslateKeyPipe,
740
+ ResolveTranslationPipe
741
+ ],
742
+ changeDetection: ChangeDetectionStrategy.OnPush,
743
+ template: "<bs-input-group>\n <input\n type=\"text\"\n [id]=\"inputId()\"\n [value]=\"displayValue()\"\n readonly\n [class.is-invalid]=\"isInvalid()\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"open()\">\n ...\n </button>\n</bs-input-group>\n\n<bs-modal [isOpen]=\"showModal()\" (isOpenChange)=\"!$event && close()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.select' | t }} {{ title() }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- bs-modal portals its content into a CDK overlay outside the host form, so the\n modal needs its own <bs-form> context for inputs to pick up .form-control styles. -->\n <bs-form>\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [col]>\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\">\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"searchTerm.set('')\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n </div>\n </bs-grid>\n\n <!-- List of items -->\n <bs-table [striped]=\"true\" [hover]=\"true\">\n <tbody class=\"align-middle\">\n @for (item of filteredItems(); track item.key) {\n <tr\n [class.table-primary]=\"value() === item.key\"\n (click)=\"select(item)\"\n style=\"cursor: pointer;\">\n <td>{{ item.values | resolveTranslation:item.key }}</td>\n </tr>\n } @empty {\n <tr>\n <td class=\"text-center text-muted\">{{ 'common.noItemsFound' | t }}</td>\n </tr>\n }\n </tbody>\n </bs-table>\n </bs-form>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"close()\">{{ 'common.cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n"
744
+ }]
745
+ }],
746
+ propDecorators: {
747
+ value: [{
748
+ type: i0.Input,
749
+ args: [{
750
+ isSignal: true,
751
+ alias: "value",
752
+ required: false
753
+ }]
754
+ }],
755
+ options: [{
756
+ type: i0.Input,
757
+ args: [{
758
+ isSignal: true,
759
+ alias: "options",
760
+ required: false
761
+ }]
762
+ }],
763
+ isInvalid: [{
764
+ type: i0.Input,
765
+ args: [{
766
+ isSignal: true,
767
+ alias: "isInvalid",
768
+ required: false
769
+ }]
770
+ }],
771
+ inputId: [{
772
+ type: i0.Input,
773
+ args: [{
774
+ isSignal: true,
775
+ alias: "inputId",
776
+ required: false
777
+ }]
778
+ }],
779
+ title: [{
780
+ type: i0.Input,
781
+ args: [{
782
+ isSignal: true,
783
+ alias: "title",
784
+ required: false
785
+ }]
786
+ }],
787
+ valueChange: [{
788
+ type: i0.Output,
789
+ args: ["valueChange"]
790
+ }]
791
+ }
792
+ });
793
+ var RefreshCoordinator = class {
794
+ host;
795
+ queue = Promise.resolve();
796
+ sequence = 0;
797
+ settled = 0;
798
+ pending = /* @__PURE__ */ new Set();
799
+ timers = /* @__PURE__ */ new Map();
800
+ constructor(host) {
801
+ this.host = host;
802
+ }
803
+ get isRefreshing() {
804
+ return this.sequence !== this.settled;
805
+ }
806
+ markPending(attributeName) {
807
+ this.pending.add(attributeName);
808
+ }
809
+ dispatch(attributeName, mode) {
810
+ switch (mode) {
811
+ case "immediate": return this.trigger(attributeName);
812
+ case "debounced": return this.trigger(attributeName, { debounceMs: 300 });
813
+ case "blur":
814
+ this.markPending(attributeName);
815
+ return Promise.resolve();
816
+ }
817
+ }
818
+ blur(attributeName) {
819
+ this.clearTimer(attributeName);
820
+ if (!this.pending.delete(attributeName)) return Promise.resolve();
821
+ return this.trigger(attributeName);
822
+ }
823
+ async flush() {
824
+ for (const name of [...this.timers.keys()]) this.clearTimer(name);
825
+ const names = [...this.pending];
826
+ this.pending.clear();
827
+ for (const name of names) await this.trigger(name);
828
+ await this.queue;
829
+ }
830
+ dispose() {
831
+ for (const name of [...this.timers.keys()]) this.clearTimer(name);
832
+ this.pending.clear();
833
+ }
834
+ trigger(attributeName, options) {
835
+ const debounceMs = options?.debounceMs ?? 0;
836
+ if (debounceMs > 0) {
837
+ this.clearTimer(attributeName);
838
+ this.pending.add(attributeName);
839
+ this.timers.set(attributeName, setTimeout(() => {
840
+ this.timers.delete(attributeName);
841
+ if (this.pending.delete(attributeName)) this.trigger(attributeName);
842
+ }, debounceMs));
843
+ return Promise.resolve();
844
+ }
845
+ this.clearTimer(attributeName);
846
+ this.pending.delete(attributeName);
847
+ const ticket = ++this.sequence;
848
+ this.host.setBusy(true);
849
+ this.queue = this.queue.then(async () => {
850
+ const sent = { ...this.host.currentValues() };
851
+ try {
852
+ const response = await this.host.send(attributeName);
853
+ if (ticket !== this.sequence) return;
854
+ this.host.apply(response, sent);
855
+ } finally {
856
+ this.settled = Math.max(this.settled, ticket);
857
+ if (!this.isRefreshing) this.host.setBusy(false);
858
+ }
859
+ });
860
+ return this.queue;
861
+ }
862
+ clearTimer(attributeName) {
863
+ const timer = this.timers.get(attributeName);
864
+ if (timer === void 0) return;
865
+ clearTimeout(timer);
866
+ this.timers.delete(attributeName);
867
+ }
868
+ };
869
+ function effectiveTrigger(attr) {
870
+ switch (attr?.triggersRefresh) {
871
+ case "Auto": return isDiscreteEditor(attr) ? "change" : "blur";
872
+ case "ValueChanged": return "change";
873
+ case "Blur": return isDiscreteEditor(attr) ? "change" : "blur";
874
+ default: return "none";
875
+ }
154
876
  }
155
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkReferencePickerComponent, decorators: [{
156
- type: Component,
157
- args: [{ selector: 'spark-reference-picker', imports: [FormsModule, BsFormComponent, BsFormControlDirective, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsInputGroupComponent, BsButtonTypeDirective, BsModalHostComponent, BsModalDirective, BsModalHeaderDirective, BsModalBodyDirective, BsModalFooterDirective, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective, BsSpinnerComponent, SparkIconComponent, TranslateKeyPipe, ResolveTranslationPipe, ReferenceAttrValuePipe, SparkAttributeDescriptionComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<bs-input-group>\n <input\n type=\"text\"\n [id]=\"inputId()\"\n [value]=\"displayValue()\"\n readonly\n [class.is-invalid]=\"isInvalid()\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"open()\">\n ...\n </button>\n</bs-input-group>\n\n<bs-modal [isOpen]=\"showModal()\" (isOpenChange)=\"!$event && close()\">\n <div *bsModal class=\"reference-modal\">\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.select' | t }} {{ title() }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- bs-modal portals its content into a CDK overlay outside the host form, so the\n modal needs its own <bs-form> context for inputs to pick up .form-control styles. -->\n <bs-form>\n @if (entityType()) {\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange()\">\n @if (searchTerm) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n @if (searchTerm && pagination()) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ pagination()!.totalRecords }} {{ pagination()!.totalRecords === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n\n <bs-datatable\n [(settings)]=\"settings\"\n [data]=\"rows()\"\n (rowClick)=\"select($event.row)\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ attr.label | resolveTranslation:attr.name }}\n <spark-attribute-description [description]=\"attr.description\" />\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @for (attr of visibleAttributes(); track attr.id) {\n <td>{{ $any(item) | referenceAttrValue:attr.name }}</td>\n }\n </ng-container>\n </bs-datatable>\n } @else {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n }\n </bs-form>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"close()\">{{ 'common.cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n" }]
158
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], referenceType: [{ type: i0.Input, args: [{ isSignal: true, alias: "referenceType", required: false }] }], isInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "isInvalid", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
159
-
160
- /**
161
- * Reusable LookupReference value picker: a readonly textbox showing the current selection's
162
- * translated label plus a "…" button that opens a searchable modal list over the active
163
- * `options`. Per-instance state, so it serves both top-level lookup attributes (display type
164
- * Modal) and individual inline AsDetail lookup cells. Emits the picked key via `valueChange`.
165
- */
166
- class SparkLookupPickerComponent {
167
- /** Currently selected lookup key (or null when unset). */
168
- value = input(null, /* @ts-ignore */
169
- ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
170
- /** Active lookup values to pick from. */
171
- options = input([], /* @ts-ignore */
172
- ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
173
- /** Renders the field with the Bootstrap is-invalid state. */
174
- isInvalid = input(false, /* @ts-ignore */
175
- ...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
176
- /** Optional id forwarded to the readonly input (for label association). */
177
- inputId = input(undefined, /* @ts-ignore */
178
- ...(ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []));
179
- /** Optional resolved label appended to the modal header ("Select {title}"). */
180
- title = input('', /* @ts-ignore */
181
- ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
182
- valueChange = output();
183
- colors = Color;
184
- showModal = signal(false, /* @ts-ignore */
185
- ...(ngDevMode ? [{ debugName: "showModal" }] : /* istanbul ignore next */ []));
186
- searchTerm = signal('', /* @ts-ignore */
187
- ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
188
- displayValue = computed(() => {
189
- const key = this.value();
190
- if (key == null || key === '')
191
- return '';
192
- const selected = this.options().find(o => o.key === String(key));
193
- if (!selected)
194
- return String(key);
195
- return resolveTranslation(selected.values) || selected.key;
196
- }, /* @ts-ignore */
197
- ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
198
- filteredItems = computed(() => {
199
- if (!this.searchTerm().trim()) {
200
- return this.options();
201
- }
202
- const term = this.searchTerm().toLowerCase().trim();
203
- return this.options().filter(item => {
204
- const translation = resolveTranslation(item.values);
205
- return translation.toLowerCase().includes(term) || item.key.toLowerCase().includes(term);
206
- });
207
- }, /* @ts-ignore */
208
- ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
209
- open() {
210
- this.searchTerm.set('');
211
- this.showModal.set(true);
212
- }
213
- select(item) {
214
- this.valueChange.emit(item.key);
215
- this.close();
216
- }
217
- close() {
218
- this.showModal.set(false);
219
- this.searchTerm.set('');
220
- }
221
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkLookupPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
222
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkLookupPickerComponent, isStandalone: true, selector: "spark-lookup-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, isInvalid: { classPropertyName: "isInvalid", publicName: "isInvalid", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<bs-input-group>\n <input\n type=\"text\"\n [id]=\"inputId()\"\n [value]=\"displayValue()\"\n readonly\n [class.is-invalid]=\"isInvalid()\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"open()\">\n ...\n </button>\n</bs-input-group>\n\n<bs-modal [isOpen]=\"showModal()\" (isOpenChange)=\"!$event && close()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.select' | t }} {{ title() }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- bs-modal portals its content into a CDK overlay outside the host form, so the\n modal needs its own <bs-form> context for inputs to pick up .form-control styles. -->\n <bs-form>\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [col]>\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\">\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"searchTerm.set('')\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n </div>\n </bs-grid>\n\n <!-- List of items -->\n <bs-table [striped]=\"true\" [hover]=\"true\">\n <tbody class=\"align-middle\">\n @for (item of filteredItems(); track item.key) {\n <tr\n [class.table-primary]=\"value() === item.key\"\n (click)=\"select(item)\"\n style=\"cursor: pointer;\">\n <td>{{ item.values | resolveTranslation:item.key }}</td>\n </tr>\n } @empty {\n <tr>\n <td class=\"text-center text-muted\">{{ 'common.noItemsFound' | t }}</td>\n </tr>\n }\n </tbody>\n </bs-table>\n </bs-form>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"close()\">{{ 'common.cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: BsFormComponent, selector: "bs-form", inputs: ["action", "method"], outputs: ["submitted"] }, { kind: "directive", type: BsFormControlDirective, selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColDirective, selector: "[col]", inputs: ["col"] }, { kind: "component", type: BsInputGroupComponent, selector: "bs-input-group", inputs: ["size"] }, { kind: "directive", type: BsButtonTypeDirective, selector: "button[color],input[type=\"button\"][color],input[type=\"submit\"][color],a[color]", inputs: ["color"] }, { kind: "component", type: BsModalHostComponent, selector: "bs-modal", inputs: ["isOpen", "closeOnEscape", "scrollable"], outputs: ["isOpenChange"] }, { kind: "directive", type: BsModalDirective, selector: "[bsModal]" }, { kind: "directive", type: BsModalHeaderDirective, selector: "[bsModalHeader]" }, { kind: "directive", type: BsModalBodyDirective, selector: "[bsModalBody]" }, { kind: "directive", type: BsModalFooterDirective, selector: "[bsModalFooter]" }, { kind: "component", type: BsTableComponent, selector: "bs-table", inputs: ["isResponsive", "striped", "hover", "border", "ariaRowCount"] }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
877
+ function refreshDispatch(attr) {
878
+ switch (effectiveTrigger(attr)) {
879
+ case "change": return isDiscreteEditor(attr) ? "immediate" : "debounced";
880
+ case "blur": return "blur";
881
+ default: return null;
882
+ }
223
883
  }
224
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkLookupPickerComponent, decorators: [{
225
- type: Component,
226
- args: [{ selector: 'spark-lookup-picker', imports: [FormsModule, BsFormComponent, BsFormControlDirective, BsGridComponent, BsGridRowDirective, BsGridColDirective, BsInputGroupComponent, BsButtonTypeDirective, BsModalHostComponent, BsModalDirective, BsModalHeaderDirective, BsModalBodyDirective, BsModalFooterDirective, BsTableComponent, SparkIconComponent, TranslateKeyPipe, ResolveTranslationPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<bs-input-group>\n <input\n type=\"text\"\n [id]=\"inputId()\"\n [value]=\"displayValue()\"\n readonly\n [class.is-invalid]=\"isInvalid()\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"open()\">\n ...\n </button>\n</bs-input-group>\n\n<bs-modal [isOpen]=\"showModal()\" (isOpenChange)=\"!$event && close()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.select' | t }} {{ title() }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- bs-modal portals its content into a CDK overlay outside the host form, so the\n modal needs its own <bs-form> context for inputs to pick up .form-control styles. -->\n <bs-form>\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [col]>\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\">\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"searchTerm.set('')\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n </div>\n </bs-grid>\n\n <!-- List of items -->\n <bs-table [striped]=\"true\" [hover]=\"true\">\n <tbody class=\"align-middle\">\n @for (item of filteredItems(); track item.key) {\n <tr\n [class.table-primary]=\"value() === item.key\"\n (click)=\"select(item)\"\n style=\"cursor: pointer;\">\n <td>{{ item.values | resolveTranslation:item.key }}</td>\n </tr>\n } @empty {\n <tr>\n <td class=\"text-center text-muted\">{{ 'common.noItemsFound' | t }}</td>\n </tr>\n }\n </tbody>\n </bs-table>\n </bs-form>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"close()\">{{ 'common.cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n" }]
227
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], isInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "isInvalid", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
228
-
229
- /**
230
- * Serializes refreshes for **one** form instance and drops superseded ones.
231
- *
232
- * Per-instance rather than a service, deliberately. The retry-action modal renders its own
233
- * `spark-po-form`, and a refresh can carry a retry operation — so a refresh can open a modal
234
- * containing a form whose own attributes may trigger refreshes. A shared coordinator would let the
235
- * nested form resolve or supersede the outer form's pending request. The same applies to the
236
- * recursive `spark-po-form` used for modal AsDetail editing.
237
- *
238
- * Cancellation is not available: the service layer is promise-based (`firstValueFrom`), so a stale
239
- * response *will* arrive. It is discarded by sequence number rather than prevented.
240
- */
241
- class RefreshCoordinator {
242
- host;
243
- queue = Promise.resolve();
244
- sequence = 0;
245
- settled = 0;
246
- pending = new Set();
247
- constructor(host) {
248
- this.host = host;
249
- }
250
- /** Whether a refresh is in flight. */
251
- get isRefreshing() {
252
- return this.sequence !== this.settled;
253
- }
254
- /**
255
- * Marks `attributeName` as needing a refresh without sending one — for free-text editors, which
256
- * would otherwise issue a request per keystroke. Flushed by {@link blur} or {@link flush}.
257
- */
258
- markPending(attributeName) {
259
- this.pending.add(attributeName);
260
- }
261
- /** Sends a pending refresh for `attributeName`, if one was marked. */
262
- blur(attributeName) {
263
- if (!this.pending.delete(attributeName))
264
- return Promise.resolve();
265
- return this.trigger(attributeName);
266
- }
267
- /**
268
- * Sends every refresh still marked pending. Called before save, so a value typed and never blurred
269
- * — the user tabbing straight to the save button — is still reflected before the object goes.
270
- */
271
- async flush() {
272
- const names = [...this.pending];
273
- this.pending.clear();
274
- for (const name of names)
275
- await this.trigger(name);
276
- await this.queue;
277
- }
278
- /** Sends a refresh immediately — discrete editors, where every change is a committed one. */
279
- trigger(attributeName) {
280
- const ticket = ++this.sequence;
281
- this.host.setBusy(true);
282
- this.queue = this.queue.then(async () => {
283
- const sent = { ...this.host.currentValues() };
284
- try {
285
- const response = await this.host.send(attributeName);
286
- // A newer refresh was dispatched while this one was in flight, so this response describes a
287
- // form state that no longer exists. Applying it would resurrect superseded metadata.
288
- if (ticket !== this.sequence)
289
- return;
290
- this.host.apply(response, sent);
291
- }
292
- finally {
293
- this.settled = Math.max(this.settled, ticket);
294
- if (!this.isRefreshing)
295
- this.host.setBusy(false);
296
- }
297
- });
298
- return this.queue;
299
- }
884
+ function isDiscreteEditor(attr) {
885
+ if (!attr) return false;
886
+ if (attr.lookupReferenceType) return true;
887
+ switch ((attr.dataType ?? "").toLowerCase()) {
888
+ case "reference":
889
+ case "lookupreference":
890
+ case "boolean":
891
+ case "bool":
892
+ case "date":
893
+ case "datetime":
894
+ case "dateonly":
895
+ case "enum":
896
+ case "color": return true;
897
+ default: return false;
898
+ }
300
899
  }
301
- /**
302
- * Editors where every change is a deliberate, committed one, so a refresh fires immediately.
303
- * Everything else is free text, where firing per keystroke would be unacceptable and the refresh
304
- * waits for blur.
305
- *
306
- * ⚠️ This asks how the attribute is **rendered**, not what its `dataType` says, and the difference
307
- * is not academic. A lookup attribute carries the data type of its *key* — Fleet's `Car.Status` is
308
- * `dataType: "string"` with `lookupReferenceType: "CarStatus"` — so keying on `dataType` alone
309
- * classifies a `<bs-select>` as free text. It then waits for a blur that a select never emits, and
310
- * the refresh simply never fires: no request, no error, nothing to see.
311
- */
312
- function triggersImmediately(attr) {
313
- if (!attr)
314
- return false;
315
- // A lookup renders as a select (or a modal picker) whatever its key's type is.
316
- if (attr.lookupReferenceType)
317
- return true;
318
- switch ((attr.dataType ?? '').toLowerCase()) {
319
- case 'reference':
320
- case 'lookupreference':
321
- case 'boolean':
322
- case 'bool':
323
- case 'date':
324
- case 'datetime':
325
- case 'dateonly':
326
- case 'enum':
327
- case 'color':
328
- return true;
329
- default:
330
- return false;
331
- }
332
- }
333
- /** Empty overlay, so callers do not have to spell the type. */
334
- const NO_OVERLAY = {};
335
-
336
- class SparkPoFormComponent {
337
- sparkService = inject(SparkService);
338
- translations = inject(SparkLanguageService);
339
- rendererRegistry = inject(SPARK_ATTRIBUTE_RENDERERS);
340
- entityType = input(null, /* @ts-ignore */
341
- ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
342
- formData = model({}, /* @ts-ignore */
343
- ...(ngDevMode ? [{ debugName: "formData" }] : /* istanbul ignore next */ []));
344
- validationErrors = input([], /* @ts-ignore */
345
- ...(ngDevMode ? [{ debugName: "validationErrors" }] : /* istanbul ignore next */ []));
346
- showButtons = input(false, /* @ts-ignore */
347
- ...(ngDevMode ? [{ debugName: "showButtons" }] : /* istanbul ignore next */ []));
348
- isSaving = input(false, /* @ts-ignore */
349
- ...(ngDevMode ? [{ debugName: "isSaving" }] : /* istanbul ignore next */ []));
350
- parentId = input(undefined, /* @ts-ignore */
351
- ...(ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []));
352
- parentType = input(undefined, /* @ts-ignore */
353
- ...(ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []));
354
- save = output();
355
- cancel = output();
356
- /**
357
- * The type id to refresh against. Absent means refresh is unavailable — the form still renders and
358
- * edits normally, so a host that has not opted in loses nothing.
359
- */
360
- objectTypeId = input(undefined, /* @ts-ignore */
361
- ...(ngDevMode ? [{ debugName: "objectTypeId" }] : /* istanbul ignore next */ []));
362
- /** The id of the object being edited; absent for a create. */
363
- objectId = input(undefined, /* @ts-ignore */
364
- ...(ngDevMode ? [{ debugName: "objectId" }] : /* istanbul ignore next */ []));
365
- /**
366
- * Set when this form renders a single embedded AsDetail object inside a host form: the name of
367
- * the attribute holding it, e.g. `"Gate"`. Presence of a prefix switches the form from issuing
368
- * its own refreshes to reporting them upward, because an embedded object is not something the
369
- * server can be asked about on its own — the request must describe, and be authorized against,
370
- * the object that owns it.
371
- */
372
- triggerPathPrefix = input(null, /* @ts-ignore */
373
- ...(ngDevMode ? [{ debugName: "triggerPathPrefix" }] : /* istanbul ignore next */ []));
374
- /** Raised instead of self-issuing when {@link triggerPathPrefix} is set. */
375
- nestedTriggerRequested = output();
376
- /** Blur counterpart, flushing a trigger the host only marked pending. */
377
- nestedTriggerBlurred = output();
378
- /**
379
- * What the last refresh changed about each attribute's presentation, keyed by attribute name.
380
- *
381
- * Deliberately NOT folded back into `entityType`. All option loading hangs off one effect keyed on
382
- * `entityType` identity and `SparkService` caches nothing, so re-setting it would re-issue every
383
- * reference query and lookup fetch on every refresh; mutating it in place would not re-render at
384
- * all.
385
- *
386
- * ⚠️ A `model`, not a plain signal, because the page that owns the save has to see it. This form
387
- * renders from the overlaid attribute list; `spark-po-edit` and `spark-po-create` build the
388
- * payload from their own copy of the same filter. While the overlay stopped here, those two
389
- * filtered on the attribute's state *as loaded* — so an attribute a refresh hook revealed was
390
- * rendered, filled in by the user, and then silently left out of the save. When the same hook also
391
- * made it required (the ordinary case — a field appears *because* it is now needed) the server
392
- * refused the save for the very field the user had just filled, and the form became a dead end
393
- * with no way forward from inside it.
394
- */
395
- refreshOverlay = model({}, /* @ts-ignore */
396
- ...(ngDevMode ? [{ debugName: "refreshOverlay" }] : /* istanbul ignore next */ []));
397
- isRefreshing = signal(false, /* @ts-ignore */
398
- ...(ngDevMode ? [{ debugName: "isRefreshing" }] : /* istanbul ignore next */ []));
399
- colors = Color;
400
- referenceOptions = signal({}, /* @ts-ignore */
401
- ...(ngDevMode ? [{ debugName: "referenceOptions" }] : /* istanbul ignore next */ []));
402
- // Multi-reference (Reference && isArray) editor state. Built from the same query
403
- // results as referenceOptions: a flat TreeNode list per attribute drives an
404
- // InMemoryTreeSelectProvider (the <bs-tree-select> data port), and a node lookup
405
- // resolves selected ids back to chip labels.
406
- referenceProviders = signal({}, /* @ts-ignore */
407
- ...(ngDevMode ? [{ debugName: "referenceProviders" }] : /* istanbul ignore next */ []));
408
- referenceNodes = signal({}, /* @ts-ignore */
409
- ...(ngDevMode ? [{ debugName: "referenceNodes" }] : /* istanbul ignore next */ []));
410
- asDetailTypes = signal({}, /* @ts-ignore */
411
- ...(ngDevMode ? [{ debugName: "asDetailTypes" }] : /* istanbul ignore next */ []));
412
- lookupReferenceOptions = signal({}, /* @ts-ignore */
413
- ...(ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : /* istanbul ignore next */ []));
414
- // Modal state for AsDetail object editing
415
- editingAsDetailAttr = signal(null, /* @ts-ignore */
416
- ...(ngDevMode ? [{ debugName: "editingAsDetailAttr" }] : /* istanbul ignore next */ []));
417
- asDetailFormData = signal({}, /* @ts-ignore */
418
- ...(ngDevMode ? [{ debugName: "asDetailFormData" }] : /* istanbul ignore next */ []));
419
- showAsDetailModal = signal(false, /* @ts-ignore */
420
- ...(ngDevMode ? [{ debugName: "showAsDetailModal" }] : /* istanbul ignore next */ []));
421
- editingArrayIndex = signal(null, /* @ts-ignore */
422
- ...(ngDevMode ? [{ debugName: "editingArrayIndex" }] : /* istanbul ignore next */ []));
423
- // Permissions for array AsDetail entity types
424
- asDetailPermissions = signal({}, /* @ts-ignore */
425
- ...(ngDevMode ? [{ debugName: "asDetailPermissions" }] : /* istanbul ignore next */ []));
426
- /**
427
- * What the server said when it refused to construct or release a row.
428
- *
429
- * A local signal rather than the `validationErrors` input, because this is the one class of error
430
- * the form raises on its own behalf: `validationErrors` is owned by the host (po-edit / po-create)
431
- * and arrives from a save. Cleared at the start of each lifecycle call, so a stale refusal never
432
- * outlives the click that caused it.
433
- *
434
- * Keyed by attribute name so a message renders against the grid it came from. A single global
435
- * alert would have been less code and worse: a form with two detail grids would show one grid's
436
- * refusal above the other, and the user's next click is on the grid, not the top of the page.
437
- */
438
- rowLifecycleErrors = signal({}, /* @ts-ignore */
439
- ...(ngDevMode ? [{ debugName: "rowLifecycleErrors" }] : /* istanbul ignore next */ []));
440
- /** The refusal to render under `attrName`'s grid, if any. */
441
- rowLifecycleErrorsFor(attrName) {
442
- return this.rowLifecycleErrors()[attrName] ?? [];
443
- }
444
- // Reference options for columns within array AsDetail types (keyed by parent attr name, then column name)
445
- asDetailReferenceOptions = signal({}, /* @ts-ignore */
446
- ...(ngDevMode ? [{ debugName: "asDetailReferenceOptions" }] : /* istanbul ignore next */ []));
447
- // Reference/Lookup picking is owned by the standalone spark-reference-picker /
448
- // spark-lookup-picker components (per-instance modal state); the form just feeds them
449
- // options and writes the emitted value back into formData / the row.
450
- ELookupDisplayType = ELookupDisplayType;
451
- EReferenceDisplayType = EReferenceDisplayType;
452
- /**
453
- * Every attribute this form could ever need option data for — including ones the model hides,
454
- * because a refresh may reveal them.
455
- *
456
- * Read by the option-loading effect, and deliberately independent of `refreshOverlay`: the loaders
457
- * read this synchronously, so an overlay dependency here would make every refresh re-issue every
458
- * reference query and lookup fetch. That is the whole reason the overlay is a separate signal.
459
- */
460
- optionSourceAttributes = computed(() => {
461
- return this.entityType()?.attributes
462
- .filter(a => hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject))
463
- .sort((a, b) => a.order - b.order) || [];
464
- }, /* @ts-ignore */
465
- ...(ngDevMode ? [{ debugName: "optionSourceAttributes" }] : /* istanbul ignore next */ []));
466
- editableAttributes = computed(() => {
467
- const overlay = this.refreshOverlay();
468
- return this.entityType()?.attributes
469
- .map(a => applyOverlay(a, overlay[a.name]))
470
- .filter(a => a.isVisible && !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject))
471
- .sort((a, b) => a.order - b.order) || [];
472
- }, /* @ts-ignore */
473
- ...(ngDevMode ? [{ debugName: "editableAttributes" }] : /* istanbul ignore next */ []));
474
- static DEFAULT_TAB = { id: '__default__', name: 'Algemeen', label: { nl: 'Algemeen', en: 'General' }, order: 0 };
475
- ungroupedAttributes = computed(() => {
476
- const attrs = this.editableAttributes();
477
- const groupIds = new Set((this.entityType()?.groups || []).map(g => g.id));
478
- return attrs.filter(a => !a.group || !groupIds.has(a.group));
479
- }, /* @ts-ignore */
480
- ...(ngDevMode ? [{ debugName: "ungroupedAttributes" }] : /* istanbul ignore next */ []));
481
- resolvedTabs = computed(() => {
482
- const et = this.entityType();
483
- const definedTabs = et?.tabs?.length ? [...et.tabs].sort((a, b) => a.order - b.order) : [];
484
- const hasUngroupedAttrs = this.ungroupedAttributes().length > 0;
485
- const hasUntabbedGroups = (et?.groups || []).some(g => !g.tab);
486
- if (hasUngroupedAttrs || hasUntabbedGroups || definedTabs.length === 0) {
487
- return [SparkPoFormComponent.DEFAULT_TAB, ...definedTabs];
488
- }
489
- return definedTabs;
490
- }, /* @ts-ignore */
491
- ...(ngDevMode ? [{ debugName: "resolvedTabs" }] : /* istanbul ignore next */ []));
492
- groupsForTab(tab) {
493
- const groups = this.entityType()?.groups || [];
494
- if (tab.id === '__default__') {
495
- return groups.filter(g => !g.tab).sort((a, b) => a.order - b.order);
496
- }
497
- return groups.filter(g => g.tab === tab.id).sort((a, b) => a.order - b.order);
498
- }
499
- attrsForGroup(group) {
500
- return this.editableAttributes().filter(a => a.group === group.id);
501
- }
502
- constructor() {
503
- effect(() => {
504
- const et = this.entityType();
505
- const _pid = this.parentId();
506
- const _ptype = this.parentType();
507
- if (et) {
508
- this.loadReferenceOptions();
509
- this.loadAsDetailTypes();
510
- this.loadLookupReferenceOptions();
511
- }
512
- });
513
- }
514
- toRecord(entries) {
515
- const result = {};
516
- for (const [key, value] of entries) {
517
- result[key] = value;
518
- }
519
- return result;
520
- }
521
- async loadReferenceOptions() {
522
- const refAttrs = this.optionSourceAttributes().filter(a => a.dataType === 'Reference' && a.query);
523
- if (refAttrs.length === 0)
524
- return;
525
- const entries = await Promise.all(refAttrs.filter(a => a.query).map(async (attr) => {
526
- const result = await this.sparkService.executeQueryByName(attr.query, {
527
- parentId: this.parentId(),
528
- parentType: this.parentType(),
529
- });
530
- return [attr.name, result.items];
531
- }));
532
- const optionsByAttr = this.toRecord(entries);
533
- this.referenceOptions.set(optionsByAttr);
534
- // For multi-reference attributes (Reference && isArray), turn each query result
535
- // into a flat TreeNode list + an in-memory provider for <bs-tree-select>, and a
536
- // by-id node map used to render chip labels for the current selection.
537
- const providers = {};
538
- const nodesByAttr = {};
539
- for (const attr of refAttrs) {
540
- if (!attr.isArray)
541
- continue;
542
- const pos = optionsByAttr[attr.name] || [];
543
- const nodes = pos
544
- .filter(po => !!po.id)
545
- .map(po => ({ id: po.id, label: po.breadcrumb || po.id }));
546
- providers[attr.name] = new InMemoryTreeSelectProvider(nodes);
547
- nodesByAttr[attr.name] = this.toRecord(nodes.map(n => [n.id, n]));
548
- }
549
- this.referenceProviders.set(providers);
550
- this.referenceNodes.set(nodesByAttr);
551
- }
552
- /**
553
- * Stable TreeNode[] value per multi-reference attribute, derived from the id array
554
- * in formData and the resolved node map. Recomputes only when formData or the node
555
- * map changes, so binding it into <bs-tree-select> doesn't churn on every CD pass.
556
- * Ids without a resolved node fall back to a label of the id itself.
557
- */
558
- referenceTreeValues = computed(() => {
559
- const fd = this.formData();
560
- const nodesByAttr = this.referenceNodes();
561
- const result = {};
562
- for (const attr of this.editableAttributes()) {
563
- if (attr.dataType !== 'Reference' || !attr.isArray)
564
- continue;
565
- const ids = Array.isArray(fd[attr.name]) ? fd[attr.name] : [];
566
- const nodeMap = nodesByAttr[attr.name] || {};
567
- result[attr.name] = ids.map(id => nodeMap[id] ?? { id, label: id });
568
- }
569
- return result;
570
- }, /* @ts-ignore */
571
- ...(ngDevMode ? [{ debugName: "referenceTreeValues" }] : /* istanbul ignore next */ []));
572
- getReferenceProvider(attr) {
573
- return this.referenceProviders()[attr.name];
574
- }
575
- onReferenceTreeChange(attr, value) {
576
- const nodes = Array.isArray(value) ? value : value ? [value] : [];
577
- const data = { ...this.formData() };
578
- data[attr.name] = nodes.map(n => n.id);
579
- this.formData.set(data);
580
- }
581
- async loadAsDetailTypes() {
582
- const asDetailAttrs = this.optionSourceAttributes().filter(a => a.dataType === 'AsDetail' && a.asDetailType);
583
- if (asDetailAttrs.length === 0)
584
- return;
585
- const types = await this.sparkService.getEntityTypes();
586
- const newAsDetailTypes = {};
587
- // The catalogue is Query-gated, and a row type edited through its parent usually has no rights
588
- // of its own — so it is normally absent from `types`, and resolving from there alone left
589
- // `asDetailTypes` empty. `asDetailColumns` then returned [] and the table rendered with no
590
- // columns at all. The server now ships the row definitions alongside the parent, gated on the
591
- // parent's right; the catalogue stays the first source so nothing changes for a type that is
592
- // in it (#385).
593
- const embedded = this.entityType()?.detailTypes ?? [];
594
- for (const attr of asDetailAttrs) {
595
- const asDetailType = types.find(t => t.clrType === attr.asDetailType)
596
- ?? embedded.find(t => t.clrType === attr.asDetailType);
597
- if (asDetailType) {
598
- newAsDetailTypes[attr.name] = asDetailType;
599
- if (attr.isArray) {
600
- // Ask by CLR type when the row type carries no alias — the embedded copy has its alias
601
- // pruned, and `getPermissions` accepts either.
602
- const perms = await this.sparkService.getPermissions(asDetailType.id);
603
- this.asDetailPermissions.update(prev => ({ ...prev, [attr.name]: perms }));
604
- const refCols = asDetailType.attributes.filter(a => a.dataType === 'Reference' && a.query);
605
- if (refCols.length > 0) {
606
- const refEntries = await Promise.all(refCols.filter(c => c.query).map(async (col) => {
607
- const result = await this.sparkService.executeQueryByName(col.query, {
608
- parentId: this.parentId(),
609
- parentType: this.parentType(),
610
- });
611
- return [col.name, result.items];
612
- }));
613
- this.asDetailReferenceOptions.update(prev => ({ ...prev, [attr.name]: this.toRecord(refEntries) }));
614
- }
615
- // Inline editing of LookupReference child columns needs their options loaded
616
- // (keyed by lookup name in the shared lookupReferenceOptions, deduped).
617
- const lookupCols = asDetailType.attributes.filter(a => a.lookupReferenceType);
618
- for (const col of lookupCols) {
619
- const lookupName = col.lookupReferenceType;
620
- if (!this.lookupReferenceOptions()[lookupName]) {
621
- const ref = await this.sparkService.getLookupReference(lookupName);
622
- this.lookupReferenceOptions.update(prev => ({ ...prev, [lookupName]: ref }));
623
- }
624
- }
625
- }
626
- }
627
- }
628
- this.asDetailTypes.set(newAsDetailTypes);
629
- }
630
- async loadLookupReferenceOptions() {
631
- const lookupAttrs = this.optionSourceAttributes().filter(a => a.lookupReferenceType);
632
- if (lookupAttrs.length === 0)
633
- return;
634
- const lookupNames = [...new Set(lookupAttrs.map(a => a.lookupReferenceType))];
635
- const entries = await Promise.all(lookupNames.map(async (name) => {
636
- const ref = await this.sparkService.getLookupReference(name);
637
- return [name, ref];
638
- }));
639
- this.lookupReferenceOptions.set(this.toRecord(entries));
640
- }
641
- getLookupOptions(attr) {
642
- const lookupRef = attr.lookupReferenceType ? this.lookupReferenceOptions()[attr.lookupReferenceType] : null;
643
- return lookupRef?.values.filter(v => v.isActive) || [];
644
- }
645
- // Write the value emitted by a top-level spark-reference-picker / spark-lookup-picker
646
- // back into formData (the same write the old in-form modal selectors performed).
647
- onReferenceValueChange(attr, id) {
648
- const data = { ...this.formData() };
649
- data[attr.name] = id;
650
- this.formData.set(data);
651
- }
652
- onLookupValueChange(attr, key) {
653
- const data = { ...this.formData() };
654
- data[attr.name] = key;
655
- this.formData.set(data);
656
- }
657
- getEditRendererComponent(attr) {
658
- if (!attr.renderer)
659
- return null;
660
- const reg = this.rendererRegistry.find(r => r.name === attr.renderer);
661
- return reg?.editComponent ?? null;
662
- }
663
- getEditRendererInputs(component, attr) {
664
- return withDeclaredInputs(component, {
665
- value: this.formData()[attr.name],
666
- attribute: attr,
667
- options: attr.rendererOptions,
668
- valueChange: (newValue) => {
669
- const data = { ...this.formData() };
670
- data[attr.name] = newValue;
671
- this.formData.set(data);
672
- },
673
- });
674
- }
675
- /** Column renderer for a cell of an AsDetail sub-table (so embedded rows honor `col.renderer` too). */
676
- getAsDetailCellRendererComponent(col) {
677
- if (!col.renderer)
678
- return null;
679
- return this.rendererRegistry.find(r => r.name === col.renderer)?.columnComponent ?? null;
680
- }
681
- getAsDetailCellRendererInputs(component, row, col) {
682
- return withDeclaredInputs(component, {
683
- value: row[col.name],
684
- attribute: col,
685
- options: col.rendererOptions,
686
- item: row,
687
- });
688
- }
689
- /** Edit-renderer for an inline AsDetail cell (so inline editing honors `col.renderer`, not just display). */
690
- getAsDetailCellEditRenderer(col) {
691
- if (!col.renderer)
692
- return null;
693
- return this.rendererRegistry.find(r => r.name === col.renderer)?.editComponent ?? null;
694
- }
695
- getAsDetailCellEditRendererInputs(component, row, col) {
696
- return withDeclaredInputs(component, {
697
- value: row[col.name],
698
- attribute: col,
699
- options: col.rendererOptions,
700
- item: row,
701
- valueChange: (newValue) => {
702
- row[col.name] = newValue;
703
- this.onFieldChange();
704
- },
705
- });
706
- }
707
- /**
708
- * Rules evaluated in the browser, against the *effective* metadata — so a rule a refresh hook
709
- * imposed is visible before the round-trip rather than only after the server rejects the save.
710
- */
711
- clientRuleFailures = computed(() => {
712
- const values = this.formData();
713
- return this.editableAttributes().flatMap(attr => evaluateRules(attr, values[attr.name]));
714
- }, /* @ts-ignore */
715
- ...(ngDevMode ? [{ debugName: "clientRuleFailures" }] : /* istanbul ignore next */ []));
716
- hasError(attrName) {
717
- return this.clientRuleFailures().some(f => f.attributeName === attrName)
718
- || this.validationErrors().some(e => e.attributeName === attrName);
719
- }
720
- // Per-cell validation for inline AsDetail rows. Server-emitted errors are keyed by the
721
- // path "{attr}[{rowIndex}].{col}"; the client surfaces them like top-level field errors.
722
- inlineErrorPath(attr, rowIndex, col) {
723
- return `${attr.name}[${rowIndex}].${col.name}`;
724
- }
725
- hasInlineError(attr, rowIndex, col) {
726
- const path = this.inlineErrorPath(attr, rowIndex, col);
727
- return this.validationErrors().some(e => e.attributeName === path);
728
- }
729
- inlineErrorMessage(attr, rowIndex, col) {
730
- const path = this.inlineErrorPath(attr, rowIndex, col);
731
- const error = this.validationErrors().find(e => e.attributeName === path);
732
- return error ? resolveTranslation(error.errorMessage) : null;
733
- }
734
- /**
735
- * The single funnel every scalar / boolean / inline-cell edit passes through.
736
- *
737
- * `attr` is optional because some editors have nothing useful to say about which attribute moved.
738
- * A caller that knows should always say so — without it no refresh can fire.
739
- */
740
- onFieldChange(attr) {
741
- this.formData.set({ ...this.formData() });
742
- if (attr)
743
- this.noteChange(attr);
744
- }
745
- /**
746
- * A trigger inside an AsDetail row. Addressed by the same `{attr}[{index}].{col}` path the inline
747
- * validation errors already use, so the server can tell which row asked without a second
748
- * addressing scheme being invented for it.
749
- */
750
- onInlineCellChange(attr, rowIndex, col) {
751
- this.onFieldChange();
752
- if (col.triggersRefresh !== true || !this.objectTypeId())
753
- return;
754
- const path = this.inlineErrorPath(attr, rowIndex, col);
755
- this.pendingNestedTrigger = { kind: 'row', attribute: attr.name, rowIndex };
756
- if (triggersImmediately(col)) {
757
- void this.refreshCoordinator.trigger(path);
758
- }
759
- else {
760
- this.refreshCoordinator.markPending(path);
761
- }
762
- }
763
- /**
764
- * Which embedded target the in-flight refresh belongs to, if any.
765
- *
766
- * ⚠️ Tagged, not a nullable `rowIndex`. The two arms write to different places — a row inside
767
- * `formData`, versus the modal's working copy in `asDetailFormData` — and an index that happens
768
- * to be absent is not the same statement as "this attribute holds one object". With a nullable
769
- * index, a row refresh carrying a bad index would fall silently into the object arm.
770
- */
771
- pendingNestedTrigger = null;
772
- /**
773
- * A trigger inside a single embedded AsDetail object, raised by the modal's recursive form.
774
- *
775
- * The child form does not issue the request itself, for two reasons that both bite. Its
776
- * `entityType` is the embedded type, so a payload built there describes `GateSettings` while the
777
- * request must describe the Repository that owns it. And nested AsDetail types carry no rights of
778
- * their own — the server authorizes a nested refresh on the OWNING type — so a child-issued
779
- * request would be asking the wrong question of security.json.
780
- */
781
- onEmbeddedTrigger(attributeName, event) {
782
- if (!this.objectTypeId())
783
- return;
784
- // Set on both arms, exactly as onInlineCellChange does: the blur that later flushes a pending
785
- // trigger does not know which target it belongs to, so the mark is what remembers.
786
- this.pendingNestedTrigger = { kind: 'object', attribute: attributeName };
787
- if (event.immediate) {
788
- void this.refreshCoordinator.trigger(event.path);
789
- }
790
- else {
791
- this.refreshCoordinator.markPending(event.path);
792
- }
793
- }
794
- /** Blur counterpart of {@link onEmbeddedTrigger}, for the child's free-text editors. */
795
- onEmbeddedTriggerBlur(path) {
796
- if (!this.objectTypeId())
797
- return;
798
- void this.refreshCoordinator.blur(path);
799
- }
800
- /**
801
- * Applies a refresh that ran against a detail row: the row's own values, and the column metadata
802
- * for the grid it lives in.
803
- *
804
- * The column metadata comes from `asDetailTypes` — a different signal from `entityType` — which is
805
- * why a nested response cannot go through the top-level overlay.
806
- *
807
- * ⚠️ The row array is mutated in place rather than replaced. Rows are tracked by index, so handing
808
- * the template a new array destroys and rebuilds every row's DOM and takes focus with it, mid-edit.
809
- */
810
- applyNestedResponse(nested, response) {
811
- this.pendingNestedTrigger = null;
812
- if (nested.kind === 'row') {
813
- const rows = this.formData()[nested.attribute];
814
- const row = Array.isArray(rows) ? rows[nested.rowIndex] : undefined;
815
- if (row) {
816
- for (const attribute of response.attributes ?? []) {
817
- row[attribute.name] = attribute.value ?? null;
818
- }
819
- // The array identity is unchanged; this only tells the signal graph the contents moved.
820
- this.formData.set({ ...this.formData() });
821
- }
822
- }
823
- else {
824
- // ⚠️ The modal's working copy, never `formData`. `openAsDetailEditor` copies the embedded dict
825
- // precisely so dismissing the modal discards the edit; writing a refreshed value through to
826
- // the parent would make a cancelled edit stick. `saveAsDetailObject` remains the only writer.
827
- //
828
- // Replacing the object identity is also correct here, where mutating in place is correct for
829
- // a row: rows are tracked by index and rebuilding them costs focus, while the recursive form
830
- // re-renders from this signal by design.
831
- const current = this.asDetailFormData();
832
- const next = { ...current };
833
- for (const attribute of response.attributes ?? []) {
834
- next[attribute.name] = attribute.value ?? null;
835
- }
836
- this.asDetailFormData.set(next);
837
- }
838
- const overlay = overlayFromResponse(response);
839
- this.asDetailTypes.update(prev => {
840
- const type = prev[nested.attribute];
841
- if (!type)
842
- return prev;
843
- return {
844
- ...prev,
845
- [nested.attribute]: {
846
- ...type,
847
- attributes: type.attributes.map(a => applyOverlay(a, overlay[a.name])),
848
- },
849
- };
850
- });
851
- }
852
- onInlineCellBlur(attr, rowIndex, col) {
853
- if (col.triggersRefresh !== true || !this.objectTypeId())
854
- return;
855
- void this.refreshCoordinator.blur(this.inlineErrorPath(attr, rowIndex, col));
856
- }
857
- /** Blur handler for free-text editors — sends the refresh their keystrokes only marked pending. */
858
- onFieldBlur(attr) {
859
- if (this.triggerPathPrefix()) {
860
- if (attr.triggersRefresh !== true)
861
- return;
862
- this.nestedTriggerBlurred.emit(this.nestedTriggerPath(attr));
863
- return;
864
- }
865
- if (!this.canRefresh(attr))
866
- return;
867
- void this.refreshCoordinator.blur(attr.name);
868
- }
869
- noteChange(attr) {
870
- // Embedded: report upward and stop. The parent owns the request because only it knows the
871
- // object being edited and holds the right the server authorizes against.
872
- if (this.triggerPathPrefix()) {
873
- if (attr.triggersRefresh !== true)
874
- return;
875
- this.nestedTriggerRequested.emit({
876
- path: this.nestedTriggerPath(attr),
877
- immediate: triggersImmediately(attr),
878
- });
879
- return;
880
- }
881
- if (!this.canRefresh(attr))
882
- return;
883
- if (triggersImmediately(attr)) {
884
- void this.refreshCoordinator.trigger(attr.name);
885
- }
886
- else {
887
- // Free text: marking is all a keystroke earns. The request goes on blur, or on save.
888
- this.refreshCoordinator.markPending(attr.name);
889
- }
890
- }
891
- /** `Gate.ProjectMode` — the index-free counterpart of the inline grid's `Jobs[1].Kind`. */
892
- nestedTriggerPath(attr) {
893
- return `${this.triggerPathPrefix()}.${attr.name}`;
894
- }
895
- canRefresh(attr) {
896
- return attr.triggersRefresh === true && !!this.objectTypeId();
897
- }
898
- /**
899
- * Per-instance, never a service: the retry-action modal renders its own `spark-po-form`, and a
900
- * refresh may carry a retry operation — so a refresh can open a modal containing a form that
901
- * refreshes. A shared coordinator would let the nested form supersede this one's request.
902
- */
903
- refreshCoordinator = new RefreshCoordinator({
904
- send: (triggeredBy) => this.sparkService.refresh(this.objectTypeId(), this.buildRefreshPayload(), triggeredBy),
905
- currentValues: () => this.formData(),
906
- apply: (response, sent) => {
907
- // A nested response describes a detail ROW, not this form. Applying it to the top-level
908
- // overlay would hide every attribute the row does not happen to have.
909
- const nested = this.pendingNestedTrigger;
910
- if (nested) {
911
- this.applyNestedResponse(nested, response);
912
- return;
913
- }
914
- this.refreshOverlay.set(overlayFromResponse(response));
915
- this.formData.set(mergeRefreshValues(sent, this.formData(), response));
916
- this.applyRefreshedOptions(response);
917
- },
918
- setBusy: (busy) => this.isRefreshing.set(busy),
919
- });
920
- buildRefreshPayload() {
921
- const et = this.entityType();
922
- const values = this.embeddedValues();
923
- return {
924
- id: this.objectId(),
925
- name: et?.name ?? '',
926
- // The EntityType's id, never `objectTypeId()` — that is the ROUTE segment, which is an alias
927
- // ("car") as often as a guid. The server types this field as a Guid, so an alias fails
928
- // deserialization and the request 500s before the handler is reached: no hook, no error the
929
- // client can act on. The route segment still carries the alias, which the server resolves.
930
- objectTypeId: et?.id ?? this.objectTypeId(),
931
- attributes: (et?.attributes ?? []).map(a => ({
932
- name: a.name,
933
- value: values[a.name] ?? null,
934
- isValueChanged: true,
935
- })),
936
- };
937
- }
938
- /**
939
- * The values to post, with the open modal's working copy substituted for the attribute it edits.
940
- *
941
- * ⚠️ Without this the server receives the embedded object as it was when the modal opened — `{}`
942
- * for a gate that has never been set — and the hook decides against values the user cannot see it
943
- * ignoring. `formData` deliberately does not hold the in-progress edit (cancelling the modal has
944
- * to discard it), so the refresh has to reach into `asDetailFormData` instead. Only the attribute
945
- * under edit is substituted; everything else is the form's own state.
946
- */
947
- embeddedValues() {
948
- const nested = this.pendingNestedTrigger;
949
- if (nested?.kind !== 'object')
950
- return this.formData();
951
- return { ...this.formData(), [nested.attribute]: this.asDetailFormData() };
952
- }
953
- /**
954
- * Folds replaced option lists into the signals the editors already read, so a refreshed dropdown
955
- * renders through the same path as a loaded one.
956
- *
957
- * `undefined` means the hook did not touch this attribute's options and the loaded set stands; an
958
- * empty array means it deliberately left none. Collapsing the two would blank every dropdown the
959
- * hook never mentioned.
960
- */
961
- applyRefreshedOptions(response) {
962
- const replaced = (response.attributes ?? [])
963
- .map(a => [a.name, a.options])
964
- .filter(([, options]) => options !== undefined && options !== null);
965
- if (replaced.length === 0)
966
- return;
967
- const byName = new Map(this.optionSourceAttributes().map(a => [a.name, a]));
968
- this.lookupReferenceOptions.update(prev => {
969
- const next = { ...prev };
970
- for (const [name, options] of replaced) {
971
- const attr = byName.get(name);
972
- if (!attr?.lookupReferenceType)
973
- continue;
974
- next[attr.lookupReferenceType] = {
975
- ...(next[attr.lookupReferenceType] ?? { name: attr.lookupReferenceType, isTransient: true, displayType: ELookupDisplayType.Dropdown }),
976
- values: (options ?? []).map(o => ({ key: o.key, values: o.label ?? { en: o.key }, isActive: true })),
977
- };
978
- }
979
- return next;
980
- });
981
- this.referenceOptions.update(prev => {
982
- const next = { ...prev };
983
- for (const [name, options] of replaced) {
984
- const attr = byName.get(name);
985
- if (attr?.dataType !== 'Reference')
986
- continue;
987
- // A refresh hook replaced this attribute's options, so synthesise the same row shape the
988
- // query path produces: an id and a display string, which is all an option list ever needed.
989
- next[name] = (options ?? []).map(o => ({
990
- id: o.key,
991
- breadcrumb: o.label ? resolveTranslation(o.label) : o.key,
992
- values: [],
993
- }));
994
- }
995
- return next;
996
- });
997
- }
998
- /** Sends anything still pending, so a typed-but-never-blurred trigger is reflected before save. */
999
- async flushPendingRefresh() {
1000
- await this.refreshCoordinator.flush();
1001
- }
1002
- async onSave() {
1003
- // A value typed and never blurred — the user tabbing straight to Save — must still reshape the
1004
- // object before it goes, or the server enforces rules the user was never shown.
1005
- await this.flushPendingRefresh();
1006
- // Evaluated AFTER the flush: the refresh may be what imposes the rule being checked.
1007
- if (this.clientRuleFailures().length > 0)
1008
- return;
1009
- this.save.emit();
1010
- }
1011
- onCancel() {
1012
- this.cancel.emit();
1013
- }
1014
- // AsDetail object modal methods
1015
- openAsDetailEditor(attr) {
1016
- this.editingAsDetailAttr.set(attr);
1017
- this.editingArrayIndex.set(null);
1018
- this.asDetailFormData.set({ ...(this.formData()[attr.name] || {}) });
1019
- this.showAsDetailModal.set(true);
1020
- }
1021
- saveAsDetailObject() {
1022
- const attr = this.editingAsDetailAttr();
1023
- if (attr) {
1024
- const data = { ...this.formData() };
1025
- if (attr.isArray) {
1026
- const arr = [...(data[attr.name] || [])];
1027
- const idx = this.editingArrayIndex();
1028
- if (idx !== null) {
1029
- arr[idx] = { ...this.asDetailFormData() };
1030
- }
1031
- else {
1032
- arr.push({ ...this.asDetailFormData() });
1033
- }
1034
- data[attr.name] = arr;
1035
- }
1036
- else {
1037
- data[attr.name] = { ...this.asDetailFormData() };
1038
- }
1039
- this.formData.set(data);
1040
- }
1041
- this.closeAsDetailModal();
1042
- }
1043
- closeAsDetailModal() {
1044
- this.showAsDetailModal.set(false);
1045
- this.editingAsDetailAttr.set(null);
1046
- this.editingArrayIndex.set(null);
1047
- this.asDetailFormData.set({});
1048
- }
1049
- // ---- Server-side row lifecycle ------------------------------------------------------------
1050
- //
1051
- // A row type may opt in (EntityType.serverSideRowLifecycle) to having the server construct its
1052
- // new rows and be consulted before one is removed. Everything below is a no-op for a type that
1053
- // has not — which is the point: not opting in costs no request, and the four mutators behave
1054
- // exactly as they did before.
1055
- /**
1056
- * Whether `attr`'s row type asks the server before a row is added or removed.
1057
- *
1058
- * Read off the ROW type, never the parent. `asDetailTypes` already resolves it from the Query-gated
1059
- * catalogue first and the parent's `detailTypes` second, and the flag survives the server-side
1060
- * pruning of the embedded copy, so both sources give the same answer.
1061
- */
1062
- roundTripsRowLifecycle(attr) {
1063
- return this.asDetailTypes()[attr.name]?.serverSideRowLifecycle === true;
1064
- }
1065
- /** The type id to address the row type by on the wire. */
1066
- rowTypeId(attr) {
1067
- return this.asDetailTypes()[attr.name]?.id;
1068
- }
1069
- /**
1070
- * How to name the PARENT's type on the wire.
1071
- *
1072
- * ⚠️ The fallback must be the type **id**, never `clrType`. The server resolves this through
1073
- * `ModelLoader.ResolveEntityType`, which accepts a GUID or a declared alias and nothing else — so
1074
- * a CLR name resolves to null and the request is refused, indistinguishably from an unknown type.
1075
- * The user would see "the server refused this change" on a perfectly ordinary Add.
1076
- *
1077
- * It is a live path, not a theoretical one: `spark-po-create` binds no `parentType` (there is no
1078
- * parent route segment on a create page), so every host that creates an object with an opted-in
1079
- * detail grid lands here.
1080
- */
1081
- parentTypeForWire() {
1082
- return this.parentType() ?? this.entityType()?.id;
1083
- }
1084
- /**
1085
- * Builds the dict a new row starts from: `{}` as before, or the object the server constructed.
1086
- *
1087
- * Returns null when the server refused — the caller must then add nothing, which is the whole
1088
- * difference between a veto and a message.
1089
- */
1090
- async buildNewRow(attr) {
1091
- const typeId = this.rowTypeId(attr);
1092
- if (!this.roundTripsRowLifecycle(attr) || !typeId)
1093
- return {};
1094
- this.clearRowLifecycleError(attr.name);
1095
- try {
1096
- const po = await this.sparkService.newObject(typeId, {
1097
- asDetailAttribute: attr.name,
1098
- parentType: this.parentTypeForWire(),
1099
- // Absent while the parent is unsaved, deliberately: the server hands the hook a null parent
1100
- // rather than trusting the client's copy of one.
1101
- parentId: this.objectId(),
1102
- });
1103
- // nestedPoToDict carries the server-minted row key across as __sparkRowKey, so the row is
1104
- // matchable on save from the moment it appears — the reason the round trip is worth a request.
1105
- return nestedPoToDict(po);
1106
- }
1107
- catch (e) {
1108
- this.captureRowLifecycleError(attr.name, e);
1109
- return null;
1110
- }
1111
- }
1112
- /**
1113
- * Asks whether a stored row may be removed. True when it may — including every case where there
1114
- * is nothing to ask about: a type that has not opted in, a parent that has never been saved, or a
1115
- * row with no key yet because it was added in this same editing session and never persisted.
1116
- */
1117
- async mayRemoveRow(attr, row) {
1118
- const typeId = this.rowTypeId(attr);
1119
- const parentId = this.objectId();
1120
- const parentType = this.parentTypeForWire();
1121
- const rowKey = row?.[AS_DETAIL_ROW_KEY];
1122
- if (!this.roundTripsRowLifecycle(attr) || !typeId || !parentId || !parentType || !rowKey) {
1123
- return true;
1124
- }
1125
- this.clearRowLifecycleError(attr.name);
1126
- try {
1127
- await this.sparkService.deleteRow(typeId, {
1128
- asDetailAttribute: attr.name,
1129
- parentType,
1130
- parentId,
1131
- rowKey,
1132
- });
1133
- return true;
1134
- }
1135
- catch (e) {
1136
- this.captureRowLifecycleError(attr.name, e);
1137
- return false;
1138
- }
1139
- }
1140
- /**
1141
- * Turns a rejected lifecycle call into something the user can read.
1142
- *
1143
- * The envelope puts validation errors under `result`, the same place the save path reads them
1144
- * from — reading `error.error.errors` matched nothing and made every veto silent.
1145
- */
1146
- clearRowLifecycleError(attrName) {
1147
- this.rowLifecycleErrors.update(prev => {
1148
- const next = { ...prev };
1149
- delete next[attrName];
1150
- return next;
1151
- });
1152
- }
1153
- captureRowLifecycleError(attrName, e) {
1154
- const error = e;
1155
- const errors = error?.error?.result?.errors ?? error?.error?.errors;
1156
- const resolved = error?.status === 400 && errors?.length
1157
- ? errors
1158
- : [{
1159
- attributeName: '',
1160
- errorMessage: { en: error?.error?.result?.error || error?.error?.error || error?.message || 'The server refused this change.' },
1161
- ruleType: 'error',
1162
- }];
1163
- this.rowLifecycleErrors.update(prev => ({ ...prev, [attrName]: resolved }));
1164
- }
1165
- // Inline AsDetail methods
1166
- async addInlineRow(attr) {
1167
- const row = await this.buildNewRow(attr);
1168
- if (row === null)
1169
- return;
1170
- const data = { ...this.formData() };
1171
- const arr = [...(data[attr.name] || [])];
1172
- arr.push(row);
1173
- data[attr.name] = arr;
1174
- this.formData.set(data);
1175
- }
1176
- // Array AsDetail methods
1177
- async addArrayItem(attr) {
1178
- const row = await this.buildNewRow(attr);
1179
- if (row === null)
1180
- return;
1181
- this.editingAsDetailAttr.set(attr);
1182
- this.editingArrayIndex.set(null);
1183
- this.asDetailFormData.set(row);
1184
- this.showAsDetailModal.set(true);
1185
- }
1186
- editArrayItem(attr, index) {
1187
- this.editingAsDetailAttr.set(attr);
1188
- this.editingArrayIndex.set(index);
1189
- const arr = this.formData()[attr.name] || [];
1190
- this.asDetailFormData.set({ ...(arr[index] || {}) });
1191
- this.showAsDetailModal.set(true);
1192
- }
1193
- async removeArrayItem(attr, index) {
1194
- const current = this.formData()[attr.name] || [];
1195
- if (!(await this.mayRemoveRow(attr, current[index])))
1196
- return;
1197
- const data = { ...this.formData() };
1198
- const arr = [...current];
1199
- arr.splice(index, 1);
1200
- data[attr.name] = arr;
1201
- this.formData.set(data);
1202
- }
1203
- // Drag-reorder for [Sortable] AsDetail arrays. Order = array position, so moving the
1204
- // row within formData()[attr.name] and re-emitting the signal IS the persisted order
1205
- // (po-edit flags AsDetail attributes isValueChanged on save).
1206
- onAsDetailReorder(attr, event) {
1207
- if (event.previousIndex === event.currentIndex)
1208
- return;
1209
- const data = { ...this.formData() };
1210
- const arr = [...(data[attr.name] ?? [])];
1211
- moveItemInArray(arr, event.previousIndex, event.currentIndex);
1212
- data[attr.name] = arr;
1213
- this.formData.set(data);
1214
- }
1215
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkPoFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1216
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkPoFormComponent, isStandalone: true, selector: "spark-po-form", inputs: { entityType: { classPropertyName: "entityType", publicName: "entityType", isSignal: true, isRequired: false, transformFunction: null }, formData: { classPropertyName: "formData", publicName: "formData", isSignal: true, isRequired: false, transformFunction: null }, validationErrors: { classPropertyName: "validationErrors", publicName: "validationErrors", isSignal: true, isRequired: false, transformFunction: null }, showButtons: { classPropertyName: "showButtons", publicName: "showButtons", isSignal: true, isRequired: false, transformFunction: null }, isSaving: { classPropertyName: "isSaving", publicName: "isSaving", isSignal: true, isRequired: false, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: false, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: false, transformFunction: null }, objectTypeId: { classPropertyName: "objectTypeId", publicName: "objectTypeId", isSignal: true, isRequired: false, transformFunction: null }, objectId: { classPropertyName: "objectId", publicName: "objectId", isSignal: true, isRequired: false, transformFunction: null }, triggerPathPrefix: { classPropertyName: "triggerPathPrefix", publicName: "triggerPathPrefix", isSignal: true, isRequired: false, transformFunction: null }, refreshOverlay: { classPropertyName: "refreshOverlay", publicName: "refreshOverlay", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formData: "formDataChange", save: "save", cancel: "cancel", nestedTriggerRequested: "nestedTriggerRequested", nestedTriggerBlurred: "nestedTriggerBlurred", refreshOverlay: "refreshOverlayChange" }, ngImport: i0, template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n <spark-attribute-description [description]=\"attr.description\" />\n </label>\n <div [md]=\"8\">\n <!-- A lookup wins over the raw data type. `[LookupReference]` does not change dataType \u2014\n only `[Reference]` does \u2014 so a lookup-backed bool still reports 'boolean' here. Tested\n first, this branch swallowed it into a two-state checkbox and the select below was\n unreachable, which made a declared lookup silently do nothing. -->\n @if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n <bs-checkbox\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\">\n </bs-checkbox>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"getLookupOptions(attr)\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onLookupValueChange(attr, $event)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <bs-tree-select\n mode=\"multiple\"\n variant=\"textbox\"\n [showClear]=\"true\"\n [placeholder]=\"'common.search' | t\"\n [provider]=\"getReferenceProvider(attr)\"\n [ngModel]=\"referenceTreeValues()[attr.name] || []\"\n (ngModelChange)=\"onReferenceTreeChange(attr, $event)\"\n [ngModelOptions]=\"{ standalone: true }\">\n </bs-tree-select>\n } @else if (attr.dataType === 'Reference') {\n <spark-reference-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"referenceOptions()[attr.name] || []\"\n [referenceType]=\"attr.referenceType\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onReferenceValueChange(attr, $event)\">\n </spark-reference-picker>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <!-- One source of truth for an inline cell, reused by the row and by the drag\n preview. The preview is a live Angular view (not a cloneNode), so its\n <bs-select> binds to row[col.name] and shows the value while dragging. -->\n <ng-template #inlineDetailCell let-row=\"row\" let-col=\"col\" let-attr=\"attr\" let-rowIndex=\"rowIndex\">\n <td>\n <!-- A row the caller may not edit renders through the same read-only span as a\n read-only column: still fully readable, just not editable. Hiding it would\n hide the data, and the save path would silently restore any edit anyway. -->\n @if (col.isReadOnly || !(attr | canEditDetailRow:asDetailPermissions())) {\n <span class=\"text-muted\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n } @else if (getAsDetailCellEditRenderer(col); as editRenderer) {\n <ng-container *ngComponentOutlet=\"editRenderer; inputs: getAsDetailCellEditRendererInputs(editRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'AsDetail') {\n <!-- Nested AsDetail can't flatten into a row: escalate to the row modal,\n where the recursive spark-po-form renders the nested structure. -->\n <div class=\"d-flex align-items-center gap-1\">\n <span class=\"text-truncate flex-grow-1\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"editArrayItem(attr, rowIndex)\">\n <spark-icon name=\"pencil\" />\n </button>\n </div>\n } @else if (col.dataType === 'boolean' && !col.lookupReferenceType) {\n <!-- Same precedence as the top-level chain above: a lookup-backed column must\n reach its select rather than being swallowed by the checkbox. -->\n <bs-checkbox\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\">\n </bs-checkbox>\n } @else if (col.dataType === 'Reference' && col.query) {\n @if (col.referenceDisplayType === EReferenceDisplayType.Modal) {\n <spark-reference-picker\n [value]=\"row[col.name]\"\n [options]=\"(attr | inlineRefOptions:col:asDetailReferenceOptions())\"\n [referenceType]=\"col.referenceType\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-reference-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.id }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.lookupReferenceType) {\n @if ((col | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"row[col.name]\"\n [options]=\"getLookupOptions(col)\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of getLookupOptions(col); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.dataType === 'MultiLineString') {\n <textarea\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n rows=\"3\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\"></textarea>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\">\n }\n @if (inlineErrorMessage(attr, rowIndex, col); as errorMsg) {\n <div class=\"invalid-feedback d-block\">{{ errorMsg }}</div>\n }\n </td>\n </ng-template>\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index; let rowIndex = $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, rowIndex)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n\n <!-- Floating preview = a LIVE view of the row's cells (not CDK's cloneNode),\n so <bs-select> shows its value instead of blanking. The in-grid\n placeholder still uses CDK's default clone (exact row height) and is\n hidden via .cdk-drag-placeholder { visibility:hidden } in the styles,\n so the drop gap is blank without collapsing/shifting the other rows. -->\n <ng-template cdkDragPreview>\n <table class=\"table mb-0 bg-body shadow-sm spark-drag-preview\">\n <tbody class=\"align-middle\">\n <tr>\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"width: 32px\"><spark-icon name=\"grip-vertical\" /></td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n </tr>\n </tbody>\n </table>\n </ng-template>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else {\n {{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canEditDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n }\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(editComp, attr)\"></ng-container>\n } @else if (attr.dataType === 'MultiLineString') {\n <textarea\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n rows=\"6\"\n [class.is-invalid]=\"hasError(attr.name)\"></textarea>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'common.cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'common.save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [triggerPathPrefix]=\"attr.isArray ? null : attr.name\"\n (nestedTriggerRequested)=\"onEmbeddedTrigger(attr.name, $event)\"\n (nestedTriggerBlurred)=\"onEmbeddedTriggerBlur($event)\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'common.cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'common.save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n", styles: [":host ::ng-deep .cdk-drag-placeholder{visibility:hidden}\n"], dependencies: [{ kind: "component", type: SparkPoFormComponent, selector: "spark-po-form", inputs: ["entityType", "formData", "validationErrors", "showButtons", "isSaving", "parentId", "parentType", "objectTypeId", "objectId", "triggerPathPrefix", "refreshOverlay"], outputs: ["formDataChange", "save", "cancel", "nestedTriggerRequested", "nestedTriggerBlurred", "refreshOverlayChange"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: CdkDragPreview, selector: "ng-template[cdkDragPreview]", inputs: ["data", "matchSize"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["color", "outline"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["color", "navStyle"] }, { kind: "component", type: BsFormComponent, selector: "bs-form", inputs: ["action", "method"], outputs: ["submitted"] }, { kind: "directive", type: BsFormControlDirective, selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "directive", type: BsColFormLabelDirective, selector: "[bsColFormLabel]" }, { kind: "directive", type: BsButtonTypeDirective, selector: "button[color],input[type=\"button\"][color],input[type=\"submit\"][color],a[color]", inputs: ["color"] }, { kind: "component", type: BsInputGroupComponent, selector: "bs-input-group", inputs: ["size"] }, { kind: "component", type: BsSelectComponent, selector: "bs-select", inputs: ["identifier", "size", "multiple", "numberVisible", "disabled"] }, { kind: "directive", type: BsSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: BsTreeSelectComponent, selector: "bs-tree-select", inputs: ["provider", "mode", "variant", "cascadeSelect", "placeholder", "showClear", "panelScrollHeight", "searchDebounceMs", "disabled", "value"], outputs: ["valueChange", "opened", "closed", "cleared"] }, { kind: "component", type: BsModalHostComponent, selector: "bs-modal", inputs: ["isOpen", "closeOnEscape", "scrollable"], outputs: ["isOpenChange"] }, { kind: "directive", type: BsModalDirective, selector: "[bsModal]" }, { kind: "directive", type: BsModalHeaderDirective, selector: "[bsModalHeader]" }, { kind: "directive", type: BsModalBodyDirective, selector: "[bsModalBody]" }, { kind: "directive", type: BsModalFooterDirective, selector: "[bsModalFooter]" }, { kind: "component", type: BsTableComponent, selector: "bs-table", inputs: ["isResponsive", "striped", "hover", "border", "ariaRowCount"] }, { kind: "component", type: BsCheckboxComponent, selector: "bs-checkbox", inputs: ["type", "isToggled", "indeterminate", "name", "value", "group"], outputs: ["isToggledChange", "indeterminateChange"] }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: BsTabControlComponent, selector: "bs-tab-control", inputs: ["border", "selectFirstTab", "tabsPosition"] }, { kind: "component", type: BsTabPageComponent, selector: "bs-tab-page", inputs: ["disabled"] }, { kind: "directive", type: BsTabPageHeaderDirective, selector: "[bsTabPageHeader]" }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "component", type: SparkReferencePickerComponent, selector: "spark-reference-picker", inputs: ["value", "options", "referenceType", "isInvalid", "inputId", "title"], outputs: ["valueChange"] }, { kind: "component", type: SparkLookupPickerComponent, selector: "spark-lookup-picker", inputs: ["value", "options", "isInvalid", "inputId", "title"], outputs: ["valueChange"] }, { kind: "component", type: SparkAttributeDescriptionComponent, selector: "spark-attribute-description", inputs: ["description", "position"] }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: InputTypePipe, name: "inputType" }, { kind: "pipe", type: LookupDisplayTypePipe, name: "lookupDisplayType" }, { kind: "pipe", type: LookupOptionsPipe, name: "lookupOptions" }, { kind: "pipe", type: AsDetailDisplayValuePipe, name: "asDetailDisplayValue" }, { kind: "pipe", type: AsDetailTypePipe, name: "asDetailType" }, { kind: "pipe", type: AsDetailColumnsPipe, name: "asDetailColumns" }, { kind: "pipe", type: AsDetailCellValuePipe, name: "asDetailCellValue" }, { kind: "pipe", type: CanCreateDetailRowPipe, name: "canCreateDetailRow" }, { kind: "pipe", type: CanDeleteDetailRowPipe, name: "canDeleteDetailRow" }, { kind: "pipe", type: CanEditDetailRowPipe, name: "canEditDetailRow" }, { kind: "pipe", type: InlineRefOptionsPipe, name: "inlineRefOptions" }, { kind: "pipe", type: ErrorForAttributePipe, name: "errorForAttribute" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1217
- }
1218
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkPoFormComponent, decorators: [{
1219
- type: Component,
1220
- args: [{ selector: 'spark-po-form', imports: [CommonModule, NgTemplateOutlet, NgComponentOutlet, FormsModule, CdkDropList, CdkDrag, CdkDragHandle, CdkDragPreview, BsCardComponent, BsCardHeaderComponent, BsFormComponent, BsFormControlDirective, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsGridColDirective, BsColFormLabelDirective, BsButtonTypeDirective, BsInputGroupComponent, BsSelectComponent, BsSelectOption, BsTreeSelectComponent, BsModalHostComponent, BsModalDirective, BsModalHeaderDirective, BsModalBodyDirective, BsModalFooterDirective, BsTableComponent, BsCheckboxComponent, BsSpinnerComponent, BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective, SparkIconComponent, SparkPoFormComponent, SparkReferencePickerComponent, SparkLookupPickerComponent, TranslateKeyPipe, ResolveTranslationPipe, InputTypePipe, LookupDisplayTypePipe, LookupOptionsPipe, AsDetailDisplayValuePipe, AsDetailTypePipe, AsDetailColumnsPipe, AsDetailCellValuePipe, CanCreateDetailRowPipe, CanDeleteDetailRowPipe, CanEditDetailRowPipe, InlineRefOptionsPipe, ErrorForAttributePipe, SparkAttributeDescriptionComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n <spark-attribute-description [description]=\"attr.description\" />\n </label>\n <div [md]=\"8\">\n <!-- A lookup wins over the raw data type. `[LookupReference]` does not change dataType \u2014\n only `[Reference]` does \u2014 so a lookup-backed bool still reports 'boolean' here. Tested\n first, this branch swallowed it into a two-state checkbox and the select below was\n unreachable, which made a declared lookup silently do nothing. -->\n @if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n <bs-checkbox\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\">\n </bs-checkbox>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"getLookupOptions(attr)\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onLookupValueChange(attr, $event)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <bs-tree-select\n mode=\"multiple\"\n variant=\"textbox\"\n [showClear]=\"true\"\n [placeholder]=\"'common.search' | t\"\n [provider]=\"getReferenceProvider(attr)\"\n [ngModel]=\"referenceTreeValues()[attr.name] || []\"\n (ngModelChange)=\"onReferenceTreeChange(attr, $event)\"\n [ngModelOptions]=\"{ standalone: true }\">\n </bs-tree-select>\n } @else if (attr.dataType === 'Reference') {\n <spark-reference-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"referenceOptions()[attr.name] || []\"\n [referenceType]=\"attr.referenceType\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onReferenceValueChange(attr, $event)\">\n </spark-reference-picker>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <!-- One source of truth for an inline cell, reused by the row and by the drag\n preview. The preview is a live Angular view (not a cloneNode), so its\n <bs-select> binds to row[col.name] and shows the value while dragging. -->\n <ng-template #inlineDetailCell let-row=\"row\" let-col=\"col\" let-attr=\"attr\" let-rowIndex=\"rowIndex\">\n <td>\n <!-- A row the caller may not edit renders through the same read-only span as a\n read-only column: still fully readable, just not editable. Hiding it would\n hide the data, and the save path would silently restore any edit anyway. -->\n @if (col.isReadOnly || !(attr | canEditDetailRow:asDetailPermissions())) {\n <span class=\"text-muted\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n } @else if (getAsDetailCellEditRenderer(col); as editRenderer) {\n <ng-container *ngComponentOutlet=\"editRenderer; inputs: getAsDetailCellEditRendererInputs(editRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'AsDetail') {\n <!-- Nested AsDetail can't flatten into a row: escalate to the row modal,\n where the recursive spark-po-form renders the nested structure. -->\n <div class=\"d-flex align-items-center gap-1\">\n <span class=\"text-truncate flex-grow-1\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"editArrayItem(attr, rowIndex)\">\n <spark-icon name=\"pencil\" />\n </button>\n </div>\n } @else if (col.dataType === 'boolean' && !col.lookupReferenceType) {\n <!-- Same precedence as the top-level chain above: a lookup-backed column must\n reach its select rather than being swallowed by the checkbox. -->\n <bs-checkbox\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\">\n </bs-checkbox>\n } @else if (col.dataType === 'Reference' && col.query) {\n @if (col.referenceDisplayType === EReferenceDisplayType.Modal) {\n <spark-reference-picker\n [value]=\"row[col.name]\"\n [options]=\"(attr | inlineRefOptions:col:asDetailReferenceOptions())\"\n [referenceType]=\"col.referenceType\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-reference-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.id }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.lookupReferenceType) {\n @if ((col | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"row[col.name]\"\n [options]=\"getLookupOptions(col)\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of getLookupOptions(col); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.dataType === 'MultiLineString') {\n <textarea\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n rows=\"3\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\"></textarea>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\">\n }\n @if (inlineErrorMessage(attr, rowIndex, col); as errorMsg) {\n <div class=\"invalid-feedback d-block\">{{ errorMsg }}</div>\n }\n </td>\n </ng-template>\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index; let rowIndex = $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, rowIndex)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n\n <!-- Floating preview = a LIVE view of the row's cells (not CDK's cloneNode),\n so <bs-select> shows its value instead of blanking. The in-grid\n placeholder still uses CDK's default clone (exact row height) and is\n hidden via .cdk-drag-placeholder { visibility:hidden } in the styles,\n so the drop gap is blank without collapsing/shifting the other rows. -->\n <ng-template cdkDragPreview>\n <table class=\"table mb-0 bg-body shadow-sm spark-drag-preview\">\n <tbody class=\"align-middle\">\n <tr>\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"width: 32px\"><spark-icon name=\"grip-vertical\" /></td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n </tr>\n </tbody>\n </table>\n </ng-template>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else {\n {{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canEditDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n }\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(editComp, attr)\"></ng-container>\n } @else if (attr.dataType === 'MultiLineString') {\n <textarea\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n rows=\"6\"\n [class.is-invalid]=\"hasError(attr.name)\"></textarea>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'common.cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'common.save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [triggerPathPrefix]=\"attr.isArray ? null : attr.name\"\n (nestedTriggerRequested)=\"onEmbeddedTrigger(attr.name, $event)\"\n (nestedTriggerBlurred)=\"onEmbeddedTriggerBlur($event)\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'common.cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'common.save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n", styles: [":host ::ng-deep .cdk-drag-placeholder{visibility:hidden}\n"] }]
1221
- }], ctorParameters: () => [], propDecorators: { entityType: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityType", required: false }] }], formData: [{ type: i0.Input, args: [{ isSignal: true, alias: "formData", required: false }] }, { type: i0.Output, args: ["formDataChange"] }], validationErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationErrors", required: false }] }], showButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showButtons", required: false }] }], isSaving: [{ type: i0.Input, args: [{ isSignal: true, alias: "isSaving", required: false }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: false }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: false }] }], save: [{ type: i0.Output, args: ["save"] }], cancel: [{ type: i0.Output, args: ["cancel"] }], objectTypeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "objectTypeId", required: false }] }], objectId: [{ type: i0.Input, args: [{ isSignal: true, alias: "objectId", required: false }] }], triggerPathPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerPathPrefix", required: false }] }], nestedTriggerRequested: [{ type: i0.Output, args: ["nestedTriggerRequested"] }], nestedTriggerBlurred: [{ type: i0.Output, args: ["nestedTriggerBlurred"] }], refreshOverlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "refreshOverlay", required: false }] }, { type: i0.Output, args: ["refreshOverlayChange"] }] } });
1222
-
1223
- /**
1224
- * Generated bundle index. Do not edit.
1225
- */
1226
-
900
+ var SparkPoFormComponent = class SparkPoFormComponent {
901
+ sparkService = inject(SparkService);
902
+ translations = inject(SparkLanguageService);
903
+ rendererRegistry = inject(SPARK_ATTRIBUTE_RENDERERS);
904
+ entityType = input(null, ...ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []);
905
+ formData = model({}, ...ngDevMode ? [{ debugName: "formData" }] : /* istanbul ignore next */ []);
906
+ validationErrors = input([], ...ngDevMode ? [{ debugName: "validationErrors" }] : /* istanbul ignore next */ []);
907
+ showButtons = input(false, ...ngDevMode ? [{ debugName: "showButtons" }] : /* istanbul ignore next */ []);
908
+ isSaving = input(false, ...ngDevMode ? [{ debugName: "isSaving" }] : /* istanbul ignore next */ []);
909
+ parentId = input(void 0, ...ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []);
910
+ parentType = input(void 0, ...ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []);
911
+ save = output();
912
+ cancel = output();
913
+ objectTypeId = input(void 0, ...ngDevMode ? [{ debugName: "objectTypeId" }] : /* istanbul ignore next */ []);
914
+ objectId = input(void 0, ...ngDevMode ? [{ debugName: "objectId" }] : /* istanbul ignore next */ []);
915
+ triggerPathPrefix = input(null, ...ngDevMode ? [{ debugName: "triggerPathPrefix" }] : /* istanbul ignore next */ []);
916
+ nestedTriggerRequested = output();
917
+ nestedTriggerBlurred = output();
918
+ refreshOverlay = model({}, ...ngDevMode ? [{ debugName: "refreshOverlay" }] : /* istanbul ignore next */ []);
919
+ isRefreshing = signal(false, ...ngDevMode ? [{ debugName: "isRefreshing" }] : /* istanbul ignore next */ []);
920
+ colors = Color;
921
+ referenceOptions = signal({}, ...ngDevMode ? [{ debugName: "referenceOptions" }] : /* istanbul ignore next */ []);
922
+ referenceProviders = signal({}, ...ngDevMode ? [{ debugName: "referenceProviders" }] : /* istanbul ignore next */ []);
923
+ referenceNodes = signal({}, ...ngDevMode ? [{ debugName: "referenceNodes" }] : /* istanbul ignore next */ []);
924
+ asDetailTypes = signal({}, ...ngDevMode ? [{ debugName: "asDetailTypes" }] : /* istanbul ignore next */ []);
925
+ lookupReferenceOptions = signal({}, ...ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : /* istanbul ignore next */ []);
926
+ editingAsDetailAttr = signal(null, ...ngDevMode ? [{ debugName: "editingAsDetailAttr" }] : /* istanbul ignore next */ []);
927
+ asDetailFormData = signal({}, ...ngDevMode ? [{ debugName: "asDetailFormData" }] : /* istanbul ignore next */ []);
928
+ showAsDetailModal = signal(false, ...ngDevMode ? [{ debugName: "showAsDetailModal" }] : /* istanbul ignore next */ []);
929
+ editingArrayIndex = signal(null, ...ngDevMode ? [{ debugName: "editingArrayIndex" }] : /* istanbul ignore next */ []);
930
+ asDetailPermissions = signal({}, ...ngDevMode ? [{ debugName: "asDetailPermissions" }] : /* istanbul ignore next */ []);
931
+ rowLifecycleErrors = signal({}, ...ngDevMode ? [{ debugName: "rowLifecycleErrors" }] : /* istanbul ignore next */ []);
932
+ rowLifecycleErrorsFor(attrName) {
933
+ return this.rowLifecycleErrors()[attrName] ?? [];
934
+ }
935
+ asDetailReferenceOptions = signal({}, ...ngDevMode ? [{ debugName: "asDetailReferenceOptions" }] : /* istanbul ignore next */ []);
936
+ ELookupDisplayType = ELookupDisplayType;
937
+ EReferenceDisplayType = EReferenceDisplayType;
938
+ optionSourceAttributes = computed(() => {
939
+ return this.entityType()?.attributes.filter((a) => hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject)).sort((a, b) => a.order - b.order) || [];
940
+ }, ...ngDevMode ? [{ debugName: "optionSourceAttributes" }] : /* istanbul ignore next */ []);
941
+ editableAttributes = computed(() => {
942
+ const overlay = this.refreshOverlay();
943
+ return this.entityType()?.attributes.map((a) => applyOverlay(a, overlay[a.name])).filter((a) => a.isVisible && !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject)).sort((a, b) => a.order - b.order) || [];
944
+ }, ...ngDevMode ? [{ debugName: "editableAttributes" }] : /* istanbul ignore next */ []);
945
+ static DEFAULT_TAB = {
946
+ id: "__default__",
947
+ name: "Algemeen",
948
+ label: {
949
+ nl: "Algemeen",
950
+ en: "General"
951
+ },
952
+ order: 0
953
+ };
954
+ ungroupedAttributes = computed(() => {
955
+ const attrs = this.editableAttributes();
956
+ const groupIds = new Set((this.entityType()?.groups || []).map((g) => g.id));
957
+ return attrs.filter((a) => !a.group || !groupIds.has(a.group));
958
+ }, ...ngDevMode ? [{ debugName: "ungroupedAttributes" }] : /* istanbul ignore next */ []);
959
+ resolvedTabs = computed(() => {
960
+ const et = this.entityType();
961
+ const definedTabs = et?.tabs?.length ? [...et.tabs].sort((a, b) => a.order - b.order) : [];
962
+ const hasUngroupedAttrs = this.ungroupedAttributes().length > 0;
963
+ const hasUntabbedGroups = (et?.groups || []).some((g) => !g.tab);
964
+ if (hasUngroupedAttrs || hasUntabbedGroups || definedTabs.length === 0) return [SparkPoFormComponent.DEFAULT_TAB, ...definedTabs];
965
+ return definedTabs;
966
+ }, ...ngDevMode ? [{ debugName: "resolvedTabs" }] : /* istanbul ignore next */ []);
967
+ groupsForTab(tab) {
968
+ const groups = this.entityType()?.groups || [];
969
+ if (tab.id === "__default__") return groups.filter((g) => !g.tab).sort((a, b) => a.order - b.order);
970
+ return groups.filter((g) => g.tab === tab.id).sort((a, b) => a.order - b.order);
971
+ }
972
+ attrsForGroup(group) {
973
+ return this.editableAttributes().filter((a) => a.group === group.id);
974
+ }
975
+ constructor() {
976
+ inject(DestroyRef).onDestroy(() => this.refreshCoordinator.dispose());
977
+ effect(() => {
978
+ const et = this.entityType();
979
+ this.parentId();
980
+ this.parentType();
981
+ if (et) {
982
+ this.loadReferenceOptions();
983
+ this.loadAsDetailTypes();
984
+ this.loadLookupReferenceOptions();
985
+ }
986
+ });
987
+ }
988
+ toRecord(entries) {
989
+ const result = {};
990
+ for (const [key, value] of entries) result[key] = value;
991
+ return result;
992
+ }
993
+ async loadReferenceOptions() {
994
+ const refAttrs = this.optionSourceAttributes().filter((a) => a.dataType === "Reference" && a.query);
995
+ if (refAttrs.length === 0) return;
996
+ const entries = await Promise.all(refAttrs.filter((a) => a.query).map(async (attr) => {
997
+ const result = await this.sparkService.executeQueryByName(attr.query, {
998
+ parentId: this.parentId(),
999
+ parentType: this.parentType()
1000
+ });
1001
+ return [attr.name, result.items];
1002
+ }));
1003
+ const optionsByAttr = this.toRecord(entries);
1004
+ this.referenceOptions.set(optionsByAttr);
1005
+ const providers = {};
1006
+ const nodesByAttr = {};
1007
+ for (const attr of refAttrs) {
1008
+ if (!attr.isArray) continue;
1009
+ const nodes = (optionsByAttr[attr.name] || []).filter((po) => !!po.id).map((po) => ({
1010
+ id: po.id,
1011
+ label: po.breadcrumb || po.id
1012
+ }));
1013
+ providers[attr.name] = new InMemoryTreeSelectProvider(nodes);
1014
+ nodesByAttr[attr.name] = this.toRecord(nodes.map((n) => [n.id, n]));
1015
+ }
1016
+ this.referenceProviders.set(providers);
1017
+ this.referenceNodes.set(nodesByAttr);
1018
+ }
1019
+ referenceTreeValues = computed(() => {
1020
+ const fd = this.formData();
1021
+ const nodesByAttr = this.referenceNodes();
1022
+ const result = {};
1023
+ for (const attr of this.editableAttributes()) {
1024
+ if (attr.dataType !== "Reference" || !attr.isArray) continue;
1025
+ const ids = Array.isArray(fd[attr.name]) ? fd[attr.name] : [];
1026
+ const nodeMap = nodesByAttr[attr.name] || {};
1027
+ result[attr.name] = ids.map((id) => nodeMap[id] ?? {
1028
+ id,
1029
+ label: id
1030
+ });
1031
+ }
1032
+ return result;
1033
+ }, ...ngDevMode ? [{ debugName: "referenceTreeValues" }] : /* istanbul ignore next */ []);
1034
+ getReferenceProvider(attr) {
1035
+ return this.referenceProviders()[attr.name];
1036
+ }
1037
+ onReferenceTreeChange(attr, value) {
1038
+ const nodes = Array.isArray(value) ? value : value ? [value] : [];
1039
+ const data = { ...this.formData() };
1040
+ data[attr.name] = nodes.map((n) => n.id);
1041
+ this.formData.set(data);
1042
+ }
1043
+ async loadAsDetailTypes() {
1044
+ const asDetailAttrs = this.optionSourceAttributes().filter((a) => a.dataType === "AsDetail" && a.asDetailType);
1045
+ if (asDetailAttrs.length === 0) return;
1046
+ const types = await this.sparkService.getEntityTypes();
1047
+ const newAsDetailTypes = {};
1048
+ const embedded = this.entityType()?.detailTypes ?? [];
1049
+ for (const attr of asDetailAttrs) {
1050
+ const asDetailType = types.find((t) => t.clrType === attr.asDetailType) ?? embedded.find((t) => t.clrType === attr.asDetailType);
1051
+ if (asDetailType) {
1052
+ newAsDetailTypes[attr.name] = asDetailType;
1053
+ if (attr.isArray) {
1054
+ const perms = await this.sparkService.getPermissions(asDetailType.id);
1055
+ this.asDetailPermissions.update((prev) => ({
1056
+ ...prev,
1057
+ [attr.name]: perms
1058
+ }));
1059
+ const refCols = asDetailType.attributes.filter((a) => a.dataType === "Reference" && a.query);
1060
+ if (refCols.length > 0) {
1061
+ const refEntries = await Promise.all(refCols.filter((c) => c.query).map(async (col) => {
1062
+ const result = await this.sparkService.executeQueryByName(col.query, {
1063
+ parentId: this.parentId(),
1064
+ parentType: this.parentType()
1065
+ });
1066
+ return [col.name, result.items];
1067
+ }));
1068
+ this.asDetailReferenceOptions.update((prev) => ({
1069
+ ...prev,
1070
+ [attr.name]: this.toRecord(refEntries)
1071
+ }));
1072
+ }
1073
+ const lookupCols = asDetailType.attributes.filter((a) => a.lookupReferenceType);
1074
+ for (const col of lookupCols) {
1075
+ const lookupName = col.lookupReferenceType;
1076
+ if (!this.lookupReferenceOptions()[lookupName]) {
1077
+ const ref = await this.sparkService.getLookupReference(lookupName);
1078
+ this.lookupReferenceOptions.update((prev) => ({
1079
+ ...prev,
1080
+ [lookupName]: ref
1081
+ }));
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+ }
1087
+ this.asDetailTypes.set(newAsDetailTypes);
1088
+ }
1089
+ async loadLookupReferenceOptions() {
1090
+ const lookupAttrs = this.optionSourceAttributes().filter((a) => a.lookupReferenceType);
1091
+ if (lookupAttrs.length === 0) return;
1092
+ const lookupNames = [...new Set(lookupAttrs.map((a) => a.lookupReferenceType))];
1093
+ const entries = await Promise.all(lookupNames.map(async (name) => {
1094
+ return [name, await this.sparkService.getLookupReference(name)];
1095
+ }));
1096
+ this.lookupReferenceOptions.set(this.toRecord(entries));
1097
+ }
1098
+ getLookupOptions(attr) {
1099
+ return (attr.lookupReferenceType ? this.lookupReferenceOptions()[attr.lookupReferenceType] : null)?.values.filter((v) => v.isActive) || [];
1100
+ }
1101
+ onReferenceValueChange(attr, id) {
1102
+ const data = { ...this.formData() };
1103
+ data[attr.name] = id;
1104
+ this.formData.set(data);
1105
+ }
1106
+ onLookupValueChange(attr, key) {
1107
+ const data = { ...this.formData() };
1108
+ data[attr.name] = key;
1109
+ this.formData.set(data);
1110
+ }
1111
+ getEditRendererComponent(attr) {
1112
+ if (!attr.renderer) return null;
1113
+ return this.rendererRegistry.find((r) => r.name === attr.renderer)?.editComponent ?? null;
1114
+ }
1115
+ getEditRendererInputs(component, attr) {
1116
+ return withDeclaredInputs(component, {
1117
+ value: this.formData()[attr.name],
1118
+ attribute: attr,
1119
+ options: attr.rendererOptions,
1120
+ valueChange: (newValue) => {
1121
+ const data = { ...this.formData() };
1122
+ data[attr.name] = newValue;
1123
+ this.formData.set(data);
1124
+ }
1125
+ });
1126
+ }
1127
+ getAsDetailCellRendererComponent(col) {
1128
+ if (!col.renderer) return null;
1129
+ return this.rendererRegistry.find((r) => r.name === col.renderer)?.columnComponent ?? null;
1130
+ }
1131
+ getRowRendererInputs(renderer, row) {
1132
+ return rowRendererInputs(renderer, row, this.objectId());
1133
+ }
1134
+ getAsDetailCellRendererInputs(component, row, col) {
1135
+ return withDeclaredInputs(component, {
1136
+ value: row[col.name],
1137
+ attribute: col,
1138
+ options: col.rendererOptions,
1139
+ item: row
1140
+ });
1141
+ }
1142
+ getAsDetailCellEditRenderer(col) {
1143
+ if (!col.renderer) return null;
1144
+ return this.rendererRegistry.find((r) => r.name === col.renderer)?.editComponent ?? null;
1145
+ }
1146
+ getAsDetailCellEditRendererInputs(component, row, col) {
1147
+ return withDeclaredInputs(component, {
1148
+ value: row[col.name],
1149
+ attribute: col,
1150
+ options: col.rendererOptions,
1151
+ item: row,
1152
+ valueChange: (newValue) => {
1153
+ row[col.name] = newValue;
1154
+ this.onFieldChange();
1155
+ }
1156
+ });
1157
+ }
1158
+ clientRuleFailures = computed(() => {
1159
+ const values = this.formData();
1160
+ return this.editableAttributes().flatMap((attr) => evaluateRules(attr, values[attr.name]));
1161
+ }, ...ngDevMode ? [{ debugName: "clientRuleFailures" }] : /* istanbul ignore next */ []);
1162
+ hasError(attrName) {
1163
+ return this.clientRuleFailures().some((f) => f.attributeName === attrName) || this.validationErrors().some((e) => e.attributeName === attrName);
1164
+ }
1165
+ inlineErrorPath(attr, rowIndex, col) {
1166
+ return `${attr.name}[${rowIndex}].${col.name}`;
1167
+ }
1168
+ hasInlineError(attr, rowIndex, col) {
1169
+ const path = this.inlineErrorPath(attr, rowIndex, col);
1170
+ return this.validationErrors().some((e) => e.attributeName === path);
1171
+ }
1172
+ inlineErrorMessage(attr, rowIndex, col) {
1173
+ const path = this.inlineErrorPath(attr, rowIndex, col);
1174
+ const error = this.validationErrors().find((e) => e.attributeName === path);
1175
+ return error ? resolveTranslation(error.errorMessage) : null;
1176
+ }
1177
+ onFieldChange(attr) {
1178
+ this.formData.set({ ...this.formData() });
1179
+ if (attr) this.noteChange(attr);
1180
+ }
1181
+ onInlineCellChange(attr, rowIndex, col) {
1182
+ this.onFieldChange();
1183
+ const dispatch = refreshDispatch(col);
1184
+ if (!dispatch || !this.objectTypeId()) return;
1185
+ const path = this.inlineErrorPath(attr, rowIndex, col);
1186
+ this.pendingNestedTrigger = {
1187
+ kind: "row",
1188
+ attribute: attr.name,
1189
+ rowIndex
1190
+ };
1191
+ this.refreshCoordinator.dispatch(path, dispatch);
1192
+ }
1193
+ pendingNestedTrigger = null;
1194
+ onEmbeddedTrigger(attributeName, event) {
1195
+ if (!this.objectTypeId()) return;
1196
+ this.pendingNestedTrigger = {
1197
+ kind: "object",
1198
+ attribute: attributeName
1199
+ };
1200
+ this.refreshCoordinator.dispatch(event.path, event.dispatch);
1201
+ }
1202
+ onEmbeddedTriggerBlur(path) {
1203
+ if (!this.objectTypeId()) return;
1204
+ this.refreshCoordinator.blur(path);
1205
+ }
1206
+ applyNestedResponse(nested, response) {
1207
+ this.pendingNestedTrigger = null;
1208
+ if (nested.kind === "row") {
1209
+ const rows = this.formData()[nested.attribute];
1210
+ const row = Array.isArray(rows) ? rows[nested.rowIndex] : void 0;
1211
+ if (row) {
1212
+ for (const attribute of response.attributes ?? []) row[attribute.name] = attribute.value ?? null;
1213
+ this.formData.set({ ...this.formData() });
1214
+ }
1215
+ } else {
1216
+ const next = { ...this.asDetailFormData() };
1217
+ for (const attribute of response.attributes ?? []) next[attribute.name] = attribute.value ?? null;
1218
+ this.asDetailFormData.set(next);
1219
+ }
1220
+ const overlay = overlayFromResponse(response);
1221
+ this.asDetailTypes.update((prev) => {
1222
+ const type = prev[nested.attribute];
1223
+ if (!type) return prev;
1224
+ return {
1225
+ ...prev,
1226
+ [nested.attribute]: {
1227
+ ...type,
1228
+ attributes: type.attributes.map((a) => applyOverlay(a, overlay[a.name]))
1229
+ }
1230
+ };
1231
+ });
1232
+ }
1233
+ onInlineCellBlur(attr, rowIndex, col) {
1234
+ if (effectiveTrigger(col) === "none" || !this.objectTypeId()) return;
1235
+ this.refreshCoordinator.blur(this.inlineErrorPath(attr, rowIndex, col));
1236
+ }
1237
+ onFieldBlur(attr) {
1238
+ if (this.triggerPathPrefix()) {
1239
+ if (effectiveTrigger(attr) === "none") return;
1240
+ this.nestedTriggerBlurred.emit(this.nestedTriggerPath(attr));
1241
+ return;
1242
+ }
1243
+ if (!this.canRefresh(attr)) return;
1244
+ this.refreshCoordinator.blur(attr.name);
1245
+ }
1246
+ noteChange(attr) {
1247
+ const dispatch = refreshDispatch(attr);
1248
+ if (this.triggerPathPrefix()) {
1249
+ if (!dispatch) return;
1250
+ this.nestedTriggerRequested.emit({
1251
+ path: this.nestedTriggerPath(attr),
1252
+ dispatch
1253
+ });
1254
+ return;
1255
+ }
1256
+ if (!dispatch || !this.canRefresh(attr)) return;
1257
+ this.refreshCoordinator.dispatch(attr.name, dispatch);
1258
+ }
1259
+ nestedTriggerPath(attr) {
1260
+ return `${this.triggerPathPrefix()}.${attr.name}`;
1261
+ }
1262
+ canRefresh(attr) {
1263
+ return effectiveTrigger(attr) !== "none" && !!this.objectTypeId();
1264
+ }
1265
+ refreshCoordinator = new RefreshCoordinator({
1266
+ send: (triggeredBy) => this.sparkService.refresh(this.objectTypeId(), this.buildRefreshPayload(), triggeredBy),
1267
+ currentValues: () => this.formData(),
1268
+ apply: (response, sent) => {
1269
+ const nested = this.pendingNestedTrigger;
1270
+ if (nested) {
1271
+ this.applyNestedResponse(nested, response);
1272
+ return;
1273
+ }
1274
+ this.refreshOverlay.set(overlayFromResponse(response));
1275
+ this.formData.set(mergeRefreshValues(sent, this.formData(), response));
1276
+ this.applyRefreshedOptions(response);
1277
+ },
1278
+ setBusy: (busy) => this.isRefreshing.set(busy)
1279
+ });
1280
+ buildRefreshPayload() {
1281
+ const et = this.entityType();
1282
+ const values = this.embeddedValues();
1283
+ return {
1284
+ id: this.objectId(),
1285
+ name: et?.name ?? "",
1286
+ objectTypeId: et?.id ?? this.objectTypeId(),
1287
+ attributes: (et?.attributes ?? []).map((a) => ({
1288
+ name: a.name,
1289
+ value: values[a.name] ?? null,
1290
+ isValueChanged: true
1291
+ }))
1292
+ };
1293
+ }
1294
+ embeddedValues() {
1295
+ const nested = this.pendingNestedTrigger;
1296
+ if (nested?.kind !== "object") return this.formData();
1297
+ return {
1298
+ ...this.formData(),
1299
+ [nested.attribute]: this.asDetailFormData()
1300
+ };
1301
+ }
1302
+ applyRefreshedOptions(response) {
1303
+ const replaced = (response.attributes ?? []).map((a) => [a.name, a.options]).filter(([, options]) => options !== void 0 && options !== null);
1304
+ if (replaced.length === 0) return;
1305
+ const byName = new Map(this.optionSourceAttributes().map((a) => [a.name, a]));
1306
+ this.lookupReferenceOptions.update((prev) => {
1307
+ const next = { ...prev };
1308
+ for (const [name, options] of replaced) {
1309
+ const attr = byName.get(name);
1310
+ if (!attr?.lookupReferenceType) continue;
1311
+ next[attr.lookupReferenceType] = {
1312
+ ...next[attr.lookupReferenceType] ?? {
1313
+ name: attr.lookupReferenceType,
1314
+ isTransient: true,
1315
+ displayType: ELookupDisplayType.Dropdown
1316
+ },
1317
+ values: (options ?? []).map((o) => ({
1318
+ key: o.key,
1319
+ values: o.label ?? { en: o.key },
1320
+ isActive: true
1321
+ }))
1322
+ };
1323
+ }
1324
+ return next;
1325
+ });
1326
+ this.referenceOptions.update((prev) => {
1327
+ const next = { ...prev };
1328
+ for (const [name, options] of replaced) {
1329
+ if (byName.get(name)?.dataType !== "Reference") continue;
1330
+ next[name] = (options ?? []).map((o) => ({
1331
+ id: o.key,
1332
+ breadcrumb: o.label ? resolveTranslation(o.label) : o.key,
1333
+ values: []
1334
+ }));
1335
+ }
1336
+ return next;
1337
+ });
1338
+ }
1339
+ async flushPendingRefresh() {
1340
+ await this.refreshCoordinator.flush();
1341
+ }
1342
+ async onSave() {
1343
+ await this.flushPendingRefresh();
1344
+ if (this.clientRuleFailures().length > 0) return;
1345
+ this.save.emit();
1346
+ }
1347
+ onCancel() {
1348
+ this.cancel.emit();
1349
+ }
1350
+ openAsDetailEditor(attr) {
1351
+ this.editingAsDetailAttr.set(attr);
1352
+ this.editingArrayIndex.set(null);
1353
+ this.asDetailFormData.set({ ...this.formData()[attr.name] || {} });
1354
+ this.showAsDetailModal.set(true);
1355
+ }
1356
+ saveAsDetailObject() {
1357
+ const attr = this.editingAsDetailAttr();
1358
+ if (attr) {
1359
+ const data = { ...this.formData() };
1360
+ if (attr.isArray) {
1361
+ const arr = [...data[attr.name] || []];
1362
+ const idx = this.editingArrayIndex();
1363
+ if (idx !== null) arr[idx] = { ...this.asDetailFormData() };
1364
+ else arr.push({ ...this.asDetailFormData() });
1365
+ data[attr.name] = arr;
1366
+ } else data[attr.name] = { ...this.asDetailFormData() };
1367
+ this.formData.set(data);
1368
+ }
1369
+ this.closeAsDetailModal();
1370
+ }
1371
+ closeAsDetailModal() {
1372
+ this.showAsDetailModal.set(false);
1373
+ this.editingAsDetailAttr.set(null);
1374
+ this.editingArrayIndex.set(null);
1375
+ this.asDetailFormData.set({});
1376
+ }
1377
+ roundTripsRowLifecycle(attr) {
1378
+ return this.asDetailTypes()[attr.name]?.serverSideRowLifecycle === true;
1379
+ }
1380
+ rowTypeId(attr) {
1381
+ return this.asDetailTypes()[attr.name]?.id;
1382
+ }
1383
+ parentTypeForWire() {
1384
+ return this.parentType() ?? this.entityType()?.id;
1385
+ }
1386
+ async buildNewRow(attr) {
1387
+ const typeId = this.rowTypeId(attr);
1388
+ if (!this.roundTripsRowLifecycle(attr) || !typeId) return {};
1389
+ this.clearRowLifecycleError(attr.name);
1390
+ try {
1391
+ const po = await this.sparkService.newObject(typeId, {
1392
+ asDetailAttribute: attr.name,
1393
+ parentType: this.parentTypeForWire(),
1394
+ parentId: this.objectId()
1395
+ });
1396
+ return nestedPoToDict(po);
1397
+ } catch (e) {
1398
+ this.captureRowLifecycleError(attr.name, e);
1399
+ return null;
1400
+ }
1401
+ }
1402
+ async mayRemoveRow(attr, row) {
1403
+ const typeId = this.rowTypeId(attr);
1404
+ const parentId = this.objectId();
1405
+ const parentType = this.parentTypeForWire();
1406
+ const rowKey = row?.[AS_DETAIL_ROW_KEY];
1407
+ if (!this.roundTripsRowLifecycle(attr) || !typeId || !parentId || !parentType || !rowKey) return true;
1408
+ this.clearRowLifecycleError(attr.name);
1409
+ try {
1410
+ await this.sparkService.deleteRow(typeId, {
1411
+ asDetailAttribute: attr.name,
1412
+ parentType,
1413
+ parentId,
1414
+ rowKey
1415
+ });
1416
+ return true;
1417
+ } catch (e) {
1418
+ this.captureRowLifecycleError(attr.name, e);
1419
+ return false;
1420
+ }
1421
+ }
1422
+ clearRowLifecycleError(attrName) {
1423
+ this.rowLifecycleErrors.update((prev) => {
1424
+ const next = { ...prev };
1425
+ delete next[attrName];
1426
+ return next;
1427
+ });
1428
+ }
1429
+ captureRowLifecycleError(attrName, e) {
1430
+ const error = e;
1431
+ const errors = error?.error?.result?.errors ?? error?.error?.errors;
1432
+ const resolved = error?.status === 400 && errors?.length ? errors : [{
1433
+ attributeName: "",
1434
+ errorMessage: { en: error?.error?.result?.error || error?.error?.error || error?.message || "The server refused this change." },
1435
+ ruleType: "error"
1436
+ }];
1437
+ this.rowLifecycleErrors.update((prev) => ({
1438
+ ...prev,
1439
+ [attrName]: resolved
1440
+ }));
1441
+ }
1442
+ async addInlineRow(attr) {
1443
+ const row = await this.buildNewRow(attr);
1444
+ if (row === null) return;
1445
+ const data = { ...this.formData() };
1446
+ const arr = [...data[attr.name] || []];
1447
+ arr.push(row);
1448
+ data[attr.name] = arr;
1449
+ this.formData.set(data);
1450
+ }
1451
+ async addArrayItem(attr) {
1452
+ const row = await this.buildNewRow(attr);
1453
+ if (row === null) return;
1454
+ this.editingAsDetailAttr.set(attr);
1455
+ this.editingArrayIndex.set(null);
1456
+ this.asDetailFormData.set(row);
1457
+ this.showAsDetailModal.set(true);
1458
+ }
1459
+ editArrayItem(attr, index) {
1460
+ this.editingAsDetailAttr.set(attr);
1461
+ this.editingArrayIndex.set(index);
1462
+ const arr = this.formData()[attr.name] || [];
1463
+ this.asDetailFormData.set({ ...arr[index] || {} });
1464
+ this.showAsDetailModal.set(true);
1465
+ }
1466
+ async removeArrayItem(attr, index) {
1467
+ const current = this.formData()[attr.name] || [];
1468
+ if (!await this.mayRemoveRow(attr, current[index])) return;
1469
+ const data = { ...this.formData() };
1470
+ const arr = [...current];
1471
+ arr.splice(index, 1);
1472
+ data[attr.name] = arr;
1473
+ this.formData.set(data);
1474
+ }
1475
+ onAsDetailReorder(attr, event) {
1476
+ if (event.previousIndex === event.currentIndex) return;
1477
+ const data = { ...this.formData() };
1478
+ const arr = [...data[attr.name] ?? []];
1479
+ moveItemInArray(arr, event.previousIndex, event.currentIndex);
1480
+ data[attr.name] = arr;
1481
+ this.formData.set(data);
1482
+ }
1483
+ static ɵfac = i0.ɵɵngDeclareFactory({
1484
+ minVersion: "12.0.0",
1485
+ version: "22.2.0",
1486
+ ngImport: i0,
1487
+ type: SparkPoFormComponent,
1488
+ deps: [],
1489
+ target: i0.ɵɵFactoryTarget.Component
1490
+ });
1491
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1492
+ minVersion: "17.0.0",
1493
+ version: "22.2.0",
1494
+ type: SparkPoFormComponent,
1495
+ isStandalone: true,
1496
+ selector: "spark-po-form",
1497
+ inputs: {
1498
+ entityType: {
1499
+ classPropertyName: "entityType",
1500
+ publicName: "entityType",
1501
+ isSignal: true,
1502
+ isRequired: false,
1503
+ transformFunction: null
1504
+ },
1505
+ formData: {
1506
+ classPropertyName: "formData",
1507
+ publicName: "formData",
1508
+ isSignal: true,
1509
+ isRequired: false,
1510
+ transformFunction: null
1511
+ },
1512
+ validationErrors: {
1513
+ classPropertyName: "validationErrors",
1514
+ publicName: "validationErrors",
1515
+ isSignal: true,
1516
+ isRequired: false,
1517
+ transformFunction: null
1518
+ },
1519
+ showButtons: {
1520
+ classPropertyName: "showButtons",
1521
+ publicName: "showButtons",
1522
+ isSignal: true,
1523
+ isRequired: false,
1524
+ transformFunction: null
1525
+ },
1526
+ isSaving: {
1527
+ classPropertyName: "isSaving",
1528
+ publicName: "isSaving",
1529
+ isSignal: true,
1530
+ isRequired: false,
1531
+ transformFunction: null
1532
+ },
1533
+ parentId: {
1534
+ classPropertyName: "parentId",
1535
+ publicName: "parentId",
1536
+ isSignal: true,
1537
+ isRequired: false,
1538
+ transformFunction: null
1539
+ },
1540
+ parentType: {
1541
+ classPropertyName: "parentType",
1542
+ publicName: "parentType",
1543
+ isSignal: true,
1544
+ isRequired: false,
1545
+ transformFunction: null
1546
+ },
1547
+ objectTypeId: {
1548
+ classPropertyName: "objectTypeId",
1549
+ publicName: "objectTypeId",
1550
+ isSignal: true,
1551
+ isRequired: false,
1552
+ transformFunction: null
1553
+ },
1554
+ objectId: {
1555
+ classPropertyName: "objectId",
1556
+ publicName: "objectId",
1557
+ isSignal: true,
1558
+ isRequired: false,
1559
+ transformFunction: null
1560
+ },
1561
+ triggerPathPrefix: {
1562
+ classPropertyName: "triggerPathPrefix",
1563
+ publicName: "triggerPathPrefix",
1564
+ isSignal: true,
1565
+ isRequired: false,
1566
+ transformFunction: null
1567
+ },
1568
+ refreshOverlay: {
1569
+ classPropertyName: "refreshOverlay",
1570
+ publicName: "refreshOverlay",
1571
+ isSignal: true,
1572
+ isRequired: false,
1573
+ transformFunction: null
1574
+ }
1575
+ },
1576
+ outputs: {
1577
+ formData: "formDataChange",
1578
+ save: "save",
1579
+ cancel: "cancel",
1580
+ nestedTriggerRequested: "nestedTriggerRequested",
1581
+ nestedTriggerBlurred: "nestedTriggerBlurred",
1582
+ refreshOverlay: "refreshOverlayChange"
1583
+ },
1584
+ ngImport: i0,
1585
+ template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n <spark-attribute-description [description]=\"attr.description\" />\n </label>\n <div [md]=\"8\">\n <!-- A lookup wins over the raw data type. `[LookupReference]` does not change dataType —\n only `[Reference]` does — so a lookup-backed bool still reports 'boolean' here. Tested\n first, this branch swallowed it into a two-state checkbox and the select below was\n unreachable, which made a declared lookup silently do nothing. -->\n @if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n <bs-checkbox\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\">\n </bs-checkbox>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"getLookupOptions(attr)\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onLookupValueChange(attr, $event)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <bs-tree-select\n mode=\"multiple\"\n variant=\"textbox\"\n [showClear]=\"true\"\n [placeholder]=\"'common.search' | t\"\n [provider]=\"getReferenceProvider(attr)\"\n [ngModel]=\"referenceTreeValues()[attr.name] || []\"\n (ngModelChange)=\"onReferenceTreeChange(attr, $event)\"\n [ngModelOptions]=\"{ standalone: true }\">\n </bs-tree-select>\n } @else if (attr.dataType === 'Reference') {\n <spark-reference-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"referenceOptions()[attr.name] || []\"\n [referenceType]=\"attr.referenceType\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onReferenceValueChange(attr, $event)\">\n </spark-reference-picker>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <!-- One source of truth for an inline cell, reused by the row and by the drag\n preview. The preview is a live Angular view (not a cloneNode), so its\n <bs-select> binds to row[col.name] and shows the value while dragging. -->\n <ng-template #inlineDetailCell let-row=\"row\" let-col=\"col\" let-attr=\"attr\" let-rowIndex=\"rowIndex\" let-rowRenderer=\"rowRenderer\">\n <td>\n <!-- A row the caller may not edit renders through the same read-only span as a\n read-only column: still fully readable, just not editable. Hiding it would\n hide the data, and the save path would silently restore any edit anyway. -->\n @if (col.isReadOnly || !(attr | canEditDetailRow:asDetailPermissions())) {\n <span class=\"text-muted\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n } @else if (getAsDetailCellEditRenderer(col); as editRenderer) {\n <ng-container *ngComponentOutlet=\"editRenderer; inputs: getAsDetailCellEditRendererInputs(editRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'AsDetail') {\n <!-- Nested AsDetail can't flatten into a row: escalate to the row modal,\n where the recursive spark-po-form renders the nested structure. -->\n <div class=\"d-flex align-items-center gap-1\">\n <span class=\"text-truncate flex-grow-1\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"editArrayItem(attr, rowIndex)\">\n <spark-icon name=\"pencil\" />\n </button>\n </div>\n } @else if (col.dataType === 'boolean' && !col.lookupReferenceType) {\n <!-- Same precedence as the top-level chain above: a lookup-backed column must\n reach its select rather than being swallowed by the checkbox. -->\n <bs-checkbox\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\">\n </bs-checkbox>\n } @else if (col.dataType === 'Reference' && col.query) {\n @if (col.referenceDisplayType === EReferenceDisplayType.Modal) {\n <spark-reference-picker\n [value]=\"row[col.name]\"\n [options]=\"(attr | inlineRefOptions:col:asDetailReferenceOptions())\"\n [referenceType]=\"col.referenceType\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-reference-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.id }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.lookupReferenceType) {\n @if ((col | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"row[col.name]\"\n [options]=\"getLookupOptions(col)\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of getLookupOptions(col); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.dataType === 'MultiLineString') {\n <textarea\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n rows=\"3\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\"></textarea>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\">\n }\n @if (inlineErrorMessage(attr, rowIndex, col); as errorMsg) {\n <div class=\"invalid-feedback d-block\">{{ errorMsg }}</div>\n }\n <!-- Passed for the first cell only: a row renderer (rowComponent) draws its\n attributes once per row, under that cell, instead of as columns. -->\n @if (rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row)\"></ng-container>\n </div>\n }\n </td>\n </ng-template>\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n @let inlineRowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index; let rowIndex = $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex, rowRenderer: firstCol ? inlineRowRenderer : null }\"></ng-container>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, rowIndex)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n\n <!-- Floating preview = a LIVE view of the row's cells (not CDK's cloneNode),\n so <bs-select> shows its value instead of blanking. The in-grid\n placeholder still uses CDK's default clone (exact row height) and is\n hidden via .cdk-drag-placeholder { visibility:hidden } in the styles,\n so the drop gap is blank without collapsing/shifting the other rows. -->\n <ng-template cdkDragPreview>\n <table class=\"table mb-0 bg-body shadow-sm spark-drag-preview\">\n <tbody class=\"align-middle\">\n <tr>\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"width: 32px\"><spark-icon name=\"grip-vertical\" /></td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n </tr>\n </tbody>\n </table>\n </ng-template>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @let modalRowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of formData()[attr.name] || []; track $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else {\n {{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}\n }\n @if (firstCol && modalRowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"modalRowRenderer.component; inputs: getRowRendererInputs(modalRowRenderer, row)\"></ng-container>\n </div>\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canEditDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n }\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(editComp, attr)\"></ng-container>\n } @else if (attr.dataType === 'MultiLineString') {\n <textarea\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n rows=\"6\"\n [class.is-invalid]=\"hasError(attr.name)\"></textarea>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'common.cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'common.save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [triggerPathPrefix]=\"attr.isArray ? null : attr.name\"\n (nestedTriggerRequested)=\"onEmbeddedTrigger(attr.name, $event)\"\n (nestedTriggerBlurred)=\"onEmbeddedTriggerBlur($event)\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'common.cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'common.save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n",
1586
+ styles: [":host ::ng-deep .cdk-drag-placeholder{visibility:hidden}\n"],
1587
+ dependencies: [
1588
+ {
1589
+ kind: "component",
1590
+ type: SparkPoFormComponent,
1591
+ selector: "spark-po-form",
1592
+ inputs: [
1593
+ "entityType",
1594
+ "formData",
1595
+ "validationErrors",
1596
+ "showButtons",
1597
+ "isSaving",
1598
+ "parentId",
1599
+ "parentType",
1600
+ "objectTypeId",
1601
+ "objectId",
1602
+ "triggerPathPrefix",
1603
+ "refreshOverlay"
1604
+ ],
1605
+ outputs: [
1606
+ "formDataChange",
1607
+ "save",
1608
+ "cancel",
1609
+ "nestedTriggerRequested",
1610
+ "nestedTriggerBlurred",
1611
+ "refreshOverlayChange"
1612
+ ]
1613
+ },
1614
+ {
1615
+ kind: "ngmodule",
1616
+ type: CommonModule
1617
+ },
1618
+ {
1619
+ kind: "directive",
1620
+ type: i1.NgComponentOutlet,
1621
+ selector: "[ngComponentOutlet]",
1622
+ inputs: [
1623
+ "ngComponentOutlet",
1624
+ "ngComponentOutletInputs",
1625
+ "ngComponentOutletInjector",
1626
+ "ngComponentOutletEnvironmentInjector",
1627
+ "ngComponentOutletContent",
1628
+ "ngComponentOutletNgModule"
1629
+ ],
1630
+ exportAs: ["ngComponentOutlet"]
1631
+ },
1632
+ {
1633
+ kind: "directive",
1634
+ type: i1.NgTemplateOutlet,
1635
+ selector: "[ngTemplateOutlet]",
1636
+ inputs: [
1637
+ "ngTemplateOutletContext",
1638
+ "ngTemplateOutlet",
1639
+ "ngTemplateOutletInjector"
1640
+ ]
1641
+ },
1642
+ {
1643
+ kind: "ngmodule",
1644
+ type: FormsModule
1645
+ },
1646
+ {
1647
+ kind: "directive",
1648
+ type: i1$1.NgSelectOption,
1649
+ selector: "option",
1650
+ inputs: ["ngValue", "value"]
1651
+ },
1652
+ {
1653
+ kind: "directive",
1654
+ type: i1$1.ɵNgSelectMultipleOption,
1655
+ selector: "option",
1656
+ inputs: ["ngValue", "value"]
1657
+ },
1658
+ {
1659
+ kind: "directive",
1660
+ type: i1$1.DefaultValueAccessor,
1661
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
1662
+ },
1663
+ {
1664
+ kind: "directive",
1665
+ type: i1$1.NgControlStatus,
1666
+ selector: "[formControlName],[ngModel],[formControl]"
1667
+ },
1668
+ {
1669
+ kind: "directive",
1670
+ type: i1$1.RequiredValidator,
1671
+ selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",
1672
+ inputs: ["required"]
1673
+ },
1674
+ {
1675
+ kind: "directive",
1676
+ type: i1$1.NgModel,
1677
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
1678
+ inputs: [
1679
+ "name",
1680
+ "disabled",
1681
+ "ngModel",
1682
+ "ngModelOptions"
1683
+ ],
1684
+ outputs: ["ngModelChange"],
1685
+ exportAs: ["ngModel"]
1686
+ },
1687
+ {
1688
+ kind: "directive",
1689
+ type: CdkDropList,
1690
+ selector: "[cdkDropList], cdk-drop-list",
1691
+ inputs: [
1692
+ "cdkDropListConnectedTo",
1693
+ "cdkDropListData",
1694
+ "cdkDropListOrientation",
1695
+ "id",
1696
+ "cdkDropListLockAxis",
1697
+ "cdkDropListDisabled",
1698
+ "cdkDropListSortingDisabled",
1699
+ "cdkDropListEnterPredicate",
1700
+ "cdkDropListSortPredicate",
1701
+ "cdkDropListAutoScrollDisabled",
1702
+ "cdkDropListAutoScrollStep",
1703
+ "cdkDropListElementContainer",
1704
+ "cdkDropListHasAnchor"
1705
+ ],
1706
+ outputs: [
1707
+ "cdkDropListDropped",
1708
+ "cdkDropListEntered",
1709
+ "cdkDropListExited",
1710
+ "cdkDropListSorted"
1711
+ ],
1712
+ exportAs: ["cdkDropList"]
1713
+ },
1714
+ {
1715
+ kind: "directive",
1716
+ type: CdkDrag,
1717
+ selector: "[cdkDrag]",
1718
+ inputs: [
1719
+ "cdkDragData",
1720
+ "cdkDragLockAxis",
1721
+ "cdkDragRootElement",
1722
+ "cdkDragBoundary",
1723
+ "cdkDragStartDelay",
1724
+ "cdkDragFreeDragPosition",
1725
+ "cdkDragDisabled",
1726
+ "cdkDragConstrainPosition",
1727
+ "cdkDragPreviewClass",
1728
+ "cdkDragPreviewContainer",
1729
+ "cdkDragScale"
1730
+ ],
1731
+ outputs: [
1732
+ "cdkDragStarted",
1733
+ "cdkDragReleased",
1734
+ "cdkDragEnded",
1735
+ "cdkDragEntered",
1736
+ "cdkDragExited",
1737
+ "cdkDragDropped",
1738
+ "cdkDragMoved"
1739
+ ],
1740
+ exportAs: ["cdkDrag"]
1741
+ },
1742
+ {
1743
+ kind: "directive",
1744
+ type: CdkDragHandle,
1745
+ selector: "[cdkDragHandle]",
1746
+ inputs: ["cdkDragHandleDisabled"]
1747
+ },
1748
+ {
1749
+ kind: "directive",
1750
+ type: CdkDragPreview,
1751
+ selector: "ng-template[cdkDragPreview]",
1752
+ inputs: ["data", "matchSize"]
1753
+ },
1754
+ {
1755
+ kind: "component",
1756
+ type: BsCardComponent,
1757
+ selector: "bs-card",
1758
+ inputs: ["color", "outline"]
1759
+ },
1760
+ {
1761
+ kind: "component",
1762
+ type: BsCardHeaderComponent,
1763
+ selector: "bs-card-header",
1764
+ inputs: ["color", "navStyle"]
1765
+ },
1766
+ {
1767
+ kind: "component",
1768
+ type: BsFormComponent,
1769
+ selector: "bs-form",
1770
+ inputs: ["action", "method"],
1771
+ outputs: ["submitted"]
1772
+ },
1773
+ {
1774
+ kind: "directive",
1775
+ type: BsFormControlDirective,
1776
+ selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)"
1777
+ },
1778
+ {
1779
+ kind: "component",
1780
+ type: BsGridComponent,
1781
+ selector: "bs-grid",
1782
+ inputs: ["stopFullWidthAt"]
1783
+ },
1784
+ {
1785
+ kind: "directive",
1786
+ type: BsGridRowDirective,
1787
+ selector: "[bsRow]"
1788
+ },
1789
+ {
1790
+ kind: "directive",
1791
+ type: BsGridColumnDirective,
1792
+ selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]",
1793
+ inputs: [
1794
+ "xxs",
1795
+ "xs",
1796
+ "sm",
1797
+ "md",
1798
+ "lg",
1799
+ "xl",
1800
+ "xxl"
1801
+ ]
1802
+ },
1803
+ {
1804
+ kind: "directive",
1805
+ type: BsColFormLabelDirective,
1806
+ selector: "[bsColFormLabel]"
1807
+ },
1808
+ {
1809
+ kind: "directive",
1810
+ type: BsButtonTypeDirective,
1811
+ selector: "button[color],input[type=\"button\"][color],input[type=\"submit\"][color],a[color]",
1812
+ inputs: ["color"]
1813
+ },
1814
+ {
1815
+ kind: "component",
1816
+ type: BsInputGroupComponent,
1817
+ selector: "bs-input-group",
1818
+ inputs: ["size"]
1819
+ },
1820
+ {
1821
+ kind: "component",
1822
+ type: BsSelectComponent,
1823
+ selector: "bs-select",
1824
+ inputs: [
1825
+ "identifier",
1826
+ "size",
1827
+ "multiple",
1828
+ "numberVisible",
1829
+ "disabled"
1830
+ ]
1831
+ },
1832
+ {
1833
+ kind: "directive",
1834
+ type: BsSelectOption,
1835
+ selector: "option",
1836
+ inputs: ["ngValue", "value"]
1837
+ },
1838
+ {
1839
+ kind: "component",
1840
+ type: BsTreeSelectComponent,
1841
+ selector: "bs-tree-select",
1842
+ inputs: [
1843
+ "provider",
1844
+ "mode",
1845
+ "variant",
1846
+ "cascadeSelect",
1847
+ "placeholder",
1848
+ "showClear",
1849
+ "panelScrollHeight",
1850
+ "searchDebounceMs",
1851
+ "disabled",
1852
+ "value"
1853
+ ],
1854
+ outputs: [
1855
+ "valueChange",
1856
+ "opened",
1857
+ "closed",
1858
+ "cleared"
1859
+ ]
1860
+ },
1861
+ {
1862
+ kind: "component",
1863
+ type: BsModalHostComponent,
1864
+ selector: "bs-modal",
1865
+ inputs: [
1866
+ "isOpen",
1867
+ "closeOnEscape",
1868
+ "scrollable"
1869
+ ],
1870
+ outputs: ["isOpenChange"]
1871
+ },
1872
+ {
1873
+ kind: "directive",
1874
+ type: BsModalDirective,
1875
+ selector: "[bsModal]"
1876
+ },
1877
+ {
1878
+ kind: "directive",
1879
+ type: BsModalHeaderDirective,
1880
+ selector: "[bsModalHeader]"
1881
+ },
1882
+ {
1883
+ kind: "directive",
1884
+ type: BsModalBodyDirective,
1885
+ selector: "[bsModalBody]"
1886
+ },
1887
+ {
1888
+ kind: "directive",
1889
+ type: BsModalFooterDirective,
1890
+ selector: "[bsModalFooter]"
1891
+ },
1892
+ {
1893
+ kind: "component",
1894
+ type: BsTableComponent,
1895
+ selector: "bs-table",
1896
+ inputs: [
1897
+ "isResponsive",
1898
+ "striped",
1899
+ "hover",
1900
+ "border",
1901
+ "ariaRowCount"
1902
+ ]
1903
+ },
1904
+ {
1905
+ kind: "component",
1906
+ type: BsCheckboxComponent,
1907
+ selector: "bs-checkbox",
1908
+ inputs: [
1909
+ "type",
1910
+ "isToggled",
1911
+ "indeterminate",
1912
+ "name",
1913
+ "value",
1914
+ "group"
1915
+ ],
1916
+ outputs: ["isToggledChange", "indeterminateChange"]
1917
+ },
1918
+ {
1919
+ kind: "component",
1920
+ type: BsSpinnerComponent,
1921
+ selector: "bs-spinner",
1922
+ inputs: ["type", "color"]
1923
+ },
1924
+ {
1925
+ kind: "component",
1926
+ type: BsTabControlComponent,
1927
+ selector: "bs-tab-control",
1928
+ inputs: [
1929
+ "border",
1930
+ "selectFirstTab",
1931
+ "tabsPosition"
1932
+ ]
1933
+ },
1934
+ {
1935
+ kind: "component",
1936
+ type: BsTabPageComponent,
1937
+ selector: "bs-tab-page",
1938
+ inputs: ["disabled"]
1939
+ },
1940
+ {
1941
+ kind: "directive",
1942
+ type: BsTabPageHeaderDirective,
1943
+ selector: "[bsTabPageHeader]"
1944
+ },
1945
+ {
1946
+ kind: "component",
1947
+ type: SparkIconComponent,
1948
+ selector: "spark-icon",
1949
+ inputs: ["name"]
1950
+ },
1951
+ {
1952
+ kind: "component",
1953
+ type: SparkReferencePickerComponent,
1954
+ selector: "spark-reference-picker",
1955
+ inputs: [
1956
+ "value",
1957
+ "options",
1958
+ "referenceType",
1959
+ "isInvalid",
1960
+ "inputId",
1961
+ "title"
1962
+ ],
1963
+ outputs: ["valueChange"]
1964
+ },
1965
+ {
1966
+ kind: "component",
1967
+ type: SparkLookupPickerComponent,
1968
+ selector: "spark-lookup-picker",
1969
+ inputs: [
1970
+ "value",
1971
+ "options",
1972
+ "isInvalid",
1973
+ "inputId",
1974
+ "title"
1975
+ ],
1976
+ outputs: ["valueChange"]
1977
+ },
1978
+ {
1979
+ kind: "component",
1980
+ type: SparkAttributeDescriptionComponent,
1981
+ selector: "spark-attribute-description",
1982
+ inputs: ["description", "position"]
1983
+ },
1984
+ {
1985
+ kind: "pipe",
1986
+ type: TranslateKeyPipe,
1987
+ name: "t"
1988
+ },
1989
+ {
1990
+ kind: "pipe",
1991
+ type: ResolveTranslationPipe,
1992
+ name: "resolveTranslation"
1993
+ },
1994
+ {
1995
+ kind: "pipe",
1996
+ type: InputTypePipe,
1997
+ name: "inputType"
1998
+ },
1999
+ {
2000
+ kind: "pipe",
2001
+ type: LookupDisplayTypePipe,
2002
+ name: "lookupDisplayType"
2003
+ },
2004
+ {
2005
+ kind: "pipe",
2006
+ type: LookupOptionsPipe,
2007
+ name: "lookupOptions"
2008
+ },
2009
+ {
2010
+ kind: "pipe",
2011
+ type: AsDetailDisplayValuePipe,
2012
+ name: "asDetailDisplayValue"
2013
+ },
2014
+ {
2015
+ kind: "pipe",
2016
+ type: AsDetailTypePipe,
2017
+ name: "asDetailType"
2018
+ },
2019
+ {
2020
+ kind: "pipe",
2021
+ type: AsDetailColumnsPipe,
2022
+ name: "asDetailColumns"
2023
+ },
2024
+ {
2025
+ kind: "pipe",
2026
+ type: AsDetailRowRendererPipe,
2027
+ name: "asDetailRowRenderer"
2028
+ },
2029
+ {
2030
+ kind: "pipe",
2031
+ type: AsDetailCellValuePipe,
2032
+ name: "asDetailCellValue"
2033
+ },
2034
+ {
2035
+ kind: "pipe",
2036
+ type: CanCreateDetailRowPipe,
2037
+ name: "canCreateDetailRow"
2038
+ },
2039
+ {
2040
+ kind: "pipe",
2041
+ type: CanDeleteDetailRowPipe,
2042
+ name: "canDeleteDetailRow"
2043
+ },
2044
+ {
2045
+ kind: "pipe",
2046
+ type: CanEditDetailRowPipe,
2047
+ name: "canEditDetailRow"
2048
+ },
2049
+ {
2050
+ kind: "pipe",
2051
+ type: InlineRefOptionsPipe,
2052
+ name: "inlineRefOptions"
2053
+ },
2054
+ {
2055
+ kind: "pipe",
2056
+ type: ErrorForAttributePipe,
2057
+ name: "errorForAttribute"
2058
+ }
2059
+ ],
2060
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
2061
+ });
2062
+ };
2063
+ i0.ɵɵngDeclareClassMetadata({
2064
+ minVersion: "12.0.0",
2065
+ version: "22.2.0",
2066
+ ngImport: i0,
2067
+ type: SparkPoFormComponent,
2068
+ decorators: [{
2069
+ type: Component,
2070
+ args: [{
2071
+ selector: "spark-po-form",
2072
+ imports: [
2073
+ CommonModule,
2074
+ NgTemplateOutlet,
2075
+ NgComponentOutlet,
2076
+ FormsModule,
2077
+ CdkDropList,
2078
+ CdkDrag,
2079
+ CdkDragHandle,
2080
+ CdkDragPreview,
2081
+ BsCardComponent,
2082
+ BsCardHeaderComponent,
2083
+ BsFormComponent,
2084
+ BsFormControlDirective,
2085
+ BsGridComponent,
2086
+ BsGridRowDirective,
2087
+ BsGridColumnDirective,
2088
+ BsGridColDirective,
2089
+ BsColFormLabelDirective,
2090
+ BsButtonTypeDirective,
2091
+ BsInputGroupComponent,
2092
+ BsSelectComponent,
2093
+ BsSelectOption,
2094
+ BsTreeSelectComponent,
2095
+ BsModalHostComponent,
2096
+ BsModalDirective,
2097
+ BsModalHeaderDirective,
2098
+ BsModalBodyDirective,
2099
+ BsModalFooterDirective,
2100
+ BsTableComponent,
2101
+ BsCheckboxComponent,
2102
+ BsSpinnerComponent,
2103
+ BsTabControlComponent,
2104
+ BsTabPageComponent,
2105
+ BsTabPageHeaderDirective,
2106
+ SparkIconComponent,
2107
+ SparkPoFormComponent,
2108
+ SparkReferencePickerComponent,
2109
+ SparkLookupPickerComponent,
2110
+ TranslateKeyPipe,
2111
+ ResolveTranslationPipe,
2112
+ InputTypePipe,
2113
+ LookupDisplayTypePipe,
2114
+ LookupOptionsPipe,
2115
+ AsDetailDisplayValuePipe,
2116
+ AsDetailTypePipe,
2117
+ AsDetailColumnsPipe,
2118
+ AsDetailRowRendererPipe,
2119
+ AsDetailCellValuePipe,
2120
+ CanCreateDetailRowPipe,
2121
+ CanDeleteDetailRowPipe,
2122
+ CanEditDetailRowPipe,
2123
+ InlineRefOptionsPipe,
2124
+ ErrorForAttributePipe,
2125
+ SparkAttributeDescriptionComponent
2126
+ ],
2127
+ changeDetection: ChangeDetectionStrategy.OnPush,
2128
+ template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n <spark-attribute-description [description]=\"attr.description\" />\n </label>\n <div [md]=\"8\">\n <!-- A lookup wins over the raw data type. `[LookupReference]` does not change dataType —\n only `[Reference]` does — so a lookup-backed bool still reports 'boolean' here. Tested\n first, this branch swallowed it into a two-state checkbox and the select below was\n unreachable, which made a declared lookup silently do nothing. -->\n @if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n <bs-checkbox\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\">\n </bs-checkbox>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"getLookupOptions(attr)\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onLookupValueChange(attr, $event)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <bs-tree-select\n mode=\"multiple\"\n variant=\"textbox\"\n [showClear]=\"true\"\n [placeholder]=\"'common.search' | t\"\n [provider]=\"getReferenceProvider(attr)\"\n [ngModel]=\"referenceTreeValues()[attr.name] || []\"\n (ngModelChange)=\"onReferenceTreeChange(attr, $event)\"\n [ngModelOptions]=\"{ standalone: true }\">\n </bs-tree-select>\n } @else if (attr.dataType === 'Reference') {\n <spark-reference-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"referenceOptions()[attr.name] || []\"\n [referenceType]=\"attr.referenceType\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onReferenceValueChange(attr, $event)\">\n </spark-reference-picker>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <!-- One source of truth for an inline cell, reused by the row and by the drag\n preview. The preview is a live Angular view (not a cloneNode), so its\n <bs-select> binds to row[col.name] and shows the value while dragging. -->\n <ng-template #inlineDetailCell let-row=\"row\" let-col=\"col\" let-attr=\"attr\" let-rowIndex=\"rowIndex\" let-rowRenderer=\"rowRenderer\">\n <td>\n <!-- A row the caller may not edit renders through the same read-only span as a\n read-only column: still fully readable, just not editable. Hiding it would\n hide the data, and the save path would silently restore any edit anyway. -->\n @if (col.isReadOnly || !(attr | canEditDetailRow:asDetailPermissions())) {\n <span class=\"text-muted\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n } @else if (getAsDetailCellEditRenderer(col); as editRenderer) {\n <ng-container *ngComponentOutlet=\"editRenderer; inputs: getAsDetailCellEditRendererInputs(editRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'AsDetail') {\n <!-- Nested AsDetail can't flatten into a row: escalate to the row modal,\n where the recursive spark-po-form renders the nested structure. -->\n <div class=\"d-flex align-items-center gap-1\">\n <span class=\"text-truncate flex-grow-1\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"editArrayItem(attr, rowIndex)\">\n <spark-icon name=\"pencil\" />\n </button>\n </div>\n } @else if (col.dataType === 'boolean' && !col.lookupReferenceType) {\n <!-- Same precedence as the top-level chain above: a lookup-backed column must\n reach its select rather than being swallowed by the checkbox. -->\n <bs-checkbox\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\">\n </bs-checkbox>\n } @else if (col.dataType === 'Reference' && col.query) {\n @if (col.referenceDisplayType === EReferenceDisplayType.Modal) {\n <spark-reference-picker\n [value]=\"row[col.name]\"\n [options]=\"(attr | inlineRefOptions:col:asDetailReferenceOptions())\"\n [referenceType]=\"col.referenceType\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-reference-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.id }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.lookupReferenceType) {\n @if ((col | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"row[col.name]\"\n [options]=\"getLookupOptions(col)\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of getLookupOptions(col); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.dataType === 'MultiLineString') {\n <textarea\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n rows=\"3\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\"></textarea>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\">\n }\n @if (inlineErrorMessage(attr, rowIndex, col); as errorMsg) {\n <div class=\"invalid-feedback d-block\">{{ errorMsg }}</div>\n }\n <!-- Passed for the first cell only: a row renderer (rowComponent) draws its\n attributes once per row, under that cell, instead of as columns. -->\n @if (rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row)\"></ng-container>\n </div>\n }\n </td>\n </ng-template>\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n @let inlineRowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index; let rowIndex = $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex, rowRenderer: firstCol ? inlineRowRenderer : null }\"></ng-container>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, rowIndex)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n\n <!-- Floating preview = a LIVE view of the row's cells (not CDK's cloneNode),\n so <bs-select> shows its value instead of blanking. The in-grid\n placeholder still uses CDK's default clone (exact row height) and is\n hidden via .cdk-drag-placeholder { visibility:hidden } in the styles,\n so the drop gap is blank without collapsing/shifting the other rows. -->\n <ng-template cdkDragPreview>\n <table class=\"table mb-0 bg-body shadow-sm spark-drag-preview\">\n <tbody class=\"align-middle\">\n <tr>\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"width: 32px\"><spark-icon name=\"grip-vertical\" /></td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n </tr>\n </tbody>\n </table>\n </ng-template>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @let modalRowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of formData()[attr.name] || []; track $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else {\n {{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}\n }\n @if (firstCol && modalRowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"modalRowRenderer.component; inputs: getRowRendererInputs(modalRowRenderer, row)\"></ng-container>\n </div>\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canEditDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n }\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(editComp, attr)\"></ng-container>\n } @else if (attr.dataType === 'MultiLineString') {\n <textarea\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n rows=\"6\"\n [class.is-invalid]=\"hasError(attr.name)\"></textarea>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'common.cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'common.save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [triggerPathPrefix]=\"attr.isArray ? null : attr.name\"\n (nestedTriggerRequested)=\"onEmbeddedTrigger(attr.name, $event)\"\n (nestedTriggerBlurred)=\"onEmbeddedTriggerBlur($event)\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'common.cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'common.save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n",
2129
+ styles: [":host ::ng-deep .cdk-drag-placeholder{visibility:hidden}\n"]
2130
+ }]
2131
+ }],
2132
+ ctorParameters: () => [],
2133
+ propDecorators: {
2134
+ entityType: [{
2135
+ type: i0.Input,
2136
+ args: [{
2137
+ isSignal: true,
2138
+ alias: "entityType",
2139
+ required: false
2140
+ }]
2141
+ }],
2142
+ formData: [{
2143
+ type: i0.Input,
2144
+ args: [{
2145
+ isSignal: true,
2146
+ alias: "formData",
2147
+ required: false
2148
+ }]
2149
+ }, {
2150
+ type: i0.Output,
2151
+ args: ["formDataChange"]
2152
+ }],
2153
+ validationErrors: [{
2154
+ type: i0.Input,
2155
+ args: [{
2156
+ isSignal: true,
2157
+ alias: "validationErrors",
2158
+ required: false
2159
+ }]
2160
+ }],
2161
+ showButtons: [{
2162
+ type: i0.Input,
2163
+ args: [{
2164
+ isSignal: true,
2165
+ alias: "showButtons",
2166
+ required: false
2167
+ }]
2168
+ }],
2169
+ isSaving: [{
2170
+ type: i0.Input,
2171
+ args: [{
2172
+ isSignal: true,
2173
+ alias: "isSaving",
2174
+ required: false
2175
+ }]
2176
+ }],
2177
+ parentId: [{
2178
+ type: i0.Input,
2179
+ args: [{
2180
+ isSignal: true,
2181
+ alias: "parentId",
2182
+ required: false
2183
+ }]
2184
+ }],
2185
+ parentType: [{
2186
+ type: i0.Input,
2187
+ args: [{
2188
+ isSignal: true,
2189
+ alias: "parentType",
2190
+ required: false
2191
+ }]
2192
+ }],
2193
+ save: [{
2194
+ type: i0.Output,
2195
+ args: ["save"]
2196
+ }],
2197
+ cancel: [{
2198
+ type: i0.Output,
2199
+ args: ["cancel"]
2200
+ }],
2201
+ objectTypeId: [{
2202
+ type: i0.Input,
2203
+ args: [{
2204
+ isSignal: true,
2205
+ alias: "objectTypeId",
2206
+ required: false
2207
+ }]
2208
+ }],
2209
+ objectId: [{
2210
+ type: i0.Input,
2211
+ args: [{
2212
+ isSignal: true,
2213
+ alias: "objectId",
2214
+ required: false
2215
+ }]
2216
+ }],
2217
+ triggerPathPrefix: [{
2218
+ type: i0.Input,
2219
+ args: [{
2220
+ isSignal: true,
2221
+ alias: "triggerPathPrefix",
2222
+ required: false
2223
+ }]
2224
+ }],
2225
+ nestedTriggerRequested: [{
2226
+ type: i0.Output,
2227
+ args: ["nestedTriggerRequested"]
2228
+ }],
2229
+ nestedTriggerBlurred: [{
2230
+ type: i0.Output,
2231
+ args: ["nestedTriggerBlurred"]
2232
+ }],
2233
+ refreshOverlay: [{
2234
+ type: i0.Input,
2235
+ args: [{
2236
+ isSignal: true,
2237
+ alias: "refreshOverlay",
2238
+ required: false
2239
+ }]
2240
+ }, {
2241
+ type: i0.Output,
2242
+ args: ["refreshOverlayChange"]
2243
+ }]
2244
+ }
2245
+ });
1227
2246
  export { SparkLookupPickerComponent, SparkPoFormComponent, SparkReferencePickerComponent };
1228
- //# sourceMappingURL=mintplayer-ng-spark-po-form.mjs.map
2247
+
2248
+ //# sourceMappingURL=mintplayer-ng-spark-po-form.mjs.map