@mintplayer/ng-spark 22.24.0 → 22.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +15 -0
  2. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +5 -3
  3. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-contributions.mjs +721 -0
  5. package/fesm2022/mintplayer-ng-spark-contributions.mjs.map +1 -0
  6. package/fesm2022/mintplayer-ng-spark-grid.mjs +131 -4
  7. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-history.mjs +1 -14
  9. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -1
  10. package/fesm2022/mintplayer-ng-spark-models.mjs +41 -2
  11. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  12. package/fesm2022/mintplayer-ng-spark-moderation.mjs +4 -4
  13. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -1
  14. package/fesm2022/mintplayer-ng-spark-panels.mjs +9 -1
  15. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-pipes.mjs +39 -3
  17. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  18. package/fesm2022/mintplayer-ng-spark-po-create.mjs +16 -12
  19. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  20. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +15 -6
  21. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  22. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +926 -16
  23. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-form.mjs +142 -30
  25. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-query-list.mjs +60 -5
  27. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-renderers.mjs +28 -1
  29. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +2 -0
  31. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-services.mjs +3 -2
  33. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-shell.mjs +31 -13
  35. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark.mjs +4 -1
  37. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  38. package/package.json +7 -2
  39. package/types/mintplayer-ng-spark-client-operations.d.ts +7 -1
  40. package/types/mintplayer-ng-spark-contributions.d.ts +231 -0
  41. package/types/mintplayer-ng-spark-grid.d.ts +35 -3
  42. package/types/mintplayer-ng-spark-history.d.ts +3 -10
  43. package/types/mintplayer-ng-spark-models.d.ts +60 -3
  44. package/types/mintplayer-ng-spark-panels.d.ts +36 -1
  45. package/types/mintplayer-ng-spark-pipes.d.ts +12 -1
  46. package/types/mintplayer-ng-spark-po-create.d.ts +10 -0
  47. package/types/mintplayer-ng-spark-po-detail.d.ts +4 -1
  48. package/types/mintplayer-ng-spark-po-edit.d.ts +305 -1
  49. package/types/mintplayer-ng-spark-po-form.d.ts +84 -15
  50. package/types/mintplayer-ng-spark-query-list.d.ts +26 -1
  51. package/types/mintplayer-ng-spark-renderers.d.ts +44 -2
  52. package/types/mintplayer-ng-spark-services.d.ts +10 -1
  53. package/types/mintplayer-ng-spark-shell.d.ts +24 -9
  54. package/types/mintplayer-ng-spark.d.ts +12 -0
@@ -1,5 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { ChangeDetectionStrategy, Component, computed, effect, inject, input, model, output, signal } from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, model, output, signal } from "@angular/core";
3
3
  import * as i1 from "@angular/common";
4
4
  import { CommonModule, NgComponentOutlet, NgTemplateOutlet } from "@angular/common";
5
5
  import * as i1$1 from "@angular/forms";
@@ -19,11 +19,11 @@ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
19
19
  import { BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective } from "@mintplayer/ng-bootstrap/tab-control";
20
20
  import { BsTableComponent } from "@mintplayer/ng-bootstrap/table";
21
21
  import { SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
22
- import { AsDetailCellValuePipe, AsDetailColumnsPipe, AsDetailDisplayValuePipe, AsDetailTypePipe, CanCreateDetailRowPipe, CanDeleteDetailRowPipe, CanEditDetailRowPipe, ErrorForAttributePipe, InlineRefOptionsPipe, InputTypePipe, LookupDisplayTypePipe, LookupOptionsPipe, ReferenceAttrValuePipe, ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
22
+ import { AsDetailCellValuePipe, AsDetailColumnsPipe, AsDetailDisplayValuePipe, AsDetailRowRendererPipe, AsDetailTypePipe, CanCreateDetailRowPipe, CanDeleteDetailRowPipe, CanEditDetailRowPipe, ErrorForAttributePipe, InlineRefOptionsPipe, InputTypePipe, LookupDisplayTypePipe, LookupOptionsPipe, ReferenceAttrValuePipe, ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
23
23
  import { AS_DETAIL_ROW_KEY, ELookupDisplayType, EReferenceDisplayType, ShowedOn, applyOverlay, evaluateRules, hasShowedOnFlag, mergeRefreshValues, nestedPoToDict, overlayFromResponse, resolveTranslation } 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
- import { SPARK_ATTRIBUTE_RENDERERS, withDeclaredInputs } from "@mintplayer/ng-spark/renderers";
26
+ import { SPARK_ATTRIBUTE_RENDERERS, rowRendererInputs, withDeclaredInputs } from "@mintplayer/ng-spark/renderers";
27
27
  import { BsDatatableColumnDirective, BsDatatableComponent, BsRowTemplateDirective, DatatableSettings } from "@mintplayer/ng-bootstrap/datatable";
28
28
  var SparkReferencePickerComponent = class SparkReferencePickerComponent {
29
29
  sparkService = inject(SparkService);
@@ -796,6 +796,7 @@ var RefreshCoordinator = class {
796
796
  sequence = 0;
797
797
  settled = 0;
798
798
  pending = /* @__PURE__ */ new Set();
799
+ timers = /* @__PURE__ */ new Map();
799
800
  constructor(host) {
800
801
  this.host = host;
801
802
  }
@@ -805,17 +806,44 @@ var RefreshCoordinator = class {
805
806
  markPending(attributeName) {
806
807
  this.pending.add(attributeName);
807
808
  }
809
+ dispatch(attributeName, mode) {
810
+ switch (mode) {
811
+ case "immediate": return this.trigger(attributeName);
812
+ case "debounced": return this.trigger(attributeName, { debounceMs: 300 });
813
+ case "blur":
814
+ this.markPending(attributeName);
815
+ return Promise.resolve();
816
+ }
817
+ }
808
818
  blur(attributeName) {
819
+ this.clearTimer(attributeName);
809
820
  if (!this.pending.delete(attributeName)) return Promise.resolve();
810
821
  return this.trigger(attributeName);
811
822
  }
812
823
  async flush() {
824
+ for (const name of [...this.timers.keys()]) this.clearTimer(name);
813
825
  const names = [...this.pending];
814
826
  this.pending.clear();
815
827
  for (const name of names) await this.trigger(name);
816
828
  await this.queue;
817
829
  }
818
- trigger(attributeName) {
830
+ dispose() {
831
+ for (const name of [...this.timers.keys()]) this.clearTimer(name);
832
+ this.pending.clear();
833
+ }
834
+ trigger(attributeName, options) {
835
+ const debounceMs = options?.debounceMs ?? 0;
836
+ if (debounceMs > 0) {
837
+ this.clearTimer(attributeName);
838
+ this.pending.add(attributeName);
839
+ this.timers.set(attributeName, setTimeout(() => {
840
+ this.timers.delete(attributeName);
841
+ if (this.pending.delete(attributeName)) this.trigger(attributeName);
842
+ }, debounceMs));
843
+ return Promise.resolve();
844
+ }
845
+ this.clearTimer(attributeName);
846
+ this.pending.delete(attributeName);
819
847
  const ticket = ++this.sequence;
820
848
  this.host.setBusy(true);
821
849
  this.queue = this.queue.then(async () => {
@@ -831,8 +859,29 @@ var RefreshCoordinator = class {
831
859
  });
832
860
  return this.queue;
833
861
  }
862
+ clearTimer(attributeName) {
863
+ const timer = this.timers.get(attributeName);
864
+ if (timer === void 0) return;
865
+ clearTimeout(timer);
866
+ this.timers.delete(attributeName);
867
+ }
834
868
  };
835
- function triggersImmediately(attr) {
869
+ function effectiveTrigger(attr) {
870
+ switch (attr?.triggersRefresh) {
871
+ case "Auto": return isDiscreteEditor(attr) ? "change" : "blur";
872
+ case "ValueChanged": return "change";
873
+ case "Blur": return isDiscreteEditor(attr) ? "change" : "blur";
874
+ default: return "none";
875
+ }
876
+ }
877
+ function refreshDispatch(attr) {
878
+ switch (effectiveTrigger(attr)) {
879
+ case "change": return isDiscreteEditor(attr) ? "immediate" : "debounced";
880
+ case "blur": return "blur";
881
+ default: return null;
882
+ }
883
+ }
884
+ function isDiscreteEditor(attr) {
836
885
  if (!attr) return false;
837
886
  if (attr.lookupReferenceType) return true;
838
887
  switch ((attr.dataType ?? "").toLowerCase()) {
@@ -859,6 +908,15 @@ var SparkPoFormComponent = class SparkPoFormComponent {
859
908
  isSaving = input(false, ...ngDevMode ? [{ debugName: "isSaving" }] : /* istanbul ignore next */ []);
860
909
  parentId = input(void 0, ...ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []);
861
910
  parentType = input(void 0, ...ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []);
911
+ referenceParentId = input(void 0, ...ngDevMode ? [{ debugName: "referenceParentId" }] : /* istanbul ignore next */ []);
912
+ referenceParentType = input(void 0, ...ngDevMode ? [{ debugName: "referenceParentType" }] : /* istanbul ignore next */ []);
913
+ optionQueryParent = computed(() => this.referenceParentId() && this.referenceParentType() ? {
914
+ parentId: this.referenceParentId(),
915
+ parentType: this.referenceParentType()
916
+ } : {
917
+ parentId: this.parentId(),
918
+ parentType: this.parentType()
919
+ }, ...ngDevMode ? [{ debugName: "optionQueryParent" }] : /* istanbul ignore next */ []);
862
920
  save = output();
863
921
  cancel = output();
864
922
  objectTypeId = input(void 0, ...ngDevMode ? [{ debugName: "objectTypeId" }] : /* istanbul ignore next */ []);
@@ -924,10 +982,10 @@ var SparkPoFormComponent = class SparkPoFormComponent {
924
982
  return this.editableAttributes().filter((a) => a.group === group.id);
925
983
  }
926
984
  constructor() {
985
+ inject(DestroyRef).onDestroy(() => this.refreshCoordinator.dispose());
927
986
  effect(() => {
928
987
  const et = this.entityType();
929
- this.parentId();
930
- this.parentType();
988
+ this.optionQueryParent();
931
989
  if (et) {
932
990
  this.loadReferenceOptions();
933
991
  this.loadAsDetailTypes();
@@ -944,10 +1002,7 @@ var SparkPoFormComponent = class SparkPoFormComponent {
944
1002
  const refAttrs = this.optionSourceAttributes().filter((a) => a.dataType === "Reference" && a.query);
945
1003
  if (refAttrs.length === 0) return;
946
1004
  const entries = await Promise.all(refAttrs.filter((a) => a.query).map(async (attr) => {
947
- const result = await this.sparkService.executeQueryByName(attr.query, {
948
- parentId: this.parentId(),
949
- parentType: this.parentType()
950
- });
1005
+ const result = await this.sparkService.executeQueryByName(attr.query, this.optionQueryParent());
951
1006
  return [attr.name, result.items];
952
1007
  }));
953
1008
  const optionsByAttr = this.toRecord(entries);
@@ -989,6 +1044,7 @@ var SparkPoFormComponent = class SparkPoFormComponent {
989
1044
  const data = { ...this.formData() };
990
1045
  data[attr.name] = nodes.map((n) => n.id);
991
1046
  this.formData.set(data);
1047
+ this.noteChange(attr);
992
1048
  }
993
1049
  async loadAsDetailTypes() {
994
1050
  const asDetailAttrs = this.optionSourceAttributes().filter((a) => a.dataType === "AsDetail" && a.asDetailType);
@@ -1009,10 +1065,7 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1009
1065
  const refCols = asDetailType.attributes.filter((a) => a.dataType === "Reference" && a.query);
1010
1066
  if (refCols.length > 0) {
1011
1067
  const refEntries = await Promise.all(refCols.filter((c) => c.query).map(async (col) => {
1012
- const result = await this.sparkService.executeQueryByName(col.query, {
1013
- parentId: this.parentId(),
1014
- parentType: this.parentType()
1015
- });
1068
+ const result = await this.sparkService.executeQueryByName(col.query, this.optionQueryParent());
1016
1069
  return [col.name, result.items];
1017
1070
  }));
1018
1071
  this.asDetailReferenceOptions.update((prev) => ({
@@ -1052,11 +1105,13 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1052
1105
  const data = { ...this.formData() };
1053
1106
  data[attr.name] = id;
1054
1107
  this.formData.set(data);
1108
+ this.noteChange(attr);
1055
1109
  }
1056
1110
  onLookupValueChange(attr, key) {
1057
1111
  const data = { ...this.formData() };
1058
1112
  data[attr.name] = key;
1059
1113
  this.formData.set(data);
1114
+ this.noteChange(attr);
1060
1115
  }
1061
1116
  getEditRendererComponent(attr) {
1062
1117
  if (!attr.renderer) return null;
@@ -1078,6 +1133,9 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1078
1133
  if (!col.renderer) return null;
1079
1134
  return this.rendererRegistry.find((r) => r.name === col.renderer)?.columnComponent ?? null;
1080
1135
  }
1136
+ getRowRendererInputs(renderer, row) {
1137
+ return rowRendererInputs(renderer, row, this.objectId());
1138
+ }
1081
1139
  getAsDetailCellRendererInputs(component, row, col) {
1082
1140
  return withDeclaredInputs(component, {
1083
1141
  value: row[col.name],
@@ -1127,15 +1185,15 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1127
1185
  }
1128
1186
  onInlineCellChange(attr, rowIndex, col) {
1129
1187
  this.onFieldChange();
1130
- if (col.triggersRefresh !== true || !this.objectTypeId()) return;
1188
+ const dispatch = refreshDispatch(col);
1189
+ if (!dispatch || !this.objectTypeId()) return;
1131
1190
  const path = this.inlineErrorPath(attr, rowIndex, col);
1132
1191
  this.pendingNestedTrigger = {
1133
1192
  kind: "row",
1134
1193
  attribute: attr.name,
1135
1194
  rowIndex
1136
1195
  };
1137
- if (triggersImmediately(col)) this.refreshCoordinator.trigger(path);
1138
- else this.refreshCoordinator.markPending(path);
1196
+ this.refreshCoordinator.dispatch(path, dispatch);
1139
1197
  }
1140
1198
  pendingNestedTrigger = null;
1141
1199
  onEmbeddedTrigger(attributeName, event) {
@@ -1144,8 +1202,7 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1144
1202
  kind: "object",
1145
1203
  attribute: attributeName
1146
1204
  };
1147
- if (event.immediate) this.refreshCoordinator.trigger(event.path);
1148
- else this.refreshCoordinator.markPending(event.path);
1205
+ this.refreshCoordinator.dispatch(event.path, event.dispatch);
1149
1206
  }
1150
1207
  onEmbeddedTriggerBlur(path) {
1151
1208
  if (!this.objectTypeId()) return;
@@ -1179,12 +1236,12 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1179
1236
  });
1180
1237
  }
1181
1238
  onInlineCellBlur(attr, rowIndex, col) {
1182
- if (col.triggersRefresh !== true || !this.objectTypeId()) return;
1239
+ if (effectiveTrigger(col) === "none" || !this.objectTypeId()) return;
1183
1240
  this.refreshCoordinator.blur(this.inlineErrorPath(attr, rowIndex, col));
1184
1241
  }
1185
1242
  onFieldBlur(attr) {
1186
1243
  if (this.triggerPathPrefix()) {
1187
- if (attr.triggersRefresh !== true) return;
1244
+ if (effectiveTrigger(attr) === "none") return;
1188
1245
  this.nestedTriggerBlurred.emit(this.nestedTriggerPath(attr));
1189
1246
  return;
1190
1247
  }
@@ -1192,23 +1249,23 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1192
1249
  this.refreshCoordinator.blur(attr.name);
1193
1250
  }
1194
1251
  noteChange(attr) {
1252
+ const dispatch = refreshDispatch(attr);
1195
1253
  if (this.triggerPathPrefix()) {
1196
- if (attr.triggersRefresh !== true) return;
1254
+ if (!dispatch) return;
1197
1255
  this.nestedTriggerRequested.emit({
1198
1256
  path: this.nestedTriggerPath(attr),
1199
- immediate: triggersImmediately(attr)
1257
+ dispatch
1200
1258
  });
1201
1259
  return;
1202
1260
  }
1203
- if (!this.canRefresh(attr)) return;
1204
- if (triggersImmediately(attr)) this.refreshCoordinator.trigger(attr.name);
1205
- else this.refreshCoordinator.markPending(attr.name);
1261
+ if (!dispatch || !this.canRefresh(attr)) return;
1262
+ this.refreshCoordinator.dispatch(attr.name, dispatch);
1206
1263
  }
1207
1264
  nestedTriggerPath(attr) {
1208
1265
  return `${this.triggerPathPrefix()}.${attr.name}`;
1209
1266
  }
1210
1267
  canRefresh(attr) {
1211
- return attr.triggersRefresh === true && !!this.objectTypeId();
1268
+ return effectiveTrigger(attr) !== "none" && !!this.objectTypeId();
1212
1269
  }
1213
1270
  refreshCoordinator = new RefreshCoordinator({
1214
1271
  send: (triggeredBy) => this.sparkService.refresh(this.objectTypeId(), this.buildRefreshPayload(), triggeredBy),
@@ -1283,6 +1340,23 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1283
1340
  }
1284
1341
  return next;
1285
1342
  });
1343
+ const multi = replaced.filter(([name]) => byName.get(name)?.dataType === "Reference" && byName.get(name)?.isArray);
1344
+ if (multi.length === 0) return;
1345
+ const providers = { ...this.referenceProviders() };
1346
+ const nodesByAttr = { ...this.referenceNodes() };
1347
+ for (const [name, options] of multi) {
1348
+ const nodes = (options ?? []).map((o) => ({
1349
+ id: o.key,
1350
+ label: o.label ? resolveTranslation(o.label) : o.key
1351
+ }));
1352
+ providers[name] = new InMemoryTreeSelectProvider(nodes);
1353
+ nodesByAttr[name] = {
1354
+ ...nodesByAttr[name] ?? {},
1355
+ ...this.toRecord(nodes.map((n) => [n.id, n]))
1356
+ };
1357
+ }
1358
+ this.referenceProviders.set(providers);
1359
+ this.referenceNodes.set(nodesByAttr);
1286
1360
  }
1287
1361
  async flushPendingRefresh() {
1288
1362
  await this.refreshCoordinator.flush();
@@ -1492,6 +1566,20 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1492
1566
  isRequired: false,
1493
1567
  transformFunction: null
1494
1568
  },
1569
+ referenceParentId: {
1570
+ classPropertyName: "referenceParentId",
1571
+ publicName: "referenceParentId",
1572
+ isSignal: true,
1573
+ isRequired: false,
1574
+ transformFunction: null
1575
+ },
1576
+ referenceParentType: {
1577
+ classPropertyName: "referenceParentType",
1578
+ publicName: "referenceParentType",
1579
+ isSignal: true,
1580
+ isRequired: false,
1581
+ transformFunction: null
1582
+ },
1495
1583
  objectTypeId: {
1496
1584
  classPropertyName: "objectTypeId",
1497
1585
  publicName: "objectTypeId",
@@ -1530,7 +1618,7 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1530
1618
  refreshOverlay: "refreshOverlayChange"
1531
1619
  },
1532
1620
  ngImport: i0,
1533
- template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n <spark-attribute-description [description]=\"attr.description\" />\n </label>\n <div [md]=\"8\">\n <!-- A lookup wins over the raw data type. `[LookupReference]` does not change dataType —\n only `[Reference]` does — so a lookup-backed bool still reports 'boolean' here. Tested\n first, this branch swallowed it into a two-state checkbox and the select below was\n unreachable, which made a declared lookup silently do nothing. -->\n @if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n <bs-checkbox\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\">\n </bs-checkbox>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"getLookupOptions(attr)\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onLookupValueChange(attr, $event)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <bs-tree-select\n mode=\"multiple\"\n variant=\"textbox\"\n [showClear]=\"true\"\n [placeholder]=\"'common.search' | t\"\n [provider]=\"getReferenceProvider(attr)\"\n [ngModel]=\"referenceTreeValues()[attr.name] || []\"\n (ngModelChange)=\"onReferenceTreeChange(attr, $event)\"\n [ngModelOptions]=\"{ standalone: true }\">\n </bs-tree-select>\n } @else if (attr.dataType === 'Reference') {\n <spark-reference-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"referenceOptions()[attr.name] || []\"\n [referenceType]=\"attr.referenceType\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onReferenceValueChange(attr, $event)\">\n </spark-reference-picker>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <!-- One source of truth for an inline cell, reused by the row and by the drag\n preview. The preview is a live Angular view (not a cloneNode), so its\n <bs-select> binds to row[col.name] and shows the value while dragging. -->\n <ng-template #inlineDetailCell let-row=\"row\" let-col=\"col\" let-attr=\"attr\" let-rowIndex=\"rowIndex\">\n <td>\n <!-- A row the caller may not edit renders through the same read-only span as a\n read-only column: still fully readable, just not editable. Hiding it would\n hide the data, and the save path would silently restore any edit anyway. -->\n @if (col.isReadOnly || !(attr | canEditDetailRow:asDetailPermissions())) {\n <span class=\"text-muted\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n } @else if (getAsDetailCellEditRenderer(col); as editRenderer) {\n <ng-container *ngComponentOutlet=\"editRenderer; inputs: getAsDetailCellEditRendererInputs(editRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'AsDetail') {\n <!-- Nested AsDetail can't flatten into a row: escalate to the row modal,\n where the recursive spark-po-form renders the nested structure. -->\n <div class=\"d-flex align-items-center gap-1\">\n <span class=\"text-truncate flex-grow-1\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"editArrayItem(attr, rowIndex)\">\n <spark-icon name=\"pencil\" />\n </button>\n </div>\n } @else if (col.dataType === 'boolean' && !col.lookupReferenceType) {\n <!-- Same precedence as the top-level chain above: a lookup-backed column must\n reach its select rather than being swallowed by the checkbox. -->\n <bs-checkbox\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\">\n </bs-checkbox>\n } @else if (col.dataType === 'Reference' && col.query) {\n @if (col.referenceDisplayType === EReferenceDisplayType.Modal) {\n <spark-reference-picker\n [value]=\"row[col.name]\"\n [options]=\"(attr | inlineRefOptions:col:asDetailReferenceOptions())\"\n [referenceType]=\"col.referenceType\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-reference-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.id }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.lookupReferenceType) {\n @if ((col | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"row[col.name]\"\n [options]=\"getLookupOptions(col)\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of getLookupOptions(col); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.dataType === 'MultiLineString') {\n <textarea\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n rows=\"3\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\"></textarea>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\">\n }\n @if (inlineErrorMessage(attr, rowIndex, col); as errorMsg) {\n <div class=\"invalid-feedback d-block\">{{ errorMsg }}</div>\n }\n </td>\n </ng-template>\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index; let rowIndex = $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, rowIndex)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n\n <!-- Floating preview = a LIVE view of the row's cells (not CDK's cloneNode),\n so <bs-select> shows its value instead of blanking. The in-grid\n placeholder still uses CDK's default clone (exact row height) and is\n hidden via .cdk-drag-placeholder { visibility:hidden } in the styles,\n so the drop gap is blank without collapsing/shifting the other rows. -->\n <ng-template cdkDragPreview>\n <table class=\"table mb-0 bg-body shadow-sm spark-drag-preview\">\n <tbody class=\"align-middle\">\n <tr>\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"width: 32px\"><spark-icon name=\"grip-vertical\" /></td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n </tr>\n </tbody>\n </table>\n </ng-template>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else {\n {{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canEditDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n }\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(editComp, attr)\"></ng-container>\n } @else if (attr.dataType === 'MultiLineString') {\n <textarea\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n rows=\"6\"\n [class.is-invalid]=\"hasError(attr.name)\"></textarea>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'common.cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'common.save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [triggerPathPrefix]=\"attr.isArray ? null : attr.name\"\n (nestedTriggerRequested)=\"onEmbeddedTrigger(attr.name, $event)\"\n (nestedTriggerBlurred)=\"onEmbeddedTriggerBlur($event)\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'common.cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'common.save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n",
1621
+ template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n <spark-attribute-description [description]=\"attr.description\" />\n </label>\n <div [md]=\"8\">\n <!-- A lookup wins over the raw data type. `[LookupReference]` does not change dataType —\n only `[Reference]` does — so a lookup-backed bool still reports 'boolean' here. Tested\n first, this branch swallowed it into a two-state checkbox and the select below was\n unreachable, which made a declared lookup silently do nothing. -->\n @if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n <bs-checkbox\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\">\n </bs-checkbox>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"getLookupOptions(attr)\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onLookupValueChange(attr, $event)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <bs-tree-select\n mode=\"multiple\"\n variant=\"textbox\"\n [showClear]=\"true\"\n [placeholder]=\"'common.search' | t\"\n [provider]=\"getReferenceProvider(attr)\"\n [ngModel]=\"referenceTreeValues()[attr.name] || []\"\n (ngModelChange)=\"onReferenceTreeChange(attr, $event)\"\n [ngModelOptions]=\"{ standalone: true }\">\n </bs-tree-select>\n } @else if (attr.dataType === 'Reference') {\n <spark-reference-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"referenceOptions()[attr.name] || []\"\n [referenceType]=\"attr.referenceType\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onReferenceValueChange(attr, $event)\">\n </spark-reference-picker>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <!-- One source of truth for an inline cell, reused by the row and by the drag\n preview. The preview is a live Angular view (not a cloneNode), so its\n <bs-select> binds to row[col.name] and shows the value while dragging. -->\n <ng-template #inlineDetailCell let-row=\"row\" let-col=\"col\" let-attr=\"attr\" let-rowIndex=\"rowIndex\" let-rowRenderer=\"rowRenderer\">\n <td>\n <!-- A row the caller may not edit renders through the same read-only span as a\n read-only column: still fully readable, just not editable. Hiding it would\n hide the data, and the save path would silently restore any edit anyway. -->\n @if (col.isReadOnly || !(attr | canEditDetailRow:asDetailPermissions())) {\n <span class=\"text-muted\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n } @else if (getAsDetailCellEditRenderer(col); as editRenderer) {\n <ng-container *ngComponentOutlet=\"editRenderer; inputs: getAsDetailCellEditRendererInputs(editRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'AsDetail') {\n <!-- Nested AsDetail can't flatten into a row: escalate to the row modal,\n where the recursive spark-po-form renders the nested structure. -->\n <div class=\"d-flex align-items-center gap-1\">\n <span class=\"text-truncate flex-grow-1\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"editArrayItem(attr, rowIndex)\">\n <spark-icon name=\"pencil\" />\n </button>\n </div>\n } @else if (col.dataType === 'boolean' && !col.lookupReferenceType) {\n <!-- Same precedence as the top-level chain above: a lookup-backed column must\n reach its select rather than being swallowed by the checkbox. -->\n <bs-checkbox\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\">\n </bs-checkbox>\n } @else if (col.dataType === 'Reference' && col.query) {\n @if (col.referenceDisplayType === EReferenceDisplayType.Modal) {\n <spark-reference-picker\n [value]=\"row[col.name]\"\n [options]=\"(attr | inlineRefOptions:col:asDetailReferenceOptions())\"\n [referenceType]=\"col.referenceType\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-reference-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.id }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.lookupReferenceType) {\n @if ((col | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"row[col.name]\"\n [options]=\"getLookupOptions(col)\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of getLookupOptions(col); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.dataType === 'MultiLineString') {\n <textarea\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n rows=\"3\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\"></textarea>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\">\n }\n @if (inlineErrorMessage(attr, rowIndex, col); as errorMsg) {\n <div class=\"invalid-feedback d-block\">{{ errorMsg }}</div>\n }\n <!-- Passed for the first cell only: a row renderer (rowComponent) draws its\n attributes once per row, under that cell, instead of as columns. -->\n @if (rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row)\"></ng-container>\n </div>\n }\n </td>\n </ng-template>\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n @let inlineRowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index; let rowIndex = $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex, rowRenderer: firstCol ? inlineRowRenderer : null }\"></ng-container>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, rowIndex)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n\n <!-- Floating preview = a LIVE view of the row's cells (not CDK's cloneNode),\n so <bs-select> shows its value instead of blanking. The in-grid\n placeholder still uses CDK's default clone (exact row height) and is\n hidden via .cdk-drag-placeholder { visibility:hidden } in the styles,\n so the drop gap is blank without collapsing/shifting the other rows. -->\n <ng-template cdkDragPreview>\n <table class=\"table mb-0 bg-body shadow-sm spark-drag-preview\">\n <tbody class=\"align-middle\">\n <tr>\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"width: 32px\"><spark-icon name=\"grip-vertical\" /></td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n </tr>\n </tbody>\n </table>\n </ng-template>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @let modalRowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of formData()[attr.name] || []; track $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else {\n {{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}\n }\n @if (firstCol && modalRowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"modalRowRenderer.component; inputs: getRowRendererInputs(modalRowRenderer, row)\"></ng-container>\n </div>\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canEditDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n }\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(editComp, attr)\"></ng-container>\n } @else if (attr.dataType === 'MultiLineString') {\n <textarea\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n rows=\"6\"\n [class.is-invalid]=\"hasError(attr.name)\"></textarea>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'common.cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'common.save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [referenceParentId]=\"referenceParentId()\"\n [referenceParentType]=\"referenceParentType()\"\n [triggerPathPrefix]=\"attr.isArray ? null : attr.name\"\n (nestedTriggerRequested)=\"onEmbeddedTrigger(attr.name, $event)\"\n (nestedTriggerBlurred)=\"onEmbeddedTriggerBlur($event)\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'common.cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'common.save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n",
1534
1622
  styles: [":host ::ng-deep .cdk-drag-placeholder{visibility:hidden}\n"],
1535
1623
  dependencies: [
1536
1624
  {
@@ -1545,6 +1633,8 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1545
1633
  "isSaving",
1546
1634
  "parentId",
1547
1635
  "parentType",
1636
+ "referenceParentId",
1637
+ "referenceParentType",
1548
1638
  "objectTypeId",
1549
1639
  "objectId",
1550
1640
  "triggerPathPrefix",
@@ -1969,6 +2059,11 @@ var SparkPoFormComponent = class SparkPoFormComponent {
1969
2059
  type: AsDetailColumnsPipe,
1970
2060
  name: "asDetailColumns"
1971
2061
  },
2062
+ {
2063
+ kind: "pipe",
2064
+ type: AsDetailRowRendererPipe,
2065
+ name: "asDetailRowRenderer"
2066
+ },
1972
2067
  {
1973
2068
  kind: "pipe",
1974
2069
  type: AsDetailCellValuePipe,
@@ -2058,6 +2153,7 @@ i0.ɵɵngDeclareClassMetadata({
2058
2153
  AsDetailDisplayValuePipe,
2059
2154
  AsDetailTypePipe,
2060
2155
  AsDetailColumnsPipe,
2156
+ AsDetailRowRendererPipe,
2061
2157
  AsDetailCellValuePipe,
2062
2158
  CanCreateDetailRowPipe,
2063
2159
  CanDeleteDetailRowPipe,
@@ -2067,7 +2163,7 @@ i0.ɵɵngDeclareClassMetadata({
2067
2163
  SparkAttributeDescriptionComponent
2068
2164
  ],
2069
2165
  changeDetection: ChangeDetectionStrategy.OnPush,
2070
- template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n <spark-attribute-description [description]=\"attr.description\" />\n </label>\n <div [md]=\"8\">\n <!-- A lookup wins over the raw data type. `[LookupReference]` does not change dataType —\n only `[Reference]` does — so a lookup-backed bool still reports 'boolean' here. Tested\n first, this branch swallowed it into a two-state checkbox and the select below was\n unreachable, which made a declared lookup silently do nothing. -->\n @if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n <bs-checkbox\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\">\n </bs-checkbox>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"getLookupOptions(attr)\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onLookupValueChange(attr, $event)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <bs-tree-select\n mode=\"multiple\"\n variant=\"textbox\"\n [showClear]=\"true\"\n [placeholder]=\"'common.search' | t\"\n [provider]=\"getReferenceProvider(attr)\"\n [ngModel]=\"referenceTreeValues()[attr.name] || []\"\n (ngModelChange)=\"onReferenceTreeChange(attr, $event)\"\n [ngModelOptions]=\"{ standalone: true }\">\n </bs-tree-select>\n } @else if (attr.dataType === 'Reference') {\n <spark-reference-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"referenceOptions()[attr.name] || []\"\n [referenceType]=\"attr.referenceType\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onReferenceValueChange(attr, $event)\">\n </spark-reference-picker>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <!-- One source of truth for an inline cell, reused by the row and by the drag\n preview. The preview is a live Angular view (not a cloneNode), so its\n <bs-select> binds to row[col.name] and shows the value while dragging. -->\n <ng-template #inlineDetailCell let-row=\"row\" let-col=\"col\" let-attr=\"attr\" let-rowIndex=\"rowIndex\">\n <td>\n <!-- A row the caller may not edit renders through the same read-only span as a\n read-only column: still fully readable, just not editable. Hiding it would\n hide the data, and the save path would silently restore any edit anyway. -->\n @if (col.isReadOnly || !(attr | canEditDetailRow:asDetailPermissions())) {\n <span class=\"text-muted\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n } @else if (getAsDetailCellEditRenderer(col); as editRenderer) {\n <ng-container *ngComponentOutlet=\"editRenderer; inputs: getAsDetailCellEditRendererInputs(editRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'AsDetail') {\n <!-- Nested AsDetail can't flatten into a row: escalate to the row modal,\n where the recursive spark-po-form renders the nested structure. -->\n <div class=\"d-flex align-items-center gap-1\">\n <span class=\"text-truncate flex-grow-1\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"editArrayItem(attr, rowIndex)\">\n <spark-icon name=\"pencil\" />\n </button>\n </div>\n } @else if (col.dataType === 'boolean' && !col.lookupReferenceType) {\n <!-- Same precedence as the top-level chain above: a lookup-backed column must\n reach its select rather than being swallowed by the checkbox. -->\n <bs-checkbox\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\">\n </bs-checkbox>\n } @else if (col.dataType === 'Reference' && col.query) {\n @if (col.referenceDisplayType === EReferenceDisplayType.Modal) {\n <spark-reference-picker\n [value]=\"row[col.name]\"\n [options]=\"(attr | inlineRefOptions:col:asDetailReferenceOptions())\"\n [referenceType]=\"col.referenceType\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-reference-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.id }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.lookupReferenceType) {\n @if ((col | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"row[col.name]\"\n [options]=\"getLookupOptions(col)\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of getLookupOptions(col); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.dataType === 'MultiLineString') {\n <textarea\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n rows=\"3\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\"></textarea>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\">\n }\n @if (inlineErrorMessage(attr, rowIndex, col); as errorMsg) {\n <div class=\"invalid-feedback d-block\">{{ errorMsg }}</div>\n }\n </td>\n </ng-template>\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index; let rowIndex = $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, rowIndex)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n\n <!-- Floating preview = a LIVE view of the row's cells (not CDK's cloneNode),\n so <bs-select> shows its value instead of blanking. The in-grid\n placeholder still uses CDK's default clone (exact row height) and is\n hidden via .cdk-drag-placeholder { visibility:hidden } in the styles,\n so the drop gap is blank without collapsing/shifting the other rows. -->\n <ng-template cdkDragPreview>\n <table class=\"table mb-0 bg-body shadow-sm spark-drag-preview\">\n <tbody class=\"align-middle\">\n <tr>\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"width: 32px\"><spark-icon name=\"grip-vertical\" /></td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n </tr>\n </tbody>\n </table>\n </ng-template>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else {\n {{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canEditDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n }\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(editComp, attr)\"></ng-container>\n } @else if (attr.dataType === 'MultiLineString') {\n <textarea\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n rows=\"6\"\n [class.is-invalid]=\"hasError(attr.name)\"></textarea>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'common.cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'common.save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [triggerPathPrefix]=\"attr.isArray ? null : attr.name\"\n (nestedTriggerRequested)=\"onEmbeddedTrigger(attr.name, $event)\"\n (nestedTriggerBlurred)=\"onEmbeddedTriggerBlur($event)\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'common.cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'common.save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n",
2166
+ template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n <spark-attribute-description [description]=\"attr.description\" />\n </label>\n <div [md]=\"8\">\n <!-- A lookup wins over the raw data type. `[LookupReference]` does not change dataType —\n only `[Reference]` does — so a lookup-backed bool still reports 'boolean' here. Tested\n first, this branch swallowed it into a two-state checkbox and the select below was\n unreachable, which made a declared lookup silently do nothing. -->\n @if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n <bs-checkbox\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\">\n </bs-checkbox>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"getLookupOptions(attr)\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onLookupValueChange(attr, $event)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <bs-tree-select\n mode=\"multiple\"\n variant=\"textbox\"\n [showClear]=\"true\"\n [placeholder]=\"'common.search' | t\"\n [provider]=\"getReferenceProvider(attr)\"\n [ngModel]=\"referenceTreeValues()[attr.name] || []\"\n (ngModelChange)=\"onReferenceTreeChange(attr, $event)\"\n [ngModelOptions]=\"{ standalone: true }\">\n </bs-tree-select>\n } @else if (attr.dataType === 'Reference') {\n <spark-reference-picker\n [value]=\"formData()[attr.name]\"\n [options]=\"referenceOptions()[attr.name] || []\"\n [referenceType]=\"attr.referenceType\"\n [inputId]=\"attr.name\"\n [title]=\"attr.label | resolveTranslation:attr.name\"\n [isInvalid]=\"hasError(attr.name)\"\n (valueChange)=\"onReferenceValueChange(attr, $event)\">\n </spark-reference-picker>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <!-- One source of truth for an inline cell, reused by the row and by the drag\n preview. The preview is a live Angular view (not a cloneNode), so its\n <bs-select> binds to row[col.name] and shows the value while dragging. -->\n <ng-template #inlineDetailCell let-row=\"row\" let-col=\"col\" let-attr=\"attr\" let-rowIndex=\"rowIndex\" let-rowRenderer=\"rowRenderer\">\n <td>\n <!-- A row the caller may not edit renders through the same read-only span as a\n read-only column: still fully readable, just not editable. Hiding it would\n hide the data, and the save path would silently restore any edit anyway. -->\n @if (col.isReadOnly || !(attr | canEditDetailRow:asDetailPermissions())) {\n <span class=\"text-muted\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n } @else if (getAsDetailCellEditRenderer(col); as editRenderer) {\n <ng-container *ngComponentOutlet=\"editRenderer; inputs: getAsDetailCellEditRendererInputs(editRenderer, row, col)\"></ng-container>\n } @else if (col.dataType === 'AsDetail') {\n <!-- Nested AsDetail can't flatten into a row: escalate to the row modal,\n where the recursive spark-po-form renders the nested structure. -->\n <div class=\"d-flex align-items-center gap-1\">\n <span class=\"text-truncate flex-grow-1\">{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"editArrayItem(attr, rowIndex)\">\n <spark-icon name=\"pencil\" />\n </button>\n </div>\n } @else if (col.dataType === 'boolean' && !col.lookupReferenceType) {\n <!-- Same precedence as the top-level chain above: a lookup-backed column must\n reach its select rather than being swallowed by the checkbox. -->\n <bs-checkbox\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\">\n </bs-checkbox>\n } @else if (col.dataType === 'Reference' && col.query) {\n @if (col.referenceDisplayType === EReferenceDisplayType.Modal) {\n <spark-reference-picker\n [value]=\"row[col.name]\"\n [options]=\"(attr | inlineRefOptions:col:asDetailReferenceOptions())\"\n [referenceType]=\"col.referenceType\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-reference-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.id }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.lookupReferenceType) {\n @if ((col | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <spark-lookup-picker\n [value]=\"row[col.name]\"\n [options]=\"getLookupOptions(col)\"\n [title]=\"col.label | resolveTranslation:col.name\"\n [isInvalid]=\"hasInlineError(attr, rowIndex, col)\"\n (valueChange)=\"row[col.name] = $event; onInlineCellChange(attr, rowIndex, col)\">\n </spark-lookup-picker>\n } @else {\n <bs-select\n class=\"w-100\"\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\">\n <option [ngValue]=\"null\">{{ 'common.selectPlaceholder' | t }}</option>\n @for (option of getLookupOptions(col); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (col.dataType === 'MultiLineString') {\n <textarea\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n rows=\"3\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\"></textarea>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasInlineError(attr, rowIndex, col)\"\n (ngModelChange)=\"onInlineCellChange(attr, rowIndex, col)\"\n (blur)=\"onInlineCellBlur(attr, rowIndex, col)\">\n }\n @if (inlineErrorMessage(attr, rowIndex, col); as errorMsg) {\n <div class=\"invalid-feedback d-block\">{{ errorMsg }}</div>\n }\n <!-- Passed for the first cell only: a row renderer (rowComponent) draws its\n attributes once per row, under that cell, instead of as columns. -->\n @if (rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row)\"></ng-container>\n </div>\n }\n </td>\n </ng-template>\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n @let inlineRowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @for (row of formData()[attr.name] || []; track $index; let rowIndex = $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex, rowRenderer: firstCol ? inlineRowRenderer : null }\"></ng-container>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, rowIndex)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n\n <!-- Floating preview = a LIVE view of the row's cells (not CDK's cloneNode),\n so <bs-select> shows its value instead of blanking. The in-grid\n placeholder still uses CDK's default clone (exact row height) and is\n hidden via .cdk-drag-placeholder { visibility:hidden } in the styles,\n so the drop gap is blank without collapsing/shifting the other rows. -->\n <ng-template cdkDragPreview>\n <table class=\"table mb-0 bg-body shadow-sm spark-drag-preview\">\n <tbody class=\"align-middle\">\n <tr>\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"width: 32px\"><spark-icon name=\"grip-vertical\" /></td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <ng-container *ngTemplateOutlet=\"inlineDetailCell; context: { row: row, col: col, attr: attr, rowIndex: rowIndex }\"></ng-container>\n }\n </tr>\n </tbody>\n </table>\n </ng-template>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @if (attr.isSortable) {\n <th style=\"width: 32px\"></th>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody class=\"align-middle\" cdkDropList [cdkDropListDisabled]=\"!attr.isSortable\" (cdkDropListDropped)=\"onAsDetailReorder(attr, $event)\">\n @let modalRowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of formData()[attr.name] || []; track $index) {\n <tr cdkDrag [cdkDragDisabled]=\"!attr.isSortable\">\n @if (attr.isSortable) {\n <td class=\"text-center text-muted\" style=\"cursor: move; touch-action: none\" cdkDragHandle>\n <spark-icon name=\"grip-vertical\" />\n </td>\n }\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\n <td>\n @if (getAsDetailCellRendererComponent(col); as cellRenderer) {\n <ng-container *ngComponentOutlet=\"cellRenderer; inputs: getAsDetailCellRendererInputs(cellRenderer, row, col)\"></ng-container>\n } @else {\n {{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}\n }\n @if (firstCol && modalRowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"modalRowRenderer.component; inputs: getRowRendererInputs(modalRowRenderer, row)\"></ng-container>\n </div>\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canEditDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n }\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).length + (attr.isSortable ? 2 : 1)\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @for (err of rowLifecycleErrorsFor(attr.name); track $index) {\n <div class=\"text-danger small mt-1\">{{ err.errorMessage | resolveTranslation }}</div>\n }\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'common.add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(editComp, attr)\"></ng-container>\n } @else if (attr.dataType === 'MultiLineString') {\n <textarea\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n rows=\"6\"\n [class.is-invalid]=\"hasError(attr.name)\"></textarea>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange(attr)\"\n (blur)=\"onFieldBlur(attr)\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'common.cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'common.save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'common.edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [referenceParentId]=\"referenceParentId()\"\n [referenceParentType]=\"referenceParentType()\"\n [triggerPathPrefix]=\"attr.isArray ? null : attr.name\"\n (nestedTriggerRequested)=\"onEmbeddedTrigger(attr.name, $event)\"\n (nestedTriggerBlurred)=\"onEmbeddedTriggerBlur($event)\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'common.cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'common.save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n",
2071
2167
  styles: [":host ::ng-deep .cdk-drag-placeholder{visibility:hidden}\n"]
2072
2168
  }]
2073
2169
  }],
@@ -2132,6 +2228,22 @@ i0.ɵɵngDeclareClassMetadata({
2132
2228
  required: false
2133
2229
  }]
2134
2230
  }],
2231
+ referenceParentId: [{
2232
+ type: i0.Input,
2233
+ args: [{
2234
+ isSignal: true,
2235
+ alias: "referenceParentId",
2236
+ required: false
2237
+ }]
2238
+ }],
2239
+ referenceParentType: [{
2240
+ type: i0.Input,
2241
+ args: [{
2242
+ isSignal: true,
2243
+ alias: "referenceParentType",
2244
+ required: false
2245
+ }]
2246
+ }],
2135
2247
  save: [{
2136
2248
  type: i0.Output,
2137
2249
  args: ["save"]