@mintplayer/ng-spark 22.16.0 → 22.18.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.
@@ -20,7 +20,7 @@ import { BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective } f
20
20
  import { BsTableComponent } from '@mintplayer/ng-bootstrap/table';
21
21
  import { SparkService, SparkLanguageService } from '@mintplayer/ng-spark/services';
22
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 } from '@mintplayer/ng-spark/models';
23
+ import { resolveTranslation, ELookupDisplayType, EReferenceDisplayType, hasShowedOnFlag, ShowedOn, applyOverlay, evaluateRules, overlayFromResponse, mergeRefreshValues, nestedPoToDict, AS_DETAIL_ROW_KEY } from '@mintplayer/ng-spark/models';
24
24
  import { SparkIconComponent } from '@mintplayer/ng-spark/icon';
25
25
  import { SparkAttributeDescriptionComponent } from '@mintplayer/ng-spark/attribute-description';
26
26
  import { SPARK_ATTRIBUTE_RENDERERS, withDeclaredInputs } from '@mintplayer/ng-spark/renderers';
@@ -369,8 +369,17 @@ class SparkPoFormComponent {
369
369
  * `entityType` identity and `SparkService` caches nothing, so re-setting it would re-issue every
370
370
  * reference query and lookup fetch on every refresh; mutating it in place would not re-render at
371
371
  * all.
372
+ *
373
+ * ⚠️ A `model`, not a plain signal, because the page that owns the save has to see it. This form
374
+ * renders from the overlaid attribute list; `spark-po-edit` and `spark-po-create` build the
375
+ * payload from their own copy of the same filter. While the overlay stopped here, those two
376
+ * filtered on the attribute's state *as loaded* — so an attribute a refresh hook revealed was
377
+ * rendered, filled in by the user, and then silently left out of the save. When the same hook also
378
+ * made it required (the ordinary case — a field appears *because* it is now needed) the server
379
+ * refused the save for the very field the user had just filled, and the form became a dead end
380
+ * with no way forward from inside it.
372
381
  */
373
- refreshOverlay = signal({}, /* @ts-ignore */
382
+ refreshOverlay = model({}, /* @ts-ignore */
374
383
  ...(ngDevMode ? [{ debugName: "refreshOverlay" }] : /* istanbul ignore next */ []));
375
384
  isRefreshing = signal(false, /* @ts-ignore */
376
385
  ...(ngDevMode ? [{ debugName: "isRefreshing" }] : /* istanbul ignore next */ []));
@@ -401,6 +410,24 @@ class SparkPoFormComponent {
401
410
  // Permissions for array AsDetail entity types
402
411
  asDetailPermissions = signal({}, /* @ts-ignore */
403
412
  ...(ngDevMode ? [{ debugName: "asDetailPermissions" }] : /* istanbul ignore next */ []));
413
+ /**
414
+ * What the server said when it refused to construct or release a row.
415
+ *
416
+ * A local signal rather than the `validationErrors` input, because this is the one class of error
417
+ * the form raises on its own behalf: `validationErrors` is owned by the host (po-edit / po-create)
418
+ * and arrives from a save. Cleared at the start of each lifecycle call, so a stale refusal never
419
+ * outlives the click that caused it.
420
+ *
421
+ * Keyed by attribute name so a message renders against the grid it came from. A single global
422
+ * alert would have been less code and worse: a form with two detail grids would show one grid's
423
+ * refusal above the other, and the user's next click is on the grid, not the top of the page.
424
+ */
425
+ rowLifecycleErrors = signal({}, /* @ts-ignore */
426
+ ...(ngDevMode ? [{ debugName: "rowLifecycleErrors" }] : /* istanbul ignore next */ []));
427
+ /** The refusal to render under `attrName`'s grid, if any. */
428
+ rowLifecycleErrorsFor(attrName) {
429
+ return this.rowLifecycleErrors()[attrName] ?? [];
430
+ }
404
431
  // Reference options for columns within array AsDetail types (keyed by parent attr name, then column name)
405
432
  asDetailReferenceOptions = signal({}, /* @ts-ignore */
406
433
  ...(ngDevMode ? [{ debugName: "asDetailReferenceOptions" }] : /* istanbul ignore next */ []));
@@ -919,19 +946,141 @@ class SparkPoFormComponent {
919
946
  this.editingArrayIndex.set(null);
920
947
  this.asDetailFormData.set({});
921
948
  }
949
+ // ---- Server-side row lifecycle ------------------------------------------------------------
950
+ //
951
+ // A row type may opt in (EntityType.serverSideRowLifecycle) to having the server construct its
952
+ // new rows and be consulted before one is removed. Everything below is a no-op for a type that
953
+ // has not — which is the point: not opting in costs no request, and the four mutators behave
954
+ // exactly as they did before.
955
+ /**
956
+ * Whether `attr`'s row type asks the server before a row is added or removed.
957
+ *
958
+ * Read off the ROW type, never the parent. `asDetailTypes` already resolves it from the Query-gated
959
+ * catalogue first and the parent's `detailTypes` second, and the flag survives the server-side
960
+ * pruning of the embedded copy, so both sources give the same answer.
961
+ */
962
+ roundTripsRowLifecycle(attr) {
963
+ return this.asDetailTypes()[attr.name]?.serverSideRowLifecycle === true;
964
+ }
965
+ /** The type id to address the row type by on the wire. */
966
+ rowTypeId(attr) {
967
+ return this.asDetailTypes()[attr.name]?.id;
968
+ }
969
+ /**
970
+ * How to name the PARENT's type on the wire.
971
+ *
972
+ * ⚠️ The fallback must be the type **id**, never `clrType`. The server resolves this through
973
+ * `ModelLoader.ResolveEntityType`, which accepts a GUID or a declared alias and nothing else — so
974
+ * a CLR name resolves to null and the request is refused, indistinguishably from an unknown type.
975
+ * The user would see "the server refused this change" on a perfectly ordinary Add.
976
+ *
977
+ * It is a live path, not a theoretical one: `spark-po-create` binds no `parentType` (there is no
978
+ * parent route segment on a create page), so every host that creates an object with an opted-in
979
+ * detail grid lands here.
980
+ */
981
+ parentTypeForWire() {
982
+ return this.parentType() ?? this.entityType()?.id;
983
+ }
984
+ /**
985
+ * Builds the dict a new row starts from: `{}` as before, or the object the server constructed.
986
+ *
987
+ * Returns null when the server refused — the caller must then add nothing, which is the whole
988
+ * difference between a veto and a message.
989
+ */
990
+ async buildNewRow(attr) {
991
+ const typeId = this.rowTypeId(attr);
992
+ if (!this.roundTripsRowLifecycle(attr) || !typeId)
993
+ return {};
994
+ this.clearRowLifecycleError(attr.name);
995
+ try {
996
+ const po = await this.sparkService.newObject(typeId, {
997
+ asDetailAttribute: attr.name,
998
+ parentType: this.parentTypeForWire(),
999
+ // Absent while the parent is unsaved, deliberately: the server hands the hook a null parent
1000
+ // rather than trusting the client's copy of one.
1001
+ parentId: this.objectId(),
1002
+ });
1003
+ // nestedPoToDict carries the server-minted row key across as __sparkRowKey, so the row is
1004
+ // matchable on save from the moment it appears — the reason the round trip is worth a request.
1005
+ return nestedPoToDict(po);
1006
+ }
1007
+ catch (e) {
1008
+ this.captureRowLifecycleError(attr.name, e);
1009
+ return null;
1010
+ }
1011
+ }
1012
+ /**
1013
+ * Asks whether a stored row may be removed. True when it may — including every case where there
1014
+ * is nothing to ask about: a type that has not opted in, a parent that has never been saved, or a
1015
+ * row with no key yet because it was added in this same editing session and never persisted.
1016
+ */
1017
+ async mayRemoveRow(attr, row) {
1018
+ const typeId = this.rowTypeId(attr);
1019
+ const parentId = this.objectId();
1020
+ const parentType = this.parentTypeForWire();
1021
+ const rowKey = row?.[AS_DETAIL_ROW_KEY];
1022
+ if (!this.roundTripsRowLifecycle(attr) || !typeId || !parentId || !parentType || !rowKey) {
1023
+ return true;
1024
+ }
1025
+ this.clearRowLifecycleError(attr.name);
1026
+ try {
1027
+ await this.sparkService.deleteRow(typeId, {
1028
+ asDetailAttribute: attr.name,
1029
+ parentType,
1030
+ parentId,
1031
+ rowKey,
1032
+ });
1033
+ return true;
1034
+ }
1035
+ catch (e) {
1036
+ this.captureRowLifecycleError(attr.name, e);
1037
+ return false;
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Turns a rejected lifecycle call into something the user can read.
1042
+ *
1043
+ * The envelope puts validation errors under `result`, the same place the save path reads them
1044
+ * from — reading `error.error.errors` matched nothing and made every veto silent.
1045
+ */
1046
+ clearRowLifecycleError(attrName) {
1047
+ this.rowLifecycleErrors.update(prev => {
1048
+ const next = { ...prev };
1049
+ delete next[attrName];
1050
+ return next;
1051
+ });
1052
+ }
1053
+ captureRowLifecycleError(attrName, e) {
1054
+ const error = e;
1055
+ const errors = error?.error?.result?.errors ?? error?.error?.errors;
1056
+ const resolved = error?.status === 400 && errors?.length
1057
+ ? errors
1058
+ : [{
1059
+ attributeName: '',
1060
+ errorMessage: { en: error?.error?.result?.error || error?.error?.error || error?.message || 'The server refused this change.' },
1061
+ ruleType: 'error',
1062
+ }];
1063
+ this.rowLifecycleErrors.update(prev => ({ ...prev, [attrName]: resolved }));
1064
+ }
922
1065
  // Inline AsDetail methods
923
- addInlineRow(attr) {
1066
+ async addInlineRow(attr) {
1067
+ const row = await this.buildNewRow(attr);
1068
+ if (row === null)
1069
+ return;
924
1070
  const data = { ...this.formData() };
925
1071
  const arr = [...(data[attr.name] || [])];
926
- arr.push({});
1072
+ arr.push(row);
927
1073
  data[attr.name] = arr;
928
1074
  this.formData.set(data);
929
1075
  }
930
1076
  // Array AsDetail methods
931
- addArrayItem(attr) {
1077
+ async addArrayItem(attr) {
1078
+ const row = await this.buildNewRow(attr);
1079
+ if (row === null)
1080
+ return;
932
1081
  this.editingAsDetailAttr.set(attr);
933
1082
  this.editingArrayIndex.set(null);
934
- this.asDetailFormData.set({});
1083
+ this.asDetailFormData.set(row);
935
1084
  this.showAsDetailModal.set(true);
936
1085
  }
937
1086
  editArrayItem(attr, index) {
@@ -941,9 +1090,12 @@ class SparkPoFormComponent {
941
1090
  this.asDetailFormData.set({ ...(arr[index] || {}) });
942
1091
  this.showAsDetailModal.set(true);
943
1092
  }
944
- removeArrayItem(attr, index) {
1093
+ async removeArrayItem(attr, index) {
1094
+ const current = this.formData()[attr.name] || [];
1095
+ if (!(await this.mayRemoveRow(attr, current[index])))
1096
+ return;
945
1097
  const data = { ...this.formData() };
946
- const arr = [...(data[attr.name] || [])];
1098
+ const arr = [...current];
947
1099
  arr.splice(index, 1);
948
1100
  data[attr.name] = arr;
949
1101
  this.formData.set(data);
@@ -961,12 +1113,12 @@ class SparkPoFormComponent {
961
1113
  this.formData.set(data);
962
1114
  }
963
1115
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkPoFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
964
- 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 } }, outputs: { formData: "formDataChange", save: "save", cancel: "cancel" }, 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)=\"onFieldChange()\"\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)=\"onFieldChange()\"\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 @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 @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 </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"], outputs: ["formDataChange", "save", "cancel"] }, { 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 });
1116
+ 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 }, refreshOverlay: { classPropertyName: "refreshOverlay", publicName: "refreshOverlay", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formData: "formDataChange", save: "save", cancel: "cancel", 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)=\"onFieldChange()\"\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)=\"onFieldChange()\"\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 </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", "refreshOverlay"], outputs: ["formDataChange", "save", "cancel", "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 });
965
1117
  }
966
1118
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkPoFormComponent, decorators: [{
967
1119
  type: Component,
968
- 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)=\"onFieldChange()\"\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)=\"onFieldChange()\"\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 @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 @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 </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"] }]
969
- }], 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 }] }] } });
1120
+ 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)=\"onFieldChange()\"\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)=\"onFieldChange()\"\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 </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"] }]
1121
+ }], 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 }] }], refreshOverlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "refreshOverlay", required: false }] }, { type: i0.Output, args: ["refreshOverlayChange"] }] } });
970
1122
 
971
1123
  /**
972
1124
  * Generated bundle index. Do not edit.