@acorex-platform/framework-client 0.0.14 → 0.0.18

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 (34) hide show
  1. package/fesm2022/acorex-platform-framework-client-auth.mjs.map +1 -1
  2. package/fesm2022/acorex-platform-framework-client-common.mjs +25 -211
  3. package/fesm2022/acorex-platform-framework-client-common.mjs.map +1 -1
  4. package/fesm2022/acorex-platform-framework-client-core.mjs +8 -104
  5. package/fesm2022/acorex-platform-framework-client-core.mjs.map +1 -1
  6. package/fesm2022/acorex-platform-framework-client-layout-builder.mjs +57 -12
  7. package/fesm2022/acorex-platform-framework-client-layout-builder.mjs.map +1 -1
  8. package/fesm2022/acorex-platform-framework-client-layout-components.mjs +494 -561
  9. package/fesm2022/acorex-platform-framework-client-layout-components.mjs.map +1 -1
  10. package/fesm2022/acorex-platform-framework-client-layout-designer.mjs +8 -8
  11. package/fesm2022/acorex-platform-framework-client-layout-designer.mjs.map +1 -1
  12. package/fesm2022/acorex-platform-framework-client-layout-entity.mjs +1333 -1023
  13. package/fesm2022/acorex-platform-framework-client-layout-entity.mjs.map +1 -1
  14. package/fesm2022/acorex-platform-framework-client-layout-views.mjs +51 -9
  15. package/fesm2022/acorex-platform-framework-client-layout-views.mjs.map +1 -1
  16. package/fesm2022/acorex-platform-framework-client-layout-widget-core.mjs +29 -9
  17. package/fesm2022/acorex-platform-framework-client-layout-widget-core.mjs.map +1 -1
  18. package/fesm2022/acorex-platform-framework-client-layout-widgets.mjs +48 -15
  19. package/fesm2022/acorex-platform-framework-client-layout-widgets.mjs.map +1 -1
  20. package/fesm2022/acorex-platform-framework-client-themes-default.mjs +41 -26
  21. package/fesm2022/acorex-platform-framework-client-themes-default.mjs.map +1 -1
  22. package/fesm2022/acorex-platform-framework-client-themes-shared.mjs +4 -4
  23. package/fesm2022/acorex-platform-framework-client-themes-shared.mjs.map +1 -1
  24. package/fesm2022/acorex-platform-framework-client-workflow.mjs.map +1 -1
  25. package/package.json +7 -7
  26. package/types/acorex-platform-framework-client-auth.d.ts +2 -2
  27. package/types/acorex-platform-framework-client-common.d.ts +12 -125
  28. package/types/acorex-platform-framework-client-core.d.ts +5 -22
  29. package/types/acorex-platform-framework-client-layout-builder.d.ts +10 -0
  30. package/types/acorex-platform-framework-client-layout-components.d.ts +17 -68
  31. package/types/acorex-platform-framework-client-layout-entity.d.ts +80 -7
  32. package/types/acorex-platform-framework-client-layout-views.d.ts +11 -6
  33. package/types/acorex-platform-framework-client-layout-widgets.d.ts +1 -1
  34. package/types/acorex-platform-framework-client-workflow.d.ts +1 -2
@@ -5,7 +5,7 @@ import { AXToastService } from '@acorex/components/toast';
5
5
  import * as i3$1 from '@acorex/core/translation';
6
6
  import { AXTranslationService, AXTranslationModule, resolveMultiLanguageString, translateSync } from '@acorex/core/translation';
7
7
  import * as i4$4 from '@acorex-platform/framework-client/common';
8
- import { AXPNotFoundError, AXP_ENTITY_DELETED_EVENT_TYPE, axpNavigateAppPath, AXPSettingsService, AXPCommonSettings, resolveEntityCreateListBehavior, AXPRefreshEvent, AXPFileTypeProviderService, AXPFileStorageService, resolveFileTypeDisplay, resolveEntityRowDisplayTitle, resolveEntityModifyPopupTitle, resolveSearchableText, evaluateEntityDisplayTemplate, commandMessageTextForError, AXPFilterOperatorMiddlewareService, AXPReloadEvent, axpRedirectToNotFound, AXPCleanNestedFilters, axpIsEntityRecordNotFound, coerceAXPSettingRawValueByKind, AXPDefaultMultiLanguageConfigService, withDefaultMultiLanguageOnWidgetNodeTree, axpIsEntityListPath, axpIsEntityDetailsPath, axpParseEntityListRoute, axpParseEntityDetailsRoute, axpSaveEntityListReturnUrl, AXP_NOT_FOUND_ROUTE, AXP_PROTECTED_ROUTE_GUARDS, AXP_SEARCH_DEFINITION_PROVIDER, AXP_SEARCH_DEFINITION_LAZY_RESOLVER, AXPMenuItemsDataSourceDefinition } from '@acorex-platform/framework-client/common';
8
+ import { AXPNotFoundError, AXP_ENTITY_MODIFIED_EVENT_TYPE, AXP_ENTITY_DELETED_EVENT_TYPE, axpNavigateAppPath, AXPSettingsService, AXPCommonSettings, resolveEntityCreateListBehavior, AXPRefreshEvent, AXPFileTypeProviderService, AXPFileStorageService, resolveFileTypeDisplay, resolveEntityRowDisplayTitle, resolveEntityModifyPopupTitle, commandMessageTextForError, resolveSearchableText, evaluateEntityDisplayTemplate, AXPFilterOperatorMiddlewareService, AXPReloadEvent, axpRedirectToNotFound, AXPCleanNestedFilters, axpIsEntityRecordNotFound, coerceAXPSettingRawValueByKind, AXPDefaultMultiLanguageConfigService, withDefaultMultiLanguageOnWidgetNodeTree, axpIsEntityListPath, axpIsEntityDetailsPath, axpParseEntityListRoute, axpParseEntityDetailsRoute, axpSaveEntityListReturnUrl, AXP_NOT_FOUND_ROUTE, AXP_PROTECTED_ROUTE_GUARDS, AXP_SEARCH_DEFINITION_PROVIDER, AXP_SEARCH_DEFINITION_LAZY_RESOLVER, AXPMenuItemsDataSourceDefinition } from '@acorex-platform/framework-client/common';
9
9
  export { commandMessageTextForError } from '@acorex-platform/framework-client/common';
10
10
  import { createWorkFlowEvent, AXPWorkflowEventService, ofType } from '@acorex-platform/framework-client/workflow';
11
11
  import * as i0 from '@angular/core';
@@ -457,7 +457,9 @@ class AXPEntityMiddleware {
457
457
  for (const pluginRef of entityPlugins) {
458
458
  const contrib = this.providedActionPlugins.find((x) => x.name === pluginRef.name);
459
459
  if (contrib) {
460
- steps.push(this.createPluginStep(contrib, pluginRef.name, context));
460
+ // Capture this entry's options — re-finding by name breaks multi-instance
461
+ // plugins that share a name (e.g. several `status` plugins on one entity).
462
+ steps.push(this.createPluginStep(contrib, pluginRef.options, context));
461
463
  }
462
464
  }
463
465
  const sorted = steps.sort((a, b) => a.order - b.order);
@@ -479,15 +481,11 @@ class AXPEntityMiddleware {
479
481
  run: () => entry.modifier(context),
480
482
  };
481
483
  }
482
- createPluginStep(contrib, pluginName, context) {
484
+ createPluginStep(contrib, options, context) {
483
485
  return {
484
486
  order: contrib.order ?? 0,
485
487
  run: async () => {
486
- const pluginRef = context.plugins.find(pluginName).get();
487
- if (!pluginRef) {
488
- return;
489
- }
490
- await contrib.apply(context, pluginRef.options);
488
+ await contrib.apply(context, options);
491
489
  },
492
490
  };
493
491
  }
@@ -880,7 +878,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
880
878
  }] });
881
879
 
882
880
  const AXPEntityCreateEvent = createWorkFlowEvent('[Entity] Created!');
883
- const AXPEntityModifyEvent = createWorkFlowEvent('[Entity] Modified');
881
+ const AXPEntityModifyEvent = createWorkFlowEvent(AXP_ENTITY_MODIFIED_EVENT_TYPE);
884
882
  const AXPEntityDeletedEvent = createWorkFlowEvent(AXP_ENTITY_DELETED_EVENT_TYPE);
885
883
 
886
884
  /**
@@ -971,6 +969,12 @@ async function resolveRelatedEntityColumns(columns, expressionEvaluator, scope)
971
969
  }
972
970
  //#endregion
973
971
 
972
+ /**
973
+ * Sentinel used as related-list FK filter value in create wizards before the parent
974
+ * row has a persisted id. Must never be treated as a real parent key.
975
+ */
976
+ const AXP_RELATED_LIST_UNSCOPED_PARENT_FILTER_VALUE = '__axp_unscoped_parent__';
977
+
974
978
  /** Registered widget name for entity create/update step wizard (dialog footer uses `widget:${name}.next`). */
975
979
  const ENTITY_FORM_STEP_WIZARD_NAME = 'entityFormStepWizard';
976
980
  /** Footer command: validate, persist main entity, merge context, advance wizard. */
@@ -1592,8 +1596,15 @@ class PropertyFilter {
1592
1596
  };
1593
1597
  const syncRelatedFromRoot = this.kind === 'create';
1594
1598
  const foreignKeyField = relatedEntity.persistence?.foreignKeyField;
1599
+ // Create-wizard related steps start with a never-match parent filter so the grid
1600
+ // cannot load other rows before create-and-continue patches the root id.
1595
1601
  const filters = syncRelatedFromRoot
1596
- ? []
1602
+ ? (relatedEntity.conditions ?? []).map((c) => ({
1603
+ field: c.name,
1604
+ operator: c.operator,
1605
+ value: AXP_RELATED_LIST_UNSCOPED_PARENT_FILTER_VALUE,
1606
+ hidden: true,
1607
+ }))
1597
1608
  : await Promise.all(relatedEntity.conditions?.map(async (c) => {
1598
1609
  const value = await evaluateExpressions(c.value);
1599
1610
  return {
@@ -2088,6 +2099,7 @@ function configureEntityFormWizardFooterActions(ab, wizardName, firstStepContinu
2088
2099
  color: 'primary',
2089
2100
  position: 'suffix',
2090
2101
  disabled: '{{api.getStatus().isFirst}}',
2102
+ predicateApiWidgetName: wizardName,
2091
2103
  });
2092
2104
  ab.custom({
2093
2105
  title: firstStepContinueTitleKey,
@@ -2345,7 +2357,12 @@ class AXPCreateEntityCommand {
2345
2357
  };
2346
2358
  }
2347
2359
  const entityRef = await this.entityService.resolve(moduleName, entityName);
2348
- let chain = this.entityForm.entity(`${moduleName}.${entityName}`).create(data);
2360
+ // Related-list / Assign callers put the parent FK in process.data — keep it in the create seed.
2361
+ const processData = options?.process && typeof options.process === 'object'
2362
+ ? options.process.data
2363
+ : undefined;
2364
+ const createSeed = merge({}, data && typeof data === 'object' ? data : {}, processData && typeof processData === 'object' ? processData : {});
2365
+ let chain = this.entityForm.entity(`${moduleName}.${entityName}`).create(createSeed);
2349
2366
  chain.actions((actions) => {
2350
2367
  actions
2351
2368
  .cancel('@general:actions.cancel.title')
@@ -2378,7 +2395,7 @@ class AXPCreateEntityCommand {
2378
2395
  }
2379
2396
  const createFn = entityRef.commands?.create?.execute;
2380
2397
  const updateFn = entityRef.commands?.update?.execute;
2381
- const context = merge({}, data, dialogRef.context());
2398
+ const context = merge({}, createSeed, dialogRef.context());
2382
2399
  const hasPersistedId = hasPersistedRootId(context);
2383
2400
  const persistFn = hasPersistedId ? updateFn : createFn;
2384
2401
  const missingPersistHandlerMsg = hasPersistedId
@@ -3647,7 +3664,7 @@ class AXPFileListComponent {
3647
3664
  return action.color ?? 'primary';
3648
3665
  }
3649
3666
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3650
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPFileListComponent, isStandalone: true, selector: "axp-file-list", inputs: { readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, fileEditable: { classPropertyName: "fileEditable", publicName: "fileEditable", isSignal: true, isRequired: false, transformFunction: null }, enableTitleDescription: { classPropertyName: "enableTitleDescription", publicName: "enableTitleDescription", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, look: { classPropertyName: "look", publicName: "look", isSignal: true, isRequired: false, transformFunction: null }, titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, showLabel: { classPropertyName: "showLabel", publicName: "showLabel", isSignal: true, isRequired: false, transformFunction: null }, linksLayout: { classPropertyName: "linksLayout", publicName: "linksLayout", isSignal: true, isRequired: false, transformFunction: null }, files: { classPropertyName: "files", publicName: "files", isSignal: true, isRequired: false, transformFunction: null }, plugins: { classPropertyName: "plugins", publicName: "plugins", isSignal: true, isRequired: false, transformFunction: null }, excludePlugins: { classPropertyName: "excludePlugins", publicName: "excludePlugins", isSignal: true, isRequired: false, transformFunction: null }, capabilities: { classPropertyName: "capabilities", publicName: "capabilities", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onRemove: "onRemove", onRevert: "onRevert", onRename: "onRename" }, host: { properties: { "class.--look-rows": "look() === \"rows\"", "class.--look-links": "look() === \"links\"", "class.--links-inline": "look() === \"links\" && linksLayout() === \"inline\"" } }, providers: [], ngImport: i0, template: "@if (look() === 'links') {\n @if (displayFiles().length > 0) {\n <div class=\"__links\">\n @if (showLabel()) {\n <span class=\"__links-label\">{{ titleKey() | translate | async }}:</span>\n }\n <ul class=\"__links-list\">\n @for (file of displayFiles(); track file.id ?? $index) {\n <li>\n <button\n type=\"button\"\n class=\"__link-item\"\n (click)=\"handleFileDownload($event, file)\"\n (dblclick)=\"handleFileRowActivate($event, file)\"\n >\n <i class=\"fa-light fa-paperclip flex-shrink-0\"></i>\n <span class=\"min-w-0 break-all\">{{ file.name }}</span>\n </button>\n </li>\n }\n </ul>\n </div>\n } @else {\n <div class=\"__empty-state __empty-state--links\">\n <axp-state-message\n icon=\"fa-light fa-folder-open\"\n [title]=\"'@general:widgets.file-uploader.empty-state.title'\"\n [description]=\"'@general:widgets.file-uploader.empty-state.description'\"\n look=\"minimal\"\n />\n </div>\n }\n} @else {\n @for (file of displayFiles(); track $index) {\n <div\n class=\"__item\"\n [ngClass]=\"{\n '--removed': file.status === 'deleted',\n '--attached': file.status === 'attached',\n '--with-meta': enableTitleDescription(),\n '--item-readonly': file.readOnly === true,\n '--downloadable': file.status !== 'deleted',\n }\"\n (dblclick)=\"handleFileRowActivate($event, file)\"\n >\n <div class=\"__icon\">\n <i class=\"fa-fw {{ getFileInfo(file.name).icon }}\"></i>\n </div>\n @if (enableTitleDescription()) {\n <div class=\"__content\">\n <div class=\"__primary\">{{ file.name }}{{ file.title?.trim() ? ' - ' + file.title : '' }}</div>\n @if (file.description?.trim()) {\n <div class=\"__secondary\">{{ file.description }}</div>\n }\n </div>\n } @else {\n <div class=\"__name\">{{ file.name }}</div>\n }\n <div class=\"__actions\" (dblclick)=\"$event.stopPropagation()\">\n @if (file.status === 'deleted' && multiple() && !isItemInteractionLocked(file)) {\n <ax-button [look]=\"'blank'\" class=\"ax-sm\" color=\"warning\" (onClick)=\"handleFileRevert($event, file)\">\n <ax-icon class=\"fa-light fa-rotate-left\"></ax-icon>\n </ax-button>\n } @else if (file.status !== 'deleted') {\n @for (action of actionsFor(file, $index); track action.id ?? action.textKey ?? action.text ?? $index) {\n <ax-button\n [look]=\"'blank'\"\n class=\"ax-sm\"\n [color]=\"getActionColor(action)\"\n (onClick)=\"runAction(action)\"\n >\n @if (action.icon) {\n <ax-icon class=\"{{ action.icon }}\"></ax-icon>\n }\n </ax-button>\n }\n }\n </div>\n </div>\n } @empty {\n <div class=\"__empty-state\">\n <axp-state-message\n icon=\"fa-light fa-folder-open\"\n [title]=\"'@general:widgets.file-uploader.empty-state.title'\"\n [description]=\"'@general:widgets.file-uploader.empty-state.description'\"\n >\n </axp-state-message>\n </div>\n }\n}\n", styles: ["@layer properties;:host{display:flex;width:100%;flex-direction:column;gap:calc(var(--spacing, .25rem) * .5)}:host.--look-rows{padding-block:calc(var(--spacing, .25rem) * 2)}:host.--look-links{padding-block:0}:host.--look-links .__links .__links-label{margin-inline-end:calc(var(--spacing, .25rem) * 2);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--ax-sys-color-on-surface-variant))}:host.--look-links .__links .__links-list{margin:0;margin-top:var(--spacing, .25rem);list-style-type:none;padding:0}:host.--look-links .__links .__links-list li{margin:0;padding:0}:host.--look-links .__links .__links-list .__link-item{display:inline-flex;width:100%;cursor:pointer;align-items:center;gap:calc(var(--spacing, .25rem) * 3);border-radius:var(--radius-md, .375rem);border-style:var(--tw-border-style);border-width:0px;background-color:transparent;padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2.5);text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));text-decoration-line:none;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));color:rgb(var(--ax-sys-color-primary))}:host.--look-links .__links .__links-list .__link-item:hover{background:rgb(var(--ax-sys-color-lighter-surface))}:host.--look-links.--links-inline .__links{margin-bottom:calc(var(--spacing, .25rem) * 4);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:rgba(var(--ax-sys-color-surface));padding-bottom:calc(var(--spacing, .25rem) * 4)}:host.--look-links:not(.--links-inline) .__links-list{margin-top:0;display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 2)}:host .__item{display:flex;align-items:center;gap:calc(var(--spacing, .25rem) * 3);border-left-style:var(--tw-border-style);border-left-width:4px;border-color:transparent;padding:calc(var(--spacing, .25rem) * 2)}:host .__item.--downloadable{cursor:pointer}:host .__item.--downloadable:hover{background-color:var(--color-lighter);color:var(--color-on-lighter);border-color:var(--color-border-lighter)}:host .__item:not(.--downloadable){cursor:default}:host .__item.--removed{border-color:rgba(var(--ax-sys-color-danger-500));background-color:rgba(var(--ax-sys-color-danger-50))}:host .__item.--attached{border-color:rgba(var(--ax-sys-color-success-500));animation:attached-flash 1s ease-out forwards}:host .__item.--item-readonly:not(.--removed){opacity:.85}:host .__item .__icon{width:calc(var(--spacing, .25rem) * 6);flex-shrink:0}:host .__item .__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}:host .__item .__content{display:flex;min-width:0;flex:1;flex-direction:column;gap:calc(var(--spacing, .25rem) * .5)}:host .__item .__primary{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}:host .__item .__secondary{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;text-align:start;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--ax-sys-color-text-secondary)}:host .__item.--with-meta .__content{justify-content:center}:host .__item .__actions{margin-left:auto;display:flex;flex-shrink:0}@keyframes attached-flash{0%{background-color:rgb(var(--ax-sys-color-success-50))}to{background-color:transparent}}.__empty-state{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.__empty-state:hover{opacity:80%}.__empty-state--links{padding-block:calc(var(--spacing, .25rem) * 2)}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-duration{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-border-style: solid;--tw-duration: initial}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type:
3667
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPFileListComponent, isStandalone: true, selector: "axp-file-list", inputs: { readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, fileEditable: { classPropertyName: "fileEditable", publicName: "fileEditable", isSignal: true, isRequired: false, transformFunction: null }, enableTitleDescription: { classPropertyName: "enableTitleDescription", publicName: "enableTitleDescription", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, look: { classPropertyName: "look", publicName: "look", isSignal: true, isRequired: false, transformFunction: null }, titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, showLabel: { classPropertyName: "showLabel", publicName: "showLabel", isSignal: true, isRequired: false, transformFunction: null }, linksLayout: { classPropertyName: "linksLayout", publicName: "linksLayout", isSignal: true, isRequired: false, transformFunction: null }, files: { classPropertyName: "files", publicName: "files", isSignal: true, isRequired: false, transformFunction: null }, plugins: { classPropertyName: "plugins", publicName: "plugins", isSignal: true, isRequired: false, transformFunction: null }, excludePlugins: { classPropertyName: "excludePlugins", publicName: "excludePlugins", isSignal: true, isRequired: false, transformFunction: null }, capabilities: { classPropertyName: "capabilities", publicName: "capabilities", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onRemove: "onRemove", onRevert: "onRevert", onRename: "onRename" }, host: { properties: { "class.--look-rows": "look() === \"rows\"", "class.--look-links": "look() === \"links\"", "class.--links-inline": "look() === \"links\" && linksLayout() === \"inline\"" } }, providers: [], ngImport: i0, template: "@if (look() === 'links') {\n @if (displayFiles().length > 0) {\n <div class=\"__links\">\n @if (showLabel()) {\n <span class=\"__links-label\">{{ titleKey() | translate | async }}:</span>\n }\n <ul class=\"__links-list\">\n @for (file of displayFiles(); track file.id ?? $index) {\n <li>\n <button\n type=\"button\"\n class=\"__link-item\"\n (click)=\"handleFileDownload($event, file)\"\n (dblclick)=\"handleFileRowActivate($event, file)\"\n >\n <i class=\"fa-light fa-paperclip flex-shrink-0\"></i>\n <span class=\"min-w-0 break-all\">{{ file.name }}</span>\n </button>\n </li>\n }\n </ul>\n </div>\n } @else {\n <div class=\"__empty-state __empty-state--links\">\n <axp-state-message\n icon=\"fa-light fa-folder-open\"\n [title]=\"'@general:widgets.file-uploader.empty-state.title'\"\n [description]=\"'@general:widgets.file-uploader.empty-state.description'\"\n look=\"minimal\"\n />\n </div>\n }\n} @else {\n @for (file of displayFiles(); track $index) {\n <div\n class=\"__item\"\n [ngClass]=\"{\n '--removed': file.status === 'deleted',\n '--attached': file.status === 'attached',\n '--with-meta': enableTitleDescription(),\n '--item-readonly': file.readOnly === true,\n '--downloadable': file.status !== 'deleted',\n }\"\n (dblclick)=\"handleFileRowActivate($event, file)\"\n >\n <div class=\"__icon\">\n <i class=\"fa-fw {{ getFileInfo(file.name).icon }}\"></i>\n </div>\n @if (enableTitleDescription()) {\n <div class=\"__content\">\n <div class=\"__primary\">{{ file.name }}{{ file.title?.trim() ? ' - ' + file.title : '' }}</div>\n @if (file.description?.trim()) {\n <div class=\"__secondary\">{{ file.description }}</div>\n }\n </div>\n } @else {\n <div class=\"__name\">{{ file.name }}</div>\n }\n <div class=\"__actions\" (dblclick)=\"$event.stopPropagation()\">\n @if (file.status === 'deleted' && multiple() && !isItemInteractionLocked(file)) {\n <ax-button [look]=\"'blank'\" class=\"ax-sm\" color=\"warning\" (onClick)=\"handleFileRevert($event, file)\">\n <ax-icon class=\"fa-light fa-rotate-left\"></ax-icon>\n </ax-button>\n } @else if (file.status !== 'deleted') {\n @for (action of actionsFor(file, $index); track action.id ?? action.textKey ?? action.text ?? $index) {\n <ax-button\n [look]=\"'blank'\"\n class=\"ax-sm\"\n [color]=\"getActionColor(action)\"\n (onClick)=\"runAction(action)\"\n >\n @if (action.icon) {\n <ax-icon class=\"{{ action.icon }}\"></ax-icon>\n }\n </ax-button>\n }\n }\n </div>\n </div>\n } @empty {\n <div class=\"__empty-state\">\n <axp-state-message\n icon=\"fa-light fa-folder-open\"\n [title]=\"'@general:widgets.file-uploader.empty-state.title'\"\n [description]=\"'@general:widgets.file-uploader.empty-state.description'\"\n >\n </axp-state-message>\n </div>\n }\n}\n", styles: ["@layer properties;:host{display:flex;width:100%;flex-direction:column;gap:calc(var(--spacing, .25rem) * .5)}:host.--look-rows{padding-block:calc(var(--spacing, .25rem) * 2)}:host.--look-links{padding-block:0}:host.--look-links .__links .__links-label{margin-inline-end:calc(var(--spacing, .25rem) * 2);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--ax-sys-color-on-surface-variant))}:host.--look-links .__links .__links-list{margin:0;margin-top:var(--spacing, .25rem);list-style-type:none;padding:0}:host.--look-links .__links .__links-list li{margin:0;padding:0}:host.--look-links .__links .__links-list .__link-item{display:inline-flex;width:100%;cursor:pointer;align-items:center;gap:calc(var(--spacing, .25rem) * 3);border-radius:var(--radius-md, .375rem);border-style:var(--tw-border-style);border-width:0px;background-color:transparent;padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2.5);text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));text-decoration-line:none;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));color:rgb(var(--ax-sys-color-primary))}:host.--look-links .__links .__links-list .__link-item:hover{background:rgb(var(--ax-sys-color-lighter-surface))}:host.--look-links.--links-inline .__links{margin-bottom:calc(var(--spacing, .25rem) * 4);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:rgba(var(--ax-sys-color-surface));padding-bottom:calc(var(--spacing, .25rem) * 4)}:host.--look-links:not(.--links-inline) .__links-list{margin-top:0;display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 2)}:host .__item{display:flex;align-items:center;gap:calc(var(--spacing, .25rem) * 3);border-left-style:var(--tw-border-style);border-left-width:4px;border-color:transparent;padding:calc(var(--spacing, .25rem) * 2)}:host .__item.--downloadable{cursor:pointer}:host .__item.--downloadable:hover{background-color:var(--color-lighter);color:var(--color-on-lighter);border-color:var(--color-border-lighter)}:host .__item:not(.--downloadable){cursor:default}:host .__item.--removed{border-color:rgba(var(--ax-sys-color-danger-500));background-color:rgba(var(--ax-sys-color-danger-50))}:host .__item.--attached{border-color:rgba(var(--ax-sys-color-success-500));animation:attached-flash 1s ease-out forwards}:host .__item.--item-readonly:not(.--removed){opacity:.85}:host .__item .__icon{width:calc(var(--spacing, .25rem) * 6);flex-shrink:0}:host .__item .__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}:host .__item .__content{display:flex;min-width:0;flex:1;flex-direction:column;gap:calc(var(--spacing, .25rem) * .5)}:host .__item .__primary{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}:host .__item .__secondary{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;text-align:start;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--ax-sys-color-text-secondary)}:host .__item.--with-meta .__content{justify-content:center}:host .__item .__actions{margin-left:auto;display:flex;flex-shrink:0}@keyframes attached-flash{0%{background-color:rgb(var(--ax-sys-color-success-50))}to{background-color:transparent}}.__empty-state{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.__empty-state:hover{opacity:80%}.__empty-state.__empty-state--links{padding-block:calc(var(--spacing, .25rem) * 2)}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-duration{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-border-style: solid;--tw-duration: initial}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type:
3651
3668
  // ACoreX
3652
3669
  AXFormModule }, { kind: "ngmodule", type: AXTextBoxModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXLabelModule }, { kind: "ngmodule", type: AXCheckBoxModule }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type:
3653
3670
  // Platform
@@ -3675,7 +3692,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3675
3692
  AXTranslationModule,
3676
3693
  // Platform
3677
3694
  AXPStateMessageComponent,
3678
- ], providers: [], template: "@if (look() === 'links') {\n @if (displayFiles().length > 0) {\n <div class=\"__links\">\n @if (showLabel()) {\n <span class=\"__links-label\">{{ titleKey() | translate | async }}:</span>\n }\n <ul class=\"__links-list\">\n @for (file of displayFiles(); track file.id ?? $index) {\n <li>\n <button\n type=\"button\"\n class=\"__link-item\"\n (click)=\"handleFileDownload($event, file)\"\n (dblclick)=\"handleFileRowActivate($event, file)\"\n >\n <i class=\"fa-light fa-paperclip flex-shrink-0\"></i>\n <span class=\"min-w-0 break-all\">{{ file.name }}</span>\n </button>\n </li>\n }\n </ul>\n </div>\n } @else {\n <div class=\"__empty-state __empty-state--links\">\n <axp-state-message\n icon=\"fa-light fa-folder-open\"\n [title]=\"'@general:widgets.file-uploader.empty-state.title'\"\n [description]=\"'@general:widgets.file-uploader.empty-state.description'\"\n look=\"minimal\"\n />\n </div>\n }\n} @else {\n @for (file of displayFiles(); track $index) {\n <div\n class=\"__item\"\n [ngClass]=\"{\n '--removed': file.status === 'deleted',\n '--attached': file.status === 'attached',\n '--with-meta': enableTitleDescription(),\n '--item-readonly': file.readOnly === true,\n '--downloadable': file.status !== 'deleted',\n }\"\n (dblclick)=\"handleFileRowActivate($event, file)\"\n >\n <div class=\"__icon\">\n <i class=\"fa-fw {{ getFileInfo(file.name).icon }}\"></i>\n </div>\n @if (enableTitleDescription()) {\n <div class=\"__content\">\n <div class=\"__primary\">{{ file.name }}{{ file.title?.trim() ? ' - ' + file.title : '' }}</div>\n @if (file.description?.trim()) {\n <div class=\"__secondary\">{{ file.description }}</div>\n }\n </div>\n } @else {\n <div class=\"__name\">{{ file.name }}</div>\n }\n <div class=\"__actions\" (dblclick)=\"$event.stopPropagation()\">\n @if (file.status === 'deleted' && multiple() && !isItemInteractionLocked(file)) {\n <ax-button [look]=\"'blank'\" class=\"ax-sm\" color=\"warning\" (onClick)=\"handleFileRevert($event, file)\">\n <ax-icon class=\"fa-light fa-rotate-left\"></ax-icon>\n </ax-button>\n } @else if (file.status !== 'deleted') {\n @for (action of actionsFor(file, $index); track action.id ?? action.textKey ?? action.text ?? $index) {\n <ax-button\n [look]=\"'blank'\"\n class=\"ax-sm\"\n [color]=\"getActionColor(action)\"\n (onClick)=\"runAction(action)\"\n >\n @if (action.icon) {\n <ax-icon class=\"{{ action.icon }}\"></ax-icon>\n }\n </ax-button>\n }\n }\n </div>\n </div>\n } @empty {\n <div class=\"__empty-state\">\n <axp-state-message\n icon=\"fa-light fa-folder-open\"\n [title]=\"'@general:widgets.file-uploader.empty-state.title'\"\n [description]=\"'@general:widgets.file-uploader.empty-state.description'\"\n >\n </axp-state-message>\n </div>\n }\n}\n", styles: ["@layer properties;:host{display:flex;width:100%;flex-direction:column;gap:calc(var(--spacing, .25rem) * .5)}:host.--look-rows{padding-block:calc(var(--spacing, .25rem) * 2)}:host.--look-links{padding-block:0}:host.--look-links .__links .__links-label{margin-inline-end:calc(var(--spacing, .25rem) * 2);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--ax-sys-color-on-surface-variant))}:host.--look-links .__links .__links-list{margin:0;margin-top:var(--spacing, .25rem);list-style-type:none;padding:0}:host.--look-links .__links .__links-list li{margin:0;padding:0}:host.--look-links .__links .__links-list .__link-item{display:inline-flex;width:100%;cursor:pointer;align-items:center;gap:calc(var(--spacing, .25rem) * 3);border-radius:var(--radius-md, .375rem);border-style:var(--tw-border-style);border-width:0px;background-color:transparent;padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2.5);text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));text-decoration-line:none;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));color:rgb(var(--ax-sys-color-primary))}:host.--look-links .__links .__links-list .__link-item:hover{background:rgb(var(--ax-sys-color-lighter-surface))}:host.--look-links.--links-inline .__links{margin-bottom:calc(var(--spacing, .25rem) * 4);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:rgba(var(--ax-sys-color-surface));padding-bottom:calc(var(--spacing, .25rem) * 4)}:host.--look-links:not(.--links-inline) .__links-list{margin-top:0;display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 2)}:host .__item{display:flex;align-items:center;gap:calc(var(--spacing, .25rem) * 3);border-left-style:var(--tw-border-style);border-left-width:4px;border-color:transparent;padding:calc(var(--spacing, .25rem) * 2)}:host .__item.--downloadable{cursor:pointer}:host .__item.--downloadable:hover{background-color:var(--color-lighter);color:var(--color-on-lighter);border-color:var(--color-border-lighter)}:host .__item:not(.--downloadable){cursor:default}:host .__item.--removed{border-color:rgba(var(--ax-sys-color-danger-500));background-color:rgba(var(--ax-sys-color-danger-50))}:host .__item.--attached{border-color:rgba(var(--ax-sys-color-success-500));animation:attached-flash 1s ease-out forwards}:host .__item.--item-readonly:not(.--removed){opacity:.85}:host .__item .__icon{width:calc(var(--spacing, .25rem) * 6);flex-shrink:0}:host .__item .__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}:host .__item .__content{display:flex;min-width:0;flex:1;flex-direction:column;gap:calc(var(--spacing, .25rem) * .5)}:host .__item .__primary{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}:host .__item .__secondary{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;text-align:start;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--ax-sys-color-text-secondary)}:host .__item.--with-meta .__content{justify-content:center}:host .__item .__actions{margin-left:auto;display:flex;flex-shrink:0}@keyframes attached-flash{0%{background-color:rgb(var(--ax-sys-color-success-50))}to{background-color:transparent}}.__empty-state{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.__empty-state:hover{opacity:80%}.__empty-state--links{padding-block:calc(var(--spacing, .25rem) * 2)}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-duration{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-border-style: solid;--tw-duration: initial}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"] }]
3695
+ ], providers: [], template: "@if (look() === 'links') {\n @if (displayFiles().length > 0) {\n <div class=\"__links\">\n @if (showLabel()) {\n <span class=\"__links-label\">{{ titleKey() | translate | async }}:</span>\n }\n <ul class=\"__links-list\">\n @for (file of displayFiles(); track file.id ?? $index) {\n <li>\n <button\n type=\"button\"\n class=\"__link-item\"\n (click)=\"handleFileDownload($event, file)\"\n (dblclick)=\"handleFileRowActivate($event, file)\"\n >\n <i class=\"fa-light fa-paperclip flex-shrink-0\"></i>\n <span class=\"min-w-0 break-all\">{{ file.name }}</span>\n </button>\n </li>\n }\n </ul>\n </div>\n } @else {\n <div class=\"__empty-state __empty-state--links\">\n <axp-state-message\n icon=\"fa-light fa-folder-open\"\n [title]=\"'@general:widgets.file-uploader.empty-state.title'\"\n [description]=\"'@general:widgets.file-uploader.empty-state.description'\"\n look=\"minimal\"\n />\n </div>\n }\n} @else {\n @for (file of displayFiles(); track $index) {\n <div\n class=\"__item\"\n [ngClass]=\"{\n '--removed': file.status === 'deleted',\n '--attached': file.status === 'attached',\n '--with-meta': enableTitleDescription(),\n '--item-readonly': file.readOnly === true,\n '--downloadable': file.status !== 'deleted',\n }\"\n (dblclick)=\"handleFileRowActivate($event, file)\"\n >\n <div class=\"__icon\">\n <i class=\"fa-fw {{ getFileInfo(file.name).icon }}\"></i>\n </div>\n @if (enableTitleDescription()) {\n <div class=\"__content\">\n <div class=\"__primary\">{{ file.name }}{{ file.title?.trim() ? ' - ' + file.title : '' }}</div>\n @if (file.description?.trim()) {\n <div class=\"__secondary\">{{ file.description }}</div>\n }\n </div>\n } @else {\n <div class=\"__name\">{{ file.name }}</div>\n }\n <div class=\"__actions\" (dblclick)=\"$event.stopPropagation()\">\n @if (file.status === 'deleted' && multiple() && !isItemInteractionLocked(file)) {\n <ax-button [look]=\"'blank'\" class=\"ax-sm\" color=\"warning\" (onClick)=\"handleFileRevert($event, file)\">\n <ax-icon class=\"fa-light fa-rotate-left\"></ax-icon>\n </ax-button>\n } @else if (file.status !== 'deleted') {\n @for (action of actionsFor(file, $index); track action.id ?? action.textKey ?? action.text ?? $index) {\n <ax-button\n [look]=\"'blank'\"\n class=\"ax-sm\"\n [color]=\"getActionColor(action)\"\n (onClick)=\"runAction(action)\"\n >\n @if (action.icon) {\n <ax-icon class=\"{{ action.icon }}\"></ax-icon>\n }\n </ax-button>\n }\n }\n </div>\n </div>\n } @empty {\n <div class=\"__empty-state\">\n <axp-state-message\n icon=\"fa-light fa-folder-open\"\n [title]=\"'@general:widgets.file-uploader.empty-state.title'\"\n [description]=\"'@general:widgets.file-uploader.empty-state.description'\"\n >\n </axp-state-message>\n </div>\n }\n}\n", styles: ["@layer properties;:host{display:flex;width:100%;flex-direction:column;gap:calc(var(--spacing, .25rem) * .5)}:host.--look-rows{padding-block:calc(var(--spacing, .25rem) * 2)}:host.--look-links{padding-block:0}:host.--look-links .__links .__links-label{margin-inline-end:calc(var(--spacing, .25rem) * 2);font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--ax-sys-color-on-surface-variant))}:host.--look-links .__links .__links-list{margin:0;margin-top:var(--spacing, .25rem);list-style-type:none;padding:0}:host.--look-links .__links .__links-list li{margin:0;padding:0}:host.--look-links .__links .__links-list .__link-item{display:inline-flex;width:100%;cursor:pointer;align-items:center;gap:calc(var(--spacing, .25rem) * 3);border-radius:var(--radius-md, .375rem);border-style:var(--tw-border-style);border-width:0px;background-color:transparent;padding-inline:calc(var(--spacing, .25rem) * 3);padding-block:calc(var(--spacing, .25rem) * 2.5);text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));text-decoration-line:none;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));color:rgb(var(--ax-sys-color-primary))}:host.--look-links .__links .__links-list .__link-item:hover{background:rgb(var(--ax-sys-color-lighter-surface))}:host.--look-links.--links-inline .__links{margin-bottom:calc(var(--spacing, .25rem) * 4);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:rgba(var(--ax-sys-color-surface));padding-bottom:calc(var(--spacing, .25rem) * 4)}:host.--look-links:not(.--links-inline) .__links-list{margin-top:0;display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 2)}:host .__item{display:flex;align-items:center;gap:calc(var(--spacing, .25rem) * 3);border-left-style:var(--tw-border-style);border-left-width:4px;border-color:transparent;padding:calc(var(--spacing, .25rem) * 2)}:host .__item.--downloadable{cursor:pointer}:host .__item.--downloadable:hover{background-color:var(--color-lighter);color:var(--color-on-lighter);border-color:var(--color-border-lighter)}:host .__item:not(.--downloadable){cursor:default}:host .__item.--removed{border-color:rgba(var(--ax-sys-color-danger-500));background-color:rgba(var(--ax-sys-color-danger-50))}:host .__item.--attached{border-color:rgba(var(--ax-sys-color-success-500));animation:attached-flash 1s ease-out forwards}:host .__item.--item-readonly:not(.--removed){opacity:.85}:host .__item .__icon{width:calc(var(--spacing, .25rem) * 6);flex-shrink:0}:host .__item .__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}:host .__item .__content{display:flex;min-width:0;flex:1;flex-direction:column;gap:calc(var(--spacing, .25rem) * .5)}:host .__item .__primary{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:var(--text-sm, .875rem);line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500)}:host .__item .__secondary{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;text-align:start;font-size:var(--text-xs, .75rem);line-height:var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));color:var(--ax-sys-color-text-secondary)}:host .__item.--with-meta .__content{justify-content:center}:host .__item .__actions{margin-left:auto;display:flex;flex-shrink:0}@keyframes attached-flash{0%{background-color:rgb(var(--ax-sys-color-success-50))}to{background-color:transparent}}.__empty-state{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.__empty-state:hover{opacity:80%}.__empty-state.__empty-state--links{padding-block:calc(var(--spacing, .25rem) * 2)}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-duration{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-border-style: solid;--tw-duration: initial}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"] }]
3679
3696
  }], propDecorators: { onRemove: [{ type: i0.Output, args: ["onRemove"] }], onRevert: [{ type: i0.Output, args: ["onRevert"] }], onRename: [{ type: i0.Output, args: ["onRename"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], fileEditable: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileEditable", required: false }] }], enableTitleDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableTitleDescription", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], look: [{ type: i0.Input, args: [{ isSignal: true, alias: "look", required: false }] }], titleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleKey", required: false }] }], showLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLabel", required: false }] }], linksLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "linksLayout", required: false }] }], files: [{ type: i0.Input, args: [{ isSignal: true, alias: "files", required: false }] }], plugins: [{ type: i0.Input, args: [{ isSignal: true, alias: "plugins", required: false }] }], excludePlugins: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludePlugins", required: false }] }], capabilities: [{ type: i0.Input, args: [{ isSignal: true, alias: "capabilities", required: false }] }] } });
3680
3697
 
3681
3698
  //#region ---- Attachment fingerprint (no blob comparison) ----
@@ -3971,6 +3988,36 @@ function resolveFileUploaderEntityScope(options, rowData, fieldFallback) {
3971
3988
  }
3972
3989
  //#endregion
3973
3990
 
3991
+ //#region ---- Hierarchical entity list query ----
3992
+ /**
3993
+ * Restricts master-list queries to root rows when the entity uses {@link AXPEntity.parentKey}.
3994
+ * Child rows are loaded lazily on expand (filter parentKey = parent id).
3995
+ */
3996
+ function applyHierarchicalRootListFilter(request, parentKey) {
3997
+ const key = parentKey.trim();
3998
+ if (!key) {
3999
+ return request;
4000
+ }
4001
+ const filter = request.filter;
4002
+ if (filter?.filters?.length && filter.field !== key) {
4003
+ return request;
4004
+ }
4005
+ if (filter?.field === key) {
4006
+ return request;
4007
+ }
4008
+ return {
4009
+ ...request,
4010
+ filter: {
4011
+ value: true,
4012
+ field: key,
4013
+ operator: {
4014
+ type: 'isEmpty',
4015
+ },
4016
+ },
4017
+ };
4018
+ }
4019
+ //#endregion
4020
+
3974
4021
  class AXPEntityStorageService {
3975
4022
  }
3976
4023
  class AXPEntityDataProvider {
@@ -4048,15 +4095,7 @@ class AXMEntityCrudServiceImpl {
4048
4095
  try {
4049
4096
  const entityRef = await this.entityRegistery.resolve(moduleName, entityName);
4050
4097
  if (entityRef.parentKey) {
4051
- if (!request.filter?.filters?.length && request.filter?.field !== entityRef.parentKey) {
4052
- request.filter = {
4053
- value: true,
4054
- field: entityRef.parentKey,
4055
- operator: {
4056
- type: 'isEmpty',
4057
- },
4058
- };
4059
- }
4098
+ request = applyHierarchicalRootListFilter(request, entityRef.parentKey);
4060
4099
  }
4061
4100
  }
4062
4101
  catch (error) {
@@ -5631,1118 +5670,1276 @@ var updateEntity_command = /*#__PURE__*/Object.freeze({
5631
5670
  AXPUpdateEntityCommand: AXPUpdateEntityCommand
5632
5671
  });
5633
5672
 
5634
- class AXPViewEntityDetailsCommand {
5635
- constructor() {
5636
- //#region ---- Services & Dependencies ----
5637
- this.entityForm = inject(AXPEntityFormBuilderService);
5638
- this.entityService = inject(AXPEntityDefinitionRegistryService);
5639
- this.translationService = inject(AXTranslationService);
5640
- this.expressionEvaluator = inject(AXPExpressionEvaluatorService);
5641
- this.contextEvalFactory = inject(AXPContextEvalFactory);
5673
+ /**
5674
+ * Error type that can be thrown by middlewares to abort the chain with a structured payload.
5675
+ */
5676
+ class AXPMiddlewareAbortError extends Error {
5677
+ constructor(message, options = {}) {
5678
+ super(message);
5679
+ this.message = message;
5680
+ this.options = options;
5681
+ this.name = 'AXPMiddlewareAbortError';
5642
5682
  }
5643
- //#endregion
5644
- //#region ---- Command Execution ----
5645
- async execute(input) {
5646
- const excludeProperties = input['excludeProperties'] ??
5647
- input?.options?.excludeProperties ??
5648
- input?.__context__?.options?.excludeProperties;
5649
- const { entity, entityInfo, data } = input.__context__;
5650
- const [moduleName, entityName] = (entity || '').split('.');
5651
- if (!moduleName || !entityName) {
5652
- return {
5653
- success: false,
5654
- message: {
5655
- text: await this.translationService.translateAsync('@general:messages.entity.invalid-identifier'),
5656
- },
5657
- };
5683
+ toResponse() {
5684
+ return {
5685
+ success: false,
5686
+ error: {
5687
+ code: this.options.code,
5688
+ message: this.message,
5689
+ status: this.options.status,
5690
+ details: this.options.details,
5691
+ },
5692
+ };
5693
+ }
5694
+ }
5695
+ /** Type guard for AXPMiddlewareAbortError */
5696
+ function isAXPMiddlewareAbortError(error) {
5697
+ return error instanceof AXPMiddlewareAbortError;
5698
+ }
5699
+ //#endregion
5700
+
5701
+ /**
5702
+ * Maps storage middleware failures to a command result the details view can surface.
5703
+ */
5704
+ function mapEntityStorageErrorToCommandResult(error, fallback = 'Failed to update entity') {
5705
+ if (isAXPMiddlewareAbortError(error)) {
5706
+ return {
5707
+ success: false,
5708
+ message: {
5709
+ type: 'error',
5710
+ code: error.options.code,
5711
+ text: error.message,
5712
+ details: error.options.details,
5713
+ },
5714
+ };
5715
+ }
5716
+ const text = error instanceof Error ? error.message : typeof error === 'string' ? error : fallback;
5717
+ return {
5718
+ success: false,
5719
+ message: {
5720
+ type: 'error',
5721
+ text,
5722
+ },
5723
+ };
5724
+ }
5725
+ //#region ---- EntityDataAccessor ----
5726
+ /**
5727
+ * Data accessor for entity operations.
5728
+ * Provides methods to query and execute commands with smart resolution:
5729
+ * 1. Checks entity definition queries/commands first
5730
+ * 2. Falls back to CRUD service if not defined
5731
+ */
5732
+ class EntityDataAccessor {
5733
+ constructor(module, entity, entityRegistry, queryExecutor, commandService, injector) {
5734
+ this.module = module;
5735
+ this.entity = entity;
5736
+ this.entityRegistry = entityRegistry;
5737
+ this.queryExecutor = queryExecutor;
5738
+ this.commandService = commandService;
5739
+ this.injector = injector;
5740
+ this.crudService = null;
5741
+ this.entityDef = null;
5742
+ this.entityDefPromise = null;
5743
+ }
5744
+ /**
5745
+ * Lazily creates the CRUD fallback inside an injection context so {@link AXMEntityCrudServiceImpl}
5746
+ * can resolve {@link AXPEntityStorageService} via `inject()`.
5747
+ */
5748
+ ensureCrudService() {
5749
+ if (!this.crudService) {
5750
+ this.crudService = runInInjectionContext(this.injector, () => new AXMEntityCrudServiceImpl(`${this.module}.${this.entity}`));
5658
5751
  }
5659
- const entityDef = await this.entityService.resolve(moduleName, entityName);
5660
- let dialogRef;
5661
- try {
5662
- if (!data || (typeof data !== 'string' && typeof data !== 'object')) {
5663
- return {
5664
- success: false,
5665
- message: {
5666
- text: await this.translationService.translateAsync('@general:messages.entity.invalid-data'),
5667
- },
5668
- };
5669
- }
5670
- let chain = this.entityForm.entity(`${moduleName}.${entityName}`).single(data);
5671
- if (excludeProperties && excludeProperties.length > 0) {
5672
- chain = chain.exclude(...excludeProperties);
5673
- }
5674
- const dialogTitle = await this.resolveDialogTitle(entityDef, data);
5675
- if (dialogTitle) {
5676
- chain = chain.title(dialogTitle);
5677
- }
5678
- chain = chain.actions((actions) => {
5679
- actions.submit('@general:actions.ok.title');
5680
- });
5681
- dialogRef = await chain.show();
5682
- dialogRef.close();
5683
- return {
5684
- success: true,
5685
- metadata: {
5686
- refresh: false,
5687
- },
5688
- };
5752
+ return this.crudService;
5753
+ }
5754
+ /**
5755
+ * Resolves the entity definition lazily and caches it.
5756
+ * @returns The resolved entity definition
5757
+ */
5758
+ async resolveEntityDef() {
5759
+ if (this.entityDef) {
5760
+ return this.entityDef;
5689
5761
  }
5690
- catch (error) {
5691
- return {
5692
- success: false,
5693
- message: {
5694
- text: error instanceof Error
5695
- ? error.message
5696
- : await this.translationService.translateAsync('@general:messages.entity.view-failed'),
5697
- },
5698
- };
5762
+ if (!this.entityDefPromise) {
5763
+ this.entityDefPromise = this.entityRegistry.resolve(this.module, this.entity);
5699
5764
  }
5700
- finally {
5701
- if (dialogRef && typeof dialogRef === 'object' && dialogRef !== null && 'setLoading' in dialogRef) {
5702
- dialogRef.setLoading(false);
5703
- }
5765
+ this.entityDef = await this.entityDefPromise;
5766
+ if (!this.entityDef) {
5767
+ throw new Error(`Entity "${this.module}.${this.entity}" could not be resolved`);
5704
5768
  }
5769
+ return this.entityDef;
5705
5770
  }
5706
- //#endregion
5707
- //#region ---- Title resolution ----
5708
- async resolveDialogTitle(entityDef, data) {
5709
- const titleTemplate = entityDef.interfaces?.master?.single?.title ??
5710
- entityDef.formats?.searchResult?.title ??
5711
- entityDef.formats?.individual;
5712
- if (!titleTemplate || typeof titleTemplate !== 'string') {
5713
- return this.translationService.translateAsync('@general:actions.detail.title');
5714
- }
5715
- if (!titleTemplate.includes('{{')) {
5716
- const translated = await this.translationService.translateAsync(titleTemplate);
5717
- return resolveMultiLanguageString(translated, this.translationService.getActiveLang()).trim() || translated;
5771
+ /**
5772
+ * Gets a single entity by key.
5773
+ * @param key - The entity key
5774
+ * @returns The entity detail or null if not found
5775
+ */
5776
+ async byKey(key) {
5777
+ const entityDef = await this.resolveEntityDef();
5778
+ const execute = entityDef.queries?.byKey?.execute;
5779
+ if (typeof execute === 'function') {
5780
+ return await execute(key);
5718
5781
  }
5719
- const recordContext = await this.loadRecordContext(`${entityDef.module}.${entityDef.name}`, data);
5720
- const scope = {
5721
- context: this.contextEvalFactory.createScope(recordContext),
5722
- };
5723
- const evaluated = await this.expressionEvaluator.evaluate(titleTemplate, scope);
5724
- const locale = this.translationService.getActiveLang();
5725
- const text = resolveMultiLanguageString(evaluated, locale).trim() || String(evaluated ?? '').trim();
5726
- if (text) {
5727
- return text;
5782
+ else if (typeof execute === 'string') {
5783
+ return await this.queryExecutor.fetch(execute, key);
5728
5784
  }
5729
- return this.translationService.translateAsync('@general:actions.detail.title');
5785
+ // Fallback to CRUD service
5786
+ return await this.ensureCrudService().getOne(key);
5730
5787
  }
5731
- async loadRecordContext(entityFullName, data) {
5732
- if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
5733
- const hasNestedFields = get(data, 'subject.title') != null ||
5734
- get(data, 'recordType.title') != null ||
5735
- get(data, 'occurredDate') != null;
5736
- if (hasNestedFields) {
5737
- return data;
5738
- }
5739
- }
5740
- const recordId = typeof data === 'string' ? data : String(get(data, 'id') ?? '');
5741
- if (!recordId) {
5742
- return typeof data === 'object' && data !== null ? data : {};
5788
+ /**
5789
+ * Queries a list of entities.
5790
+ * @param request - The query request (optional)
5791
+ * @returns The paged list result
5792
+ */
5793
+ async query(request) {
5794
+ const entityDef = await this.resolveEntityDef();
5795
+ const defaultRequest = { skip: 0, take: 10 };
5796
+ let queryRequest = request || defaultRequest;
5797
+ if (entityDef.parentKey) {
5798
+ queryRequest = applyHierarchicalRootListFilter(queryRequest, entityDef.parentKey);
5743
5799
  }
5744
- try {
5745
- const [moduleName, entityName] = entityFullName.split('.');
5746
- const entity = await this.entityService.resolve(moduleName, entityName);
5747
- const byKeyFn = entity?.queries?.byKey?.execute;
5748
- if (typeof byKeyFn === 'function') {
5749
- return (await byKeyFn(recordId)) ?? {};
5750
- }
5800
+ const execute = entityDef.queries?.list?.execute;
5801
+ if (typeof execute === 'function') {
5802
+ return await execute(queryRequest);
5751
5803
  }
5752
- catch {
5753
- // Fall back to list row payload below.
5804
+ else if (typeof execute === 'string') {
5805
+ const result = await this.queryExecutor.fetch(execute, queryRequest);
5806
+ if (!result) {
5807
+ throw new Error(`Query "${execute}" returned null`);
5808
+ }
5809
+ return result;
5754
5810
  }
5755
- return typeof data === 'object' && data !== null ? data : {};
5756
- }
5757
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPViewEntityDetailsCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5758
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPViewEntityDetailsCommand, providedIn: 'root' }); }
5759
- }
5760
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPViewEntityDetailsCommand, decorators: [{
5761
- type: Injectable,
5762
- args: [{
5763
- providedIn: 'root',
5764
- }]
5765
- }] });
5766
-
5767
- var viewEntityDetails_command = /*#__PURE__*/Object.freeze({
5768
- __proto__: null,
5769
- AXPViewEntityDetailsCommand: AXPViewEntityDetailsCommand
5770
- });
5771
-
5772
- /** Common keys on denormalized expose stubs (e.g. `{ id, displayName }` from lookup expose). */
5773
- const LOOKUP_EXPOSE_STUB_DISPLAY_FIELDS = ['displayName', 'title', 'name', 'fullName', 'code'];
5774
- function resolveLookupItemFieldValue(item, field) {
5775
- const direct = get(item, field);
5776
- if (direct != null && direct !== '') {
5777
- return direct;
5811
+ // Fallback to CRUD service
5812
+ return await this.ensureCrudService().query(queryRequest);
5778
5813
  }
5779
- if (!field.includes('.')) {
5780
- return direct;
5814
+ /**
5815
+ * Queries category entities linked to this entity (`{module}.{entity}Category`).
5816
+ * Falls back to CRUD category storage (same as {@link AXMEntityCrudServiceImpl.getCategoriesList}).
5817
+ */
5818
+ async getCategoriesList(request = { skip: 0, take: 1000 }) {
5819
+ return await this.ensureCrudService().getCategoriesList(request);
5781
5820
  }
5782
- for (const fallback of LOOKUP_EXPOSE_STUB_DISPLAY_FIELDS) {
5783
- const candidate = get(item, fallback);
5784
- if (candidate != null && candidate !== '') {
5785
- return candidate;
5821
+ /**
5822
+ * Creates a new entity.
5823
+ * @param data - The entity data to create
5824
+ * @returns The created entity key
5825
+ */
5826
+ async create(data) {
5827
+ const entityDef = await this.resolveEntityDef();
5828
+ const execute = entityDef.commands?.create?.execute;
5829
+ if (typeof execute === 'function') {
5830
+ const result = await execute(data);
5831
+ return result?.id || result;
5786
5832
  }
5833
+ else if (typeof execute === 'string') {
5834
+ const result = await this.commandService.execute(execute, data);
5835
+ if (!result?.success) {
5836
+ throw new Error(commandMessageTextForError(result?.message?.text) || 'Failed to create entity');
5837
+ }
5838
+ return result.data?.id || result.data;
5839
+ }
5840
+ // Fallback to CRUD service
5841
+ return await this.ensureCrudService().insertOne(data);
5787
5842
  }
5788
- return direct;
5789
- }
5790
- function formatLookupFieldValue(raw, locale, translationService, resolveMultiLanguage) {
5791
- if (raw == null || raw === '') {
5792
- return '';
5793
- }
5794
- if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
5795
- return String(raw);
5843
+ /**
5844
+ * Updates an existing entity.
5845
+ * @param id - The entity key
5846
+ * @param data - The entity data to update
5847
+ * @returns The updated entity
5848
+ */
5849
+ async update(id, data) {
5850
+ const entityDef = await this.resolveEntityDef();
5851
+ const execute = entityDef.commands?.update?.execute;
5852
+ const updateData = { id, ...data };
5853
+ if (typeof execute === 'function') {
5854
+ return await execute(updateData);
5855
+ }
5856
+ else if (typeof execute === 'string') {
5857
+ const result = await this.commandService.execute(execute, updateData);
5858
+ if (!result?.success) {
5859
+ throw new Error(commandMessageTextForError(result?.message?.text) || 'Failed to update entity');
5860
+ }
5861
+ return result.data;
5862
+ }
5863
+ // Fallback to CRUD service
5864
+ return await this.ensureCrudService().updateOne(id, data);
5796
5865
  }
5797
- if (typeof raw === 'object' && !Array.isArray(raw)) {
5798
- if (resolveMultiLanguage) {
5799
- return resolveMultiLanguage(raw);
5866
+ /**
5867
+ * Deletes an entity.
5868
+ * @param id - The entity key
5869
+ */
5870
+ async delete(id) {
5871
+ const entityDef = await this.resolveEntityDef();
5872
+ const execute = entityDef.commands?.delete?.execute;
5873
+ if (typeof execute === 'function') {
5874
+ await execute(id);
5875
+ return;
5800
5876
  }
5801
- return resolveSearchableText(raw, locale, translationService);
5877
+ else if (typeof execute === 'string') {
5878
+ const result = await this.commandService.execute(execute, id);
5879
+ if (!result?.success) {
5880
+ throw new Error(commandMessageTextForError(result?.message?.text) || 'Failed to delete entity');
5881
+ }
5882
+ return;
5883
+ }
5884
+ // Fallback to CRUD service
5885
+ return await this.ensureCrudService().deleteOne(id);
5802
5886
  }
5803
- return String(raw);
5804
5887
  }
5805
- //#region ---- Template resolution ----
5888
+ //#endregion
5889
+ //#region ---- EntityBuilder ----
5806
5890
  /**
5807
- * Resolves the display template for a lookup item.
5808
- * Priority: explicit `displayFormat` → entity `formats.lookup` (template) → entity `formats.searchResult.title`.
5809
- * Returns undefined when `textField` is set or no template applies (use {@link resolveLookupDisplayField} instead).
5891
+ * Builder class for entity operations.
5892
+ * Provides methods to create paths and access entity data.
5810
5893
  */
5811
- function resolveLookupDisplayTemplate(entity, options) {
5812
- const explicit = options.displayFormat?.trim();
5813
- if (explicit) {
5814
- return normalizeEntityDisplayTemplate(explicit);
5815
- }
5816
- if (options.textField?.trim()) {
5817
- return undefined;
5818
- }
5819
- const lookupFormat = entity?.formats?.lookup?.trim();
5820
- if (lookupFormat && (lookupFormat.includes('{{') || lookupFormat.includes('{'))) {
5821
- return normalizeEntityDisplayTemplate(lookupFormat);
5822
- }
5823
- const searchTitle = entity?.formats?.searchResult?.title?.trim();
5824
- if (searchTitle) {
5825
- return normalizeEntityDisplayTemplate(searchTitle);
5894
+ class EntityBuilder {
5895
+ constructor(module, entity, sessionService, entityRegistry, queryExecutor, commandService, injector) {
5896
+ this.sessionService = sessionService;
5897
+ this.entityRegistry = entityRegistry;
5898
+ this.queryExecutor = queryExecutor;
5899
+ this.commandService = commandService;
5900
+ this.injector = injector;
5901
+ this.module = module;
5902
+ this.entity = entity;
5826
5903
  }
5827
- return undefined;
5828
- }
5829
- /**
5830
- * Resolves the property path used for lookup display when no template applies.
5831
- * Priority: explicit `textField` → entity `formats.lookup` (simple path) → common property names.
5832
- */
5833
- function resolveLookupDisplayField(entity, options) {
5834
- if (options.textField?.trim()) {
5835
- return options.textField.trim();
5904
+ /**
5905
+ * Creates a path for the entity.
5906
+ * @param path - The path segment (defaults to 'list')
5907
+ * @returns The full path string
5908
+ */
5909
+ createPath(path = 'list') {
5910
+ return `/${this.sessionService.application?.name}/m/${this.module}/e/${this.entity}/${path}`;
5836
5911
  }
5837
- const lookupFormat = entity?.formats?.lookup?.trim();
5838
- if (lookupFormat && !lookupFormat.includes('{') && !lookupFormat.includes('{{')) {
5839
- return lookupFormat;
5912
+ /**
5913
+ * Gets the data accessor for the entity with generic type support.
5914
+ * Entity definition is resolved lazily on first method call.
5915
+ * @returns An EntityDataAccessor instance
5916
+ */
5917
+ data() {
5918
+ return new EntityDataAccessor(this.module, this.entity, this.entityRegistry, this.queryExecutor, this.commandService, this.injector);
5840
5919
  }
5841
- const defaults = ['title', 'name', 'code', 'description'];
5842
- const found = defaults.find((name) => entity?.properties?.some((p) => p.name === name));
5843
- return found ?? 'title';
5844
5920
  }
5845
- /**
5846
- * Formats a lookup row for display using template and/or field resolution (display tier).
5847
- */
5848
- async function formatLookupItemDisplayAsync(item, entity, options, formatService, displayDeps, resolveMultiLanguage) {
5849
- if (item == null) {
5850
- return '';
5851
- }
5852
- if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
5853
- return String(item);
5854
- }
5855
- if (typeof item !== 'object') {
5856
- return '';
5921
+ //#endregion
5922
+ class AXPEntityService {
5923
+ constructor() {
5924
+ this.sessionService = inject(AXPSessionService);
5925
+ this.entityRegistry = inject(AXPEntityDefinitionRegistryService);
5926
+ this.queryExecutor = inject(AXPQueryExecutor);
5927
+ this.commandService = inject(AXPCommandService);
5928
+ this.injector = inject(Injector);
5857
5929
  }
5858
- if (options.isMultiLanguage?.(item) && resolveMultiLanguage) {
5859
- return resolveMultiLanguage(item);
5930
+ /**
5931
+ * @deprecated Use `withEntity(module, entity).createPath(path)` instead.
5932
+ * This method will be removed in a future version.
5933
+ */
5934
+ createPath(module, entity, path = 'list') {
5935
+ return `/${this.sessionService.application?.name}/m/${module}/e/${entity}/${path}`;
5860
5936
  }
5861
- const template = resolveLookupDisplayTemplate(entity, options);
5862
- if (template) {
5863
- if (template.includes('{{') || template.includes('context.eval')) {
5864
- const evaluated = await evaluateEntityDisplayTemplate(template, item, displayDeps.expressionEvaluator, displayDeps.locale, displayDeps.translationService, displayDeps.contextEvalFactory);
5865
- const text = sanitizeResolvedEntityDisplayTitle(resolveSearchableText(evaluated, displayDeps.locale, displayDeps.translationService));
5866
- if (text && !isUnresolvedEntityDisplayTemplate(text)) {
5867
- return text;
5868
- }
5937
+ withEntity(moduleOrEntityKey, entity) {
5938
+ let module;
5939
+ let entityName;
5940
+ if (entity !== undefined) {
5941
+ // Two parameters: module and entity
5942
+ module = moduleOrEntityKey;
5943
+ entityName = entity;
5869
5944
  }
5870
5945
  else {
5871
- const formatted = formatService.format(template, 'string', item);
5872
- if (formatted) {
5873
- const text = String(formatted);
5874
- if (!isUnresolvedEntityDisplayTemplate(text)) {
5875
- return text;
5876
- }
5946
+ // Single parameter: "module.entity" format
5947
+ const dotIndex = moduleOrEntityKey.indexOf('.');
5948
+ if (dotIndex === -1) {
5949
+ throw new Error(`Invalid entity key format: "${moduleOrEntityKey}". Expected format: "module.entity"`);
5877
5950
  }
5951
+ module = moduleOrEntityKey.substring(0, dotIndex);
5952
+ entityName = moduleOrEntityKey.substring(dotIndex + 1);
5878
5953
  }
5954
+ return new EntityBuilder(module, entityName, this.sessionService, this.entityRegistry, this.queryExecutor, this.commandService, this.injector);
5879
5955
  }
5880
- const field = resolveLookupDisplayField(entity, options);
5881
- const raw = typeof item === 'object' && item != null ? resolveLookupItemFieldValue(item, field) : undefined;
5882
- return formatLookupFieldValue(raw, displayDeps.locale, displayDeps.translationService, resolveMultiLanguage);
5956
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5957
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityService, providedIn: 'root' }); }
5883
5958
  }
5959
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityService, decorators: [{
5960
+ type: Injectable,
5961
+ args: [{ providedIn: 'root' }]
5962
+ }] });
5963
+
5964
+ //#endregion
5965
+ //#region ---- Update entity status command ----
5884
5966
  /**
5885
- * @deprecated Prefer {@link formatLookupItemDisplayAsync} for expression templates with `context.eval`.
5967
+ * Default handler for status-chip transitions when no custom transition.command is set.
5968
+ * Patches only the status field via the entity update path (no edit dialog).
5886
5969
  */
5887
- function formatLookupItemDisplay(item, entity, options, formatService, resolveMultiLanguage) {
5888
- if (item == null) {
5889
- return '';
5970
+ class AXPUpdateEntityStatusCommand {
5971
+ constructor() {
5972
+ this.entityService = inject(AXPEntityService);
5973
+ this.entityRegistry = inject(AXPEntityDefinitionRegistryService);
5974
+ this.translationService = inject(AXTranslationService);
5975
+ this.workflowEventService = inject(AXPWorkflowEventService);
5890
5976
  }
5891
- if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
5892
- return String(item);
5893
- }
5894
- if (typeof item !== 'object') {
5895
- return '';
5896
- }
5897
- if (options.isMultiLanguage?.(item) && resolveMultiLanguage) {
5898
- return resolveMultiLanguage(item);
5899
- }
5900
- const template = resolveLookupDisplayTemplate(entity, options);
5901
- if (template && !template.includes('context.eval') && !template.includes('{{')) {
5902
- const formatted = formatService.format(template, 'string', item);
5903
- if (formatted) {
5904
- const text = String(formatted);
5905
- if (!isUnresolvedEntityDisplayTemplate(text)) {
5906
- return text;
5907
- }
5977
+ async execute(input) {
5978
+ const entity = input?.entity?.trim();
5979
+ const status = input?.status;
5980
+ const statusField = (input?.statusField ?? 'statusId').trim() || 'statusId';
5981
+ const ids = normalizeIds(input);
5982
+ if (!entity) {
5983
+ return {
5984
+ success: false,
5985
+ message: {
5986
+ text: await this.translationService.translateAsync('@general:messages.entity.invalid-identifier'),
5987
+ },
5988
+ };
5908
5989
  }
5909
- }
5910
- const field = resolveLookupDisplayField(entity, options);
5911
- const raw = typeof item === 'object' && item != null ? resolveLookupItemFieldValue(item, field) : undefined;
5912
- if (raw == null || raw === '') {
5913
- return '';
5914
- }
5915
- if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
5916
- return String(raw);
5917
- }
5918
- if (typeof raw === 'object' && !Array.isArray(raw)) {
5919
- if (resolveMultiLanguage) {
5920
- return resolveMultiLanguage(raw);
5990
+ if (status == null || String(status).length === 0) {
5991
+ return {
5992
+ success: false,
5993
+ message: { text: 'Status value is required.' },
5994
+ };
5921
5995
  }
5922
- return raw;
5923
- }
5924
- return String(raw);
5925
- }
5926
- /** Matches a resolved platform entity key such as `BusinessCore.Party`. */
5927
- const LOOKUP_ENTITY_KEY_PATTERN = /^[A-Za-z][\w-]*\.[A-Za-z][\w-]*$/;
5928
- /**
5929
- * Resolves the target entity key for a lookup column.
5930
- * Widget column options may still contain unevaluated `context.eval(...)` expressions;
5931
- * in that case the type is read from the row (e.g. `ownerRef.type`, `subjectRef.type`).
5932
- */
5933
- function resolveLookupEntityKey(entityOption, rowData, columnName, expose) {
5934
- const fromOption = typeof entityOption === 'string' ? entityOption.trim() : '';
5935
- if (fromOption && LOOKUP_ENTITY_KEY_PATTERN.test(fromOption)) {
5936
- return fromOption;
5937
- }
5938
- const typePath = resolvePolymorphicRefTypePath(columnName, fromOption, expose);
5939
- if (typePath && rowData) {
5940
- const typeValue = get(rowData, typePath);
5941
- if (typeof typeValue === 'string') {
5942
- const trimmed = typeValue.trim();
5943
- if (LOOKUP_ENTITY_KEY_PATTERN.test(trimmed)) {
5944
- return trimmed;
5996
+ if (ids.length === 0) {
5997
+ return {
5998
+ success: false,
5999
+ message: { text: 'Either id or ids must be provided.' },
6000
+ };
6001
+ }
6002
+ const [moduleName, entityName] = entity.split('.');
6003
+ if (!moduleName || !entityName) {
6004
+ return {
6005
+ success: false,
6006
+ message: {
6007
+ text: await this.translationService.translateAsync('@general:messages.entity.invalid-identifier'),
6008
+ },
6009
+ };
6010
+ }
6011
+ try {
6012
+ const entityRef = await this.entityRegistry.resolve(moduleName, entityName);
6013
+ const data = this.entityService.withEntity(entity).data();
6014
+ const updatedRecords = [];
6015
+ for (const id of ids) {
6016
+ const updated = await data.update(id, { [statusField]: status });
6017
+ const record = updated && typeof updated === 'object'
6018
+ ? updated
6019
+ : { id, [statusField]: status };
6020
+ updatedRecords.push(record);
6021
+ dispatchEntityModifiedSideEffects(this.workflowEventService, entity, { id, ...record, [statusField]: status }, entityRef?.parentKey);
5945
6022
  }
6023
+ return {
6024
+ success: true,
6025
+ data: ids.length === 1 ? updatedRecords[0] : updatedRecords,
6026
+ };
6027
+ }
6028
+ catch (error) {
6029
+ const text = error instanceof Error
6030
+ ? error.message
6031
+ : await this.translationService.translateAsync('@general:messages.entity.update-failed');
6032
+ return {
6033
+ success: false,
6034
+ message: { text },
6035
+ };
5946
6036
  }
5947
6037
  }
5948
- return fromOption;
6038
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPUpdateEntityStatusCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6039
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPUpdateEntityStatusCommand, providedIn: 'root' }); }
5949
6040
  }
5950
- function resolvePolymorphicRefTypePath(columnName, entityOption, expose) {
5951
- const fromExpression = entityOption.match(/context\.eval\(\s*['"]([^'"]+)['"]\s*\)/)?.[1];
5952
- if (fromExpression) {
5953
- return fromExpression;
6041
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPUpdateEntityStatusCommand, decorators: [{
6042
+ type: Injectable,
6043
+ args: [{ providedIn: 'root' }]
6044
+ }] });
6045
+ //#endregion
6046
+ //#region ---- Helpers ----
6047
+ function normalizeIds(input) {
6048
+ if (!input) {
6049
+ return [];
5954
6050
  }
5955
- if (columnName?.endsWith('.id')) {
5956
- return `${columnName.slice(0, -3)}.type`;
6051
+ if (Array.isArray(input.ids) && input.ids.length > 0) {
6052
+ return input.ids.map((id) => String(id)).filter((id) => id.length > 0);
5957
6053
  }
5958
- const idTarget = expose?.find((entry) => entry.source === 'id')?.target;
5959
- if (idTarget?.endsWith('.id')) {
5960
- return `${idTarget.slice(0, -3)}.type`;
6054
+ if (input.id != null && String(input.id).length > 0) {
6055
+ return [String(input.id)];
5961
6056
  }
5962
- return undefined;
6057
+ return [];
5963
6058
  }
5964
- //#endregion
5965
6059
 
5966
- class AXPEntityDetailPopoverComponent {
6060
+ var updateEntityStatus_command = /*#__PURE__*/Object.freeze({
6061
+ __proto__: null,
6062
+ AXPUpdateEntityStatusCommand: AXPUpdateEntityStatusCommand
6063
+ });
6064
+
6065
+ /**
6066
+ * Build-time only. Do not import from Angular modules.
6067
+ */
6068
+ const axpUpdateEntityStatusCommandDefinition = defineCommand({
6069
+ key: 'Entity:UpdateStatus',
6070
+ title: 'Update entity status',
6071
+ description: 'AXPUpdateEntityStatusCommand patches a status field on one or more records (entity, status, statusField, id|ids) via the entity update path; used as the default status-chip transition command.',
6072
+ executionMode: 'frontend',
6073
+ inputs: [
6074
+ {
6075
+ name: 'entity',
6076
+ title: 'Entity',
6077
+ description: 'Full entity key (Module.Entity).',
6078
+ interface: { name: 'text-editor' },
6079
+ validations: [{ rule: 'required' }],
6080
+ },
6081
+ {
6082
+ name: 'status',
6083
+ title: 'Status',
6084
+ description: 'Target status name/value to write.',
6085
+ interface: { name: 'text-editor' },
6086
+ validations: [{ rule: 'required' }],
6087
+ },
6088
+ {
6089
+ name: 'statusField',
6090
+ title: 'Status field',
6091
+ description: 'Property path to patch (default statusId).',
6092
+ interface: { name: 'text-editor' },
6093
+ },
6094
+ {
6095
+ name: 'id',
6096
+ title: 'Record id',
6097
+ description: 'Single record id (use ids for bulk).',
6098
+ interface: { name: 'text-editor' },
6099
+ },
6100
+ {
6101
+ name: 'ids',
6102
+ title: 'Record ids',
6103
+ description: 'Bulk record ids.',
6104
+ interface: { name: 'json' },
6105
+ },
6106
+ ],
6107
+ outcomes: {
6108
+ mode: 'dynamic',
6109
+ summary: 'success with updated record(s); failure when entity/status/id missing or update throws.',
6110
+ derivesFrom: 'AXPExecuteCommandResult from execute()',
6111
+ },
6112
+ capabilities: ['ai'],
6113
+ ai: {
6114
+ tags: ['entity', 'status', 'update'],
6115
+ },
6116
+ categories: [AXP_COMMAND_DEFINITION_CATEGORY_ENTITY],
6117
+ });
6118
+
6119
+ class AXPViewEntityDetailsCommand {
5967
6120
  constructor() {
5968
- //#region ---- Dependencies ----
5969
- this.commandService = inject(AXPCommandService);
5970
- this.queryService = inject(AXPQueryService);
5971
- this.formatService = inject(AXFormatService);
5972
- this.translation = inject(AXTranslationService);
6121
+ //#region ---- Services & Dependencies ----
6122
+ this.entityForm = inject(AXPEntityFormBuilderService);
6123
+ this.entityService = inject(AXPEntityDefinitionRegistryService);
6124
+ this.translationService = inject(AXTranslationService);
5973
6125
  this.expressionEvaluator = inject(AXPExpressionEvaluatorService);
5974
6126
  this.contextEvalFactory = inject(AXPContextEvalFactory);
5975
- this.changeDetectorRef = inject(ChangeDetectorRef);
5976
- //#endregion
5977
- //#region ---- Inputs ----
5978
- this.entity = input.required(...(ngDevMode ? [{ debugName: "entity" }] : /* istanbul ignore next */ []));
5979
- this.entityId = input.required(...(ngDevMode ? [{ debugName: "entityId" }] : /* istanbul ignore next */ []));
5980
- this.textField = input('title', ...(ngDevMode ? [{ debugName: "textField" }] : /* istanbul ignore next */ []));
5981
- this.valueField = input('id', ...(ngDevMode ? [{ debugName: "valueField" }] : /* istanbul ignore next */ []));
5982
- this.displayTitle = input('', ...(ngDevMode ? [{ debugName: "displayTitle" }] : /* istanbul ignore next */ []));
5983
- this.item = input(...(ngDevMode ? [undefined, { debugName: "item" }] : /* istanbul ignore next */ []));
5984
- this.breadcrumb = input(null, ...(ngDevMode ? [{ debugName: "breadcrumb" }] : /* istanbul ignore next */ []));
5985
- this.requestClose = input(...(ngDevMode ? [undefined, { debugName: "requestClose" }] : /* istanbul ignore next */ []));
5986
- //#endregion
5987
- //#region ---- Signals ----
5988
- this.entityDetails = signal(null, ...(ngDevMode ? [{ debugName: "entityDetails" }] : /* istanbul ignore next */ []));
5989
- this.isLoadingDetails = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingDetails" }] : /* istanbul ignore next */ []));
5990
- this.headerTitleVersion = 0;
5991
- /** Placeholder rows for the loading skeleton (matches typical property count). */
5992
- this.loadingSkeletonRows = [1, 2, 3, 4, 5];
5993
- this.headerTitle = signal('', ...(ngDevMode ? [{ debugName: "headerTitle" }] : /* istanbul ignore next */ []));
5994
- /**
5995
- * Stable list of property widgets for the template. Must be a signal (computed), not a method:
5996
- * calling a method from the template rebuilds nodes every CD cycle and can make the widget renderer loop.
5997
- */
5998
- this.entityPropertiesWithWidgets = computed(() => {
5999
- const details = this.entityDetails();
6000
- const data = details?.entityData;
6001
- const entityDefinition = details?.entityDefinition;
6002
- if (!data || !entityDefinition?.properties)
6003
- return [];
6004
- return this.buildEntityPropertiesWithWidgets(data, entityDefinition, this.textField(), this.valueField());
6005
- }, ...(ngDevMode ? [{ debugName: "entityPropertiesWithWidgets" }] : /* istanbul ignore next */ []));
6006
- effect(() => {
6007
- void this.translation.getActiveLang();
6008
- const details = this.entityDetails();
6009
- const preset = this.displayTitle();
6010
- const fallbackItem = this.item();
6011
- const version = ++this.headerTitleVersion;
6012
- void this.resolveHeaderTitle(details?.entityDefinition, details?.entityData, preset, fallbackItem).then((title) => {
6013
- if (version === this.headerTitleVersion) {
6014
- this.headerTitle.set(title);
6015
- }
6016
- });
6017
- });
6018
6127
  }
6019
6128
  //#endregion
6020
- //#region ---- Public Methods ----
6021
- /** Loads entity details after the overlay panel is created. */
6022
- async loadDetails() {
6023
- this.isLoadingDetails.set(true);
6024
- this.entityDetails.set(null);
6025
- this.changeDetectorRef.detectChanges();
6129
+ //#region ---- Command Execution ----
6130
+ async execute(input) {
6131
+ const excludeProperties = input['excludeProperties'] ??
6132
+ input?.options?.excludeProperties ??
6133
+ input?.__context__?.options?.excludeProperties;
6134
+ const { entity, entityInfo, data } = input.__context__;
6135
+ const [moduleName, entityName] = (entity || '').split('.');
6136
+ if (!moduleName || !entityName) {
6137
+ return {
6138
+ success: false,
6139
+ message: {
6140
+ text: await this.translationService.translateAsync('@general:messages.entity.invalid-identifier'),
6141
+ },
6142
+ };
6143
+ }
6144
+ const entityDef = await this.entityService.resolve(moduleName, entityName);
6145
+ let dialogRef;
6026
6146
  try {
6027
- const result = await this.queryService.fetch('Entity:GetDetails', {
6028
- entity: this.entity(),
6029
- id: this.entityId(),
6030
- });
6031
- if (result?.success) {
6032
- this.entityDetails.set(result);
6147
+ if (!data || (typeof data !== 'string' && typeof data !== 'object')) {
6148
+ return {
6149
+ success: false,
6150
+ message: {
6151
+ text: await this.translationService.translateAsync('@general:messages.entity.invalid-data'),
6152
+ },
6153
+ };
6154
+ }
6155
+ let chain = this.entityForm.entity(`${moduleName}.${entityName}`).single(data);
6156
+ if (excludeProperties && excludeProperties.length > 0) {
6157
+ chain = chain.exclude(...excludeProperties);
6158
+ }
6159
+ const dialogTitle = await this.resolveDialogTitle(entityDef, data);
6160
+ if (dialogTitle) {
6161
+ chain = chain.title(dialogTitle);
6033
6162
  }
6163
+ chain = chain.actions((actions) => {
6164
+ actions.submit('@general:actions.ok.title');
6165
+ });
6166
+ dialogRef = await chain.show();
6167
+ dialogRef.close();
6168
+ return {
6169
+ success: true,
6170
+ metadata: {
6171
+ refresh: false,
6172
+ },
6173
+ };
6034
6174
  }
6035
6175
  catch (error) {
6036
- console.error('Failed to fetch entity details:', error);
6037
- }
6038
- finally {
6039
- this.isLoadingDetails.set(false);
6040
- this.changeDetectorRef.detectChanges();
6176
+ return {
6177
+ success: false,
6178
+ message: {
6179
+ text: error instanceof Error
6180
+ ? error.message
6181
+ : await this.translationService.translateAsync('@general:messages.entity.view-failed'),
6182
+ },
6183
+ };
6041
6184
  }
6042
- }
6043
- async navigateToDetails() {
6044
- if (this.entityId()) {
6045
- await this.commandService.execute('Entity:OpenDetails', {
6046
- entity: this.entity(),
6047
- data: { id: this.entityId() },
6048
- });
6049
- this.requestClose()?.();
6185
+ finally {
6186
+ if (dialogRef && typeof dialogRef === 'object' && dialogRef !== null && 'setLoading' in dialogRef) {
6187
+ dialogRef.setLoading(false);
6188
+ }
6050
6189
  }
6051
6190
  }
6052
6191
  //#endregion
6053
- //#region ---- Helper Methods ----
6054
- async resolveHeaderTitle(entityDefinition, data, preset, fallbackItem) {
6055
- if (data && entityDefinition) {
6056
- const fromDetails = await this.resolveLookupDisplayLabelAsync(data, entityDefinition);
6057
- if (fromDetails) {
6058
- return fromDetails;
6192
+ //#region ---- Title resolution ----
6193
+ async resolveDialogTitle(entityDef, data) {
6194
+ const titleTemplate = entityDef.interfaces?.master?.single?.title ??
6195
+ entityDef.formats?.searchResult?.title ??
6196
+ entityDef.formats?.individual;
6197
+ if (!titleTemplate || typeof titleTemplate !== 'string') {
6198
+ return this.translationService.translateAsync('@general:actions.detail.title');
6199
+ }
6200
+ if (!titleTemplate.includes('{{')) {
6201
+ const translated = await this.translationService.translateAsync(titleTemplate);
6202
+ return resolveMultiLanguageString(translated, this.translationService.getActiveLang()).trim() || translated;
6203
+ }
6204
+ const recordContext = await this.loadRecordContext(`${entityDef.module}.${entityDef.name}`, data);
6205
+ const scope = {
6206
+ context: this.contextEvalFactory.createScope(recordContext),
6207
+ };
6208
+ const evaluated = await this.expressionEvaluator.evaluate(titleTemplate, scope);
6209
+ const locale = this.translationService.getActiveLang();
6210
+ const text = resolveMultiLanguageString(evaluated, locale).trim() || String(evaluated ?? '').trim();
6211
+ if (text) {
6212
+ return text;
6213
+ }
6214
+ return this.translationService.translateAsync('@general:actions.detail.title');
6215
+ }
6216
+ async loadRecordContext(entityFullName, data) {
6217
+ if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
6218
+ const hasNestedFields = get(data, 'subject.title') != null ||
6219
+ get(data, 'recordType.title') != null ||
6220
+ get(data, 'occurredDate') != null;
6221
+ if (hasNestedFields) {
6222
+ return data;
6059
6223
  }
6060
6224
  }
6061
- const presetTrimmed = preset?.trim();
6062
- if (presetTrimmed && !isUnresolvedEntityDisplayTemplate(presetTrimmed)) {
6063
- return presetTrimmed;
6225
+ const recordId = typeof data === 'string' ? data : String(get(data, 'id') ?? '');
6226
+ if (!recordId) {
6227
+ return typeof data === 'object' && data !== null ? data : {};
6064
6228
  }
6065
- if (fallbackItem) {
6066
- const fromItem = await this.resolveLookupDisplayLabelAsync(fallbackItem, entityDefinition);
6067
- if (fromItem) {
6068
- return fromItem;
6229
+ try {
6230
+ const [moduleName, entityName] = entityFullName.split('.');
6231
+ const entity = await this.entityService.resolve(moduleName, entityName);
6232
+ const byKeyFn = entity?.queries?.byKey?.execute;
6233
+ if (typeof byKeyFn === 'function') {
6234
+ return (await byKeyFn(recordId)) ?? {};
6069
6235
  }
6070
6236
  }
6071
- return presetTrimmed ?? '';
6237
+ catch {
6238
+ // Fall back to list row payload below.
6239
+ }
6240
+ return typeof data === 'object' && data !== null ? data : {};
6072
6241
  }
6073
- async resolveLookupDisplayLabelAsync(item, entityDefinition) {
6074
- const locale = this.translation.getActiveLang() ?? 'en-US';
6075
- const label = await formatLookupItemDisplayAsync(item, entityDefinition ?? null, { textField: this.textField() }, this.formatService, {
6076
- expressionEvaluator: this.expressionEvaluator,
6077
- contextEvalFactory: this.contextEvalFactory,
6078
- translationService: this.translation,
6079
- locale,
6080
- }, (value) => this.translation.resolve(value));
6081
- return label && !isUnresolvedEntityDisplayTemplate(label) ? label : '';
6242
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPViewEntityDetailsCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6243
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPViewEntityDetailsCommand, providedIn: 'root' }); }
6244
+ }
6245
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPViewEntityDetailsCommand, decorators: [{
6246
+ type: Injectable,
6247
+ args: [{
6248
+ providedIn: 'root',
6249
+ }]
6250
+ }] });
6251
+
6252
+ var viewEntityDetails_command = /*#__PURE__*/Object.freeze({
6253
+ __proto__: null,
6254
+ AXPViewEntityDetailsCommand: AXPViewEntityDetailsCommand
6255
+ });
6256
+
6257
+ /** Common keys on denormalized expose stubs (e.g. `{ id, displayName }` from lookup expose). */
6258
+ const LOOKUP_EXPOSE_STUB_DISPLAY_FIELDS = ['displayName', 'title', 'name', 'fullName', 'code'];
6259
+ function resolveLookupItemFieldValue(item, field) {
6260
+ const direct = get(item, field);
6261
+ if (direct != null && direct !== '') {
6262
+ return direct;
6082
6263
  }
6083
- /**
6084
- * Returns true if a value is meaningful for display (non-empty/non-null).
6085
- */
6086
- hasMeaningfulValue(value) {
6087
- if (value === null || value === undefined)
6088
- return false;
6089
- if (typeof value === 'string')
6090
- return value.trim().length > 0;
6091
- if (Array.isArray(value))
6092
- return value.length > 0;
6093
- if (typeof value === 'object')
6094
- return Object.keys(value).length > 0;
6095
- return true;
6264
+ if (!field.includes('.')) {
6265
+ return direct;
6096
6266
  }
6097
- /**
6098
- * Calculates an importance score for a property based on its widget type,
6099
- * whether it currently has a meaningful value, and metadata like title.
6100
- */
6101
- getPropertyImportanceScore(prop, data) {
6102
- const schema = prop.showAs?.type ?? prop.schema;
6103
- const importanceMap = {
6104
- [AXPWidgetsCatalog.text]: 100,
6105
- [AXPWidgetsCatalog.select]: 95,
6106
- [AXPWidgetsCatalog.number]: 90,
6107
- [AXPWidgetsCatalog.dateTime]: 85,
6108
- [AXPWidgetsCatalog.richText]: 80,
6109
- [AXPWidgetsCatalog.largeText]: 75,
6110
- [AXPWidgetsCatalog.contact]: 74,
6111
- [AXPWidgetsCatalog.toggle]: 65,
6112
- [AXPWidgetsCatalog.checkbox]: 60,
6113
- [AXPWidgetsCatalog.color]: 40,
6114
- };
6115
- let score = importanceMap[schema] ?? 50;
6116
- // Prefer fields that actually have a value for the current entity
6117
- const valuePath = prop.path ?? prop.name;
6118
- if (this.hasMeaningfulValue(get(data, valuePath))) {
6119
- score += 20;
6120
- }
6121
- // Slight preference for human-friendly titled fields
6122
- const hasTitle = prop.title && prop.title !== prop.name;
6123
- if (hasTitle) {
6124
- score += 5;
6267
+ for (const fallback of LOOKUP_EXPOSE_STUB_DISPLAY_FIELDS) {
6268
+ const candidate = get(item, fallback);
6269
+ if (candidate != null && candidate !== '') {
6270
+ return candidate;
6125
6271
  }
6126
- return score;
6127
6272
  }
6128
- /**
6129
- * Calculates a name-based importance score to prioritize common key fields
6130
- * like title, description, status, and important dates.
6131
- * Name priority MUST come before widget-type priority.
6132
- */
6133
- getNameImportanceScore(prop) {
6134
- const rawName = (prop.options?.dataPath ?? prop.name ?? '').toString();
6135
- const normalized = rawName.toLowerCase();
6136
- const collapsed = normalized.replace(/[^a-z0-9]/g, '');
6137
- const priorities = [
6138
- { keys: ['title', 'name', 'subject', 'code'], score: 120 },
6139
- { keys: ['description', 'details', 'summary', 'remark', 'remarks'], score: 110 },
6140
- { keys: ['status', 'state', 'phase', 'stage'], score: 105 },
6141
- { keys: ['priority', 'severity', 'rank'], score: 100 },
6142
- { keys: ['expirydate', 'expirationdate', 'expiredate', 'duedate', 'deadline', 'enddate'], score: 98 },
6143
- { keys: ['startdate', 'createdat', 'createdon', 'updatedat', 'updatedon'], score: 95 },
6144
- { keys: ['owner', 'assignee', 'assignedto'], score: 92 },
6145
- { keys: ['email', 'phone', 'mobile', 'contact'], score: 90 },
6146
- { keys: ['category', 'type', 'label', 'tag', 'note', 'notes'], score: 85 },
6147
- { keys: ['primary', 'active', 'active', 'enabled', 'archived', 'deleted', 'locked', 'disabled'], score: -2 }, // send to bottom,
6148
- ];
6149
- for (const group of priorities) {
6150
- for (const key of group.keys) {
6151
- if (collapsed === key || collapsed.endsWith(key)) {
6152
- return group.score;
6153
- }
6154
- }
6155
- }
6156
- // Pattern-based priorities (generic forms like validFrom/validTo, start*/end*)
6157
- if (collapsed.endsWith('from') || collapsed.endsWith('to')) {
6158
- return 97;
6159
- }
6160
- if (collapsed.startsWith('start') || collapsed.startsWith('end')) {
6161
- return 96;
6273
+ return direct;
6274
+ }
6275
+ function formatLookupFieldValue(raw, locale, translationService, resolveMultiLanguage) {
6276
+ if (raw == null || raw === '') {
6277
+ return '';
6278
+ }
6279
+ if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
6280
+ return String(raw);
6281
+ }
6282
+ if (typeof raw === 'object' && !Array.isArray(raw)) {
6283
+ if (resolveMultiLanguage) {
6284
+ return resolveMultiLanguage(raw);
6162
6285
  }
6163
- return 0;
6286
+ return resolveSearchableText(raw, locale, translationService);
6164
6287
  }
6165
- buildEntityPropertiesWithWidgets(data, entityDefinition, textFieldValue, valueFieldValue) {
6166
- // Use properties (not columns) for correct titles and schema; columns may have dataPath that doesn't match
6167
- return entityDefinition.properties
6168
- .filter((prop) => {
6169
- // Exclude technical fields
6170
- if (prop.name === 'id' || prop.name === textFieldValue || prop.name === valueFieldValue) {
6171
- return false;
6172
- }
6173
- // Only include properties that have a meaningful value (using get for dotted paths)
6174
- const path = this.resolveDataPath(prop, data);
6175
- const value = get(data, path);
6176
- if (!this.hasMeaningfulValue(value)) {
6177
- return false;
6178
- }
6179
- const schema = prop.schema?.interface?.type;
6180
- const importantWidgets = [
6181
- AXPWidgetsCatalog.text,
6182
- AXPWidgetsCatalog.largeText,
6183
- AXPWidgetsCatalog.richText,
6184
- AXPWidgetsCatalog.lookup,
6185
- AXPWidgetsCatalog.number,
6186
- AXPWidgetsCatalog.toggle,
6187
- AXPWidgetsCatalog.checkbox,
6188
- AXPWidgetsCatalog.dateTime,
6189
- AXPWidgetsCatalog.color,
6190
- AXPWidgetsCatalog.contact,
6191
- ];
6192
- return importantWidgets.includes(schema);
6193
- })
6194
- .map((prop) => {
6195
- const path = this.resolveDataPath(prop, data);
6196
- const schema = prop.schema?.interface?.type;
6197
- const options = prop.schema?.interface?.options ?? {};
6198
- return {
6199
- ...prop,
6200
- schema,
6201
- path,
6202
- options,
6203
- };
6204
- })
6205
- .sort((a, b) => {
6206
- const nameScoreA = this.getNameImportanceScore(a);
6207
- const nameScoreB = this.getNameImportanceScore(b);
6208
- if (nameScoreA !== nameScoreB)
6209
- return nameScoreB - nameScoreA;
6210
- const widgetScoreA = this.getPropertyImportanceScore(a, data);
6211
- const widgetScoreB = this.getPropertyImportanceScore(b, data);
6212
- if (widgetScoreA !== widgetScoreB)
6213
- return widgetScoreB - widgetScoreA;
6214
- const aHasValue = this.hasMeaningfulValue(get(data, a.path));
6215
- const bHasValue = this.hasMeaningfulValue(get(data, b.path));
6216
- if (aHasValue !== bHasValue)
6217
- return aHasValue ? -1 : 1;
6218
- const aHasTitle = a.title && a.title !== a.name;
6219
- const bHasTitle = b.title && b.title !== b.name;
6220
- if (aHasTitle !== bHasTitle)
6221
- return aHasTitle ? -1 : 1;
6222
- return a.name.localeCompare(b.name);
6223
- })
6224
- .slice(0, 7)
6225
- .map((prop) => {
6226
- const widgetNode = {
6227
- type: prop.schema || AXPWidgetsCatalog.text,
6228
- path: prop.path,
6229
- options: { ...prop.options },
6230
- mode: 'view',
6231
- };
6232
- return {
6233
- name: prop.name,
6234
- title: prop.title || prop.name,
6235
- node: widgetNode,
6236
- };
6237
- });
6288
+ return String(raw);
6289
+ }
6290
+ //#region ---- Template resolution ----
6291
+ /**
6292
+ * Resolves the display template for a lookup item.
6293
+ * Priority: explicit `displayFormat` → entity `formats.lookup` (template) → entity `formats.searchResult.title`.
6294
+ * Returns undefined when `textField` is set or no template applies (use {@link resolveLookupDisplayField} instead).
6295
+ */
6296
+ function resolveLookupDisplayTemplate(entity, options) {
6297
+ const explicit = options.displayFormat?.trim();
6298
+ if (explicit) {
6299
+ return normalizeEntityDisplayTemplate(explicit);
6238
6300
  }
6239
- /**
6240
- * Resolves the data path for a property. For lookups with expose, returns the expanded path
6241
- * when data has it (e.g. personTitle). Otherwise returns property name (e.g. personTitleId).
6242
- */
6243
- resolveDataPath(prop, data) {
6244
- const iface = prop.schema?.interface;
6245
- const expose = iface?.options?.['expose'];
6246
- const textField = iface?.options?.['textField']?.trim();
6247
- if (expose?.length) {
6248
- const textTarget = expose.find((e) => e.source !== 'id');
6249
- if (textTarget) {
6250
- const expandedPath = textTarget.target.split('.')[0];
6251
- const expandedValue = get(data, expandedPath);
6252
- if (expandedValue != null && typeof expandedValue === 'object') {
6253
- if (textField) {
6254
- if (this.hasMeaningfulValue(get(expandedValue, textField))) {
6255
- return expandedPath;
6256
- }
6257
- }
6258
- else {
6259
- const leaf = textTarget.target.split('.').pop();
6260
- if (leaf && this.hasMeaningfulValue(get(expandedValue, leaf))) {
6261
- return expandedPath;
6262
- }
6263
- }
6301
+ if (options.textField?.trim()) {
6302
+ return undefined;
6303
+ }
6304
+ const lookupFormat = entity?.formats?.lookup?.trim();
6305
+ if (lookupFormat && (lookupFormat.includes('{{') || lookupFormat.includes('{'))) {
6306
+ return normalizeEntityDisplayTemplate(lookupFormat);
6307
+ }
6308
+ const searchTitle = entity?.formats?.searchResult?.title?.trim();
6309
+ if (searchTitle) {
6310
+ return normalizeEntityDisplayTemplate(searchTitle);
6311
+ }
6312
+ return undefined;
6313
+ }
6314
+ /**
6315
+ * Resolves the property path used for lookup display when no template applies.
6316
+ * Priority: explicit `textField` → entity `formats.lookup` (simple path) → common property names.
6317
+ */
6318
+ function resolveLookupDisplayField(entity, options) {
6319
+ if (options.textField?.trim()) {
6320
+ return options.textField.trim();
6321
+ }
6322
+ const lookupFormat = entity?.formats?.lookup?.trim();
6323
+ if (lookupFormat && !lookupFormat.includes('{') && !lookupFormat.includes('{{')) {
6324
+ return lookupFormat;
6325
+ }
6326
+ const defaults = ['title', 'name', 'code', 'description'];
6327
+ const found = defaults.find((name) => entity?.properties?.some((p) => p.name === name));
6328
+ return found ?? 'title';
6329
+ }
6330
+ /**
6331
+ * Formats a lookup row for display using template and/or field resolution (display tier).
6332
+ */
6333
+ async function formatLookupItemDisplayAsync(item, entity, options, formatService, displayDeps, resolveMultiLanguage) {
6334
+ if (item == null) {
6335
+ return '';
6336
+ }
6337
+ if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
6338
+ return String(item);
6339
+ }
6340
+ if (typeof item !== 'object') {
6341
+ return '';
6342
+ }
6343
+ if (options.isMultiLanguage?.(item) && resolveMultiLanguage) {
6344
+ return resolveMultiLanguage(item);
6345
+ }
6346
+ const template = resolveLookupDisplayTemplate(entity, options);
6347
+ if (template) {
6348
+ if (template.includes('{{') || template.includes('context.eval')) {
6349
+ const evaluated = await evaluateEntityDisplayTemplate(template, item, displayDeps.expressionEvaluator, displayDeps.locale, displayDeps.translationService, displayDeps.contextEvalFactory);
6350
+ const text = sanitizeResolvedEntityDisplayTitle(resolveSearchableText(evaluated, displayDeps.locale, displayDeps.translationService));
6351
+ if (text && !isUnresolvedEntityDisplayTemplate(text)) {
6352
+ return text;
6353
+ }
6354
+ }
6355
+ else {
6356
+ const formatted = formatService.format(template, 'string', item);
6357
+ if (formatted) {
6358
+ const text = String(formatted);
6359
+ if (!isUnresolvedEntityDisplayTemplate(text)) {
6360
+ return text;
6264
6361
  }
6265
6362
  }
6266
6363
  }
6267
- return prop.name;
6268
6364
  }
6269
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6270
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPEntityDetailPopoverComponent, isStandalone: true, selector: "axp-entity-detail-popover", inputs: { entity: { classPropertyName: "entity", publicName: "entity", isSignal: true, isRequired: true, transformFunction: null }, entityId: { classPropertyName: "entityId", publicName: "entityId", isSignal: true, isRequired: true, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null }, valueField: { classPropertyName: "valueField", publicName: "valueField", isSignal: true, isRequired: false, transformFunction: null }, displayTitle: { classPropertyName: "displayTitle", publicName: "displayTitle", isSignal: true, isRequired: false, transformFunction: null }, item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: false, transformFunction: null }, breadcrumb: { classPropertyName: "breadcrumb", publicName: "breadcrumb", isSignal: true, isRequired: false, transformFunction: null }, requestClose: { classPropertyName: "requestClose", publicName: "requestClose", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"lightest-surface dark:dark-surface p-4 w-[min(600px,calc(100vw-2rem))] rounded-lg shadow-lg\"\n>\n <div class=\"mb-4 border-b pb-2\">\n <h3 class=\"text-base font-semibold text-on-lightest-surface\">\n {{ headerTitle() }}\n </h3>\n @if (breadcrumb()) {\n <div class=\"text-xs text-neutral-500 mt-1\">{{ breadcrumb() }}</div>\n }\n </div>\n @if (isLoadingDetails()) {\n <div class=\"space-y-2 mb-4\">\n @for (row of loadingSkeletonRows; track row) {\n <div class=\"grid grid-cols-[minmax(7rem,35%)_1fr] gap-x-4 items-start\">\n <ax-skeleton [animated]=\"true\" class=\"h-4 w-2/3 rounded\"></ax-skeleton>\n @if (row === 2) {\n <ax-skeleton [animated]=\"true\" class=\"h-16 w-full rounded\"></ax-skeleton>\n } @else {\n <ax-skeleton [animated]=\"true\" class=\"h-4 w-full rounded\"></ax-skeleton>\n }\n </div>\n }\n </div>\n <div class=\"flex justify-end\">\n <ax-skeleton [animated]=\"true\" class=\"h-9 w-28 rounded\"></ax-skeleton>\n </div>\n } @else if (entityDetails()) {\n <div class=\"space-y-3 mb-4\">\n @if (entityDetails()?.entityData) {\n <axp-widgets-container [context]=\"entityDetails()?.entityData\">\n <div class=\"space-y-2\">\n @for (item of entityPropertiesWithWidgets(); track item.name) {\n <div class=\"grid grid-cols-[minmax(7rem,35%)_1fr] gap-x-4 gap-y-1 items-start\">\n <span class=\"text-sm font-medium shrink-0\">{{ item.title | translate | async }}:</span>\n <div class=\"min-w-0 text-sm\">\n <ng-container axp-widget-renderer [node]=\"item.node\" [mode]=\"'view'\"></ng-container>\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n }\n </div>\n <div class=\"flex gap-2 justify-end ax-sm\">\n <ax-button\n [color]=\"'primary'\"\n [look]=\"'solid'\"\n [text]=\"'@general:actions.open-details.title' | translate | async\"\n (click)=\"navigateToDetails()\"\n >\n </ax-button>\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXPWidgetCoreModule }, { kind: "component", type: i1$3.AXPWidgetContainerComponent, selector: "axp-widgets-container", inputs: ["context", "functions"], outputs: ["onContextChanged"] }, { kind: "directive", type: i1$3.AXPWidgetRendererDirective, selector: "[axp-widget-renderer]", inputs: ["parentNode", "index", "mode", "node"], outputs: ["onOptionsChanged", "onValueChanged", "onLoad"], exportAs: ["widgetRenderer"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "ngmodule", type: AXSkeletonModule }, { kind: "component", type: i3$2.AXSkeletonComponent, selector: "ax-skeleton", inputs: ["animated"] }, { kind: "pipe", type: i4.AsyncPipe, name: "async" }, { kind: "pipe", type: i3$1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6365
+ const field = resolveLookupDisplayField(entity, options);
6366
+ const raw = typeof item === 'object' && item != null ? resolveLookupItemFieldValue(item, field) : undefined;
6367
+ return formatLookupFieldValue(raw, displayDeps.locale, displayDeps.translationService, resolveMultiLanguage);
6271
6368
  }
6272
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverComponent, decorators: [{
6273
- type: Component,
6274
- args: [{ selector: 'axp-entity-detail-popover', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, AXButtonModule, AXPWidgetCoreModule, AXTranslationModule, AXSkeletonModule], template: "<div\n class=\"lightest-surface dark:dark-surface p-4 w-[min(600px,calc(100vw-2rem))] rounded-lg shadow-lg\"\n>\n <div class=\"mb-4 border-b pb-2\">\n <h3 class=\"text-base font-semibold text-on-lightest-surface\">\n {{ headerTitle() }}\n </h3>\n @if (breadcrumb()) {\n <div class=\"text-xs text-neutral-500 mt-1\">{{ breadcrumb() }}</div>\n }\n </div>\n @if (isLoadingDetails()) {\n <div class=\"space-y-2 mb-4\">\n @for (row of loadingSkeletonRows; track row) {\n <div class=\"grid grid-cols-[minmax(7rem,35%)_1fr] gap-x-4 items-start\">\n <ax-skeleton [animated]=\"true\" class=\"h-4 w-2/3 rounded\"></ax-skeleton>\n @if (row === 2) {\n <ax-skeleton [animated]=\"true\" class=\"h-16 w-full rounded\"></ax-skeleton>\n } @else {\n <ax-skeleton [animated]=\"true\" class=\"h-4 w-full rounded\"></ax-skeleton>\n }\n </div>\n }\n </div>\n <div class=\"flex justify-end\">\n <ax-skeleton [animated]=\"true\" class=\"h-9 w-28 rounded\"></ax-skeleton>\n </div>\n } @else if (entityDetails()) {\n <div class=\"space-y-3 mb-4\">\n @if (entityDetails()?.entityData) {\n <axp-widgets-container [context]=\"entityDetails()?.entityData\">\n <div class=\"space-y-2\">\n @for (item of entityPropertiesWithWidgets(); track item.name) {\n <div class=\"grid grid-cols-[minmax(7rem,35%)_1fr] gap-x-4 gap-y-1 items-start\">\n <span class=\"text-sm font-medium shrink-0\">{{ item.title | translate | async }}:</span>\n <div class=\"min-w-0 text-sm\">\n <ng-container axp-widget-renderer [node]=\"item.node\" [mode]=\"'view'\"></ng-container>\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n }\n </div>\n <div class=\"flex gap-2 justify-end ax-sm\">\n <ax-button\n [color]=\"'primary'\"\n [look]=\"'solid'\"\n [text]=\"'@general:actions.open-details.title' | translate | async\"\n (click)=\"navigateToDetails()\"\n >\n </ax-button>\n </div>\n }\n</div>\n" }]
6275
- }], ctorParameters: () => [], propDecorators: { entity: [{ type: i0.Input, args: [{ isSignal: true, alias: "entity", required: true }] }], entityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityId", required: true }] }], textField: [{ type: i0.Input, args: [{ isSignal: true, alias: "textField", required: false }] }], valueField: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueField", required: false }] }], displayTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayTitle", required: false }] }], item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: false }] }], breadcrumb: [{ type: i0.Input, args: [{ isSignal: true, alias: "breadcrumb", required: false }] }], requestClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "requestClose", required: false }] }] } });
6276
-
6277
- class AXPEntityDetailPopoverService {
6278
- constructor() {
6279
- //#region ---- Services & Dependencies ----
6280
- this.overlayService = inject(AXOverlayService);
6281
- this.platform = inject(AXPlatform);
6282
- this.localeService = inject(AXLocaleService);
6283
- this.document = inject(DOCUMENT);
6284
- this.router = inject(Router);
6285
- //#endregion
6286
- //#region ---- Private Properties ----
6287
- this.overlayRef = null;
6288
- this.componentRef = null;
6289
- this.anchorElement = null;
6290
- this.outsideClickHandler = null;
6291
- this.keydownHandler = null;
6292
- this.routerSubscription = null;
6369
+ /**
6370
+ * @deprecated Prefer {@link formatLookupItemDisplayAsync} for expression templates with `context.eval`.
6371
+ */
6372
+ function formatLookupItemDisplay(item, entity, options, formatService, resolveMultiLanguage) {
6373
+ if (item == null) {
6374
+ return '';
6293
6375
  }
6294
- //#endregion
6295
- //#region ---- Public Methods ----
6296
- /**
6297
- * Show entity detail popover anchored to the target element.
6298
- */
6299
- async show(target, options) {
6300
- const anchorElement = this.resolveAnchorElement(target);
6301
- if (!anchorElement) {
6302
- return;
6303
- }
6304
- this.hide();
6305
- const panelClasses = ['animate-fadeIn', 'animate-faster', 'axp-entity-detail-popover-overlay'];
6306
- if (this.platform.isDark()) {
6307
- panelClasses.push('ax-dark');
6308
- }
6309
- const requestClose = () => {
6310
- this.hide();
6311
- };
6312
- try {
6313
- const ref = await this.overlayService.create(AXPEntityDetailPopoverComponent, {
6314
- centered: false,
6315
- width: 'fit-content',
6316
- actionSheetStyle: true,
6317
- panelClass: panelClasses,
6318
- backdrop: {
6319
- enabled: false,
6320
- background: false,
6321
- closeOnClick: false,
6322
- },
6323
- anchorOptions: {
6324
- anchor: anchorElement,
6325
- placement: this.resolveOverlayPlacement(anchorElement),
6326
- offsetX: this.resolveOverlayOffsetX(anchorElement),
6327
- offsetY: 8,
6328
- autoFlip: true,
6329
- },
6330
- inputs: {
6331
- entity: options.entity,
6332
- entityId: options.id,
6333
- textField: options.textField || 'title',
6334
- valueField: options.valueField || 'id',
6335
- displayTitle: options.displayTitle ?? '',
6336
- breadcrumb: options.breadcrumb ?? null,
6337
- item: options.item,
6338
- requestClose,
6339
- },
6340
- onDispose: () => {
6341
- this.handleOverlayDisposed();
6342
- },
6343
- });
6344
- this.overlayRef = ref;
6345
- this.componentRef = ref.instance;
6346
- this.anchorElement = anchorElement;
6347
- setTimeout(() => {
6348
- ref.updatePosition();
6349
- this.bindDismissHandlers(ref.overlayElement, anchorElement);
6350
- }, 0);
6351
- await this.componentRef.instance.loadDetails();
6376
+ if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
6377
+ return String(item);
6378
+ }
6379
+ if (typeof item !== 'object') {
6380
+ return '';
6381
+ }
6382
+ if (options.isMultiLanguage?.(item) && resolveMultiLanguage) {
6383
+ return resolveMultiLanguage(item);
6384
+ }
6385
+ const template = resolveLookupDisplayTemplate(entity, options);
6386
+ if (template && !template.includes('context.eval') && !template.includes('{{')) {
6387
+ const formatted = formatService.format(template, 'string', item);
6388
+ if (formatted) {
6389
+ const text = String(formatted);
6390
+ if (!isUnresolvedEntityDisplayTemplate(text)) {
6391
+ return text;
6392
+ }
6352
6393
  }
6353
- catch (error) {
6354
- console.error('[EntityDetailPopover] Failed to open overlay:', error);
6355
- this.handleOverlayDisposed();
6394
+ }
6395
+ const field = resolveLookupDisplayField(entity, options);
6396
+ const raw = typeof item === 'object' && item != null ? resolveLookupItemFieldValue(item, field) : undefined;
6397
+ if (raw == null || raw === '') {
6398
+ return '';
6399
+ }
6400
+ if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
6401
+ return String(raw);
6402
+ }
6403
+ if (typeof raw === 'object' && !Array.isArray(raw)) {
6404
+ if (resolveMultiLanguage) {
6405
+ return resolveMultiLanguage(raw);
6356
6406
  }
6407
+ return raw;
6357
6408
  }
6358
- /** Hide the active popover */
6359
- hide() {
6360
- this.removeDismissHandlers();
6361
- if (this.overlayRef) {
6362
- this.overlayRef.dispose();
6409
+ return String(raw);
6410
+ }
6411
+ /** Matches a resolved platform entity key such as `BusinessCore.Party`. */
6412
+ const LOOKUP_ENTITY_KEY_PATTERN = /^[A-Za-z][\w-]*\.[A-Za-z][\w-]*$/;
6413
+ /**
6414
+ * Resolves the target entity key for a lookup column.
6415
+ * Widget column options may still contain unevaluated `context.eval(...)` expressions;
6416
+ * in that case the type is read from the row (e.g. `ownerRef.type`, `subjectRef.type`).
6417
+ */
6418
+ function resolveLookupEntityKey(entityOption, rowData, columnName, expose) {
6419
+ const fromOption = typeof entityOption === 'string' ? entityOption.trim() : '';
6420
+ if (fromOption && LOOKUP_ENTITY_KEY_PATTERN.test(fromOption)) {
6421
+ return fromOption;
6422
+ }
6423
+ const typePath = resolvePolymorphicRefTypePath(columnName, fromOption, expose);
6424
+ if (typePath && rowData) {
6425
+ const typeValue = get(rowData, typePath);
6426
+ if (typeof typeValue === 'string') {
6427
+ const trimmed = typeValue.trim();
6428
+ if (LOOKUP_ENTITY_KEY_PATTERN.test(trimmed)) {
6429
+ return trimmed;
6430
+ }
6363
6431
  }
6364
6432
  }
6365
- /** Whether a popover is currently active */
6366
- get isActive() {
6367
- return this.overlayRef !== null;
6433
+ return fromOption;
6434
+ }
6435
+ function resolvePolymorphicRefTypePath(columnName, entityOption, expose) {
6436
+ const fromExpression = entityOption.match(/context\.eval\(\s*['"]([^'"]+)['"]\s*\)/)?.[1];
6437
+ if (fromExpression) {
6438
+ return fromExpression;
6439
+ }
6440
+ if (columnName?.endsWith('.id')) {
6441
+ return `${columnName.slice(0, -3)}.type`;
6442
+ }
6443
+ const idTarget = expose?.find((entry) => entry.source === 'id')?.target;
6444
+ if (idTarget?.endsWith('.id')) {
6445
+ return `${idTarget.slice(0, -3)}.type`;
6446
+ }
6447
+ return undefined;
6448
+ }
6449
+ //#endregion
6450
+
6451
+ class AXPEntityDetailPopoverComponent {
6452
+ constructor() {
6453
+ //#region ---- Dependencies ----
6454
+ this.commandService = inject(AXPCommandService);
6455
+ this.queryService = inject(AXPQueryService);
6456
+ this.formatService = inject(AXFormatService);
6457
+ this.translation = inject(AXTranslationService);
6458
+ this.expressionEvaluator = inject(AXPExpressionEvaluatorService);
6459
+ this.contextEvalFactory = inject(AXPContextEvalFactory);
6460
+ this.changeDetectorRef = inject(ChangeDetectorRef);
6461
+ //#endregion
6462
+ //#region ---- Inputs ----
6463
+ this.entity = input.required(...(ngDevMode ? [{ debugName: "entity" }] : /* istanbul ignore next */ []));
6464
+ this.entityId = input.required(...(ngDevMode ? [{ debugName: "entityId" }] : /* istanbul ignore next */ []));
6465
+ this.textField = input('title', ...(ngDevMode ? [{ debugName: "textField" }] : /* istanbul ignore next */ []));
6466
+ this.valueField = input('id', ...(ngDevMode ? [{ debugName: "valueField" }] : /* istanbul ignore next */ []));
6467
+ this.displayTitle = input('', ...(ngDevMode ? [{ debugName: "displayTitle" }] : /* istanbul ignore next */ []));
6468
+ this.item = input(...(ngDevMode ? [undefined, { debugName: "item" }] : /* istanbul ignore next */ []));
6469
+ this.breadcrumb = input(null, ...(ngDevMode ? [{ debugName: "breadcrumb" }] : /* istanbul ignore next */ []));
6470
+ this.requestClose = input(...(ngDevMode ? [undefined, { debugName: "requestClose" }] : /* istanbul ignore next */ []));
6471
+ //#endregion
6472
+ //#region ---- Signals ----
6473
+ this.entityDetails = signal(null, ...(ngDevMode ? [{ debugName: "entityDetails" }] : /* istanbul ignore next */ []));
6474
+ this.isLoadingDetails = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingDetails" }] : /* istanbul ignore next */ []));
6475
+ this.headerTitleVersion = 0;
6476
+ /** Placeholder rows for the loading skeleton (matches typical property count). */
6477
+ this.loadingSkeletonRows = [1, 2, 3, 4, 5];
6478
+ this.headerTitle = signal('', ...(ngDevMode ? [{ debugName: "headerTitle" }] : /* istanbul ignore next */ []));
6479
+ /**
6480
+ * Stable list of property widgets for the template. Must be a signal (computed), not a method:
6481
+ * calling a method from the template rebuilds nodes every CD cycle and can make the widget renderer loop.
6482
+ */
6483
+ this.entityPropertiesWithWidgets = computed(() => {
6484
+ const details = this.entityDetails();
6485
+ const data = details?.entityData;
6486
+ const entityDefinition = details?.entityDefinition;
6487
+ if (!data || !entityDefinition?.properties)
6488
+ return [];
6489
+ return this.buildEntityPropertiesWithWidgets(data, entityDefinition, this.textField(), this.valueField());
6490
+ }, ...(ngDevMode ? [{ debugName: "entityPropertiesWithWidgets" }] : /* istanbul ignore next */ []));
6491
+ effect(() => {
6492
+ void this.translation.getActiveLang();
6493
+ const details = this.entityDetails();
6494
+ const preset = this.displayTitle();
6495
+ const fallbackItem = this.item();
6496
+ const version = ++this.headerTitleVersion;
6497
+ void this.resolveHeaderTitle(details?.entityDefinition, details?.entityData, preset, fallbackItem).then((title) => {
6498
+ if (version === this.headerTitleVersion) {
6499
+ this.headerTitle.set(title);
6500
+ }
6501
+ });
6502
+ });
6368
6503
  }
6369
6504
  //#endregion
6370
- //#region ---- Utility Methods ----
6371
- resolveAnchorElement(target) {
6372
- if (target instanceof ElementRef) {
6373
- return target.nativeElement;
6505
+ //#region ---- Public Methods ----
6506
+ /** Loads entity details after the overlay panel is created. */
6507
+ async loadDetails() {
6508
+ this.isLoadingDetails.set(true);
6509
+ this.entityDetails.set(null);
6510
+ this.changeDetectorRef.detectChanges();
6511
+ try {
6512
+ const result = await this.queryService.fetch('Entity:GetDetails', {
6513
+ entity: this.entity(),
6514
+ id: this.entityId(),
6515
+ });
6516
+ if (result?.success) {
6517
+ this.entityDetails.set(result);
6518
+ }
6374
6519
  }
6375
- if (target instanceof HTMLElement) {
6376
- return target;
6520
+ catch (error) {
6521
+ console.error('Failed to fetch entity details:', error);
6377
6522
  }
6378
- const hostCandidate = target;
6379
- if (typeof hostCandidate.getHostElement === 'function') {
6380
- return hostCandidate.getHostElement();
6523
+ finally {
6524
+ this.isLoadingDetails.set(false);
6525
+ this.changeDetectorRef.detectChanges();
6381
6526
  }
6382
- return null;
6383
- }
6384
- /** Mirror popover placement for RTL so the panel aligns with the anchor's inline-start edge. */
6385
- resolveOverlayPlacement(anchor) {
6386
- return this.isRtlContext(anchor) ? 'bottom-end' : 'bottom-start';
6387
- }
6388
- resolveOverlayOffsetX(anchor) {
6389
- return this.isRtlContext(anchor) ? -8 : 8;
6390
6527
  }
6391
- isRtlContext(anchor) {
6392
- if (this.localeService.activeProfile()?.i18nMeta?.rtl === true) {
6393
- return true;
6528
+ async navigateToDetails() {
6529
+ if (this.entityId()) {
6530
+ await this.commandService.execute('Entity:OpenDetails', {
6531
+ entity: this.entity(),
6532
+ data: { id: this.entityId() },
6533
+ });
6534
+ this.requestClose()?.();
6394
6535
  }
6395
- const dir = getComputedStyle(anchor).direction || this.document.documentElement.getAttribute('dir');
6396
- return dir === 'rtl';
6397
6536
  }
6398
- bindDismissHandlers(overlayElement, anchor) {
6399
- this.removeDismissHandlers();
6400
- this.keydownHandler = (event) => {
6401
- if (event.key === 'Escape') {
6402
- this.hide();
6403
- }
6404
- };
6405
- this.document.addEventListener('keydown', this.keydownHandler);
6406
- this.outsideClickHandler = (event) => {
6407
- const target = event.target;
6408
- if (!target || !overlayElement) {
6409
- return;
6410
- }
6411
- if (this.isInsideInteractiveSurface(target, overlayElement, anchor)) {
6412
- return;
6537
+ //#endregion
6538
+ //#region ---- Helper Methods ----
6539
+ async resolveHeaderTitle(entityDefinition, data, preset, fallbackItem) {
6540
+ if (data && entityDefinition) {
6541
+ const fromDetails = await this.resolveLookupDisplayLabelAsync(data, entityDefinition);
6542
+ if (fromDetails) {
6543
+ return fromDetails;
6413
6544
  }
6414
- this.hide();
6415
- };
6416
- this.document.addEventListener('mousedown', this.outsideClickHandler);
6417
- this.routerSubscription = this.router.events
6418
- .pipe(filter((event) => event instanceof NavigationStart))
6419
- .subscribe(() => {
6420
- this.hide();
6421
- });
6422
- }
6423
- /** Ignore clicks on the anchor, overlay panel, or nested dropdown menus. */
6424
- isInsideInteractiveSurface(target, overlayElement, anchor) {
6425
- if (anchor.contains(target) || overlayElement.contains(target)) {
6426
- return true;
6427
- }
6428
- return Boolean(target.closest('ax-dropdown-panel, ax-button-item-list, ax-button-item, ax-dropdown-button, .ax-dropdown-panel'));
6429
- }
6430
- removeDismissHandlers() {
6431
- if (this.keydownHandler) {
6432
- this.document.removeEventListener('keydown', this.keydownHandler);
6433
- this.keydownHandler = null;
6434
6545
  }
6435
- if (this.outsideClickHandler) {
6436
- this.document.removeEventListener('mousedown', this.outsideClickHandler);
6437
- this.outsideClickHandler = null;
6546
+ const presetTrimmed = preset?.trim();
6547
+ if (presetTrimmed && !isUnresolvedEntityDisplayTemplate(presetTrimmed)) {
6548
+ return presetTrimmed;
6438
6549
  }
6439
- if (this.routerSubscription) {
6440
- this.routerSubscription.unsubscribe();
6441
- this.routerSubscription = null;
6550
+ if (fallbackItem) {
6551
+ const fromItem = await this.resolveLookupDisplayLabelAsync(fallbackItem, entityDefinition);
6552
+ if (fromItem) {
6553
+ return fromItem;
6554
+ }
6442
6555
  }
6556
+ return presetTrimmed ?? '';
6443
6557
  }
6444
- handleOverlayDisposed() {
6445
- this.removeDismissHandlers();
6446
- this.overlayRef = null;
6447
- this.componentRef = null;
6448
- this.anchorElement = null;
6449
- }
6450
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6451
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverService, providedIn: 'root' }); }
6452
- }
6453
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverService, decorators: [{
6454
- type: Injectable,
6455
- args: [{
6456
- providedIn: 'root',
6457
- }]
6458
- }] });
6459
-
6460
- /**
6461
- * Error type that can be thrown by middlewares to abort the chain with a structured payload.
6462
- */
6463
- class AXPMiddlewareAbortError extends Error {
6464
- constructor(message, options = {}) {
6465
- super(message);
6466
- this.message = message;
6467
- this.options = options;
6468
- this.name = 'AXPMiddlewareAbortError';
6469
- }
6470
- toResponse() {
6471
- return {
6472
- success: false,
6473
- error: {
6474
- code: this.options.code,
6475
- message: this.message,
6476
- status: this.options.status,
6477
- details: this.options.details,
6478
- },
6479
- };
6480
- }
6481
- }
6482
- /** Type guard for AXPMiddlewareAbortError */
6483
- function isAXPMiddlewareAbortError(error) {
6484
- return error instanceof AXPMiddlewareAbortError;
6485
- }
6486
- //#endregion
6487
-
6488
- /**
6489
- * Maps storage middleware failures to a command result the details view can surface.
6490
- */
6491
- function mapEntityStorageErrorToCommandResult(error, fallback = 'Failed to update entity') {
6492
- if (isAXPMiddlewareAbortError(error)) {
6493
- return {
6494
- success: false,
6495
- message: {
6496
- type: 'error',
6497
- code: error.options.code,
6498
- text: error.message,
6499
- details: error.options.details,
6500
- },
6501
- };
6502
- }
6503
- const text = error instanceof Error ? error.message : typeof error === 'string' ? error : fallback;
6504
- return {
6505
- success: false,
6506
- message: {
6507
- type: 'error',
6508
- text,
6509
- },
6510
- };
6511
- }
6512
- //#region ---- EntityDataAccessor ----
6513
- /**
6514
- * Data accessor for entity operations.
6515
- * Provides methods to query and execute commands with smart resolution:
6516
- * 1. Checks entity definition queries/commands first
6517
- * 2. Falls back to CRUD service if not defined
6518
- */
6519
- class EntityDataAccessor {
6520
- constructor(module, entity, entityRegistry, queryExecutor, commandService, injector) {
6521
- this.module = module;
6522
- this.entity = entity;
6523
- this.entityRegistry = entityRegistry;
6524
- this.queryExecutor = queryExecutor;
6525
- this.commandService = commandService;
6526
- this.injector = injector;
6527
- this.crudService = null;
6528
- this.entityDef = null;
6529
- this.entityDefPromise = null;
6558
+ async resolveLookupDisplayLabelAsync(item, entityDefinition) {
6559
+ const locale = this.translation.getActiveLang() ?? 'en-US';
6560
+ const label = await formatLookupItemDisplayAsync(item, entityDefinition ?? null, { textField: this.textField() }, this.formatService, {
6561
+ expressionEvaluator: this.expressionEvaluator,
6562
+ contextEvalFactory: this.contextEvalFactory,
6563
+ translationService: this.translation,
6564
+ locale,
6565
+ }, (value) => this.translation.resolve(value));
6566
+ return label && !isUnresolvedEntityDisplayTemplate(label) ? label : '';
6530
6567
  }
6531
6568
  /**
6532
- * Lazily creates the CRUD fallback inside an injection context so {@link AXMEntityCrudServiceImpl}
6533
- * can resolve {@link AXPEntityStorageService} via `inject()`.
6569
+ * Returns true if a value is meaningful for display (non-empty/non-null).
6534
6570
  */
6535
- ensureCrudService() {
6536
- if (!this.crudService) {
6537
- this.crudService = runInInjectionContext(this.injector, () => new AXMEntityCrudServiceImpl(`${this.module}.${this.entity}`));
6538
- }
6539
- return this.crudService;
6571
+ hasMeaningfulValue(value) {
6572
+ if (value === null || value === undefined)
6573
+ return false;
6574
+ if (typeof value === 'string')
6575
+ return value.trim().length > 0;
6576
+ if (Array.isArray(value))
6577
+ return value.length > 0;
6578
+ if (typeof value === 'object')
6579
+ return Object.keys(value).length > 0;
6580
+ return true;
6540
6581
  }
6541
6582
  /**
6542
- * Resolves the entity definition lazily and caches it.
6543
- * @returns The resolved entity definition
6583
+ * Calculates an importance score for a property based on its widget type,
6584
+ * whether it currently has a meaningful value, and metadata like title.
6544
6585
  */
6545
- async resolveEntityDef() {
6546
- if (this.entityDef) {
6547
- return this.entityDef;
6548
- }
6549
- if (!this.entityDefPromise) {
6550
- this.entityDefPromise = this.entityRegistry.resolve(this.module, this.entity);
6586
+ getPropertyImportanceScore(prop, data) {
6587
+ const schema = prop.showAs?.type ?? prop.schema;
6588
+ const importanceMap = {
6589
+ [AXPWidgetsCatalog.text]: 100,
6590
+ [AXPWidgetsCatalog.select]: 95,
6591
+ [AXPWidgetsCatalog.number]: 90,
6592
+ [AXPWidgetsCatalog.dateTime]: 85,
6593
+ [AXPWidgetsCatalog.richText]: 80,
6594
+ [AXPWidgetsCatalog.largeText]: 75,
6595
+ [AXPWidgetsCatalog.contact]: 74,
6596
+ [AXPWidgetsCatalog.toggle]: 65,
6597
+ [AXPWidgetsCatalog.checkbox]: 60,
6598
+ [AXPWidgetsCatalog.color]: 40,
6599
+ };
6600
+ let score = importanceMap[schema] ?? 50;
6601
+ // Prefer fields that actually have a value for the current entity
6602
+ const valuePath = prop.path ?? prop.name;
6603
+ if (this.hasMeaningfulValue(get(data, valuePath))) {
6604
+ score += 20;
6551
6605
  }
6552
- this.entityDef = await this.entityDefPromise;
6553
- if (!this.entityDef) {
6554
- throw new Error(`Entity "${this.module}.${this.entity}" could not be resolved`);
6606
+ // Slight preference for human-friendly titled fields
6607
+ const hasTitle = prop.title && prop.title !== prop.name;
6608
+ if (hasTitle) {
6609
+ score += 5;
6555
6610
  }
6556
- return this.entityDef;
6611
+ return score;
6557
6612
  }
6558
6613
  /**
6559
- * Gets a single entity by key.
6560
- * @param key - The entity key
6561
- * @returns The entity detail or null if not found
6614
+ * Calculates a name-based importance score to prioritize common key fields
6615
+ * like title, description, status, and important dates.
6616
+ * Name priority MUST come before widget-type priority.
6562
6617
  */
6563
- async byKey(key) {
6564
- const entityDef = await this.resolveEntityDef();
6565
- const execute = entityDef.queries?.byKey?.execute;
6566
- if (typeof execute === 'function') {
6567
- return await execute(key);
6618
+ getNameImportanceScore(prop) {
6619
+ const rawName = (prop.options?.dataPath ?? prop.name ?? '').toString();
6620
+ const normalized = rawName.toLowerCase();
6621
+ const collapsed = normalized.replace(/[^a-z0-9]/g, '');
6622
+ const priorities = [
6623
+ { keys: ['title', 'name', 'subject', 'code'], score: 120 },
6624
+ { keys: ['description', 'details', 'summary', 'remark', 'remarks'], score: 110 },
6625
+ { keys: ['status', 'state', 'phase', 'stage'], score: 105 },
6626
+ { keys: ['priority', 'severity', 'rank'], score: 100 },
6627
+ { keys: ['expirydate', 'expirationdate', 'expiredate', 'duedate', 'deadline', 'enddate'], score: 98 },
6628
+ { keys: ['startdate', 'createdat', 'createdon', 'updatedat', 'updatedon'], score: 95 },
6629
+ { keys: ['owner', 'assignee', 'assignedto'], score: 92 },
6630
+ { keys: ['email', 'phone', 'mobile', 'contact'], score: 90 },
6631
+ { keys: ['category', 'type', 'label', 'tag', 'note', 'notes'], score: 85 },
6632
+ { keys: ['primary', 'active', 'active', 'enabled', 'archived', 'deleted', 'locked', 'disabled'], score: -2 }, // send to bottom,
6633
+ ];
6634
+ for (const group of priorities) {
6635
+ for (const key of group.keys) {
6636
+ if (collapsed === key || collapsed.endsWith(key)) {
6637
+ return group.score;
6638
+ }
6639
+ }
6568
6640
  }
6569
- else if (typeof execute === 'string') {
6570
- return await this.queryExecutor.fetch(execute, key);
6641
+ // Pattern-based priorities (generic forms like validFrom/validTo, start*/end*)
6642
+ if (collapsed.endsWith('from') || collapsed.endsWith('to')) {
6643
+ return 97;
6571
6644
  }
6572
- // Fallback to CRUD service
6573
- return await this.ensureCrudService().getOne(key);
6645
+ if (collapsed.startsWith('start') || collapsed.startsWith('end')) {
6646
+ return 96;
6647
+ }
6648
+ return 0;
6649
+ }
6650
+ buildEntityPropertiesWithWidgets(data, entityDefinition, textFieldValue, valueFieldValue) {
6651
+ // Use properties (not columns) for correct titles and schema; columns may have dataPath that doesn't match
6652
+ return entityDefinition.properties
6653
+ .filter((prop) => {
6654
+ // Exclude technical fields
6655
+ if (prop.name === 'id' || prop.name === textFieldValue || prop.name === valueFieldValue) {
6656
+ return false;
6657
+ }
6658
+ // Only include properties that have a meaningful value (using get for dotted paths)
6659
+ const path = this.resolveDataPath(prop, data);
6660
+ const value = get(data, path);
6661
+ if (!this.hasMeaningfulValue(value)) {
6662
+ return false;
6663
+ }
6664
+ const schema = prop.schema?.interface?.type;
6665
+ const importantWidgets = [
6666
+ AXPWidgetsCatalog.text,
6667
+ AXPWidgetsCatalog.largeText,
6668
+ AXPWidgetsCatalog.richText,
6669
+ AXPWidgetsCatalog.lookup,
6670
+ AXPWidgetsCatalog.number,
6671
+ AXPWidgetsCatalog.toggle,
6672
+ AXPWidgetsCatalog.checkbox,
6673
+ AXPWidgetsCatalog.dateTime,
6674
+ AXPWidgetsCatalog.color,
6675
+ AXPWidgetsCatalog.contact,
6676
+ ];
6677
+ return importantWidgets.includes(schema);
6678
+ })
6679
+ .map((prop) => {
6680
+ const path = this.resolveDataPath(prop, data);
6681
+ const schema = prop.schema?.interface?.type;
6682
+ const options = prop.schema?.interface?.options ?? {};
6683
+ return {
6684
+ ...prop,
6685
+ schema,
6686
+ path,
6687
+ options,
6688
+ };
6689
+ })
6690
+ .sort((a, b) => {
6691
+ const nameScoreA = this.getNameImportanceScore(a);
6692
+ const nameScoreB = this.getNameImportanceScore(b);
6693
+ if (nameScoreA !== nameScoreB)
6694
+ return nameScoreB - nameScoreA;
6695
+ const widgetScoreA = this.getPropertyImportanceScore(a, data);
6696
+ const widgetScoreB = this.getPropertyImportanceScore(b, data);
6697
+ if (widgetScoreA !== widgetScoreB)
6698
+ return widgetScoreB - widgetScoreA;
6699
+ const aHasValue = this.hasMeaningfulValue(get(data, a.path));
6700
+ const bHasValue = this.hasMeaningfulValue(get(data, b.path));
6701
+ if (aHasValue !== bHasValue)
6702
+ return aHasValue ? -1 : 1;
6703
+ const aHasTitle = a.title && a.title !== a.name;
6704
+ const bHasTitle = b.title && b.title !== b.name;
6705
+ if (aHasTitle !== bHasTitle)
6706
+ return aHasTitle ? -1 : 1;
6707
+ return a.name.localeCompare(b.name);
6708
+ })
6709
+ .slice(0, 7)
6710
+ .map((prop) => {
6711
+ const widgetNode = {
6712
+ type: prop.schema || AXPWidgetsCatalog.text,
6713
+ path: prop.path,
6714
+ options: { ...prop.options },
6715
+ mode: 'view',
6716
+ };
6717
+ return {
6718
+ name: prop.name,
6719
+ title: prop.title || prop.name,
6720
+ node: widgetNode,
6721
+ };
6722
+ });
6574
6723
  }
6575
6724
  /**
6576
- * Queries a list of entities.
6577
- * @param request - The query request (optional)
6578
- * @returns The paged list result
6725
+ * Resolves the data path for a property. For lookups with expose, returns the expanded path
6726
+ * when data has it (e.g. personTitle). Otherwise returns property name (e.g. personTitleId).
6579
6727
  */
6580
- async query(request) {
6581
- const entityDef = await this.resolveEntityDef();
6582
- const defaultRequest = { skip: 0, take: 10 };
6583
- const queryRequest = request || defaultRequest;
6584
- const execute = entityDef.queries?.list?.execute;
6585
- if (typeof execute === 'function') {
6586
- return await execute(queryRequest);
6587
- }
6588
- else if (typeof execute === 'string') {
6589
- const result = await this.queryExecutor.fetch(execute, queryRequest);
6590
- if (!result) {
6591
- throw new Error(`Query "${execute}" returned null`);
6728
+ resolveDataPath(prop, data) {
6729
+ const iface = prop.schema?.interface;
6730
+ const expose = iface?.options?.['expose'];
6731
+ const textField = iface?.options?.['textField']?.trim();
6732
+ if (expose?.length) {
6733
+ const textTarget = expose.find((e) => e.source !== 'id');
6734
+ if (textTarget) {
6735
+ const expandedPath = textTarget.target.split('.')[0];
6736
+ const expandedValue = get(data, expandedPath);
6737
+ if (expandedValue != null && typeof expandedValue === 'object') {
6738
+ if (textField) {
6739
+ if (this.hasMeaningfulValue(get(expandedValue, textField))) {
6740
+ return expandedPath;
6741
+ }
6742
+ }
6743
+ else {
6744
+ const leaf = textTarget.target.split('.').pop();
6745
+ if (leaf && this.hasMeaningfulValue(get(expandedValue, leaf))) {
6746
+ return expandedPath;
6747
+ }
6748
+ }
6749
+ }
6592
6750
  }
6593
- return result;
6594
6751
  }
6595
- // Fallback to CRUD service
6596
- return await this.ensureCrudService().query(queryRequest);
6752
+ return prop.name;
6597
6753
  }
6598
- /**
6599
- * Queries category entities linked to this entity (`{module}.{entity}Category`).
6600
- * Falls back to CRUD category storage (same as {@link AXMEntityCrudServiceImpl.getCategoriesList}).
6601
- */
6602
- async getCategoriesList(request = { skip: 0, take: 1000 }) {
6603
- return await this.ensureCrudService().getCategoriesList(request);
6754
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6755
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPEntityDetailPopoverComponent, isStandalone: true, selector: "axp-entity-detail-popover", inputs: { entity: { classPropertyName: "entity", publicName: "entity", isSignal: true, isRequired: true, transformFunction: null }, entityId: { classPropertyName: "entityId", publicName: "entityId", isSignal: true, isRequired: true, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null }, valueField: { classPropertyName: "valueField", publicName: "valueField", isSignal: true, isRequired: false, transformFunction: null }, displayTitle: { classPropertyName: "displayTitle", publicName: "displayTitle", isSignal: true, isRequired: false, transformFunction: null }, item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: false, transformFunction: null }, breadcrumb: { classPropertyName: "breadcrumb", publicName: "breadcrumb", isSignal: true, isRequired: false, transformFunction: null }, requestClose: { classPropertyName: "requestClose", publicName: "requestClose", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"lightest-surface dark:dark-surface p-4 w-[min(600px,calc(100vw-2rem))] rounded-lg shadow-lg\"\n>\n <div class=\"mb-4 border-b pb-2\">\n <h3 class=\"text-base font-semibold text-on-lightest-surface\">\n {{ headerTitle() }}\n </h3>\n @if (breadcrumb()) {\n <div class=\"text-xs text-neutral-500 mt-1\">{{ breadcrumb() }}</div>\n }\n </div>\n @if (isLoadingDetails()) {\n <div class=\"space-y-2 mb-4\">\n @for (row of loadingSkeletonRows; track row) {\n <div class=\"grid grid-cols-[minmax(7rem,35%)_1fr] gap-x-4 items-start\">\n <ax-skeleton [animated]=\"true\" class=\"h-4 w-2/3 rounded\"></ax-skeleton>\n @if (row === 2) {\n <ax-skeleton [animated]=\"true\" class=\"h-16 w-full rounded\"></ax-skeleton>\n } @else {\n <ax-skeleton [animated]=\"true\" class=\"h-4 w-full rounded\"></ax-skeleton>\n }\n </div>\n }\n </div>\n <div class=\"flex justify-end\">\n <ax-skeleton [animated]=\"true\" class=\"h-9 w-28 rounded\"></ax-skeleton>\n </div>\n } @else if (entityDetails()) {\n <div class=\"space-y-3 mb-4\">\n @if (entityDetails()?.entityData) {\n <axp-widgets-container [context]=\"entityDetails()?.entityData\">\n <div class=\"space-y-2\">\n @for (item of entityPropertiesWithWidgets(); track item.name) {\n <div class=\"grid grid-cols-[minmax(7rem,35%)_1fr] gap-x-4 gap-y-1 items-start\">\n <span class=\"text-sm font-medium shrink-0\">{{ item.title | translate | async }}:</span>\n <div class=\"min-w-0 text-sm\">\n <ng-container axp-widget-renderer [node]=\"item.node\" [mode]=\"'view'\"></ng-container>\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n }\n </div>\n <div class=\"flex gap-2 justify-end ax-sm\">\n <ax-button\n [color]=\"'primary'\"\n [look]=\"'solid'\"\n [text]=\"'@general:actions.open-details.title' | translate | async\"\n (click)=\"navigateToDetails()\"\n >\n </ax-button>\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXPWidgetCoreModule }, { kind: "component", type: i1$3.AXPWidgetContainerComponent, selector: "axp-widgets-container", inputs: ["context", "functions"], outputs: ["onContextChanged"] }, { kind: "directive", type: i1$3.AXPWidgetRendererDirective, selector: "[axp-widget-renderer]", inputs: ["parentNode", "index", "mode", "node"], outputs: ["onOptionsChanged", "onValueChanged", "onLoad"], exportAs: ["widgetRenderer"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "ngmodule", type: AXSkeletonModule }, { kind: "component", type: i3$2.AXSkeletonComponent, selector: "ax-skeleton", inputs: ["animated"] }, { kind: "pipe", type: i4.AsyncPipe, name: "async" }, { kind: "pipe", type: i3$1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6756
+ }
6757
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverComponent, decorators: [{
6758
+ type: Component,
6759
+ args: [{ selector: 'axp-entity-detail-popover', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, AXButtonModule, AXPWidgetCoreModule, AXTranslationModule, AXSkeletonModule], template: "<div\n class=\"lightest-surface dark:dark-surface p-4 w-[min(600px,calc(100vw-2rem))] rounded-lg shadow-lg\"\n>\n <div class=\"mb-4 border-b pb-2\">\n <h3 class=\"text-base font-semibold text-on-lightest-surface\">\n {{ headerTitle() }}\n </h3>\n @if (breadcrumb()) {\n <div class=\"text-xs text-neutral-500 mt-1\">{{ breadcrumb() }}</div>\n }\n </div>\n @if (isLoadingDetails()) {\n <div class=\"space-y-2 mb-4\">\n @for (row of loadingSkeletonRows; track row) {\n <div class=\"grid grid-cols-[minmax(7rem,35%)_1fr] gap-x-4 items-start\">\n <ax-skeleton [animated]=\"true\" class=\"h-4 w-2/3 rounded\"></ax-skeleton>\n @if (row === 2) {\n <ax-skeleton [animated]=\"true\" class=\"h-16 w-full rounded\"></ax-skeleton>\n } @else {\n <ax-skeleton [animated]=\"true\" class=\"h-4 w-full rounded\"></ax-skeleton>\n }\n </div>\n }\n </div>\n <div class=\"flex justify-end\">\n <ax-skeleton [animated]=\"true\" class=\"h-9 w-28 rounded\"></ax-skeleton>\n </div>\n } @else if (entityDetails()) {\n <div class=\"space-y-3 mb-4\">\n @if (entityDetails()?.entityData) {\n <axp-widgets-container [context]=\"entityDetails()?.entityData\">\n <div class=\"space-y-2\">\n @for (item of entityPropertiesWithWidgets(); track item.name) {\n <div class=\"grid grid-cols-[minmax(7rem,35%)_1fr] gap-x-4 gap-y-1 items-start\">\n <span class=\"text-sm font-medium shrink-0\">{{ item.title | translate | async }}:</span>\n <div class=\"min-w-0 text-sm\">\n <ng-container axp-widget-renderer [node]=\"item.node\" [mode]=\"'view'\"></ng-container>\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n }\n </div>\n <div class=\"flex gap-2 justify-end ax-sm\">\n <ax-button\n [color]=\"'primary'\"\n [look]=\"'solid'\"\n [text]=\"'@general:actions.open-details.title' | translate | async\"\n (click)=\"navigateToDetails()\"\n >\n </ax-button>\n </div>\n }\n</div>\n" }]
6760
+ }], ctorParameters: () => [], propDecorators: { entity: [{ type: i0.Input, args: [{ isSignal: true, alias: "entity", required: true }] }], entityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityId", required: true }] }], textField: [{ type: i0.Input, args: [{ isSignal: true, alias: "textField", required: false }] }], valueField: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueField", required: false }] }], displayTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayTitle", required: false }] }], item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: false }] }], breadcrumb: [{ type: i0.Input, args: [{ isSignal: true, alias: "breadcrumb", required: false }] }], requestClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "requestClose", required: false }] }] } });
6761
+
6762
+ class AXPEntityDetailPopoverService {
6763
+ constructor() {
6764
+ //#region ---- Services & Dependencies ----
6765
+ this.overlayService = inject(AXOverlayService);
6766
+ this.platform = inject(AXPlatform);
6767
+ this.localeService = inject(AXLocaleService);
6768
+ this.document = inject(DOCUMENT);
6769
+ this.router = inject(Router);
6770
+ //#endregion
6771
+ //#region ---- Private Properties ----
6772
+ this.overlayRef = null;
6773
+ this.componentRef = null;
6774
+ this.anchorElement = null;
6775
+ this.outsideClickHandler = null;
6776
+ this.keydownHandler = null;
6777
+ this.routerSubscription = null;
6604
6778
  }
6779
+ //#endregion
6780
+ //#region ---- Public Methods ----
6605
6781
  /**
6606
- * Creates a new entity.
6607
- * @param data - The entity data to create
6608
- * @returns The created entity key
6782
+ * Show entity detail popover anchored to the target element.
6609
6783
  */
6610
- async create(data) {
6611
- const entityDef = await this.resolveEntityDef();
6612
- const execute = entityDef.commands?.create?.execute;
6613
- if (typeof execute === 'function') {
6614
- const result = await execute(data);
6615
- return result?.id || result;
6784
+ async show(target, options) {
6785
+ const anchorElement = this.resolveAnchorElement(target);
6786
+ if (!anchorElement) {
6787
+ return;
6788
+ }
6789
+ this.hide();
6790
+ const panelClasses = ['animate-fadeIn', 'animate-faster', 'axp-entity-detail-popover-overlay'];
6791
+ if (this.platform.isDark()) {
6792
+ panelClasses.push('ax-dark');
6793
+ }
6794
+ const requestClose = () => {
6795
+ this.hide();
6796
+ };
6797
+ try {
6798
+ const ref = await this.overlayService.create(AXPEntityDetailPopoverComponent, {
6799
+ centered: false,
6800
+ width: 'fit-content',
6801
+ actionSheetStyle: true,
6802
+ panelClass: panelClasses,
6803
+ backdrop: {
6804
+ enabled: false,
6805
+ background: false,
6806
+ closeOnClick: false,
6807
+ },
6808
+ anchorOptions: {
6809
+ anchor: anchorElement,
6810
+ placement: this.resolveOverlayPlacement(anchorElement),
6811
+ offsetX: this.resolveOverlayOffsetX(anchorElement),
6812
+ offsetY: 8,
6813
+ autoFlip: true,
6814
+ },
6815
+ inputs: {
6816
+ entity: options.entity,
6817
+ entityId: options.id,
6818
+ textField: options.textField || 'title',
6819
+ valueField: options.valueField || 'id',
6820
+ displayTitle: options.displayTitle ?? '',
6821
+ breadcrumb: options.breadcrumb ?? null,
6822
+ item: options.item,
6823
+ requestClose,
6824
+ },
6825
+ onDispose: () => {
6826
+ this.handleOverlayDisposed();
6827
+ },
6828
+ });
6829
+ this.overlayRef = ref;
6830
+ this.componentRef = ref.instance;
6831
+ this.anchorElement = anchorElement;
6832
+ setTimeout(() => {
6833
+ ref.updatePosition();
6834
+ this.bindDismissHandlers(ref.overlayElement, anchorElement);
6835
+ }, 0);
6836
+ await this.componentRef.instance.loadDetails();
6616
6837
  }
6617
- else if (typeof execute === 'string') {
6618
- const result = await this.commandService.execute(execute, data);
6619
- if (!result?.success) {
6620
- throw new Error(commandMessageTextForError(result?.message?.text) || 'Failed to create entity');
6621
- }
6622
- return result.data?.id || result.data;
6838
+ catch (error) {
6839
+ console.error('[EntityDetailPopover] Failed to open overlay:', error);
6840
+ this.handleOverlayDisposed();
6623
6841
  }
6624
- // Fallback to CRUD service
6625
- return await this.ensureCrudService().insertOne(data);
6626
6842
  }
6627
- /**
6628
- * Updates an existing entity.
6629
- * @param id - The entity key
6630
- * @param data - The entity data to update
6631
- * @returns The updated entity
6632
- */
6633
- async update(id, data) {
6634
- const entityDef = await this.resolveEntityDef();
6635
- const execute = entityDef.commands?.update?.execute;
6636
- const updateData = { id, ...data };
6637
- if (typeof execute === 'function') {
6638
- return await execute(updateData);
6639
- }
6640
- else if (typeof execute === 'string') {
6641
- const result = await this.commandService.execute(execute, updateData);
6642
- if (!result?.success) {
6643
- throw new Error(commandMessageTextForError(result?.message?.text) || 'Failed to update entity');
6644
- }
6645
- return result.data;
6843
+ /** Hide the active popover */
6844
+ hide() {
6845
+ this.removeDismissHandlers();
6846
+ if (this.overlayRef) {
6847
+ this.overlayRef.dispose();
6646
6848
  }
6647
- // Fallback to CRUD service
6648
- return await this.ensureCrudService().updateOne(id, data);
6649
6849
  }
6650
- /**
6651
- * Deletes an entity.
6652
- * @param id - The entity key
6653
- */
6654
- async delete(id) {
6655
- const entityDef = await this.resolveEntityDef();
6656
- const execute = entityDef.commands?.delete?.execute;
6657
- if (typeof execute === 'function') {
6658
- await execute(id);
6659
- return;
6850
+ /** Whether a popover is currently active */
6851
+ get isActive() {
6852
+ return this.overlayRef !== null;
6853
+ }
6854
+ //#endregion
6855
+ //#region ---- Utility Methods ----
6856
+ resolveAnchorElement(target) {
6857
+ if (target instanceof ElementRef) {
6858
+ return target.nativeElement;
6660
6859
  }
6661
- else if (typeof execute === 'string') {
6662
- const result = await this.commandService.execute(execute, id);
6663
- if (!result?.success) {
6664
- throw new Error(commandMessageTextForError(result?.message?.text) || 'Failed to delete entity');
6665
- }
6666
- return;
6860
+ if (target instanceof HTMLElement) {
6861
+ return target;
6667
6862
  }
6668
- // Fallback to CRUD service
6669
- return await this.ensureCrudService().deleteOne(id);
6863
+ const hostCandidate = target;
6864
+ if (typeof hostCandidate.getHostElement === 'function') {
6865
+ return hostCandidate.getHostElement();
6866
+ }
6867
+ return null;
6670
6868
  }
6671
- }
6672
- //#endregion
6673
- //#region ---- EntityBuilder ----
6674
- /**
6675
- * Builder class for entity operations.
6676
- * Provides methods to create paths and access entity data.
6677
- */
6678
- class EntityBuilder {
6679
- constructor(module, entity, sessionService, entityRegistry, queryExecutor, commandService, injector) {
6680
- this.sessionService = sessionService;
6681
- this.entityRegistry = entityRegistry;
6682
- this.queryExecutor = queryExecutor;
6683
- this.commandService = commandService;
6684
- this.injector = injector;
6685
- this.module = module;
6686
- this.entity = entity;
6869
+ /** Mirror popover placement for RTL so the panel aligns with the anchor's inline-start edge. */
6870
+ resolveOverlayPlacement(anchor) {
6871
+ return this.isRtlContext(anchor) ? 'bottom-end' : 'bottom-start';
6687
6872
  }
6688
- /**
6689
- * Creates a path for the entity.
6690
- * @param path - The path segment (defaults to 'list')
6691
- * @returns The full path string
6692
- */
6693
- createPath(path = 'list') {
6694
- return `/${this.sessionService.application?.name}/m/${this.module}/e/${this.entity}/${path}`;
6873
+ resolveOverlayOffsetX(anchor) {
6874
+ return this.isRtlContext(anchor) ? -8 : 8;
6695
6875
  }
6696
- /**
6697
- * Gets the data accessor for the entity with generic type support.
6698
- * Entity definition is resolved lazily on first method call.
6699
- * @returns An EntityDataAccessor instance
6700
- */
6701
- data() {
6702
- return new EntityDataAccessor(this.module, this.entity, this.entityRegistry, this.queryExecutor, this.commandService, this.injector);
6876
+ isRtlContext(anchor) {
6877
+ if (this.localeService.activeProfile()?.i18nMeta?.rtl === true) {
6878
+ return true;
6879
+ }
6880
+ const dir = getComputedStyle(anchor).direction || this.document.documentElement.getAttribute('dir');
6881
+ return dir === 'rtl';
6703
6882
  }
6704
- }
6705
- //#endregion
6706
- class AXPEntityService {
6707
- constructor() {
6708
- this.sessionService = inject(AXPSessionService);
6709
- this.entityRegistry = inject(AXPEntityDefinitionRegistryService);
6710
- this.queryExecutor = inject(AXPQueryExecutor);
6711
- this.commandService = inject(AXPCommandService);
6712
- this.injector = inject(Injector);
6883
+ bindDismissHandlers(overlayElement, anchor) {
6884
+ this.removeDismissHandlers();
6885
+ this.keydownHandler = (event) => {
6886
+ if (event.key === 'Escape') {
6887
+ this.hide();
6888
+ }
6889
+ };
6890
+ this.document.addEventListener('keydown', this.keydownHandler);
6891
+ this.outsideClickHandler = (event) => {
6892
+ const target = event.target;
6893
+ if (!target || !overlayElement) {
6894
+ return;
6895
+ }
6896
+ if (this.isInsideInteractiveSurface(target, overlayElement, anchor)) {
6897
+ return;
6898
+ }
6899
+ this.hide();
6900
+ };
6901
+ this.document.addEventListener('mousedown', this.outsideClickHandler);
6902
+ this.routerSubscription = this.router.events
6903
+ .pipe(filter((event) => event instanceof NavigationStart))
6904
+ .subscribe(() => {
6905
+ this.hide();
6906
+ });
6713
6907
  }
6714
- /**
6715
- * @deprecated Use `withEntity(module, entity).createPath(path)` instead.
6716
- * This method will be removed in a future version.
6717
- */
6718
- createPath(module, entity, path = 'list') {
6719
- return `/${this.sessionService.application?.name}/m/${module}/e/${entity}/${path}`;
6908
+ /** Ignore clicks on the anchor, overlay panel, or nested dropdown menus. */
6909
+ isInsideInteractiveSurface(target, overlayElement, anchor) {
6910
+ if (anchor.contains(target) || overlayElement.contains(target)) {
6911
+ return true;
6912
+ }
6913
+ return Boolean(target.closest('ax-dropdown-panel, ax-button-item-list, ax-button-item, ax-dropdown-button, .ax-dropdown-panel'));
6720
6914
  }
6721
- withEntity(moduleOrEntityKey, entity) {
6722
- let module;
6723
- let entityName;
6724
- if (entity !== undefined) {
6725
- // Two parameters: module and entity
6726
- module = moduleOrEntityKey;
6727
- entityName = entity;
6915
+ removeDismissHandlers() {
6916
+ if (this.keydownHandler) {
6917
+ this.document.removeEventListener('keydown', this.keydownHandler);
6918
+ this.keydownHandler = null;
6728
6919
  }
6729
- else {
6730
- // Single parameter: "module.entity" format
6731
- const dotIndex = moduleOrEntityKey.indexOf('.');
6732
- if (dotIndex === -1) {
6733
- throw new Error(`Invalid entity key format: "${moduleOrEntityKey}". Expected format: "module.entity"`);
6734
- }
6735
- module = moduleOrEntityKey.substring(0, dotIndex);
6736
- entityName = moduleOrEntityKey.substring(dotIndex + 1);
6920
+ if (this.outsideClickHandler) {
6921
+ this.document.removeEventListener('mousedown', this.outsideClickHandler);
6922
+ this.outsideClickHandler = null;
6923
+ }
6924
+ if (this.routerSubscription) {
6925
+ this.routerSubscription.unsubscribe();
6926
+ this.routerSubscription = null;
6737
6927
  }
6738
- return new EntityBuilder(module, entityName, this.sessionService, this.entityRegistry, this.queryExecutor, this.commandService, this.injector);
6739
6928
  }
6740
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6741
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityService, providedIn: 'root' }); }
6929
+ handleOverlayDisposed() {
6930
+ this.removeDismissHandlers();
6931
+ this.overlayRef = null;
6932
+ this.componentRef = null;
6933
+ this.anchorElement = null;
6934
+ }
6935
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6936
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverService, providedIn: 'root' }); }
6742
6937
  }
6743
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityService, decorators: [{
6938
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDetailPopoverService, decorators: [{
6744
6939
  type: Injectable,
6745
- args: [{ providedIn: 'root' }]
6940
+ args: [{
6941
+ providedIn: 'root',
6942
+ }]
6746
6943
  }] });
6747
6944
 
6748
6945
  class AXPEntityPreloadFiltersContainerComponent extends AXPPageLayoutBaseComponent {
@@ -8444,12 +8641,15 @@ class AXPEntityMasterListViewModel {
8444
8641
  return null;
8445
8642
  },
8446
8643
  load: async (e) => {
8644
+ const listRequest = this.entityDef.parentKey != null
8645
+ ? applyHierarchicalRootListFilter(e, this.entityDef.parentKey)
8646
+ : e;
8447
8647
  const execute = this.entityDef.queries?.list?.execute;
8448
8648
  if (typeof execute === 'function') {
8449
- return await execute(e);
8649
+ return await execute(listRequest);
8450
8650
  }
8451
8651
  else if (typeof execute === 'string') {
8452
- return await this.queryExecutor.fetch(execute, e);
8652
+ return await this.queryExecutor.fetch(execute, listRequest);
8453
8653
  }
8454
8654
  return null;
8455
8655
  },
@@ -15510,12 +15710,18 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
15510
15710
  if (!specs?.length) {
15511
15711
  return;
15512
15712
  }
15713
+ // Track both parent context and widget path — path is often empty on the first run.
15513
15714
  this.contextService.data();
15514
- if (!this.fullPath()) {
15715
+ const path = this.fullPath();
15716
+ if (!path) {
15515
15717
  return;
15516
15718
  }
15517
- untracked(() => void this.applyRelatedFiltersFromContextAndDatasource(specs));
15719
+ untracked(() => {
15720
+ void this.syncRelatedFiltersFromContextAndReload(specs);
15721
+ });
15518
15722
  }, ...(ngDevMode ? [{ debugName: "#relatedFilterSyncEffect" }] : /* istanbul ignore next */ []));
15723
+ this.#relatedFilterReloadInProgress = false;
15724
+ this.#relatedFilterReloadQueued = false;
15519
15725
  /** Patches data-list `refresh` so reload keeps parent-scoped toolbar filters on the data source. */
15520
15726
  this.#patchDataListRefreshEffect = effect(() => {
15521
15727
  const inst = this.listWidget()?.instance;
@@ -15526,15 +15732,29 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
15526
15732
  const originalRefresh = inst.refresh.bind(inst);
15527
15733
  inst.refresh = () => {
15528
15734
  void (async () => {
15529
- const o = this.options();
15530
- const specs = o['relatedFilterConditionSpecs'];
15531
- if (o['syncRelatedListFiltersFromDialogContext'] && specs?.length) {
15532
- await this.applyRelatedFiltersFromContextAndDatasource(specs);
15735
+ if (this.#relatedFilterReloadInProgress) {
15736
+ // Filters are being (re)applied by the in-progress sync — only re-fetch.
15737
+ originalRefresh();
15738
+ return;
15533
15739
  }
15534
- else {
15535
- await this.reapplyScopedFiltersToDataSource();
15740
+ this.#relatedFilterReloadInProgress = true;
15741
+ try {
15742
+ do {
15743
+ this.#relatedFilterReloadQueued = false;
15744
+ const o = this.options();
15745
+ const specs = o['relatedFilterConditionSpecs'];
15746
+ if (o['syncRelatedListFiltersFromDialogContext'] && specs?.length) {
15747
+ await this.applyRelatedFiltersFromContextAndDatasource(specs);
15748
+ }
15749
+ else {
15750
+ await this.reapplyScopedFiltersToDataSource();
15751
+ }
15752
+ originalRefresh();
15753
+ } while (this.#relatedFilterReloadQueued);
15754
+ }
15755
+ finally {
15756
+ this.#relatedFilterReloadInProgress = false;
15536
15757
  }
15537
- originalRefresh();
15538
15758
  })();
15539
15759
  };
15540
15760
  }, ...(ngDevMode ? [{ debugName: "#patchDataListRefreshEffect" }] : /* istanbul ignore next */ []));
@@ -15579,14 +15799,19 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
15579
15799
  async execute(commandName, _data) {
15580
15800
  const action = this.findToolbarAction(commandName);
15581
15801
  const command = commandName.split('&')[0];
15582
- const commandData = action?.scope == AXPEntityCommandScope.Selected
15802
+ const selectedRows = action?.scope == AXPEntityCommandScope.Selected
15583
15803
  ? this.selectedItems()
15584
- : action?.options?.['process']?.data || null;
15585
- let options = await this.evaluateToolbarExpressions(action?.options, commandData);
15804
+ : null;
15805
+ // Evaluate first (same as page-list) so process.data expressions resolve against dialog context
15806
+ // — critical for create-wizard related steps where actions are kept raw until click time.
15807
+ let options = await this.evaluateToolbarExpressions(action?.options, selectedRows);
15586
15808
  const listOpts = this.options();
15587
15809
  const foreignKeyFieldName = listOpts['foreignKeyField'];
15588
15810
  const createExcludes = listOpts['nestedCreateHiddenProperties'] ?? [];
15589
- if (command === 'Entity:Create' && foreignKeyFieldName) {
15811
+ // Inject parent FK for Entity:Create and TypeLevel create-style commands (e.g. Assign).
15812
+ if (foreignKeyFieldName &&
15813
+ (command === 'Entity:Create' ||
15814
+ action?.scope === AXPEntityCommandScope.TypeLevel)) {
15590
15815
  const root = this.contextService.snapshot();
15591
15816
  const idVal = get(root, 'id');
15592
15817
  options = merge({}, options);
@@ -15616,6 +15841,9 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
15616
15841
  ...new Set([...existing, ...exclusionsFromList]),
15617
15842
  ];
15618
15843
  }
15844
+ const commandData = selectedRows ??
15845
+ options['process']?.data ??
15846
+ null;
15619
15847
  await this.commandService.execute(command, {
15620
15848
  __context__: {
15621
15849
  entity: this.entitySource(),
@@ -15788,33 +16016,72 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
15788
16016
  listInstance.call('refresh');
15789
16017
  this.isMounted.set(true);
15790
16018
  }
16019
+ /**
16020
+ * Parent-scoped related filters must have a real PK. Empty/undefined values would otherwise
16021
+ * load unscoped rows (other employees' assignments) in create wizards.
16022
+ */
16023
+ isUsableRelatedFilterValue(value) {
16024
+ if (value === undefined || value === null) {
16025
+ return false;
16026
+ }
16027
+ if (typeof value === 'string') {
16028
+ const trimmed = value.trim();
16029
+ return (trimmed.length > 0 &&
16030
+ !trimmed.includes('{{') &&
16031
+ trimmed !== AXP_RELATED_LIST_UNSCOPED_PARENT_FILTER_VALUE);
16032
+ }
16033
+ return true;
16034
+ }
15791
16035
  /**
15792
16036
  * Re-evaluates related-entity list filters from the live dialog form context (e.g. after create saves the main row id).
16037
+ * When the parent id is not ready yet, installs a never-match filter so the grid cannot leak other rows.
15793
16038
  */
15794
16039
  async applyRelatedFiltersFromContext(specs) {
15795
16040
  const root = this.contextService.snapshot();
16041
+ const foreignKeyField = this.options()['foreignKeyField'];
16042
+ const parentId = root['id'] ?? root['_id'];
15796
16043
  const scope = {
15797
16044
  context: {
15798
16045
  eval: (path) => get(root, path),
15799
16046
  },
15800
16047
  };
15801
- const filters = await Promise.all(specs.map(async (c) => ({
15802
- field: c.name,
15803
- operator: c.operator,
15804
- value: await this.expressionEvaluator.evaluate(c.value, scope),
15805
- hidden: true,
15806
- })));
16048
+ const evaluated = await Promise.all(specs.map(async (c) => {
16049
+ let value = await this.expressionEvaluator.evaluate(c.value, scope);
16050
+ // Prefer live root id for the FK field — expression eval can lag right after create-and-continue.
16051
+ if (foreignKeyField &&
16052
+ c.name === foreignKeyField &&
16053
+ this.isUsableRelatedFilterValue(parentId)) {
16054
+ value = parentId;
16055
+ }
16056
+ return {
16057
+ field: c.name,
16058
+ operator: c.operator,
16059
+ value,
16060
+ hidden: true,
16061
+ };
16062
+ }));
16063
+ const usable = evaluated.every((f) => this.isUsableRelatedFilterValue(f.value));
16064
+ // Never-match sentinel keeps fetchDataMode loads empty until the parent id exists.
16065
+ const filters = usable
16066
+ ? evaluated
16067
+ : evaluated.map((f) => ({
16068
+ ...f,
16069
+ value: this.isUsableRelatedFilterValue(f.value)
16070
+ ? f.value
16071
+ : AXP_RELATED_LIST_UNSCOPED_PARENT_FILTER_VALUE,
16072
+ }));
15807
16073
  if (!this.fullPath()) {
15808
- return;
16074
+ return usable;
15809
16075
  }
15810
16076
  const current = this.getValue();
15811
16077
  if (isEqual(current?.toolbar?.filters, filters)) {
15812
- return;
16078
+ return usable;
15813
16079
  }
15814
16080
  this.setValue({
15815
16081
  ...current,
15816
16082
  toolbar: { ...(current?.toolbar ?? {}), filters },
15817
16083
  });
16084
+ return usable;
15818
16085
  }
15819
16086
  /** Keeps related-list filters in sync with dialog context for create wizards (main id appears after first-step save). */
15820
16087
  #relatedFilterSyncEffect;
@@ -15854,8 +16121,39 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
15854
16121
  * Writes toolbar filters from specs and pushes them onto the data source so refresh/reload keeps the parent scope.
15855
16122
  */
15856
16123
  async applyRelatedFiltersFromContextAndDatasource(specs) {
15857
- await this.applyRelatedFiltersFromContext(specs);
16124
+ const usable = await this.applyRelatedFiltersFromContext(specs);
15858
16125
  await this.ensureToolbarFiltersOnDataSource();
16126
+ return usable;
16127
+ }
16128
+ /**
16129
+ * Applies parent-scoped filters then loads/refreshes the grid.
16130
+ * Used when dialog context gains a persisted root id (create-and-continue).
16131
+ */
16132
+ async syncRelatedFiltersFromContextAndReload(specs) {
16133
+ if (this.#relatedFilterReloadInProgress) {
16134
+ this.#relatedFilterReloadQueued = true;
16135
+ return;
16136
+ }
16137
+ this.#relatedFilterReloadInProgress = true;
16138
+ try {
16139
+ do {
16140
+ this.#relatedFilterReloadQueued = false;
16141
+ await this.applyRelatedFiltersFromContextAndDatasource(specs);
16142
+ if (!this.pushToolbarFiltersToDataSource()) {
16143
+ continue;
16144
+ }
16145
+ if (!this.isMounted()) {
16146
+ this.triggerInitialLoadIfNeeded();
16147
+ continue;
16148
+ }
16149
+ const listInstance = this.listWidget()?.instance;
16150
+ // Flag is set so the patched refresh skips re-apply and only re-fetches.
16151
+ listInstance?.call?.('refresh');
16152
+ } while (this.#relatedFilterReloadQueued);
16153
+ }
16154
+ finally {
16155
+ this.#relatedFilterReloadInProgress = false;
16156
+ }
15859
16157
  }
15860
16158
  /**
15861
16159
  * Refreshes the embedded data list (toolbar / commands). In wizard mode, `refresh` is patched to re-apply scoped filters first.
@@ -15863,6 +16161,8 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
15863
16161
  refreshGridWithParentScopedFilters() {
15864
16162
  this.listWidget()?.instance?.call('refresh');
15865
16163
  }
16164
+ #relatedFilterReloadInProgress;
16165
+ #relatedFilterReloadQueued;
15866
16166
  /** Patches data-list `refresh` so reload keeps parent-scoped toolbar filters on the data source. */
15867
16167
  #patchDataListRefreshEffect;
15868
16168
  #effect;
@@ -25676,7 +25976,9 @@ const AXPCrudModifier = {
25676
25976
  if (!queries?.list) {
25677
25977
  queries.list = {
25678
25978
  execute: async (e) => {
25679
- return await queryService.fetch(queryKey, e);
25979
+ const parentKey = ctx.parentKey.get();
25980
+ const request = parentKey != null ? applyHierarchicalRootListFilter(e, parentKey) : e;
25981
+ return await queryService.fetch(queryKey, request);
25680
25982
  },
25681
25983
  type: AXPEntityQueryType.List,
25682
25984
  };
@@ -26341,6 +26643,10 @@ class AXPEntityModule {
26341
26643
  key: 'Entity:Update',
26342
26644
  command: () => Promise.resolve().then(function () { return updateEntity_command; }).then((c) => c.AXPUpdateEntityCommand),
26343
26645
  },
26646
+ {
26647
+ key: 'Entity:UpdateStatus',
26648
+ command: () => Promise.resolve().then(function () { return updateEntityStatus_command; }).then((c) => c.AXPUpdateEntityStatusCommand),
26649
+ },
26344
26650
  {
26345
26651
  key: 'Entity:View',
26346
26652
  command: () => Promise.resolve().then(function () { return viewEntityDetails_command; }).then((c) => c.AXPViewEntityDetailsCommand),
@@ -26504,6 +26810,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
26504
26810
  key: 'Entity:Update',
26505
26811
  command: () => Promise.resolve().then(function () { return updateEntity_command; }).then((c) => c.AXPUpdateEntityCommand),
26506
26812
  },
26813
+ {
26814
+ key: 'Entity:UpdateStatus',
26815
+ command: () => Promise.resolve().then(function () { return updateEntityStatus_command; }).then((c) => c.AXPUpdateEntityStatusCommand),
26816
+ },
26507
26817
  {
26508
26818
  key: 'Entity:View',
26509
26819
  command: () => Promise.resolve().then(function () { return viewEntityDetails_command; }).then((c) => c.AXPViewEntityDetailsCommand),
@@ -26780,5 +27090,5 @@ function detectEntityChanges(oldObj, newObj) {
26780
27090
  * Generated bundle index. Do not edit.
26781
27091
  */
26782
27092
 
26783
- export { ATTACHMENTS_PAGE_COMPONENT_KEY, AXMEntityCrudService, AXMEntityCrudServiceImpl, AXPCategoryTreeService, AXPCreateEntityCommand, AXPDataSeederService, AXPDeleteEntityCommand, AXPEditFileUploaderCommand, AXPEntitiesListDataSourceDefinition, AXPEntityCategoryTreeSelectorComponent, AXPEntityCategoryWidget, AXPEntityCategoryWidgetColumnComponent, AXPEntityCategoryWidgetEditComponent, AXPEntityCategoryWidgetViewComponent, AXPEntityCommandTriggerViewModel, AXPEntityCreateEvent, AXPEntityCreateViewElementViewModel, AXPEntityCreateViewModelFactory, AXPEntityCreateViewSectionViewModel, AXPEntityDataProvider, AXPEntityDataProviderImpl, AXPEntityDataSelectorRowActionsService, AXPEntityDataSelectorService, AXPEntityDefinitionProviderWidget, AXPEntityDefinitionProviderWidgetEditComponent, AXPEntityDefinitionRegistryService, AXPEntityDeletedEvent, AXPEntityDetailListViewModel, AXPEntityDetailPopoverComponent, AXPEntityDetailPopoverService, AXPEntityDetailViewModelFactory, AXPEntityDetailViewModelResolver, AXPEntityEventDispatcherService, AXPEntityFormBuilderService, AXPEntityLifecycleHookKeys, AXPEntityListPersistenceModeDefault, AXPEntityListTableService, AXPEntityListToolbarService, AXPEntityListViewCardFieldViewModel, AXPEntityListViewColumnViewModel, AXPEntityListViewModelFactory, AXPEntityListViewModelResolver, AXPEntityListWidget, AXPEntityListWidgetViewComponent, AXPEntityMasterCreateViewModel, AXPEntityMasterListCardSelectActionName, AXPEntityMasterListViewModel, AXPEntityMasterListViewQueryViewModel, AXPEntityMasterSingleElementViewModel, AXPEntityMasterSingleViewGroupViewModel, AXPEntityMasterSingleViewModel, AXPEntityMasterUpdateElementViewModel, AXPEntityMasterUpdateViewModel, AXPEntityMasterUpdateViewModelFactory, AXPEntityMiddleware, AXPEntityModifyEvent, AXPEntityModule, AXPEntityPreloadFiltersContainerComponent, AXPEntityPreloadFiltersViewModel, AXPEntityPreloadFiltersViewModelResolver, AXPEntityResolver, AXPEntityService, AXPEntityStorageService, AXPEntityUpdateViewSectionViewModel, AXPFileListComponent, AXPFileUploaderLoadFilesQuery, AXPFileUploaderSaveFilesCommand, AXPFileUploaderWidget, AXPFileUploaderWidgetColumnComponent, AXPFileUploaderWidgetEditComponent, AXPFileUploaderWidgetService, AXPFileUploaderWidgetViewComponent, AXPGetEntityDetailsQuery, AXPLayoutOrderingConfigService, AXPLookupWidget, AXPLookupWidgetColumnComponent, AXPLookupWidgetEditComponent, AXPLookupWidgetViewComponent, AXPMiddlewareAbortError, AXPMiddlewareEntityStorageService, AXPMultiSourceDefinitionProviderContext, AXPMultiSourceDefinitionProviderService, AXPMultiSourceFederatedSearchService, AXPMultiSourceSelectorComponent, AXPMultiSourceSelectorService, AXPMultiSourceSelectorWidget, AXPMultiSourceSelectorWidgetColumnComponent, AXPMultiSourceSelectorWidgetEditComponent, AXPMultiSourceSelectorWidgetViewComponent, AXPMultiSourceType, AXPOpenEntityDetailsCommand, AXPOpenEntityListCommand, AXPRelatedColumnEnrichmentService, AXPRelatedColumnMetadataResolver, AXPSelectorStructureWidget, AXPSelectorStructureWidgetColumnComponent, AXPSelectorStructureWidgetEditComponent, AXPSelectorStructureWidgetViewComponent, AXPShowFileUploaderPopupCommand, AXPTruncatedBreadcrumbComponent, AXPUpdateEntityCommand, AXPViewEntityDetailsCommand, AXP_APPEARANCE_SECTION_ORDER, AXP_CATEGORY_TREE_ROOT_TITLE_I18N_KEY, AXP_CLASSIFICATION_SECTION_ORDER, AXP_DATA_SEEDER_TOKEN, AXP_ENTITY_ACTION_PLUGIN, AXP_ENTITY_CONFIG_TOKEN, AXP_ENTITY_DEFINITION_LOADER, AXP_ENTITY_MODIFIER, AXP_ENTITY_STORAGE_BACKEND, AXP_ENTITY_STORAGE_MIDDLEWARE, AXP_MULTI_SOURCE_DEFINITION_PROVIDER, DEFAULT_COLUMN_ORDER, DEFAULT_PAIR_SPAN_RULES, DEFAULT_PROPERTY_ORDER, DEFAULT_SECTION_ORDER, ENTITY_LIST_ROUTE_CONTEXT_SESSION_KEY, ENTITY_MASTER_LIST_TRACE_NS, EntityBuilder, EntityDataAccessor, GALLERY_PAGE_COMPONENT_KEY, actionExists, applyDataSourcePagingWithoutLoad, attachmentFieldCount, attachmentNeedsHydration, attachmentsPlugin, attachmentsSemanticallyEqual, axpCreateEntityAiToolInputDefaults, axpCreateEntityCommandDefinition, axpDeleteEntityCommandDefinition, axpOpenEntityListCommandDefinition, axpShowFileUploaderPopupCommandDefinition, buildDataSourceTraceSnapshot, canPersistEntityListState, cloneLayoutArrays, columnOrderingMiddleware, columnOrderingMiddlewareProvider, columnWidthMiddleware, columnWidthMiddlewareProvider, committedAttachments, computeEntityAggregates, createColumnOrderingMiddlewareProvider, createLayoutOrderingMiddlewareProvider, createModifierContext, defaultCardLayoutMiddleware, defaultCardLayoutMiddlewareProvider, defaultMultiLanguageMiddleware, defaultMultiLanguageMiddlewareProvider, detectEntityChanges, ensureLayoutPropertyView, ensureLayoutSection, ensureListActions, eventDispatchMiddleware, filterSortEntityRows, findEntityListRowDataInTree, fingerprintAttachmentItem, fingerprintAttachments, fireEntityLifecycleHook, formatLookupItemDisplay, formatLookupItemDisplayAsync, galleryPlugin, getDataSourcePageIndex, getEntityListRowId, getMasterInterfacePropertySortKey, hasFileUploaderTitleOrDescriptionFields, hydrateAttachmentFieldToFileList, isAXPMiddlewareAbortError, isAttachmentFieldPresentOnRow, isAttachmentListEntry, isAttachmentStorageRef, isCategoryEntity, isCategoryFilter, isFileListItem, isFileUploaderEditDialogAuto, isLegacyEntityDataSelectorOptions, layoutOrderingMiddlewareFactory, layoutOrderingMiddlewareProvider, mapEntityStorageErrorToCommandResult, mapLegacyEntityDataSelectorOptions, normalizeEntityDataSelectorOptions, normalizeEntityFieldToFileList, normalizeEntityListPersistenceMode, normalizeListPaging, persistedAttachments, provideEntity, resolveAttachmentFieldPath, resolveEntityPluginDetailPageOrder, resolveFileUploaderEditDialog, resolveFileUploaderEntityScope, resolveLookupDisplayField, resolveLookupDisplayTemplate, resolveLookupEntityKey, restoreEntityListExpandedRows, runEntityQuery, searchResultDescriptionMiddleware, searchResultDescriptionMiddlewareProvider, shouldLoadEntityListStateFromStorage, shouldResetEntityListStateOnRouteEntry, traceEntityMasterList };
27093
+ export { ATTACHMENTS_PAGE_COMPONENT_KEY, AXMEntityCrudService, AXMEntityCrudServiceImpl, AXPCategoryTreeService, AXPCreateEntityCommand, AXPDataSeederService, AXPDeleteEntityCommand, AXPEditFileUploaderCommand, AXPEntitiesListDataSourceDefinition, AXPEntityCategoryTreeSelectorComponent, AXPEntityCategoryWidget, AXPEntityCategoryWidgetColumnComponent, AXPEntityCategoryWidgetEditComponent, AXPEntityCategoryWidgetViewComponent, AXPEntityCommandTriggerViewModel, AXPEntityCreateEvent, AXPEntityCreateViewElementViewModel, AXPEntityCreateViewModelFactory, AXPEntityCreateViewSectionViewModel, AXPEntityDataProvider, AXPEntityDataProviderImpl, AXPEntityDataSelectorRowActionsService, AXPEntityDataSelectorService, AXPEntityDefinitionProviderWidget, AXPEntityDefinitionProviderWidgetEditComponent, AXPEntityDefinitionRegistryService, AXPEntityDeletedEvent, AXPEntityDetailListViewModel, AXPEntityDetailPopoverComponent, AXPEntityDetailPopoverService, AXPEntityDetailViewModelFactory, AXPEntityDetailViewModelResolver, AXPEntityEventDispatcherService, AXPEntityFormBuilderService, AXPEntityLifecycleHookKeys, AXPEntityListPersistenceModeDefault, AXPEntityListTableService, AXPEntityListToolbarService, AXPEntityListViewCardFieldViewModel, AXPEntityListViewColumnViewModel, AXPEntityListViewModelFactory, AXPEntityListViewModelResolver, AXPEntityListWidget, AXPEntityListWidgetViewComponent, AXPEntityMasterCreateViewModel, AXPEntityMasterListCardSelectActionName, AXPEntityMasterListViewModel, AXPEntityMasterListViewQueryViewModel, AXPEntityMasterSingleElementViewModel, AXPEntityMasterSingleViewGroupViewModel, AXPEntityMasterSingleViewModel, AXPEntityMasterUpdateElementViewModel, AXPEntityMasterUpdateViewModel, AXPEntityMasterUpdateViewModelFactory, AXPEntityMiddleware, AXPEntityModifyEvent, AXPEntityModule, AXPEntityPreloadFiltersContainerComponent, AXPEntityPreloadFiltersViewModel, AXPEntityPreloadFiltersViewModelResolver, AXPEntityResolver, AXPEntityService, AXPEntityStorageService, AXPEntityUpdateViewSectionViewModel, AXPFileListComponent, AXPFileUploaderLoadFilesQuery, AXPFileUploaderSaveFilesCommand, AXPFileUploaderWidget, AXPFileUploaderWidgetColumnComponent, AXPFileUploaderWidgetEditComponent, AXPFileUploaderWidgetService, AXPFileUploaderWidgetViewComponent, AXPGetEntityDetailsQuery, AXPLayoutOrderingConfigService, AXPLookupWidget, AXPLookupWidgetColumnComponent, AXPLookupWidgetEditComponent, AXPLookupWidgetViewComponent, AXPMiddlewareAbortError, AXPMiddlewareEntityStorageService, AXPMultiSourceDefinitionProviderContext, AXPMultiSourceDefinitionProviderService, AXPMultiSourceFederatedSearchService, AXPMultiSourceSelectorComponent, AXPMultiSourceSelectorService, AXPMultiSourceSelectorWidget, AXPMultiSourceSelectorWidgetColumnComponent, AXPMultiSourceSelectorWidgetEditComponent, AXPMultiSourceSelectorWidgetViewComponent, AXPMultiSourceType, AXPOpenEntityDetailsCommand, AXPOpenEntityListCommand, AXPRelatedColumnEnrichmentService, AXPRelatedColumnMetadataResolver, AXPSelectorStructureWidget, AXPSelectorStructureWidgetColumnComponent, AXPSelectorStructureWidgetEditComponent, AXPSelectorStructureWidgetViewComponent, AXPShowFileUploaderPopupCommand, AXPTruncatedBreadcrumbComponent, AXPUpdateEntityCommand, AXPUpdateEntityStatusCommand, AXPViewEntityDetailsCommand, AXP_APPEARANCE_SECTION_ORDER, AXP_CATEGORY_TREE_ROOT_TITLE_I18N_KEY, AXP_CLASSIFICATION_SECTION_ORDER, AXP_DATA_SEEDER_TOKEN, AXP_ENTITY_ACTION_PLUGIN, AXP_ENTITY_CONFIG_TOKEN, AXP_ENTITY_DEFINITION_LOADER, AXP_ENTITY_MODIFIER, AXP_ENTITY_STORAGE_BACKEND, AXP_ENTITY_STORAGE_MIDDLEWARE, AXP_MULTI_SOURCE_DEFINITION_PROVIDER, DEFAULT_COLUMN_ORDER, DEFAULT_PAIR_SPAN_RULES, DEFAULT_PROPERTY_ORDER, DEFAULT_SECTION_ORDER, ENTITY_LIST_ROUTE_CONTEXT_SESSION_KEY, ENTITY_MASTER_LIST_TRACE_NS, EntityBuilder, EntityDataAccessor, GALLERY_PAGE_COMPONENT_KEY, actionExists, applyDataSourcePagingWithoutLoad, attachmentFieldCount, attachmentNeedsHydration, attachmentsPlugin, attachmentsSemanticallyEqual, axpCreateEntityAiToolInputDefaults, axpCreateEntityCommandDefinition, axpDeleteEntityCommandDefinition, axpOpenEntityListCommandDefinition, axpShowFileUploaderPopupCommandDefinition, axpUpdateEntityStatusCommandDefinition, buildDataSourceTraceSnapshot, canPersistEntityListState, cloneLayoutArrays, columnOrderingMiddleware, columnOrderingMiddlewareProvider, columnWidthMiddleware, columnWidthMiddlewareProvider, committedAttachments, computeEntityAggregates, createColumnOrderingMiddlewareProvider, createLayoutOrderingMiddlewareProvider, createModifierContext, defaultCardLayoutMiddleware, defaultCardLayoutMiddlewareProvider, defaultMultiLanguageMiddleware, defaultMultiLanguageMiddlewareProvider, detectEntityChanges, ensureLayoutPropertyView, ensureLayoutSection, ensureListActions, eventDispatchMiddleware, filterSortEntityRows, findEntityListRowDataInTree, fingerprintAttachmentItem, fingerprintAttachments, fireEntityLifecycleHook, formatLookupItemDisplay, formatLookupItemDisplayAsync, galleryPlugin, getDataSourcePageIndex, getEntityListRowId, getMasterInterfacePropertySortKey, hasFileUploaderTitleOrDescriptionFields, hydrateAttachmentFieldToFileList, isAXPMiddlewareAbortError, isAttachmentFieldPresentOnRow, isAttachmentListEntry, isAttachmentStorageRef, isCategoryEntity, isCategoryFilter, isFileListItem, isFileUploaderEditDialogAuto, isLegacyEntityDataSelectorOptions, layoutOrderingMiddlewareFactory, layoutOrderingMiddlewareProvider, mapEntityStorageErrorToCommandResult, mapLegacyEntityDataSelectorOptions, normalizeEntityDataSelectorOptions, normalizeEntityFieldToFileList, normalizeEntityListPersistenceMode, normalizeListPaging, persistedAttachments, provideEntity, resolveAttachmentFieldPath, resolveEntityPluginDetailPageOrder, resolveFileUploaderEditDialog, resolveFileUploaderEntityScope, resolveLookupDisplayField, resolveLookupDisplayTemplate, resolveLookupEntityKey, restoreEntityListExpandedRows, runEntityQuery, searchResultDescriptionMiddleware, searchResultDescriptionMiddlewareProvider, shouldLoadEntityListStateFromStorage, shouldResetEntityListStateOnRouteEntry, traceEntityMasterList };
26784
27094
  //# sourceMappingURL=acorex-platform-framework-client-layout-entity.mjs.map