@acorex/platform 21.0.0-next.65 → 21.0.0-next.67

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 (50) hide show
  1. package/fesm2022/acorex-platform-common.mjs +94 -18
  2. package/fesm2022/acorex-platform-common.mjs.map +1 -1
  3. package/fesm2022/acorex-platform-core.mjs +42 -1
  4. package/fesm2022/acorex-platform-core.mjs.map +1 -1
  5. package/fesm2022/acorex-platform-layout-builder.mjs +29 -7
  6. package/fesm2022/acorex-platform-layout-builder.mjs.map +1 -1
  7. package/fesm2022/acorex-platform-layout-components.mjs +282 -108
  8. package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
  9. package/fesm2022/acorex-platform-layout-designer.mjs +1 -1
  10. package/fesm2022/acorex-platform-layout-designer.mjs.map +1 -1
  11. package/fesm2022/acorex-platform-layout-entity-attachments-page.component-BaTS183I.mjs +383 -0
  12. package/fesm2022/acorex-platform-layout-entity-attachments-page.component-BaTS183I.mjs.map +1 -0
  13. package/fesm2022/{acorex-platform-layout-widgets-file-list-popup.component-CDYAGBku.mjs → acorex-platform-layout-entity-file-list-popup.component-_yrP5SQe.mjs} +15 -18
  14. package/fesm2022/acorex-platform-layout-entity-file-list-popup.component-_yrP5SQe.mjs.map +1 -0
  15. package/fesm2022/acorex-platform-layout-entity.mjs +3339 -301
  16. package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
  17. package/fesm2022/acorex-platform-layout-views.mjs +0 -1
  18. package/fesm2022/acorex-platform-layout-views.mjs.map +1 -1
  19. package/fesm2022/acorex-platform-layout-widget-core.mjs +1377 -4
  20. package/fesm2022/acorex-platform-layout-widget-core.mjs.map +1 -1
  21. package/fesm2022/acorex-platform-layout-widgets.mjs +9214 -11991
  22. package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
  23. package/fesm2022/{acorex-platform-themes-default-entity-master-create-view.component-Cx1lLUaR.mjs → acorex-platform-themes-default-entity-master-create-view.component-CWLfNqV0.mjs} +3 -3
  24. package/fesm2022/{acorex-platform-themes-default-entity-master-create-view.component-Cx1lLUaR.mjs.map → acorex-platform-themes-default-entity-master-create-view.component-CWLfNqV0.mjs.map} +1 -1
  25. package/fesm2022/{acorex-platform-themes-default-entity-master-modify-view.component-AOrcgjDF.mjs → acorex-platform-themes-default-entity-master-modify-view.component-C7cT82K2.mjs} +3 -3
  26. package/fesm2022/{acorex-platform-themes-default-entity-master-modify-view.component-AOrcgjDF.mjs.map → acorex-platform-themes-default-entity-master-modify-view.component-C7cT82K2.mjs.map} +1 -1
  27. package/fesm2022/{acorex-platform-themes-default-entity-master-single-view.component-BfCeUU5F.mjs → acorex-platform-themes-default-entity-master-single-view.component-Br9p5aXT.mjs} +4 -4
  28. package/fesm2022/{acorex-platform-themes-default-entity-master-single-view.component-BfCeUU5F.mjs.map → acorex-platform-themes-default-entity-master-single-view.component-Br9p5aXT.mjs.map} +1 -1
  29. package/fesm2022/acorex-platform-themes-default.mjs +685 -287
  30. package/fesm2022/acorex-platform-themes-default.mjs.map +1 -1
  31. package/fesm2022/{acorex-platform-themes-shared-settings.provider-BgXYCFia.mjs → acorex-platform-themes-shared-settings.provider-BjuzSe0T.mjs} +29 -2
  32. package/fesm2022/acorex-platform-themes-shared-settings.provider-BjuzSe0T.mjs.map +1 -0
  33. package/fesm2022/acorex-platform-themes-shared.mjs +94 -24
  34. package/fesm2022/acorex-platform-themes-shared.mjs.map +1 -1
  35. package/fesm2022/acorex-platform-workflow.mjs +176 -26
  36. package/fesm2022/acorex-platform-workflow.mjs.map +1 -1
  37. package/package.json +1 -1
  38. package/types/acorex-platform-common.d.ts +73 -9
  39. package/types/acorex-platform-core.d.ts +63 -2
  40. package/types/acorex-platform-layout-builder.d.ts +7 -1
  41. package/types/acorex-platform-layout-components.d.ts +162 -36
  42. package/types/acorex-platform-layout-entity.d.ts +704 -14
  43. package/types/acorex-platform-layout-views.d.ts +28 -0
  44. package/types/acorex-platform-layout-widget-core.d.ts +156 -3
  45. package/types/acorex-platform-layout-widgets.d.ts +29 -393
  46. package/types/acorex-platform-themes-default.d.ts +137 -30
  47. package/types/acorex-platform-themes-shared.d.ts +23 -1
  48. package/types/acorex-platform-workflow.d.ts +89 -4
  49. package/fesm2022/acorex-platform-layout-widgets-file-list-popup.component-CDYAGBku.mjs.map +0 -1
  50. package/fesm2022/acorex-platform-themes-shared-settings.provider-BgXYCFia.mjs.map +0 -1
@@ -1,20 +1,20 @@
1
1
  import { AXToastService } from '@acorex/components/toast';
2
2
  import * as i6 from '@acorex/core/translation';
3
- import { AXTranslationService, AXTranslationModule, resolveMultiLanguageString } from '@acorex/core/translation';
3
+ import { AXTranslationService, AXTranslationModule, resolveMultiLanguageString, translateSync } from '@acorex/core/translation';
4
4
  import * as i4$4 from '@acorex/platform/common';
5
- import { AXPEntityCommandScope, AXPSettingsService, AXPCommonSettings, AXPFilterOperatorMiddlewareService, getEntityInfo, AXPRefreshEvent, AXPReloadEvent, AXPCleanNestedFilters, AXPDefaultMultiLanguageConfigService, withDefaultMultiLanguageOnWidgetNodeTree, AXPEntityQueryType, AXPWorkflowNavigateAction, AXPToastAction, AXP_SEARCH_DEFINITION_PROVIDER, AXPMenuItemsDataSourceDefinition } from '@acorex/platform/common';
5
+ import { AXPEntityCommandScope, AXPSettingsService, AXPCommonSettings, AXPFilterOperatorMiddlewareService, isCardFieldBadgeDisplay, resolveCardFieldBadgeColor, getEntityInfo, resolveEnabledMasterListLayouts, resolveDefaultMasterListLayout, AXPRefreshEvent, AXPReloadEvent, AXPCleanNestedFilters, AXPFileTypeProviderService, AXPFileStorageService, AXPFileActionsService, AXPDefaultMultiLanguageConfigService, withDefaultMultiLanguageOnWidgetNodeTree, AXPEntityQueryType, AXPWorkflowNavigateAction, AXPToastAction, AXP_SEARCH_DEFINITION_PROVIDER, AXPMenuItemsDataSourceDefinition } from '@acorex/platform/common';
6
6
  import * as i0 from '@angular/core';
7
- import { InjectionToken, inject, Injector, runInInjectionContext, Injectable, input, viewChild, signal, computed, ElementRef, ChangeDetectionStrategy, Component, ApplicationRef, EnvironmentInjector, createComponent, ChangeDetectorRef, effect, Input, afterNextRender, untracked, ViewEncapsulation, viewChildren, linkedSignal, HostBinding, output, NgModule, makeEnvironmentProviders } from '@angular/core';
7
+ import { InjectionToken, inject, Injector, runInInjectionContext, Injectable, input, viewChild, signal, computed, ElementRef, ChangeDetectionStrategy, Component, ApplicationRef, EnvironmentInjector, createComponent, NgModule, ChangeDetectorRef, effect, Input, afterNextRender, untracked, ViewEncapsulation, viewChildren, linkedSignal, HostBinding, output, makeEnvironmentProviders } from '@angular/core';
8
8
  import { Subject, takeUntil } from 'rxjs';
9
9
  import { AXPLayoutBuilderService, LayoutBuilderModule } from '@acorex/platform/layout/builder';
10
10
  import * as i3 from '@acorex/platform/layout/widget-core';
11
- import { AXPWidgetsCatalog, AXPWidgetCoreModule, AXPPageStatus, AXPWidgetRegistryService, AXPColumnWidgetComponent, AXPValueWidgetComponent, AXPWidgetGroupEnum, createBooleanProperty, AXPWidgetRendererDirective, createSelectProperty, createStringProperty, AXP_WIDGETS_EDITOR_CATEGORY, AXP_WIDGET_DEFINITION_PROVIDER } from '@acorex/platform/layout/widget-core';
12
- import { AXPSystemActionType, AXPDeviceService, AXPExpressionEvaluatorService, AXPBroadcastEventService, applyFilterArray, applySortArray, resolveActionLook, AXPDistributedEventListenerService, AXPPlatformScope, AXHighlightService, extractValue, setSmart, getChangedPaths, objectKeyValueTransforms, AXPColumnWidthService, AXPModuleManifestRegistry, defaultColumnWidthProvider, AXP_COLUMN_WIDTH_PROVIDER, AXP_DATASOURCE_DEFINITION_PROVIDER, AXPModuleManifestsDataSourceDefinition } from '@acorex/platform/core';
13
- import { cloneDeep, merge, get, castArray, set, orderBy, omit, isNil, isEmpty, isEqual as isEqual$1 } from 'lodash-es';
11
+ import { AXPWidgetsCatalog, AXPWidgetCoreModule, AXPPageStatus, AXPWidgetRegistryService, AXPColumnWidgetComponent, AXPValueWidgetComponent, AXPWidgetGroupEnum, createBooleanProperty, AXPWidgetRendererDirective, createStringProperty, createNumberProperty, AXP_WIDGETS_ADVANCE_SUB_MEDIA, AXP_WIDGETS_ADVANCE_CATEGORY, createSelectProperty, AXP_WIDGETS_EDITOR_CATEGORY, AXP_WIDGET_DEFINITION_PROVIDER } from '@acorex/platform/layout/widget-core';
12
+ import { AXPSystemActionType, AXPDeviceService, AXPExpressionEvaluatorService, AXPBroadcastEventService, applyFilterArray, applySortArray, resolveActionLook, AXPDistributedEventListenerService, AXPPlatformScope, AXHighlightService, extractValue, setSmart, getChangedPaths, objectKeyValueTransforms, AXPHookService, AXPDataGenerator, AXPComponentSlotModule, AXPColumnWidthService, AXPModuleManifestRegistry, defaultColumnWidthProvider, AXP_COLUMN_WIDTH_PROVIDER, AXP_DATASOURCE_DEFINITION_PROVIDER, AXPModuleManifestsDataSourceDefinition } from '@acorex/platform/core';
13
+ import { cloneDeep, merge, get, castArray, set, orderBy, omit, isNil, isEmpty, isEqual as isEqual$1, isArray, isString } from 'lodash-es';
14
14
  import { transform, isEqual } from 'lodash';
15
15
  import { AXPSessionService, AXPAuthGuard, AXPPermissionDefinitionsDataSourceDefinition } from '@acorex/platform/auth';
16
16
  import { Router, ActivatedRoute, RouterModule, ROUTES } from '@angular/router';
17
- import { defineCommand, AXP_COMMAND_DEFINITION_CATEGORY_ENTITY, AXPCommandService, AXPQueryService, AXPQueryExecutor, provideCommandSetups, provideQuerySetups, AXPCommandRegistry, AXPQueryRegistry } from '@acorex/platform/runtime';
17
+ import { defineCommand, AXP_COMMAND_DEFINITION_CATEGORY_ENTITY, AXPCommandService, AXPQueryService, AXPQueryExecutor, AXPCommandExecutor, provideCommandSetups, provideQuerySetups, AXPCommandRegistry, AXPQueryRegistry } from '@acorex/platform/runtime';
18
18
  import * as i1 from '@acorex/components/button';
19
19
  import { AXButtonModule } from '@acorex/components/button';
20
20
  import * as i4 from '@acorex/components/skeleton';
@@ -24,7 +24,7 @@ import { AXPopoverModule } from '@acorex/components/popover';
24
24
  import { AXFormatService } from '@acorex/core/format';
25
25
  import * as i5 from '@angular/common';
26
26
  import { CommonModule, AsyncPipe } from '@angular/common';
27
- import { AXPThemeLayoutBlockComponent, AXPPreloadFiltersComponent, AXPStateMessageComponent, AXPColumnItemListComponent, AXPDataSelectorService, AXPPageComponentRegistryService } from '@acorex/platform/layout/components';
27
+ import { AXPThemeLayoutBlockComponent, AXPPreloadFiltersComponent, AXP_PAGE_COMPONENT_PROVIDER, AXPStateMessageComponent, AXPColumnItemListComponent, AXPDataSelectorService, AXPPageComponentRegistryService } from '@acorex/platform/layout/components';
28
28
  import { AXPPageLayoutBaseComponent, AXPPageLayoutComponent, AXPPageComponentInstanceRegistryService } from '@acorex/platform/layout/views';
29
29
  import { AXDataSource } from '@acorex/cdk/common';
30
30
  import * as i1$3 from '@acorex/platform/workflow';
@@ -57,19 +57,22 @@ import { AXLoadingModule } from '@acorex/components/loading';
57
57
  import * as i6$1 from '@acorex/components/tag-box';
58
58
  import { AXTagBoxModule } from '@acorex/components/tag-box';
59
59
  import { AXValidationModule } from '@acorex/core/validation';
60
- import { AXP_DISABLED_PROPERTY, AXP_ALLOW_CLEAR_PROPERTY, AXP_DATA_PATH_PROPERTY, AXP_DATA_PROPERTY_GROUP, AXP_ALLOW_MULTIPLE_PROPERTY, AXP_NAME_PROPERTY, AXP_READONLY_PROPERTY, AXP_PLACEHOLDER_PROPERTY, AXP_BEHAVIOR_PROPERTY_GROUP, AXPProviderSelectWidgetEditBase, AXP_ROW_EXPR_PREFIX, AXPFileUploaderWidgetService } from '@acorex/platform/layout/widgets';
60
+ import { AXP_DISABLED_PROPERTY, AXP_ALLOW_CLEAR_PROPERTY, AXP_DATA_PATH_PROPERTY, AXP_DATA_PROPERTY_GROUP, AXP_ALLOW_MULTIPLE_PROPERTY, AXP_NAME_PROPERTY, AXP_READONLY_PROPERTY, AXP_PLACEHOLDER_PROPERTY, AXP_BEHAVIOR_PROPERTY_GROUP, AXPProviderSelectWidgetEditBase, AXP_ROW_EXPR_PREFIX, AXP_DOWNLOADABLE_PROPERTY, AXP_DESCRIPTION_PROPERTY } from '@acorex/platform/layout/widgets';
61
61
  import * as i2$2 from '@acorex/components/select-box';
62
62
  import { AXSelectBoxModule } from '@acorex/components/select-box';
63
63
  import * as i4$2 from '@acorex/components/dropdown';
64
64
  import { AXDropdownModule } from '@acorex/components/dropdown';
65
65
  import * as i2$3 from '@acorex/components/tabs';
66
66
  import { AXTabsModule } from '@acorex/components/tabs';
67
- import * as i4$3 from '@acorex/components/collapse';
68
- import { AXCollapseModule } from '@acorex/components/collapse';
69
67
  import * as i5$2 from '@acorex/components/label';
70
68
  import { AXLabelModule } from '@acorex/components/label';
71
69
  import * as i7 from '@acorex/components/text-box';
72
70
  import { AXTextBoxModule } from '@acorex/components/text-box';
71
+ import { AXFileService } from '@acorex/core/file';
72
+ import { AXUploaderZoneDirective } from '@acorex/cdk/uploader';
73
+ import { AXUploaderModule } from '@acorex/components/uploader';
74
+ import * as i4$3 from '@acorex/components/collapse';
75
+ import { AXCollapseModule } from '@acorex/components/collapse';
73
76
 
74
77
  function ensureListActions(ctx) {
75
78
  ctx.interfaces.update((i) => {
@@ -116,6 +119,18 @@ function ensureLayoutPropertyView(layout, prop) {
116
119
 
117
120
  const AXP_ENTITY_ACTION_PLUGIN = new InjectionToken('AXP_ENTITY_ACTION_PLUGIN');
118
121
 
122
+ function ensureMasterListView(entity) {
123
+ entity.interfaces ??= {};
124
+ entity.interfaces.master ??= {};
125
+ if (!entity.interfaces.master.list) {
126
+ entity.interfaces.master.list = { views: [] };
127
+ }
128
+ return entity.interfaces.master.list;
129
+ }
130
+ function ensureMasterListLayouts(list) {
131
+ list.layouts ??= {};
132
+ return list.layouts;
133
+ }
119
134
  function createModifierContext(entity) {
120
135
  const ctx = {
121
136
  entity,
@@ -293,6 +308,27 @@ function createModifierContext(entity) {
293
308
  entity.interfaces.master.list = updater(entity.interfaces.master.list);
294
309
  return ctx;
295
310
  },
311
+ card: {
312
+ get: () => entity.interfaces?.master?.list?.layouts?.card,
313
+ set: (layout) => {
314
+ const list = ensureMasterListView(entity);
315
+ ensureMasterListLayouts(list).card = layout;
316
+ return ctx;
317
+ },
318
+ update: (updater) => {
319
+ const list = ensureMasterListView(entity);
320
+ const layouts = ensureMasterListLayouts(list);
321
+ layouts.card = updater(layouts.card);
322
+ return ctx;
323
+ },
324
+ remove: () => {
325
+ const layouts = entity.interfaces?.master?.list?.layouts;
326
+ if (layouts?.card) {
327
+ delete layouts.card;
328
+ }
329
+ return ctx;
330
+ },
331
+ },
296
332
  },
297
333
  create: {
298
334
  get: () => entity.interfaces?.master?.create,
@@ -1173,7 +1209,7 @@ function isTruthyVisible(value) {
1173
1209
  * Resolves `AXPRelatedEntity.columns` for `entity-list`:
1174
1210
  * - `string[]` → `includeColumns` only (legacy).
1175
1211
  * - `AXPEntityTableColumn[]` → evaluates `options.visible` when it is a string (parent detail context),
1176
- * drops hidden / invisible columns, and returns `relatedTableColumns` + matching `includeColumns`.
1212
+ * drops invisible columns, and returns `relatedTableColumns` + matching `includeColumns`.
1177
1213
  */
1178
1214
  async function resolveRelatedEntityColumns(columns, expressionEvaluator, scope) {
1179
1215
  if (!columns?.length) {
@@ -1185,9 +1221,6 @@ async function resolveRelatedEntityColumns(columns, expressionEvaluator, scope)
1185
1221
  const relatedTableColumns = [];
1186
1222
  for (const raw of columns) {
1187
1223
  const col = cloneDeep(raw);
1188
- if (col.hidden === true) {
1189
- continue;
1190
- }
1191
1224
  let visible = col.options?.visible;
1192
1225
  if (visible === undefined) {
1193
1226
  visible = true;
@@ -4112,6 +4145,47 @@ class AXPEntityListViewColumnViewModel {
4112
4145
  }, ...(ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []));
4113
4146
  }
4114
4147
  }
4148
+ class AXPEntityListViewCardFieldViewModel {
4149
+ constructor(property, field) {
4150
+ this.property = property;
4151
+ this.field = field;
4152
+ this.name = this.property.name;
4153
+ this.title = this.field.title ?? this.property.title;
4154
+ this.visible = this.field?.options?.visible ?? true;
4155
+ this.layout = this.field.layout;
4156
+ this.display = isCardFieldBadgeDisplay(this.field.display) ? 'badge' : 'simple';
4157
+ this.isBadgeDisplay = isCardFieldBadgeDisplay(this.field.display);
4158
+ this.badgeColor = resolveCardFieldBadgeColor(this.field);
4159
+ this.isStatusWidget = this.property.schema?.interface?.type === 'status-widget';
4160
+ this.node = computed(() => {
4161
+ const widget = this.property.schema.interface;
4162
+ return {
4163
+ path: this.field.options?.dataPath ?? this.name,
4164
+ type: widget.type,
4165
+ options: {
4166
+ ...widget?.options,
4167
+ ...this.field?.options,
4168
+ columnName: this.name,
4169
+ ...(widget.children ? { children: widget.children } : {}),
4170
+ },
4171
+ };
4172
+ }, ...(ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []));
4173
+ /** Readonly column node for status fields shown as header badges. */
4174
+ this.headerBadgeNode = computed(() => {
4175
+ const base = this.node();
4176
+ if (!this.isStatusWidget) {
4177
+ return base;
4178
+ }
4179
+ return {
4180
+ ...base,
4181
+ options: {
4182
+ ...base.options,
4183
+ readonly: true,
4184
+ },
4185
+ };
4186
+ }, ...(ngDevMode ? [{ debugName: "headerBadgeNode" }] : /* istanbul ignore next */ []));
4187
+ }
4188
+ }
4115
4189
 
4116
4190
  class AXPEntityDetailListViewModel {
4117
4191
  constructor(injector, detailEntityConfig, parent) {
@@ -4816,6 +4890,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
4816
4890
  args: [{ providedIn: 'root' }]
4817
4891
  }] });
4818
4892
 
4893
+ /** Built-in card list row action that toggles multi-select mode via {@link AXPEntityMasterListViewModel.selectedItems}. */
4894
+ const AXPEntityMasterListCardSelectActionName = 'axp-entity-card:select';
4819
4895
  class AXPEntityMasterListViewQueryViewModel {
4820
4896
  constructor(entity, section, view) {
4821
4897
  this.entity = entity;
@@ -4831,6 +4907,83 @@ class AXPEntityMasterListViewQueryViewModel {
4831
4907
  }
4832
4908
  }
4833
4909
  class AXPEntityMasterListViewModel {
4910
+ /**
4911
+ * Sets global card body visibility for card layout.
4912
+ */
4913
+ setCardContentExpanded(expanded) {
4914
+ if (this.activeListLayout() !== 'card' || this.cardContentExpanded() === expanded) {
4915
+ return;
4916
+ }
4917
+ this.cardContentExpanded.set(expanded);
4918
+ void this.saveSettings('cardContentExpanded', expanded);
4919
+ }
4920
+ /**
4921
+ * Switches table vs compact/expanded card presentation.
4922
+ */
4923
+ setListDisplayMode(mode) {
4924
+ if (mode === 'table') {
4925
+ if (this.enabledListLayouts().includes('table')) {
4926
+ this.setListLayout('table');
4927
+ }
4928
+ return;
4929
+ }
4930
+ if (!this.enabledListLayouts().includes('card')) {
4931
+ return;
4932
+ }
4933
+ const expanded = mode === 'card-expanded';
4934
+ if (this.activeListLayout() !== 'card') {
4935
+ this.setListLayout('card');
4936
+ }
4937
+ if (this.cardContentExpanded() !== expanded) {
4938
+ this.cardContentExpanded.set(expanded);
4939
+ void this.saveSettings('cardContentExpanded', expanded);
4940
+ }
4941
+ }
4942
+ static { this.LAYOUT_META = {
4943
+ table: { title: '@general:terms.interface.table-view', icon: 'fa-table' },
4944
+ card: { title: '@general:terms.interface.card-view', icon: 'fa-grid-2' },
4945
+ }; }
4946
+ /**
4947
+ * Switches to the given layout when it is enabled.
4948
+ * Persists the choice at entity list level (shared across all views).
4949
+ */
4950
+ setListLayout(layout) {
4951
+ if (!this.enabledListLayouts().includes(layout) || this.activeListLayout() === layout) {
4952
+ return;
4953
+ }
4954
+ this.activeListLayout.set(layout);
4955
+ void this.saveSettings('listLayout', layout);
4956
+ }
4957
+ /**
4958
+ * Cycles to the next enabled layout (table → card → table …).
4959
+ * No-op when only one layout is enabled.
4960
+ */
4961
+ switchListLayout() {
4962
+ const enabled = this.enabledListLayouts();
4963
+ if (enabled.length <= 1) {
4964
+ return this.activeListLayout();
4965
+ }
4966
+ const currentIndex = enabled.indexOf(this.activeListLayout());
4967
+ const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % enabled.length;
4968
+ const next = enabled[nextIndex];
4969
+ this.setListLayout(next);
4970
+ return next;
4971
+ }
4972
+ resolveActiveListLayout(preferred) {
4973
+ const enabled = this.enabledListLayouts();
4974
+ const layout = preferred && enabled.includes(preferred) ? preferred : enabled[0];
4975
+ this.activeListLayout.set(layout);
4976
+ }
4977
+ applyListLayoutFromSettings(entitySetting) {
4978
+ const savedLayout = entitySetting.list?.layout;
4979
+ if (savedLayout) {
4980
+ this.resolveActiveListLayout(savedLayout);
4981
+ }
4982
+ if (typeof entitySetting.list?.cardContentExpanded === 'boolean') {
4983
+ this.cardContentExpanded.set(entitySetting.list.cardContentExpanded);
4984
+ }
4985
+ }
4986
+ //#endregion
4834
4987
  createExpressionScope(data) {
4835
4988
  return {
4836
4989
  context: {
@@ -4860,6 +5013,62 @@ class AXPEntityMasterListViewModel {
4860
5013
  const resolved = result?.value;
4861
5014
  return typeof resolved === 'string' && resolved.trim() !== '' ? resolved : title;
4862
5015
  }
5016
+ /**
5017
+ * Resolves a card header value (title, description, icon) from a property path, expression, or literal.
5018
+ * Supports multiple `{{ ... }}` segments in one string (e.g. `{{context.eval("a")}} - {{context.eval("b")}}`).
5019
+ * Multi-language maps (e.g. `{ "en-US": "...", "fa-IR": "..." }`) are returned as-is for the translate pipe.
5020
+ */
5021
+ async resolveCardHeaderValue(value, rowData) {
5022
+ if (!value) {
5023
+ return '';
5024
+ }
5025
+ if (value.includes('{{')) {
5026
+ const scope = this.createExpressionScope(rowData);
5027
+ const result = await this.expressionEvaluator.evaluate(value, scope);
5028
+ if (result === false) {
5029
+ return '';
5030
+ }
5031
+ return this.normalizeCardHeaderDisplayValue(result);
5032
+ }
5033
+ const resolved = get(rowData, value);
5034
+ return this.normalizeCardHeaderDisplayValue(resolved);
5035
+ }
5036
+ /**
5037
+ * Keeps i18n payloads intact for `translate`; coerces only plain scalars to string.
5038
+ */
5039
+ normalizeCardHeaderDisplayValue(value) {
5040
+ if (value == null || value === false) {
5041
+ return '';
5042
+ }
5043
+ if (typeof value === 'string') {
5044
+ return value;
5045
+ }
5046
+ if (typeof value === 'number' || typeof value === 'boolean') {
5047
+ return String(value);
5048
+ }
5049
+ if (typeof value === 'object' && !Array.isArray(value) && this.translation.isValidMultiLanguageObject(value)) {
5050
+ return value;
5051
+ }
5052
+ return '';
5053
+ }
5054
+ /**
5055
+ * Resolves badge color for a card header field (semantic token or expression per row).
5056
+ */
5057
+ async resolveCardBadgeColor(field, rowData) {
5058
+ return this.resolveCardBadgeColorValue(field.badgeColor, rowData);
5059
+ }
5060
+ async resolveCardBadgeColorValue(color, rowData) {
5061
+ if (color == null || color === '') {
5062
+ return 'default';
5063
+ }
5064
+ const scope = this.createExpressionScope(rowData);
5065
+ const result = await this.expressionEvaluator.evaluate(color, scope);
5066
+ if (result === false || result == null) {
5067
+ return 'default';
5068
+ }
5069
+ const resolved = String(result).trim();
5070
+ return resolved !== '' ? resolved : 'default';
5071
+ }
4863
5072
  async resolveTriggerName(triggerName, data) {
4864
5073
  if (!triggerName) {
4865
5074
  return triggerName;
@@ -4883,6 +5092,7 @@ class AXPEntityMasterListViewModel {
4883
5092
  this.view.set(this.views().find((c) => c.name == (viewName || selectedViewName)) ?? this.views()[0]);
4884
5093
  this.applyViewSorts();
4885
5094
  this.applyViewColumns();
5095
+ this.applyCardFields();
4886
5096
  this.applyViewFilters();
4887
5097
  this.resolvedListPaging.set(null);
4888
5098
  await this.applySettings();
@@ -4899,10 +5109,12 @@ class AXPEntityMasterListViewModel {
4899
5109
  this.settings = this.injector.get(AXPSettingsService);
4900
5110
  this.widgetResolver = this.injector.get(AXPWidgetRegistryService);
4901
5111
  this.expressionEvaluator = this.injector.get(AXPExpressionEvaluatorService);
5112
+ this.translation = this.injector.get(AXTranslationService);
4902
5113
  this.commandService = this.injector.get(AXPCommandService);
4903
5114
  this.eventService = this.injector.get(AXPBroadcastEventService);
4904
5115
  this.queryExecutor = this.injector.get(AXPQueryExecutor);
4905
5116
  this.filterOperatorMiddleware = this.injector.get(AXPFilterOperatorMiddlewareService);
5117
+ this.entityDefinitionRegistry = this.injector.get(AXPEntityDefinitionRegistryService);
4906
5118
  this.settingEntityKey = `${this.config.module}:${this.config.name}`;
4907
5119
  this.destroyed = new Subject();
4908
5120
  this.lastAppliedSortKey = null;
@@ -4938,6 +5150,32 @@ class AXPEntityMasterListViewModel {
4938
5150
  }
4939
5151
  return this.showRowIndexColumnEnabled();
4940
5152
  }, ...(ngDevMode ? [{ debugName: "showIndexColumn" }] : /* istanbul ignore next */ []));
5153
+ //#region ---- List Layouts (table / card) ----
5154
+ /** Layouts enabled via `interfaces.master.list.layouts.*.enabled`. */
5155
+ this.enabledListLayouts = computed(() => resolveEnabledMasterListLayouts(this._viewDef?.layouts), ...(ngDevMode ? [{ debugName: "enabledListLayouts" }] : /* istanbul ignore next */ []));
5156
+ /** True when the user can switch between multiple enabled layouts. */
5157
+ this.canSwitchListLayout = computed(() => this.enabledListLayouts().length > 1, ...(ngDevMode ? [{ debugName: "canSwitchListLayout" }] : /* istanbul ignore next */ []));
5158
+ /** Active list layout for the current view. */
5159
+ this.activeListLayout = signal(resolveDefaultMasterListLayout(this._viewDef?.layouts), ...(ngDevMode ? [{ debugName: "activeListLayout" }] : /* istanbul ignore next */ []));
5160
+ this.tableLayout = computed(() => this._viewDef?.layouts?.table, ...(ngDevMode ? [{ debugName: "tableLayout" }] : /* istanbul ignore next */ []));
5161
+ this.cardLayout = computed(() => this._viewDef?.layouts?.card, ...(ngDevMode ? [{ debugName: "cardLayout" }] : /* istanbul ignore next */ []));
5162
+ /** Global expanded state for all card bodies in card layout. */
5163
+ this.cardContentExpanded = signal(true, ...(ngDevMode ? [{ debugName: "cardContentExpanded" }] : /* istanbul ignore next */ []));
5164
+ /** Current presentation mode (table, compact cards, or expanded cards). */
5165
+ this.listDisplayMode = computed(() => {
5166
+ if (this.activeListLayout() === 'table') {
5167
+ return 'table';
5168
+ }
5169
+ return this.cardContentExpanded() ? 'card-expanded' : 'card-compact';
5170
+ }, ...(ngDevMode ? [{ debugName: "listDisplayMode" }] : /* istanbul ignore next */ []));
5171
+ /** Enabled layouts with display metadata for the title layout switcher. */
5172
+ this.availableListLayouts = computed(() => this.enabledListLayouts().map((id) => ({
5173
+ id,
5174
+ title: AXPEntityMasterListViewModel.LAYOUT_META[id].title,
5175
+ icon: AXPEntityMasterListViewModel.LAYOUT_META[id].icon,
5176
+ })), ...(ngDevMode ? [{ debugName: "availableListLayouts" }] : /* istanbul ignore next */ []));
5177
+ /** Active layout option (for title icon and switcher selection state). */
5178
+ this.currentListLayout = computed(() => this.availableListLayouts().find((layout) => layout.id === this.activeListLayout()) ?? null, ...(ngDevMode ? [{ debugName: "currentListLayout" }] : /* istanbul ignore next */ []));
4941
5179
  this.dataSource = new AXDataSource({
4942
5180
  byKey: async (key) => {
4943
5181
  const execute = this.entityDef.queries?.byKey?.execute;
@@ -5031,7 +5269,7 @@ class AXPEntityMasterListViewModel {
5031
5269
  // const visibleProperties = properties.filter(({ schema }) => schema?.visible !== false);
5032
5270
  const propNames = new Set(properties.map(({ name }) => name));
5033
5271
  return columns
5034
- .filter(({ name, showAs, hidden }) => (propNames.has(name) || showAs) && !hidden)
5272
+ .filter(({ name, showAs, options }) => (propNames.has(name) || showAs) && options?.visible !== false)
5035
5273
  .map((column) => {
5036
5274
  if (column.showAs) {
5037
5275
  const widgetConfig = this.widgetResolver.resolve(column.showAs.type);
@@ -5060,6 +5298,13 @@ class AXPEntityMasterListViewModel {
5060
5298
  return this.columns().filter((c) => c.visible == true).length;
5061
5299
  };
5062
5300
  this.columns = signal([], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
5301
+ this.allAvailableCardFields = () => {
5302
+ const fields = this.cardLayout()?.fields ?? [];
5303
+ return fields
5304
+ .map((field) => this.resolveCardField(field))
5305
+ .filter((field) => field != null);
5306
+ };
5307
+ this.cardFields = signal([], ...(ngDevMode ? [{ debugName: "cardFields" }] : /* istanbul ignore next */ []));
5063
5308
  //****************** Sort ******************//
5064
5309
  this.sortableFields = () => {
5065
5310
  const props = this.entityDef.properties.filter((c) => c.options?.sort?.enabled);
@@ -5182,12 +5427,17 @@ class AXPEntityMasterListViewModel {
5182
5427
  this.saveSettings('view');
5183
5428
  }
5184
5429
  if (!(await this.shouldLoadPersistedListState())) {
5430
+ const entitySetting = await this.settings.get(this.settingEntityKey);
5431
+ if (entitySetting) {
5432
+ this.applyListLayoutFromSettings(entitySetting);
5433
+ }
5185
5434
  this.loadListPagingFromViewSettings(null);
5186
5435
  this.expandedRowIds.set([]);
5187
5436
  return;
5188
5437
  }
5189
5438
  const listViewSetting = await this.settings.get(this.settingEntityKey);
5190
5439
  if (listViewSetting) {
5440
+ this.applyListLayoutFromSettings(listViewSetting);
5191
5441
  const columns = listViewSetting.list?.views?.[this.view().name]?.columns;
5192
5442
  const sorts = listViewSetting.list?.views?.[this.view().name]?.sorts;
5193
5443
  const filters = listViewSetting.list?.views?.[this.view().name]?.filters;
@@ -5438,6 +5688,24 @@ class AXPEntityMasterListViewModel {
5438
5688
  },
5439
5689
  }));
5440
5690
  break;
5691
+ case 'listLayout':
5692
+ updateSettings((prev) => ({
5693
+ ...prev,
5694
+ list: {
5695
+ ...prev?.list,
5696
+ layout: data,
5697
+ },
5698
+ }));
5699
+ break;
5700
+ case 'cardContentExpanded':
5701
+ updateSettings((prev) => ({
5702
+ ...prev,
5703
+ list: {
5704
+ ...prev?.list,
5705
+ cardContentExpanded: data,
5706
+ },
5707
+ }));
5708
+ break;
5441
5709
  default:
5442
5710
  break;
5443
5711
  }
@@ -5501,6 +5769,32 @@ class AXPEntityMasterListViewModel {
5501
5769
  }));
5502
5770
  return actions.filter(Boolean);
5503
5771
  }
5772
+ /**
5773
+ * Secondary actions for card layout rows, with the built-in Select action last (divider above it).
5774
+ */
5775
+ async cardSecondaryRowActions(rowData) {
5776
+ const actions = await this.secondaryRowActions(rowData);
5777
+ const items = actions
5778
+ .filter((a) => a.name !== AXPEntityMasterListCardSelectActionName)
5779
+ .map((a) => ({
5780
+ name: a.name,
5781
+ title: a.title,
5782
+ icon: a.icon,
5783
+ color: a.color,
5784
+ disabled: a.disabled,
5785
+ divided: a.separated,
5786
+ }));
5787
+ items.push({
5788
+ name: AXPEntityMasterListCardSelectActionName,
5789
+ title: '@general:actions.select.title',
5790
+ icon: 'fa-square-check',
5791
+ color: 'default',
5792
+ disabled: false,
5793
+ divided: items.length > 0,
5794
+ isCardSelect: true,
5795
+ });
5796
+ return items;
5797
+ }
5504
5798
  static { this.URL_FILTER_OPERATOR_TYPES = new Set([
5505
5799
  'equal',
5506
5800
  'isNull',
@@ -5725,6 +6019,141 @@ class AXPEntityMasterListViewModel {
5725
6019
  this.columns.set(columns);
5726
6020
  this.saveSettings('columnOrders', columns);
5727
6021
  }
6022
+ //****************** Card fields ******************//
6023
+ mergeDisplayFieldWithColumn(field, column) {
6024
+ if (!column) {
6025
+ return field;
6026
+ }
6027
+ return {
6028
+ ...field,
6029
+ title: field.title ?? column.title,
6030
+ showAs: field.showAs ?? column.showAs,
6031
+ options: { ...column.options, ...field.options },
6032
+ };
6033
+ }
6034
+ buildVirtualDisplayProperty(name, title, showAs) {
6035
+ const widgetConfig = this.widgetResolver.resolve(showAs.type);
6036
+ const { title: _wcTitle, description: _wcDescription, ...widgetRest } = widgetConfig;
6037
+ return {
6038
+ ...widgetRest,
6039
+ name,
6040
+ title: title ?? '',
6041
+ schema: {
6042
+ dataType: 'string',
6043
+ interface: {
6044
+ type: showAs.type,
6045
+ options: showAs.options,
6046
+ },
6047
+ },
6048
+ };
6049
+ }
6050
+ normalizeRelatedColumnNames(columns) {
6051
+ if (!columns?.length) {
6052
+ return [];
6053
+ }
6054
+ return columns.map((column) => (typeof column === 'string' ? column : column.name));
6055
+ }
6056
+ findRelatedEntityForField(fieldName) {
6057
+ for (const related of this.entityDef.relatedEntities ?? []) {
6058
+ const columnNames = this.normalizeRelatedColumnNames(related.columns);
6059
+ if (columnNames.includes(fieldName)) {
6060
+ return { entity: related.entity };
6061
+ }
6062
+ }
6063
+ return null;
6064
+ }
6065
+ getRegisteredEntity(fullName) {
6066
+ return this.entityDefinitionRegistry
6067
+ .getAll()
6068
+ .find((entity) => `${entity.module}.${entity.name}` === fullName);
6069
+ }
6070
+ resolveRelatedEntityProperty(fieldName) {
6071
+ const relatedRef = this.findRelatedEntityForField(fieldName);
6072
+ if (!relatedRef) {
6073
+ return null;
6074
+ }
6075
+ const relatedDef = this.getRegisteredEntity(relatedRef.entity);
6076
+ return relatedDef?.properties?.find((property) => property.name === fieldName) ?? null;
6077
+ }
6078
+ resolveRelatedEntityColumnDataPath(relatedEntityKey, fieldName) {
6079
+ const relatedDef = this.getRegisteredEntity(relatedEntityKey);
6080
+ const column = relatedDef?.columns?.find(({ name }) => name === fieldName);
6081
+ return column?.options?.dataPath;
6082
+ }
6083
+ deriveDataPathFromLookupProperty(property) {
6084
+ const iface = property.schema?.interface;
6085
+ if (iface?.type !== 'lookup-editor') {
6086
+ return undefined;
6087
+ }
6088
+ const textField = iface.options?.['textField'] ?? 'title';
6089
+ const expose = iface.options?.['expose'];
6090
+ return expose?.find((entry) => entry.source === textField)?.target;
6091
+ }
6092
+ resolveParentDataPathForRelatedField(fieldName, relatedEntityKey, relatedProperty) {
6093
+ const parentColumn = this.entityDef.columns?.find(({ name }) => name === fieldName);
6094
+ if (parentColumn?.options?.dataPath) {
6095
+ return parentColumn.options.dataPath;
6096
+ }
6097
+ const relatedDataPath = this.resolveRelatedEntityColumnDataPath(relatedEntityKey, fieldName) ??
6098
+ this.deriveDataPathFromLookupProperty(relatedProperty);
6099
+ if (!relatedDataPath) {
6100
+ return undefined;
6101
+ }
6102
+ const relatedLeaf = relatedDataPath.split('.').pop();
6103
+ for (const property of this.entityDef.properties ?? []) {
6104
+ const expose = property.schema?.interface?.options?.['expose'];
6105
+ if (!expose) {
6106
+ continue;
6107
+ }
6108
+ const match = expose.find((entry) => entry.target === relatedDataPath ||
6109
+ entry.target.endsWith(`.${relatedDataPath}`) ||
6110
+ (relatedLeaf != null && entry.source === relatedLeaf));
6111
+ if (match) {
6112
+ return match.target;
6113
+ }
6114
+ }
6115
+ return relatedDataPath;
6116
+ }
6117
+ resolveDisplayFieldProperty(field, column) {
6118
+ const showAs = field.showAs ?? column?.showAs;
6119
+ if (showAs) {
6120
+ return this.buildVirtualDisplayProperty(field.name, field.title ?? column?.title, showAs);
6121
+ }
6122
+ const property = this.entityDef.properties.find(({ name }) => name === field.name);
6123
+ if (property) {
6124
+ return property;
6125
+ }
6126
+ return this.resolveRelatedEntityProperty(field.name);
6127
+ }
6128
+ resolveCardField(field) {
6129
+ const column = this.entityDef.columns?.find(({ name }) => name === field.name);
6130
+ const mergedField = this.mergeDisplayFieldWithColumn(field, column);
6131
+ if (field.options?.visible === false) {
6132
+ return null;
6133
+ }
6134
+ const property = this.resolveDisplayFieldProperty(mergedField, column);
6135
+ if (!property) {
6136
+ return null;
6137
+ }
6138
+ const isOwnProperty = this.entityDef.properties.some(({ name }) => name === field.name);
6139
+ let resolvedField = mergedField;
6140
+ if (!isOwnProperty && !resolvedField.options?.dataPath) {
6141
+ const relatedRef = this.findRelatedEntityForField(field.name);
6142
+ if (relatedRef) {
6143
+ const dataPath = this.resolveParentDataPathForRelatedField(field.name, relatedRef.entity, property);
6144
+ if (dataPath) {
6145
+ resolvedField = {
6146
+ ...resolvedField,
6147
+ options: { ...resolvedField.options, dataPath },
6148
+ };
6149
+ }
6150
+ }
6151
+ }
6152
+ return new AXPEntityListViewCardFieldViewModel(property, resolvedField);
6153
+ }
6154
+ applyCardFields() {
6155
+ this.cardFields.set(this.allAvailableCardFields());
6156
+ }
5728
6157
  resetSorts() {
5729
6158
  this.applyViewSorts();
5730
6159
  }
@@ -7263,6 +7692,234 @@ function resolveEntityPluginDetailPageOrder(input, slot, options) {
7263
7692
  * Consumers that only need the token/interface can import from @acorex/platform/domain.
7264
7693
  */
7265
7694
 
7695
+ /** Component key used when display is 'page'. Register a page component with this key via AXP_PAGE_COMPONENT_PROVIDER. */
7696
+ const ATTACHMENTS_PAGE_COMPONENT_KEY = 'entity-attachments-page';
7697
+ /** Default section order for attachments when display is 'section' (after meta-data-form). */
7698
+ const DEFAULT_ATTACHMENTS_SECTION_ORDER = 200;
7699
+ const DEFAULT_ATTACHMENTS_PAGE_ICON = 'fa-light fa-paperclip';
7700
+ function buildAttachmentsFileUploaderOptions(opts, displayMode) {
7701
+ return {
7702
+ multiple: opts.multiple ?? true,
7703
+ accept: opts.accept,
7704
+ readonly: opts['readonly'] ?? false,
7705
+ fileEditable: opts.fileEditable ?? true,
7706
+ editDialog: opts.editDialog,
7707
+ plugins: opts.plugins ?? [],
7708
+ showBorder: opts.showBorder ?? false,
7709
+ showAddItemButton: opts.showAddItemButton ?? displayMode !== 'page',
7710
+ };
7711
+ }
7712
+ function buildAttachmentsListActionCommandOptions(opts, field) {
7713
+ const base = buildAttachmentsFileUploaderOptions(opts, 'section');
7714
+ return {
7715
+ id: '{{ context.eval("id") }}',
7716
+ key: field,
7717
+ ...base,
7718
+ accept: opts.accept ?? '.jpg,.jpeg,.png,.gif,.webp',
7719
+ showAddItemButton: true,
7720
+ };
7721
+ }
7722
+ function applyAttachmentsFileUploaderOptions(ctx, field, opts, displayMode) {
7723
+ const fileUploaderOptions = {
7724
+ ...buildAttachmentsFileUploaderOptions(opts, displayMode),
7725
+ entityName: `${ctx.entity.module}.${ctx.entity.name}`,
7726
+ entityId: '{{ context.eval("id") }}',
7727
+ entityField: field,
7728
+ };
7729
+ const prop = (ctx.properties.list() ?? []).find((p) => p.name === field);
7730
+ if (prop?.schema?.interface?.type === 'attachments') {
7731
+ prop.schema.interface.options = {
7732
+ ...(prop.schema.interface.options ?? {}),
7733
+ ...fileUploaderOptions,
7734
+ };
7735
+ }
7736
+ }
7737
+ /**
7738
+ * Attachments plugin.
7739
+ * - Always ensures an attachments group and property; when display is 'section' (default), adds a section to single/create/update layouts.
7740
+ * - When display is 'page', adds a page to entity.pages with ATTACHMENTS_PAGE_COMPONENT_KEY; register a page component with that key via AXP_PAGE_COMPONENT_PROVIDER.
7741
+ * - List column and list upload action can be turned off with `showListColumn` / `showListAction` (both default true).
7742
+ * - Uses provided accept/multiple/fileEditable; others are fixed.
7743
+ */
7744
+ const attachmentsPlugin = {
7745
+ name: 'attachments',
7746
+ order: 50,
7747
+ apply: (ctx, options) => {
7748
+ const opts = options ?? {};
7749
+ const field = opts.field && opts.field.trim().length > 0 ? opts.field : 'attachments';
7750
+ const displayTitle = opts.title ?? '@document-management:terms.common.attachments';
7751
+ const displayMode = opts.display ?? 'section';
7752
+ const sectionTitle = opts.section?.title ?? displayTitle;
7753
+ // Ensure group and section ids (unique per field)
7754
+ const sectionId = `attachments-${field}`;
7755
+ const groupIdForProperty = `attachments-${field}`;
7756
+ const existingGroups = ctx.groups.list() ?? [];
7757
+ if (!existingGroups.some((g) => g.id === groupIdForProperty)) {
7758
+ ctx.groups.add({ id: groupIdForProperty, title: displayTitle });
7759
+ }
7760
+ // Ensure property exists (using a generic file-uploader interface)
7761
+ const props = ctx.properties.list();
7762
+ if (!props.some((p) => p.name === field)) {
7763
+ ctx.properties.add({
7764
+ name: field,
7765
+ title: displayTitle,
7766
+ groupId: groupIdForProperty,
7767
+ schema: {
7768
+ dataType: 'string',
7769
+ interface: {
7770
+ type: 'attachments',
7771
+ options: {
7772
+ ...buildAttachmentsFileUploaderOptions({ ...opts, field }, displayMode),
7773
+ entityName: `${ctx.entity.module}.${ctx.entity.name}`,
7774
+ entityId: '{{ context.eval("id") }}',
7775
+ entityField: field,
7776
+ },
7777
+ },
7778
+ },
7779
+ });
7780
+ }
7781
+ applyAttachmentsFileUploaderOptions(ctx, field, opts, displayMode);
7782
+ // Ensure column exists (optional)
7783
+ const showListColumn = opts.showListColumn ?? true;
7784
+ if (showListColumn) {
7785
+ const cols = ctx.columns.list() ?? [];
7786
+ if (!cols?.some((c) => c.name === field)) {
7787
+ ctx.columns.add({ name: field });
7788
+ }
7789
+ }
7790
+ if (displayMode === 'section') {
7791
+ const propertyLayoutOrder = 8;
7792
+ const layoutSectionOrder = opts.section?.order ?? DEFAULT_ATTACHMENTS_SECTION_ORDER;
7793
+ const colSpan = 12;
7794
+ ctx.interfaces.master.single.update((single) => {
7795
+ const next = cloneLayoutArrays((single ?? { title: ctx.entity.title, sections: [], properties: [] }));
7796
+ ensureLayoutSection(next, { id: sectionId, title: sectionTitle, order: layoutSectionOrder });
7797
+ ensureLayoutPropertyView(next, {
7798
+ name: field,
7799
+ layout: { label: { visible: false }, positions: { lg: { colSpan, order: propertyLayoutOrder } } },
7800
+ });
7801
+ return next;
7802
+ });
7803
+ ctx.interfaces.master.create.update((create) => {
7804
+ const next = cloneLayoutArrays((create ?? { sections: [], properties: [] }));
7805
+ ensureLayoutSection(next, { id: sectionId, title: sectionTitle, order: layoutSectionOrder });
7806
+ ensureLayoutPropertyView(next, {
7807
+ name: field,
7808
+ layout: { label: { visible: false }, positions: { lg: { colSpan, order: 6 } } },
7809
+ });
7810
+ return next;
7811
+ });
7812
+ ctx.interfaces.master.modify.update((modify) => {
7813
+ const next = cloneLayoutArrays((modify ?? { sections: [], properties: [] }));
7814
+ ensureLayoutSection(next, { id: sectionId, title: sectionTitle, order: layoutSectionOrder });
7815
+ ensureLayoutPropertyView(next, {
7816
+ name: field,
7817
+ layout: { label: { visible: false }, positions: { lg: { colSpan, order: 6 } } },
7818
+ });
7819
+ return next;
7820
+ });
7821
+ }
7822
+ else {
7823
+ ctx.entity.pages ??= [];
7824
+ const pageExists = ctx.entity.pages.some((p) => p.componentKey === ATTACHMENTS_PAGE_COMPONENT_KEY && p.field === field);
7825
+ const pageOrder = resolveEntityPluginDetailPageOrder({ relatedEntities: ctx.entity.relatedEntities, pages: ctx.entity.pages }, 'attachments', {
7826
+ componentKey: ATTACHMENTS_PAGE_COMPONENT_KEY,
7827
+ field,
7828
+ skipPage: pageExists
7829
+ ? { componentKey: ATTACHMENTS_PAGE_COMPONENT_KEY, field }
7830
+ : undefined,
7831
+ });
7832
+ const page = {
7833
+ componentKey: ATTACHMENTS_PAGE_COMPONENT_KEY,
7834
+ title: displayTitle,
7835
+ icon: DEFAULT_ATTACHMENTS_PAGE_ICON,
7836
+ layout: { order: pageOrder },
7837
+ field,
7838
+ };
7839
+ if (!pageExists) {
7840
+ ctx.entity.pages.push(page);
7841
+ }
7842
+ else {
7843
+ const pages = ctx.entity.pages;
7844
+ const index = pages.findIndex((p) => p.componentKey === ATTACHMENTS_PAGE_COMPONENT_KEY && p.field === field);
7845
+ if (index >= 0) {
7846
+ pages[index] = { ...pages[index], ...page };
7847
+ }
7848
+ }
7849
+ }
7850
+ // Ensure list action to open file uploader popup exists (optional)
7851
+ const showListAction = opts.showListAction ?? true;
7852
+ if (showListAction) {
7853
+ ensureListActions(ctx);
7854
+ ctx.interfaces.update((i) => {
7855
+ const actions = i.master.list.actions;
7856
+ const cmdName = 'show-file-uploader-popup';
7857
+ const actionName = `attachments-${field}`;
7858
+ if (!actionExists(actions, cmdName, actionName)) {
7859
+ actions.push({
7860
+ name: actionName,
7861
+ title: '@document-management:actions.upload-files',
7862
+ command: {
7863
+ name: cmdName,
7864
+ options: { ...buildAttachmentsListActionCommandOptions(opts, field),
7865
+ entity: {
7866
+ name: `${ctx.entity.module}.${ctx.entity.name}`,
7867
+ id: '{{ context.eval("id") }}',
7868
+ field: field,
7869
+ },
7870
+ },
7871
+ },
7872
+ priority: 'secondary',
7873
+ type: 'upload',
7874
+ scope: AXPEntityCommandScope.Individual,
7875
+ });
7876
+ }
7877
+ return i;
7878
+ });
7879
+ }
7880
+ // Stamp normalized config for middleware
7881
+ ctx.entity.extensions ??= {};
7882
+ ctx.entity.extensions.attachments ??= {};
7883
+ ctx.entity.extensions.attachments[field] = { title: displayTitle, display: displayMode };
7884
+ },
7885
+ };
7886
+
7887
+ //#region ---- Attachments Page Component Provider ----
7888
+ class AXMAttachmentsPageComponentProvider {
7889
+ async components() {
7890
+ return [
7891
+ {
7892
+ key: ATTACHMENTS_PAGE_COMPONENT_KEY,
7893
+ loader: () => import('./acorex-platform-layout-entity-attachments-page.component-BaTS183I.mjs').then((m) => m.AXMAttachmentsPageComponent),
7894
+ },
7895
+ ];
7896
+ }
7897
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAttachmentsPageComponentProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
7898
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAttachmentsPageComponentProvider }); }
7899
+ }
7900
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAttachmentsPageComponentProvider, decorators: [{
7901
+ type: Injectable
7902
+ }] });
7903
+
7904
+ class AXPAttachmentsPluginModule {
7905
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAttachmentsPluginModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
7906
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: AXPAttachmentsPluginModule }); }
7907
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAttachmentsPluginModule, providers: [{ provide: AXP_ENTITY_ACTION_PLUGIN, multi: true, useValue: attachmentsPlugin },
7908
+ { provide: AXP_PAGE_COMPONENT_PROVIDER, multi: true, useClass: AXMAttachmentsPageComponentProvider },
7909
+ ] }); }
7910
+ }
7911
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAttachmentsPluginModule, decorators: [{
7912
+ type: NgModule,
7913
+ args: [{
7914
+ declarations: [],
7915
+ imports: [],
7916
+ exports: [],
7917
+ providers: [{ provide: AXP_ENTITY_ACTION_PLUGIN, multi: true, useValue: attachmentsPlugin },
7918
+ { provide: AXP_PAGE_COMPONENT_PROVIDER, multi: true, useClass: AXMAttachmentsPageComponentProvider },
7919
+ ],
7920
+ }]
7921
+ }] });
7922
+
7266
7923
  //#region ---- Constants ----
7267
7924
  /**
7268
7925
  * i18n key for the synthetic root node ("all categories"). Resolve in templates with the translate pipe.
@@ -11339,14 +11996,22 @@ function mapPropertyToWidgetColumn(property) {
11339
11996
  */
11340
11997
  function mapEntityColumnToWidgetColumn(entity, column) {
11341
11998
  const property = entity.properties.find((p) => p.name === column.name);
11999
+ const interfaceType = property?.schema?.interface?.type;
11342
12000
  const widgetFromProperty = property?.schema?.interface
11343
12001
  ? {
11344
- type: property.schema.interface.type || 'text-editor',
12002
+ type: interfaceType || 'text-editor',
11345
12003
  path: column.options?.dataPath || column.name,
11346
12004
  options: {
11347
12005
  readonly: true,
11348
12006
  columnName: column.name,
11349
12007
  ...property.schema.interface.options,
12008
+ ...(interfaceType === 'attachments'
12009
+ ? {
12010
+ entityName: property.schema.interface.options?.['entityName'] ??
12011
+ `${entity.module}.${entity.name}`,
12012
+ entityField: property.schema.interface.options?.['entityField'] ?? column.name,
12013
+ }
12014
+ : {}),
11350
12015
  ...(property.schema.interface.children?.length && { children: property.schema.interface.children }),
11351
12016
  },
11352
12017
  }
@@ -12859,54 +13524,352 @@ var lookupWidgetView_component = /*#__PURE__*/Object.freeze({
12859
13524
  });
12860
13525
 
12861
13526
  //#endregion
12862
- class AXPEntityDataSelectorService {
13527
+ class AXPEntityDataSelectorRowActionsService {
12863
13528
  constructor() {
12864
13529
  //#region ---- Services & Dependencies ----
12865
- this.dataSelectorService = inject(AXPDataSelectorService);
12866
- this.filterOperatorMiddleware = inject(AXPFilterOperatorMiddlewareService);
12867
- this.widgetResolver = inject(AXPWidgetRegistryService);
12868
- this.entityResolver = inject(AXPEntityDefinitionRegistryService);
13530
+ this.expressionEvaluator = inject(AXPExpressionEvaluatorService);
13531
+ this.workflow = inject(AXPWorkflowService);
12869
13532
  this.commandService = inject(AXPCommandService);
12870
13533
  }
12871
13534
  //#endregion
12872
13535
  //#region ---- Public Methods ----
12873
13536
  /**
12874
- * Open entity data selector popup
13537
+ * Build a row-actions handler for {@link AXPDataSelectorComponent} from entity list actions.
12875
13538
  */
12876
- async open(options) {
12877
- const config = this.createDataSelectorConfig(options);
12878
- // Handle category filter
12879
- if (options.categoryFilter) {
12880
- // Use provided category filter config
12881
- config.categoryFilter = {
12882
- enabled: true,
12883
- ...options.categoryFilter,
12884
- };
13539
+ createHandler(entity, config) {
13540
+ const primaryFilters = config.primary ?? [];
13541
+ const secondaryFilters = config.secondary ?? [];
13542
+ if (primaryFilters.length === 0 && secondaryFilters.length === 0) {
13543
+ return undefined;
12885
13544
  }
12886
- else {
12887
- // Auto-detect category filter from entity plugin if enabled
12888
- const showCategoryFilter = options.showCategoryFilter ?? true;
12889
- if (showCategoryFilter) {
12890
- const categoryExt = options.entity?.extensions?.category;
12891
- if (categoryExt) {
12892
- const categoryTreeDataSource = await this.createCategoryTreeDataSource(categoryExt.entity);
12893
- if (categoryTreeDataSource) {
12894
- const propertyName = categoryExt.propertyName || 'categoryIds';
12895
- const filterOperator = 'contains';
12896
- config.categoryFilter = {
12897
- enabled: true,
12898
- title: '@general:terms.classification.categories',
12899
- dataSource: categoryTreeDataSource,
12900
- filterField: propertyName,
12901
- filterOperator,
12902
- };
12903
- }
12904
- }
12905
- }
13545
+ const allActions = this.buildIndividualActions(entity);
13546
+ const primaryActions = this.filterActions(allActions, primaryFilters);
13547
+ const secondaryActions = this.filterActions(allActions, secondaryFilters);
13548
+ if (primaryActions.length === 0 && secondaryActions.length === 0) {
13549
+ return undefined;
12906
13550
  }
12907
- const result = (await this.dataSelectorService.open(config));
12908
- return result;
12909
- }
13551
+ const flattenedActions = this.flattenActions([...primaryActions, ...secondaryActions]);
13552
+ return {
13553
+ hasPrimary: () => primaryActions.length > 0,
13554
+ hasDropdown: () => secondaryActions.length > 0,
13555
+ getPrimaryItems: () => this.toRowCommandItems(primaryActions),
13556
+ getDropdownItems: async (rowData) => {
13557
+ const evaluated = await this.evaluateActionsForRow(secondaryActions, rowData);
13558
+ return this.toRowCommandItems(evaluated);
13559
+ },
13560
+ execute: async (commandName, rowData) => {
13561
+ await this.executeCommand(entity, flattenedActions, commandName, rowData);
13562
+ },
13563
+ };
13564
+ }
13565
+ //#endregion
13566
+ //#region ---- Private Methods ----
13567
+ buildIndividualActions(entity) {
13568
+ const listActions = entity.interfaces?.master?.list?.actions ?? [];
13569
+ const individualActions = listActions.filter((action) => action.scope === AXPEntityCommandScope.Individual);
13570
+ return orderBy(individualActions.map((action) => new AXPEntityCommandTriggerViewModel(entity, action)), 'order', 'asc');
13571
+ }
13572
+ filterActions(actions, filters) {
13573
+ if (!filters.length) {
13574
+ return [];
13575
+ }
13576
+ return actions.filter((action) => this.matchesActionFilter(action, filters));
13577
+ }
13578
+ matchesActionFilter(action, filters) {
13579
+ const commandName = this.getCommandNameFromTrigger(action.name);
13580
+ return filters.some((filter) => {
13581
+ const normalizedFilter = filter.trim();
13582
+ if (!normalizedFilter) {
13583
+ return false;
13584
+ }
13585
+ return (commandName === normalizedFilter ||
13586
+ action.name === normalizedFilter ||
13587
+ commandName.endsWith(normalizedFilter) ||
13588
+ action.name.endsWith(normalizedFilter) ||
13589
+ action.name.startsWith(`${normalizedFilter}&`));
13590
+ });
13591
+ }
13592
+ getCommandNameFromTrigger(triggerName) {
13593
+ const ampIndex = triggerName.indexOf('&');
13594
+ return ampIndex >= 0 ? triggerName.slice(0, ampIndex) : triggerName;
13595
+ }
13596
+ flattenActions(actions) {
13597
+ const result = [];
13598
+ const visit = (items) => {
13599
+ for (const item of items) {
13600
+ result.push(item);
13601
+ if (item.items?.length) {
13602
+ visit(item.items);
13603
+ }
13604
+ }
13605
+ };
13606
+ visit(actions);
13607
+ return result;
13608
+ }
13609
+ toRowCommandItems(actions) {
13610
+ return actions.map((action) => ({
13611
+ name: action.name,
13612
+ text: translateSync(action.title),
13613
+ icon: action.icon ?? '',
13614
+ color: action.color,
13615
+ disabled: action.disabled,
13616
+ default: action.default,
13617
+ }));
13618
+ }
13619
+ async evaluateActionsForRow(actions, rowData) {
13620
+ const scope = this.createExpressionScope(rowData);
13621
+ const evaluated = await Promise.all(actions.map(async (action) => {
13622
+ const isHidden = await this.expressionEvaluator.evaluate(action.hidden, scope);
13623
+ if (isHidden) {
13624
+ return null;
13625
+ }
13626
+ const disabled = await this.expressionEvaluator.evaluate(action.disabled, scope);
13627
+ const name = await this.resolveTriggerName(action.name, rowData);
13628
+ const title = await this.resolveTitleExpression(action.title, rowData);
13629
+ return { ...action, disabled, name, title };
13630
+ }));
13631
+ return evaluated.filter(Boolean);
13632
+ }
13633
+ createExpressionScope(data) {
13634
+ return {
13635
+ context: {
13636
+ eval: (path) => get(data, path),
13637
+ },
13638
+ };
13639
+ }
13640
+ async resolveTriggerName(triggerName, data) {
13641
+ if (!triggerName) {
13642
+ return triggerName;
13643
+ }
13644
+ const ampIndex = triggerName.indexOf('&');
13645
+ if (ampIndex < 0) {
13646
+ return this.resolveCommandNameExpression(triggerName, data);
13647
+ }
13648
+ const commandPart = triggerName.slice(0, ampIndex);
13649
+ const suffix = triggerName.slice(ampIndex);
13650
+ const resolvedCommand = await this.resolveCommandNameExpression(commandPart, data);
13651
+ return `${resolvedCommand}${suffix}`;
13652
+ }
13653
+ async resolveCommandNameExpression(commandName, data) {
13654
+ if (!commandName || !commandName.includes('{{')) {
13655
+ return commandName;
13656
+ }
13657
+ const scope = this.createExpressionScope(data);
13658
+ const result = await this.expressionEvaluator.evaluate({ value: commandName }, scope);
13659
+ const resolved = result?.value;
13660
+ return typeof resolved === 'string' && resolved.trim() !== '' ? resolved : commandName;
13661
+ }
13662
+ async resolveTitleExpression(title, data) {
13663
+ if (!title || !title.includes('{{')) {
13664
+ return title;
13665
+ }
13666
+ const scope = this.createExpressionScope(data);
13667
+ const result = await this.expressionEvaluator.evaluate({ value: title }, scope);
13668
+ const resolved = result?.value;
13669
+ return typeof resolved === 'string' && resolved.trim() !== '' ? resolved : title;
13670
+ }
13671
+ async executeCommand(entity, actions, commandName, rowData) {
13672
+ const action = actions.find((candidate) => candidate.name === commandName);
13673
+ if (!action) {
13674
+ return;
13675
+ }
13676
+ const resolvedTriggerName = await this.resolveTriggerName(commandName, rowData);
13677
+ const command = resolvedTriggerName.split('&')[0];
13678
+ const options = await this.evaluateActionOptions(action.options, rowData);
13679
+ if (this.workflow.exists(command)) {
13680
+ await this.workflow.execute(command, {
13681
+ entity: getEntityInfo(entity).source,
13682
+ entityInfo: {
13683
+ name: entity.name,
13684
+ module: entity.module,
13685
+ title: entity.title,
13686
+ parentKey: entity.parentKey,
13687
+ source: `${entity.module}.${entity.name}`,
13688
+ },
13689
+ data: rowData,
13690
+ options,
13691
+ metadata: action.metadata,
13692
+ });
13693
+ return;
13694
+ }
13695
+ await this.commandService.execute(command, {
13696
+ __context__: {
13697
+ data: rowData,
13698
+ entity: getEntityInfo(entity).source,
13699
+ entityInfo: {
13700
+ name: entity.name,
13701
+ module: entity.module,
13702
+ title: entity.title,
13703
+ parentKey: entity.parentKey,
13704
+ },
13705
+ options,
13706
+ },
13707
+ __metadata__: action.metadata,
13708
+ });
13709
+ }
13710
+ async evaluateActionOptions(options, rowData) {
13711
+ if (!options || typeof options !== 'object') {
13712
+ return {};
13713
+ }
13714
+ const scope = this.createExpressionScope(rowData);
13715
+ const evaluated = await this.expressionEvaluator.evaluate(options, scope);
13716
+ return evaluated ?? {};
13717
+ }
13718
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDataSelectorRowActionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
13719
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDataSelectorRowActionsService, providedIn: 'root' }); }
13720
+ }
13721
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityDataSelectorRowActionsService, decorators: [{
13722
+ type: Injectable,
13723
+ args: [{
13724
+ providedIn: 'root',
13725
+ }]
13726
+ }] });
13727
+
13728
+ const LEGACY_ENTITY_DATA_SELECTOR_OPTION_KEYS = [
13729
+ 'allowMultiple',
13730
+ 'allowUnselect',
13731
+ 'filters',
13732
+ 'parentFilters',
13733
+ 'columns',
13734
+ 'sortedFields',
13735
+ 'allowCreate',
13736
+ 'searchFields',
13737
+ 'initialSearchTerm',
13738
+ 'selectedItemIds',
13739
+ 'categoryFilter',
13740
+ 'showCategoryFilter',
13741
+ ];
13742
+ //#endregion
13743
+ //#region ---- Normalization ----
13744
+ /**
13745
+ * Returns true when flat (legacy) option fields are present on the input object.
13746
+ */
13747
+ function isLegacyEntityDataSelectorOptions(options) {
13748
+ const legacy = options;
13749
+ return LEGACY_ENTITY_DATA_SELECTOR_OPTION_KEYS.some((key) => legacy[key] !== undefined);
13750
+ }
13751
+ /**
13752
+ * Maps deprecated flat options to {@link AXPEntityDataSelectorOpenOptions}.
13753
+ */
13754
+ function mapLegacyEntityDataSelectorOptions(legacy) {
13755
+ const normalized = {
13756
+ entity: legacy.entity,
13757
+ title: legacy.title,
13758
+ };
13759
+ if (legacy.allowMultiple !== undefined ||
13760
+ legacy.selectedItemIds !== undefined ||
13761
+ legacy.allowUnselect !== undefined) {
13762
+ normalized.selection = {
13763
+ ...(legacy.allowMultiple !== undefined && { multiple: legacy.allowMultiple }),
13764
+ ...(legacy.selectedItemIds !== undefined && { selectedItemIds: legacy.selectedItemIds }),
13765
+ ...(legacy.allowUnselect !== undefined && { allowUnselect: legacy.allowUnselect }),
13766
+ };
13767
+ }
13768
+ if (legacy.columns !== undefined) {
13769
+ normalized.grid = { columns: legacy.columns };
13770
+ }
13771
+ if (legacy.filters !== undefined || legacy.parentFilters !== undefined) {
13772
+ normalized.filter = {
13773
+ ...(legacy.filters !== undefined && { value: legacy.filters }),
13774
+ ...(legacy.parentFilters !== undefined && { parent: legacy.parentFilters }),
13775
+ };
13776
+ }
13777
+ if (legacy.searchFields !== undefined || legacy.initialSearchTerm !== undefined) {
13778
+ normalized.search = {
13779
+ ...(legacy.searchFields !== undefined && { fields: legacy.searchFields }),
13780
+ ...(legacy.initialSearchTerm !== undefined && { initial: legacy.initialSearchTerm }),
13781
+ };
13782
+ }
13783
+ if (legacy.sortedFields !== undefined) {
13784
+ normalized.sort = { fields: legacy.sortedFields };
13785
+ }
13786
+ if (legacy.categoryFilter) {
13787
+ normalized.category = {
13788
+ enabled: true,
13789
+ title: legacy.categoryFilter.title,
13790
+ width: legacy.categoryFilter.width,
13791
+ dataSource: legacy.categoryFilter.dataSource,
13792
+ filter: {
13793
+ field: legacy.categoryFilter.filterField,
13794
+ operator: legacy.categoryFilter.filterOperator,
13795
+ },
13796
+ };
13797
+ }
13798
+ else if (legacy.showCategoryFilter !== undefined) {
13799
+ normalized.category = { enabled: legacy.showCategoryFilter };
13800
+ }
13801
+ if (legacy.allowCreate !== undefined) {
13802
+ normalized.create = { mode: legacy.allowCreate };
13803
+ }
13804
+ if (legacy.rowActions !== undefined) {
13805
+ normalized.rowActions = legacy.rowActions;
13806
+ }
13807
+ return normalized;
13808
+ }
13809
+ /**
13810
+ * Normalizes legacy flat or nested options to {@link AXPEntityDataSelectorOpenOptions}.
13811
+ * When both shapes are mixed, nested groups take precedence over mapped legacy values.
13812
+ */
13813
+ function normalizeEntityDataSelectorOptions(options) {
13814
+ const normalized = isLegacyEntityDataSelectorOptions(options)
13815
+ ? mapLegacyEntityDataSelectorOptions(options)
13816
+ : { ...options };
13817
+ const nested = options;
13818
+ if (nested.selection) {
13819
+ normalized.selection = { ...normalized.selection, ...nested.selection };
13820
+ }
13821
+ if (nested.grid) {
13822
+ normalized.grid = { ...normalized.grid, ...nested.grid };
13823
+ }
13824
+ if (nested.filter) {
13825
+ normalized.filter = { ...normalized.filter, ...nested.filter };
13826
+ }
13827
+ if (nested.search) {
13828
+ normalized.search = { ...normalized.search, ...nested.search };
13829
+ }
13830
+ if (nested.sort) {
13831
+ normalized.sort = { ...normalized.sort, ...nested.sort };
13832
+ }
13833
+ if (nested.category) {
13834
+ normalized.category = { ...normalized.category, ...nested.category };
13835
+ if (nested.category.filter) {
13836
+ normalized.category.filter = { ...normalized.category?.filter, ...nested.category.filter };
13837
+ }
13838
+ }
13839
+ if (nested.create) {
13840
+ normalized.create = { ...normalized.create, ...nested.create };
13841
+ }
13842
+ if (nested.rowActions !== undefined) {
13843
+ normalized.rowActions = nested.rowActions;
13844
+ }
13845
+ return normalized;
13846
+ }
13847
+ //#endregion
13848
+ class AXPEntityDataSelectorService {
13849
+ constructor() {
13850
+ //#region ---- Services & Dependencies ----
13851
+ this.dataSelectorService = inject(AXPDataSelectorService);
13852
+ this.filterOperatorMiddleware = inject(AXPFilterOperatorMiddlewareService);
13853
+ this.widgetResolver = inject(AXPWidgetRegistryService);
13854
+ this.entityResolver = inject(AXPEntityDefinitionRegistryService);
13855
+ this.commandService = inject(AXPCommandService);
13856
+ this.categoryTreeService = inject(AXPCategoryTreeService);
13857
+ this.translationService = inject(AXTranslationService);
13858
+ this.rowActionsService = inject(AXPEntityDataSelectorRowActionsService);
13859
+ }
13860
+ //#endregion
13861
+ //#region ---- Public Methods ----
13862
+ /**
13863
+ * Open entity data selector popup.
13864
+ * Accepts nested {@link AXPEntityDataSelectorOpenOptions} or deprecated flat {@link AXPEntityDataSelectorOptions}.
13865
+ */
13866
+ async open(options) {
13867
+ const normalized = normalizeEntityDataSelectorOptions(options);
13868
+ const config = this.createDataSelectorConfig(normalized);
13869
+ await this.applyCategoryFilter(normalized, config);
13870
+ const result = (await this.dataSelectorService.open(config));
13871
+ return result;
13872
+ }
12910
13873
  /**
12911
13874
  * Execute Entity:Create and return created item.
12912
13875
  * Used by lookup widget and data selector for inline create.
@@ -12923,6 +13886,51 @@ class AXPEntityDataSelectorService {
12923
13886
  }
12924
13887
  //#endregion
12925
13888
  //#region ---- Private Methods ----
13889
+ /**
13890
+ * Apply category sidebar config from normalized options or entity plugin auto-detection.
13891
+ */
13892
+ async applyCategoryFilter(options, config) {
13893
+ const category = options.category;
13894
+ if (category?.dataSource) {
13895
+ config.category = {
13896
+ enabled: category.enabled ?? true,
13897
+ title: category.title ?? '@general:terms.classification.categories',
13898
+ dataSource: category.dataSource,
13899
+ filter: {
13900
+ field: category.filter?.field ?? 'categoryIds',
13901
+ operator: category.filter?.operator,
13902
+ },
13903
+ width: category.width,
13904
+ };
13905
+ return;
13906
+ }
13907
+ if (category?.enabled === false) {
13908
+ return;
13909
+ }
13910
+ const categoryExt = options.entity?.extensions?.category;
13911
+ if (!categoryExt) {
13912
+ return;
13913
+ }
13914
+ const categoryTreeDataSource = await this.createCategoryTreeDataSource(categoryExt.entity, {
13915
+ textField: categoryExt.textField,
13916
+ valueField: categoryExt.valueField,
13917
+ parentKey: categoryExt.parentKey,
13918
+ });
13919
+ if (!categoryTreeDataSource) {
13920
+ return;
13921
+ }
13922
+ const propertyName = categoryExt.propertyName || 'categoryIds';
13923
+ config.category = {
13924
+ enabled: true,
13925
+ title: category?.title ?? '@general:terms.classification.categories',
13926
+ dataSource: categoryTreeDataSource,
13927
+ filter: {
13928
+ field: category?.filter?.field ?? propertyName,
13929
+ operator: category?.filter?.operator ?? 'contains',
13930
+ },
13931
+ width: category?.width,
13932
+ };
13933
+ }
12926
13934
  /**
12927
13935
  * Create data selector configuration from entity options
12928
13936
  */
@@ -12930,27 +13938,43 @@ class AXPEntityDataSelectorService {
12930
13938
  const dataSource = this.createDataSource(options);
12931
13939
  const columns = this.createColumns(options);
12932
13940
  const searchFields = this.getSearchFields(options);
12933
- const allowCreate = options.allowCreate ?? this.getAllowCreate(options.entity);
13941
+ const allowCreate = options.create?.mode ?? this.getAllowCreate(options.entity);
13942
+ const rowActions = options.rowActions
13943
+ ? this.rowActionsService.createHandler(options.entity, options.rowActions)
13944
+ : undefined;
12934
13945
  return {
12935
13946
  title: options.title,
12936
13947
  dataSource,
12937
- columns,
12938
- selectionMode: options.allowMultiple ? 'multiple' : 'single',
12939
- searchFields,
12940
- initialSearchTerm: options.initialSearchTerm,
12941
- parentField: options.entity.parentKey,
12942
- filters: options.filters || undefined,
12943
- allowCreate,
12944
- onCreate: allowCreate !== 'none' ? (mode) => this.executeCreate(options.entity, mode) : undefined,
12945
- selectedItemIds: options.selectedItemIds,
12946
- // Note: Custom filters will be applied to the dataSource in createDataSource
13948
+ grid: {
13949
+ columns,
13950
+ parentField: options.entity.parentKey,
13951
+ },
13952
+ selection: {
13953
+ mode: options.selection?.multiple ? 'multiple' : 'single',
13954
+ selectedItemIds: options.selection?.selectedItemIds,
13955
+ allowUnselect: options.selection?.allowUnselect ?? true,
13956
+ },
13957
+ search: searchFields.length > 0 || options.search?.initial
13958
+ ? {
13959
+ fields: searchFields,
13960
+ initial: options.search?.initial,
13961
+ }
13962
+ : undefined,
13963
+ filter: options.filter?.value ? { value: options.filter.value } : undefined,
13964
+ create: {
13965
+ mode: allowCreate,
13966
+ onCreate: allowCreate !== 'none' ? (mode) => this.executeCreate(options.entity, mode) : undefined,
13967
+ },
13968
+ rowActions,
12947
13969
  };
12948
13970
  }
12949
13971
  /**
12950
13972
  * Create data source from entity definition
12951
13973
  */
12952
13974
  createDataSource(options) {
12953
- const { entity, filters, parentFilters } = options;
13975
+ const { entity } = options;
13976
+ const filters = options.filter?.value;
13977
+ const parentFilters = options.filter?.parent;
12954
13978
  return new AXDataSource({
12955
13979
  byKey: (key) => {
12956
13980
  const func = entity.queries?.byKey?.execute;
@@ -12964,7 +13988,7 @@ class AXPEntityDataSelectorService {
12964
13988
  this.mergeFilters(e, filters, parentFilters);
12965
13989
  return func(e);
12966
13990
  },
12967
- pageSize: 10,
13991
+ pageSize: options.grid?.pageSize ?? 10,
12968
13992
  key: 'id',
12969
13993
  });
12970
13994
  }
@@ -12972,7 +13996,8 @@ class AXPEntityDataSelectorService {
12972
13996
  * Create columns configuration from entity definition
12973
13997
  */
12974
13998
  createColumns(options) {
12975
- const { entity, columns = [] } = options;
13999
+ const { entity } = options;
14000
+ const columns = options.grid?.columns ?? [];
12976
14001
  const { columns: entityColumns = [], properties } = entity;
12977
14002
  const visibleProperties = properties.filter(({ schema }) => schema?.visible !== false);
12978
14003
  const visiblePropNames = new Set(visibleProperties.map(({ name }) => name));
@@ -13013,12 +14038,18 @@ class AXPEntityDataSelectorService {
13013
14038
  dataType: 'string',
13014
14039
  interface: {
13015
14040
  type: column.showAs.type,
13016
- options: { ...column.showAs.options, disableDetailPopover: true },
14041
+ options: {
14042
+ ...column.showAs.options,
14043
+ popover: { ...column.showAs.options?.['popover'], enabled: false },
14044
+ },
13017
14045
  },
13018
14046
  },
13019
14047
  };
13020
14048
  widgetType = column.showAs.type;
13021
- widgetOptions = { ...column.showAs.options, disableDetailPopover: true };
14049
+ widgetOptions = {
14050
+ ...column.showAs.options,
14051
+ popover: { ...column.showAs.options?.['popover'], enabled: false },
14052
+ };
13022
14053
  }
13023
14054
  else {
13024
14055
  // Use entity property configuration - same columns, title, and interface as entity
@@ -13028,7 +14059,7 @@ class AXPEntityDataSelectorService {
13028
14059
  widgetOptions = {
13029
14060
  ...(iface?.options ?? {}),
13030
14061
  ...(iface?.children?.length && { children: iface.children }),
13031
- disableDetailPopover: true, // Column content is visible in lookup popup - no need for detail popover
14062
+ popover: { ...iface?.options?.['popover'], enabled: false },
13032
14063
  };
13033
14064
  }
13034
14065
  // Get width from column options (set by middleware)
@@ -13052,7 +14083,8 @@ class AXPEntityDataSelectorService {
13052
14083
  * Get searchable fields from entity properties
13053
14084
  */
13054
14085
  getSearchFields(options) {
13055
- const { entity, searchFields } = options;
14086
+ const { entity } = options;
14087
+ const searchFields = options.search?.fields;
13056
14088
  const searchableFields = entity.properties
13057
14089
  .filter((p) => p.options?.filter?.inline?.enabled)
13058
14090
  .map((p) => {
@@ -13149,89 +14181,26 @@ class AXPEntityDataSelectorService {
13149
14181
  return normalized;
13150
14182
  }
13151
14183
  /**
13152
- * Create category tree data source from category entity key
14184
+ * Create category tree data source from category entity key.
14185
+ * Uses the same {@link AXPCategoryTreeService} as the entity master list category sidebar.
13153
14186
  */
13154
- async createCategoryTreeDataSource(categoryEntityKey) {
14187
+ async createCategoryTreeDataSource(categoryEntityKey, categoryOptions) {
13155
14188
  try {
13156
- const [module, entity] = categoryEntityKey.split('.');
13157
- if (!module || !entity) {
13158
- return null;
13159
- }
13160
- const categoryEntity = await this.entityResolver.resolve(module, entity);
13161
- if (!categoryEntity?.queries?.list?.execute || typeof categoryEntity.queries.list.execute !== 'function') {
14189
+ const treeConfig = {
14190
+ entityKey: categoryEntityKey,
14191
+ textField: categoryOptions?.textField,
14192
+ valueField: categoryOptions?.valueField,
14193
+ parentKey: categoryOptions?.parentKey,
14194
+ };
14195
+ const treeData = await this.categoryTreeService.initializeCategoryTree(treeConfig);
14196
+ if (!treeData) {
13162
14197
  return null;
13163
14198
  }
13164
- const listQuery = categoryEntity.queries.list.execute;
13165
- const parentKey = categoryEntity.parentKey || 'parentId';
13166
- const categoryTreeDataSource = {
13167
- loadRootNodes: async () => {
13168
- const result = await listQuery({
13169
- skip: 0,
13170
- take: 1000,
13171
- filter: {
13172
- field: parentKey,
13173
- operator: { type: 'isNull' },
13174
- },
13175
- sort: [{ field: 'title', dir: 'asc' }],
13176
- });
13177
- return (result?.items || []).map((item) => ({
13178
- id: item.id,
13179
- title: item.title || item.name,
13180
- description: item.description || '',
13181
- parentId: item[parentKey],
13182
- childrenCount: item.childrenCount || 0,
13183
- }));
13184
- },
13185
- loadChildNodes: async (parentId) => {
13186
- const result = await listQuery({
13187
- skip: 0,
13188
- take: 1000,
13189
- filter: {
13190
- field: parentKey,
13191
- operator: { type: 'equal' },
13192
- value: parentId,
13193
- },
13194
- sort: [{ field: 'title', dir: 'asc' }],
13195
- });
13196
- return (result?.items || []).map((item) => ({
13197
- id: item.id,
13198
- title: item.title || item.name,
13199
- description: item.description || '',
13200
- parentId: item[parentKey],
13201
- childrenCount: item.childrenCount || 0,
13202
- }));
13203
- },
13204
- searchNodes: async (searchValue) => {
13205
- const result = await listQuery({
13206
- skip: 0,
13207
- take: 1000,
13208
- filter: {
13209
- filters: [
13210
- {
13211
- field: 'title',
13212
- operator: { type: 'contains' },
13213
- value: searchValue,
13214
- },
13215
- {
13216
- field: 'name',
13217
- operator: { type: 'contains' },
13218
- value: searchValue,
13219
- },
13220
- ],
13221
- logic: 'or',
13222
- },
13223
- sort: [{ field: 'title', dir: 'asc' }],
13224
- });
13225
- return (result?.items || []).map((item) => ({
13226
- id: item.id,
13227
- title: item.title || item.name,
13228
- description: item.description || '',
13229
- parentId: item[parentKey],
13230
- childrenCount: item.childrenCount || 0,
13231
- }));
13232
- },
13233
- };
13234
- return categoryTreeDataSource;
14199
+ const parentKey = treeConfig.parentKey || treeData.categoryEntityDef?.parentKey || 'parentId';
14200
+ treeConfig.parentKey = parentKey;
14201
+ treeConfig.textField = treeConfig.textField ?? 'title';
14202
+ treeConfig.valueField = treeConfig.valueField ?? 'id';
14203
+ return this.buildCategoryTreeDataSource(treeData, treeConfig, parentKey);
13235
14204
  }
13236
14205
  catch (error) {
13237
14206
  console.error('Error creating category tree data source:', error);
@@ -13239,28 +14208,109 @@ class AXPEntityDataSelectorService {
13239
14208
  }
13240
14209
  }
13241
14210
  /**
13242
- * Merge custom and parent filters into data source query
14211
+ * Build category tree data source callbacks backed by shared category tree service.
13243
14212
  */
13244
- mergeFilters(request, customFilter, parentFilters) {
13245
- if (!customFilter && !parentFilters) {
13246
- return request;
13247
- }
13248
- const filters = [];
13249
- // Add custom filter
13250
- if (customFilter) {
13251
- const cleanedFilters = AXPCleanNestedFilters([customFilter]);
13252
- if (cleanedFilters.length > 0) {
13253
- filters.push(this.filterOperatorMiddleware.transformFilter(cleanedFilters[0]));
13254
- }
14213
+ buildCategoryTreeDataSource(treeData, treeConfig, parentKey) {
14214
+ return {
14215
+ loadRootNodes: async () => {
14216
+ const items = await this.categoryTreeService.loadRootCategories(treeData, treeConfig);
14217
+ if (!items) {
14218
+ return [];
14219
+ }
14220
+ return items
14221
+ .filter((item) => isNil(item[parentKey]) || item[parentKey] === '')
14222
+ .map((item) => this.mapCategoryItemToEntity(item, treeConfig, parentKey));
14223
+ },
14224
+ loadChildNodes: async (parentId) => {
14225
+ const targetNode = { id: parentId, title: '', data: {} };
14226
+ const children = await this.categoryTreeService.loadChildren(targetNode, treeData, treeConfig);
14227
+ return children.map((node) => this.mapCategoryTreeNodeToEntity(node, parentKey));
14228
+ },
14229
+ searchNodes: async (searchValue) => {
14230
+ const items = await this.categoryTreeService.searchCategories(searchValue, treeData, treeConfig);
14231
+ if (!items) {
14232
+ return [];
14233
+ }
14234
+ return items.map((item) => this.mapCategoryItemToEntity(item, treeConfig, parentKey));
14235
+ },
14236
+ };
14237
+ }
14238
+ /**
14239
+ * Map a category entity row to {@link AXPCategoryEntity} for the reusable category tree component.
14240
+ */
14241
+ mapCategoryItemToEntity(item, config, parentKey) {
14242
+ const textField = config.textField ?? 'title';
14243
+ const valueField = config.valueField ?? 'id';
14244
+ let childrenCount = 0;
14245
+ if (typeof item['childrenCount'] === 'number') {
14246
+ childrenCount = item['childrenCount'];
13255
14247
  }
13256
- // Add parent filters
13257
- if (parentFilters) {
13258
- filters.push(parentFilters);
14248
+ else if (Array.isArray(item['children'])) {
14249
+ childrenCount = item['children'].length;
13259
14250
  }
13260
- // Merge with existing filter
13261
- if (request.filter) {
13262
- request.filter = {
13263
- logic: 'and',
14251
+ return {
14252
+ id: String(item[valueField] ?? ''),
14253
+ title: this.resolveCategoryText(item[textField] ?? item['name']),
14254
+ description: this.resolveCategoryText(item['description']),
14255
+ parentId: item[parentKey],
14256
+ childrenCount,
14257
+ };
14258
+ }
14259
+ /**
14260
+ * Map a lazy-loaded tree node to {@link AXPCategoryEntity}.
14261
+ */
14262
+ mapCategoryTreeNodeToEntity(node, parentKey) {
14263
+ const data = node['data'] ?? {};
14264
+ let childrenCount = 0;
14265
+ if (typeof node['childrenCount'] === 'number') {
14266
+ childrenCount = node['childrenCount'];
14267
+ }
14268
+ else if (typeof data['childrenCount'] === 'number') {
14269
+ childrenCount = data['childrenCount'];
14270
+ }
14271
+ return {
14272
+ id: String(node['id'] ?? ''),
14273
+ title: String(node['title'] ?? ''),
14274
+ description: this.resolveCategoryText(data['description']),
14275
+ parentId: data[parentKey],
14276
+ childrenCount,
14277
+ };
14278
+ }
14279
+ /**
14280
+ * Resolve category display text, including multi-language titles.
14281
+ */
14282
+ resolveCategoryText(value) {
14283
+ if (isNil(value)) {
14284
+ return '';
14285
+ }
14286
+ if (typeof value === 'object') {
14287
+ return resolveMultiLanguageString(value, this.translationService.getActiveLang());
14288
+ }
14289
+ return String(value);
14290
+ }
14291
+ /**
14292
+ * Merge custom and parent filters into data source query
14293
+ */
14294
+ mergeFilters(request, customFilter, parentFilters) {
14295
+ if (!customFilter && !parentFilters) {
14296
+ return request;
14297
+ }
14298
+ const filters = [];
14299
+ // Add custom filter
14300
+ if (customFilter) {
14301
+ const cleanedFilters = AXPCleanNestedFilters([customFilter]);
14302
+ if (cleanedFilters.length > 0) {
14303
+ filters.push(this.filterOperatorMiddleware.transformFilter(cleanedFilters[0]));
14304
+ }
14305
+ }
14306
+ // Add parent filters
14307
+ if (parentFilters) {
14308
+ filters.push(parentFilters);
14309
+ }
14310
+ // Merge with existing filter
14311
+ if (request.filter) {
14312
+ request.filter = {
14313
+ logic: 'and',
13264
14314
  filters: [...[request.filter], ...filters],
13265
14315
  };
13266
14316
  }
@@ -13573,11 +14623,13 @@ class AXPLookupWidgetTagboxComponent extends LookupWidgetLookBase {
13573
14623
  this.placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
13574
14624
  this.hasClearButton = input(false, ...(ngDevMode ? [{ debugName: "hasClearButton" }] : /* istanbul ignore next */ []));
13575
14625
  this.allowCreate = input('none', ...(ngDevMode ? [{ debugName: "allowCreate" }] : /* istanbul ignore next */ []));
14626
+ this.allowUnselect = input(true, ...(ngDevMode ? [{ debugName: "allowUnselect" }] : /* istanbul ignore next */ []));
13576
14627
  // Entity and configuration data (for showSelector and searchByValue)
13577
14628
  this.entityDef = input.required(...(ngDevMode ? [{ debugName: "entityDef" }] : /* istanbul ignore next */ []));
13578
14629
  this.customFilter = input.required(...(ngDevMode ? [{ debugName: "customFilter" }] : /* istanbul ignore next */ []));
13579
14630
  this.columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
13580
14631
  this.parentFilters = input(null, ...(ngDevMode ? [{ debugName: "parentFilters" }] : /* istanbul ignore next */ []));
14632
+ this.rowActions = input(undefined, ...(ngDevMode ? [{ debugName: "rowActions" }] : /* istanbul ignore next */ []));
13581
14633
  // Business logic callbacks
13582
14634
  this.onSetLoading = input.required(...(ngDevMode ? [{ debugName: "onSetLoading" }] : /* istanbul ignore next */ []));
13583
14635
  //#endregion
@@ -13757,12 +14809,14 @@ class AXPLookupWidgetTagboxComponent extends LookupWidgetLookBase {
13757
14809
  entity,
13758
14810
  title: `${this.translateService.translateSync(entity.formats.plural ?? '')}`,
13759
14811
  allowMultiple: this.multiple()(),
14812
+ allowUnselect: this.allowUnselect(),
13760
14813
  filters: this.customFilter()(),
13761
14814
  parentFilters: this.parentFilters(),
13762
14815
  columns: this.columns()(),
13763
14816
  allowCreate: this.allowCreate(),
13764
14817
  initialSearchTerm: searchTerm,
13765
14818
  selectedItemIds: selectedItemIds.length > 0 ? selectedItemIds : undefined,
14819
+ rowActions: this.rowActions(),
13766
14820
  });
13767
14821
  if (result && 'items' in result) {
13768
14822
  this.valueChanged.emit(result.items);
@@ -13785,7 +14839,7 @@ class AXPLookupWidgetTagboxComponent extends LookupWidgetLookBase {
13785
14839
  this.clear();
13786
14840
  }
13787
14841
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLookupWidgetTagboxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
13788
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPLookupWidgetTagboxComponent, isStandalone: true, selector: "axp-lookup-widget-tagbox", inputs: { selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: true, transformFunction: null }, displayField: { classPropertyName: "displayField", publicName: "displayField", isSignal: true, isRequired: true, transformFunction: null }, valueField: { classPropertyName: "valueField", publicName: "valueField", isSignal: true, isRequired: true, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: true, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, validationRules: { classPropertyName: "validationRules", publicName: "validationRules", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hasClearButton: { classPropertyName: "hasClearButton", publicName: "hasClearButton", isSignal: true, isRequired: false, transformFunction: null }, allowCreate: { classPropertyName: "allowCreate", publicName: "allowCreate", isSignal: true, isRequired: false, transformFunction: null }, entityDef: { classPropertyName: "entityDef", publicName: "entityDef", isSignal: true, isRequired: true, transformFunction: null }, customFilter: { classPropertyName: "customFilter", publicName: "customFilter", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, parentFilters: { classPropertyName: "parentFilters", publicName: "parentFilters", isSignal: true, isRequired: false, transformFunction: null }, onSetLoading: { classPropertyName: "onSetLoading", publicName: "onSetLoading", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { valueChanged: "valueChanged" }, providers: [
14842
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPLookupWidgetTagboxComponent, isStandalone: true, selector: "axp-lookup-widget-tagbox", inputs: { selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: true, transformFunction: null }, displayField: { classPropertyName: "displayField", publicName: "displayField", isSignal: true, isRequired: true, transformFunction: null }, valueField: { classPropertyName: "valueField", publicName: "valueField", isSignal: true, isRequired: true, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: true, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, validationRules: { classPropertyName: "validationRules", publicName: "validationRules", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hasClearButton: { classPropertyName: "hasClearButton", publicName: "hasClearButton", isSignal: true, isRequired: false, transformFunction: null }, allowCreate: { classPropertyName: "allowCreate", publicName: "allowCreate", isSignal: true, isRequired: false, transformFunction: null }, allowUnselect: { classPropertyName: "allowUnselect", publicName: "allowUnselect", isSignal: true, isRequired: false, transformFunction: null }, entityDef: { classPropertyName: "entityDef", publicName: "entityDef", isSignal: true, isRequired: true, transformFunction: null }, customFilter: { classPropertyName: "customFilter", publicName: "customFilter", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, parentFilters: { classPropertyName: "parentFilters", publicName: "parentFilters", isSignal: true, isRequired: false, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, onSetLoading: { classPropertyName: "onSetLoading", publicName: "onSetLoading", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { valueChanged: "valueChanged" }, providers: [
13789
14843
  {
13790
14844
  provide: LookupWidgetLookBase,
13791
14845
  useExisting: AXPLookupWidgetTagboxComponent,
@@ -13924,7 +14978,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
13924
14978
  },
13925
14979
  ],
13926
14980
  }]
13927
- }], propDecorators: { selectedItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedItems", required: true }] }], displayField: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayField", required: true }] }], valueField: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueField", required: true }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: true }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], validationRules: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationRules", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hasClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasClearButton", required: false }] }], allowCreate: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCreate", required: false }] }], entityDef: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityDef", required: true }] }], customFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "customFilter", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], parentFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentFilters", required: false }] }], onSetLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "onSetLoading", required: true }] }], valueChanged: [{ type: i0.Output, args: ["valueChanged"] }], tagBox: [{ type: i0.ViewChild, args: ['tagBoxComponent', { isSignal: true }] }] } });
14981
+ }], propDecorators: { selectedItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedItems", required: true }] }], displayField: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayField", required: true }] }], valueField: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueField", required: true }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: true }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], validationRules: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationRules", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hasClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasClearButton", required: false }] }], allowCreate: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCreate", required: false }] }], allowUnselect: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowUnselect", required: false }] }], entityDef: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityDef", required: true }] }], customFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "customFilter", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], parentFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentFilters", required: false }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], onSetLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "onSetLoading", required: true }] }], valueChanged: [{ type: i0.Output, args: ["valueChanged"] }], tagBox: [{ type: i0.ViewChild, args: ['tagBoxComponent', { isSignal: true }] }] } });
13928
14982
 
13929
14983
  class AXPLookupWidgetEditComponent extends AXPValueWidgetComponent {
13930
14984
  constructor() {
@@ -13964,9 +15018,11 @@ class AXPLookupWidgetEditComponent extends AXPValueWidgetComponent {
13964
15018
  this.placeholder = computed(() => this.options()['placeholder'] ?? '', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
13965
15019
  this.filterMode = computed(() => this.options()['filterMode'], ...(ngDevMode ? [{ debugName: "filterMode" }] : /* istanbul ignore next */ []));
13966
15020
  this.multiple = computed(() => (this.options()['multiple'] ?? false), ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
15021
+ this.allowUnselect = computed(() => this.options()['allowUnselect'] ?? true, ...(ngDevMode ? [{ debugName: "allowUnselect" }] : /* istanbul ignore next */ []));
13967
15022
  this.look = computed(() => this.options()['look'] ?? 'lookup', ...(ngDevMode ? [{ debugName: "look" }] : /* istanbul ignore next */ []));
13968
15023
  this.displayField = computed(() => resolveLookupDisplayField(this.entityDef(), { textField: this.textField() }), ...(ngDevMode ? [{ debugName: "displayField" }] : /* istanbul ignore next */ []));
13969
15024
  this.allowCreate = computed(() => this.options()['allowCreate'] ?? 'none', ...(ngDevMode ? [{ debugName: "allowCreate" }] : /* istanbul ignore next */ []));
15025
+ this.selectorRowActions = computed(() => this.options()['selectorRowActions'], ...(ngDevMode ? [{ debugName: "selectorRowActions" }] : /* istanbul ignore next */ []));
13970
15026
  this.valueField = computed(() => this.entityDef()?.properties.find((c) => c.name == 'id')?.name ?? 'id', ...(ngDevMode ? [{ debugName: "valueField" }] : /* istanbul ignore next */ []));
13971
15027
  this.displayFormat = computed(() => resolveLookupDisplayTemplate(this.entityDef(), {
13972
15028
  displayFormat: this.options()['displayFormat'],
@@ -14247,10 +15303,12 @@ class AXPLookupWidgetEditComponent extends AXPValueWidgetComponent {
14247
15303
  [validationRules]="validationRules"
14248
15304
  [hasClearButton]="hasClearButton()"
14249
15305
  [allowCreate]="allowCreate()"
15306
+ [allowUnselect]="allowUnselect()"
14250
15307
  [entityDef]="entityDef"
14251
15308
  [customFilter]="customFilter"
14252
15309
  [columns]="columns"
14253
15310
  [parentFilters]="filter"
15311
+ [rowActions]="selectorRowActions()"
14254
15312
  [onSetLoading]="setLoading.bind(this)"
14255
15313
  (valueChanged)="handleComponentValueChanged($event)"
14256
15314
  />
@@ -14271,7 +15329,7 @@ class AXPLookupWidgetEditComponent extends AXPValueWidgetComponent {
14271
15329
  }
14272
15330
  </div>
14273
15331
  }
14274
- `, isInline: true, 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: AXDecoratorModule }, { kind: "component", type: i3$1.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type: AXPLookupWidgetSelectComponent, selector: "axp-lookup-widget-select", inputs: ["entityDef", "customFilter", "selectedItems", "displayField", "valueField", "disabled", "multiple", "validationRules", "placeholder", "hasClearButton", "allowCreate", "showItemTooltip", "isItemTruncated"], outputs: ["valueChanged"] }, { kind: "component", type: AXPLookupWidgetTagboxComponent, selector: "axp-lookup-widget-tagbox", inputs: ["selectedItems", "displayField", "valueField", "displayFormat", "disabled", "multiple", "isLoading", "validationRules", "placeholder", "hasClearButton", "allowCreate", "entityDef", "customFilter", "columns", "parentFilters", "onSetLoading"], outputs: ["valueChanged"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i6.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15332
+ `, isInline: true, 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: AXDecoratorModule }, { kind: "component", type: i3$1.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type: AXPLookupWidgetSelectComponent, selector: "axp-lookup-widget-select", inputs: ["entityDef", "customFilter", "selectedItems", "displayField", "valueField", "disabled", "multiple", "validationRules", "placeholder", "hasClearButton", "allowCreate", "showItemTooltip", "isItemTruncated"], outputs: ["valueChanged"] }, { kind: "component", type: AXPLookupWidgetTagboxComponent, selector: "axp-lookup-widget-tagbox", inputs: ["selectedItems", "displayField", "valueField", "displayFormat", "disabled", "multiple", "isLoading", "validationRules", "placeholder", "hasClearButton", "allowCreate", "allowUnselect", "entityDef", "customFilter", "columns", "parentFilters", "rowActions", "onSetLoading"], outputs: ["valueChanged"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i6.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14275
15333
  }
14276
15334
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLookupWidgetEditComponent, decorators: [{
14277
15335
  type: Component,
@@ -14314,10 +15372,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
14314
15372
  [validationRules]="validationRules"
14315
15373
  [hasClearButton]="hasClearButton()"
14316
15374
  [allowCreate]="allowCreate()"
15375
+ [allowUnselect]="allowUnselect()"
14317
15376
  [entityDef]="entityDef"
14318
15377
  [customFilter]="customFilter"
14319
15378
  [columns]="columns"
14320
15379
  [parentFilters]="filter"
15380
+ [rowActions]="selectorRowActions()"
14321
15381
  [onSetLoading]="setLoading.bind(this)"
14322
15382
  (valueChanged)="handleComponentValueChanged($event)"
14323
15383
  />
@@ -14393,6 +15453,10 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
14393
15453
  return this.columnResolve()?.strategy ?? 'hydrated';
14394
15454
  }, ...(ngDevMode ? [{ debugName: "resolveStrategy" }] : /* istanbul ignore next */ []));
14395
15455
  this.isHydratedStrategy = computed(() => this.resolveStrategy() === 'hydrated', ...(ngDevMode ? [{ debugName: "isHydratedStrategy" }] : /* istanbul ignore next */ []));
15456
+ this.detailPopoverEnabled = computed(() => {
15457
+ const popover = this.options.popover;
15458
+ return popover?.enabled !== false;
15459
+ }, ...(ngDevMode ? [{ debugName: "detailPopoverEnabled" }] : /* istanbul ignore next */ []));
14396
15460
  //#endregion
14397
15461
  //#region ---- Signals ----
14398
15462
  this.isMorePopoverOpen = signal(false, ...(ngDevMode ? [{ debugName: "isMorePopoverOpen" }] : /* istanbul ignore next */ []));
@@ -14519,8 +15583,7 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
14519
15583
  return;
14520
15584
  }
14521
15585
  const values = castArray(raw).filter((value) => value != null && value !== '');
14522
- const needsHydrate = values.length > 0 &&
14523
- values.every((value) => typeof value !== 'object' && this.isLikelyEntityId(value));
15586
+ const needsHydrate = values.length > 0 && values.every((value) => typeof value !== 'object' && this.isLikelyEntityId(value));
14524
15587
  if (!needsHydrate) {
14525
15588
  this.hydratedDisplayItems.set(null);
14526
15589
  return;
@@ -14549,7 +15612,7 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
14549
15612
  }
14550
15613
  }
14551
15614
  async showItemDetail(item, index) {
14552
- if (this.options['disableDetailPopover']) {
15615
+ if (!this.detailPopoverEnabled()) {
14553
15616
  return;
14554
15617
  }
14555
15618
  this.selectedItemIndex.set(index);
@@ -14581,9 +15644,7 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
14581
15644
  let resolvedTitle = '';
14582
15645
  if (headerItem != null && this.hasDisplayTextOnLookupValue(headerItem)) {
14583
15646
  resolvedTitle =
14584
- typeof headerItem === 'object'
14585
- ? this.renderLookupLabel(headerItem)
14586
- : this.formatDisplayValue(headerItem);
15647
+ typeof headerItem === 'object' ? this.renderLookupLabel(headerItem) : this.formatDisplayValue(headerItem);
14587
15648
  }
14588
15649
  else if (!this.isMultiValueLookup()) {
14589
15650
  const rowText = this.getRowDisplayTextFromTextField();
@@ -14608,7 +15669,7 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
14608
15669
  });
14609
15670
  }
14610
15671
  handleItemClick(index) {
14611
- if (this.options['disableDetailPopover']) {
15672
+ if (!this.detailPopoverEnabled()) {
14612
15673
  return;
14613
15674
  }
14614
15675
  const items = this.allItems();
@@ -14617,7 +15678,7 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
14617
15678
  }
14618
15679
  }
14619
15680
  handleResolvedItemClick(index) {
14620
- if (this.options['disableDetailPopover']) {
15681
+ if (!this.detailPopoverEnabled()) {
14621
15682
  return;
14622
15683
  }
14623
15684
  const items = this.resolvedPopoverItems();
@@ -14643,9 +15704,7 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
14643
15704
  return values.length > 0 && values.every((value) => this.hasDisplayTextOnLookupValue(value));
14644
15705
  }
14645
15706
  const rowText = this.getRowDisplayTextFromTextField();
14646
- if (!isNil(rowText) &&
14647
- rowText !== '' &&
14648
- (typeof rowText !== 'string' || !this.isLikelyEntityId(rowText))) {
15707
+ if (!isNil(rowText) && rowText !== '' && (typeof rowText !== 'string' || !this.isLikelyEntityId(rowText))) {
14649
15708
  return true;
14650
15709
  }
14651
15710
  const raw = this.rawValue;
@@ -14991,11 +16050,11 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
14991
16050
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) || /^\d+$/.test(value);
14992
16051
  }
14993
16052
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLookupWidgetColumnComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
14994
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPLookupWidgetColumnComponent, isStandalone: true, selector: "ng-component", inputs: { rawValue: "rawValue", rowData: "rowData" }, viewQueries: [{ propertyName: "moreButton", first: true, predicate: ["moreButton"], descendants: true, isSignal: true }, { propertyName: "lazyTrigger", first: true, predicate: ["lazyTrigger"], descendants: true, isSignal: true }, { propertyName: "morePopover", first: true, predicate: ["morePopover"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "@if (isHydratedStrategy()) {\n<div class=\"ax-relative ax-flex ax-min-w-0 ax-items-center ax-gap-1\">\n @for (item of visibleItems(); track $index) {\n @let label = getDisplayRaw(item);\n <span\n [class.ax-cursor-pointer]=\"!options['disableDetailPopover']\"\n [class.hover:ax-text-primary]=\"!options['disableDetailPopover']\"\n [class.hover:ax-underline]=\"!options['disableDetailPopover']\"\n (click)=\"handleItemClick($index)\"\n >\n {{ label }}\n </span>\n @if ($index < visibleItems().length - 1) {\n <span class=\"ax-text-muted\">\u2022</span>\n }\n } @empty {\n <span class=\"ax-text-muted\">---</span>\n }\n @if (hasMoreItems()) {\n <span\n class=\"ax-absolute ax-end-0 ax-flex ax-h-full ax-cursor-pointer ax-items-center ax-px-1 hover:ax-primary-lighter\"\n (click)=\"showMoreItems(); $event.stopPropagation()\"\n #moreButton\n >\n <i class=\"fa-light fa-ellipsis-vertical\"></i>\n </span>\n }\n</div>\n} @else {\n<div class=\"ax-flex ax-min-w-0 ax-items-center ax-gap-1\">\n @if (displayCount() > 0) {\n <span\n class=\"ax-cursor-pointer ax-text-primary hover:ax-underline\"\n (click)=\"openLazyPopover(); $event.stopPropagation()\"\n #lazyTrigger\n >\n {{ summaryLabel() }}\n </span>\n } @else {\n <span class=\"ax-text-muted\">---</span>\n }\n</div>\n}\n\n<ax-popover [openOn]=\"'manual'\" #morePopover (openChange)=\"onPopoverOpenChange($event)\">\n <div class=\"ax-lightest-surface ax-min-w-[280px] ax-rounded-lg ax-border ax-p-4 ax-shadow-lg\">\n <div class=\"ax-mb-4 ax-border-b ax-pb-2\">\n <h3 class=\"ax-text-base ax-font-semibold\">{{ popoverHeader() }}</h3>\n </div>\n\n @if (!isHydratedStrategy() && resolveStatus() === 'loading') {\n <div class=\"ax-flex ax-min-h-[120px] ax-items-center ax-justify-center\">\n <ax-loading></ax-loading>\n </div>\n } @else if (!isHydratedStrategy() && resolveStatus() === 'error') {\n <div class=\"ax-text-danger ax-text-sm\">{{ resolveError() }}</div>\n } @else {\n <div class=\"ax-flex ax-max-h-64 ax-flex-col ax-gap-3\">\n @for (item of popoverListItems(); track $index) {\n @let label = getDisplayRaw(item);\n <span\n [class.ax-cursor-pointer]=\"!options['disableDetailPopover']\"\n [class.hover:ax-text-primary]=\"!options['disableDetailPopover']\"\n [class.hover:ax-underline]=\"!options['disableDetailPopover']\"\n (click)=\"handlePopoverItemClick($index)\"\n >\n {{ label }}\n </span>\n } @empty {\n <span class=\"ax-text-muted\">---</span>\n }\n </div>\n }\n </div>\n</ax-popover>\n", dependencies: [{ kind: "ngmodule", type: AXLoadingModule }, { kind: "component", type: i1$2.AXLoadingComponent, selector: "ax-loading", inputs: ["visible", "type", "context"], outputs: ["visibleChange"] }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "component", type: i2.AXPopoverComponent, selector: "ax-popover", inputs: ["width", "disablePanelClass", "disabled", "offsetX", "offsetY", "target", "placement", "content", "openOn", "closeOn", "hasBackdrop", "openAfter", "closeAfter", "closeOnScroll", "backdropClass", "panelClass", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
16053
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPLookupWidgetColumnComponent, isStandalone: true, selector: "ng-component", inputs: { rawValue: "rawValue", rowData: "rowData" }, viewQueries: [{ propertyName: "moreButton", first: true, predicate: ["moreButton"], descendants: true, isSignal: true }, { propertyName: "lazyTrigger", first: true, predicate: ["lazyTrigger"], descendants: true, isSignal: true }, { propertyName: "morePopover", first: true, predicate: ["morePopover"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "@if (isHydratedStrategy()) {\n<div class=\"ax-relative ax-flex ax-min-w-0 ax-items-center ax-gap-1\">\n @for (item of visibleItems(); track $index) {\n @let label = getDisplayRaw(item);\n <span\n [class.ax-cursor-pointer]=\"detailPopoverEnabled()\"\n [class.hover:ax-text-primary]=\"detailPopoverEnabled()\"\n [class.hover:ax-underline]=\"detailPopoverEnabled()\"\n (click)=\"handleItemClick($index)\"\n >\n {{ label }}\n </span>\n @if ($index < visibleItems().length - 1) {\n <span class=\"ax-text-muted\">\u2022</span>\n }\n } @empty {\n <span class=\"ax-text-muted\">---</span>\n }\n @if (hasMoreItems()) {\n <span\n class=\"ax-absolute ax-end-0 ax-flex ax-h-full ax-cursor-pointer ax-items-center ax-px-1 hover:ax-primary-lighter\"\n (click)=\"showMoreItems(); $event.stopPropagation()\"\n #moreButton\n >\n <i class=\"fa-light fa-ellipsis-vertical\"></i>\n </span>\n }\n</div>\n} @else {\n<div class=\"ax-flex ax-min-w-0 ax-items-center ax-gap-1\">\n @if (displayCount() > 0) {\n <span\n class=\"ax-cursor-pointer ax-text-primary hover:ax-underline\"\n (click)=\"openLazyPopover(); $event.stopPropagation()\"\n #lazyTrigger\n >\n {{ summaryLabel() }}\n </span>\n } @else {\n <span class=\"ax-text-muted\">---</span>\n }\n</div>\n}\n\n<ax-popover [openOn]=\"'manual'\" #morePopover (openChange)=\"onPopoverOpenChange($event)\">\n <div class=\"ax-lightest-surface ax-min-w-[280px] ax-rounded-lg ax-border ax-p-4 ax-shadow-lg\">\n <div class=\"ax-mb-4 ax-border-b ax-pb-2\">\n <h3 class=\"ax-text-base ax-font-semibold\">{{ popoverHeader() }}</h3>\n </div>\n\n @if (!isHydratedStrategy() && resolveStatus() === 'loading') {\n <div class=\"ax-flex ax-min-h-[120px] ax-items-center ax-justify-center\">\n <ax-loading></ax-loading>\n </div>\n } @else if (!isHydratedStrategy() && resolveStatus() === 'error') {\n <div class=\"ax-text-danger ax-text-sm\">{{ resolveError() }}</div>\n } @else {\n <div class=\"ax-flex ax-max-h-64 ax-flex-col ax-gap-3\">\n @for (item of popoverListItems(); track $index) {\n @let label = getDisplayRaw(item);\n <span\n [class.ax-cursor-pointer]=\"detailPopoverEnabled()\"\n [class.hover:ax-text-primary]=\"detailPopoverEnabled()\"\n [class.hover:ax-underline]=\"detailPopoverEnabled()\"\n (click)=\"handlePopoverItemClick($index)\"\n >\n {{ label }}\n </span>\n } @empty {\n <span class=\"ax-text-muted\">---</span>\n }\n </div>\n }\n </div>\n</ax-popover>\n", dependencies: [{ kind: "ngmodule", type: AXLoadingModule }, { kind: "component", type: i1$2.AXLoadingComponent, selector: "ax-loading", inputs: ["visible", "type", "context"], outputs: ["visibleChange"] }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "component", type: i2.AXPopoverComponent, selector: "ax-popover", inputs: ["width", "disablePanelClass", "disabled", "offsetX", "offsetY", "target", "placement", "content", "openOn", "closeOn", "hasBackdrop", "openAfter", "closeAfter", "closeOnScroll", "backdropClass", "panelClass", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14995
16054
  }
14996
16055
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLookupWidgetColumnComponent, decorators: [{
14997
16056
  type: Component,
14998
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, imports: [AXLoadingModule, AXPopoverModule], inputs: ['rawValue', 'rowData'], template: "@if (isHydratedStrategy()) {\n<div class=\"ax-relative ax-flex ax-min-w-0 ax-items-center ax-gap-1\">\n @for (item of visibleItems(); track $index) {\n @let label = getDisplayRaw(item);\n <span\n [class.ax-cursor-pointer]=\"!options['disableDetailPopover']\"\n [class.hover:ax-text-primary]=\"!options['disableDetailPopover']\"\n [class.hover:ax-underline]=\"!options['disableDetailPopover']\"\n (click)=\"handleItemClick($index)\"\n >\n {{ label }}\n </span>\n @if ($index < visibleItems().length - 1) {\n <span class=\"ax-text-muted\">\u2022</span>\n }\n } @empty {\n <span class=\"ax-text-muted\">---</span>\n }\n @if (hasMoreItems()) {\n <span\n class=\"ax-absolute ax-end-0 ax-flex ax-h-full ax-cursor-pointer ax-items-center ax-px-1 hover:ax-primary-lighter\"\n (click)=\"showMoreItems(); $event.stopPropagation()\"\n #moreButton\n >\n <i class=\"fa-light fa-ellipsis-vertical\"></i>\n </span>\n }\n</div>\n} @else {\n<div class=\"ax-flex ax-min-w-0 ax-items-center ax-gap-1\">\n @if (displayCount() > 0) {\n <span\n class=\"ax-cursor-pointer ax-text-primary hover:ax-underline\"\n (click)=\"openLazyPopover(); $event.stopPropagation()\"\n #lazyTrigger\n >\n {{ summaryLabel() }}\n </span>\n } @else {\n <span class=\"ax-text-muted\">---</span>\n }\n</div>\n}\n\n<ax-popover [openOn]=\"'manual'\" #morePopover (openChange)=\"onPopoverOpenChange($event)\">\n <div class=\"ax-lightest-surface ax-min-w-[280px] ax-rounded-lg ax-border ax-p-4 ax-shadow-lg\">\n <div class=\"ax-mb-4 ax-border-b ax-pb-2\">\n <h3 class=\"ax-text-base ax-font-semibold\">{{ popoverHeader() }}</h3>\n </div>\n\n @if (!isHydratedStrategy() && resolveStatus() === 'loading') {\n <div class=\"ax-flex ax-min-h-[120px] ax-items-center ax-justify-center\">\n <ax-loading></ax-loading>\n </div>\n } @else if (!isHydratedStrategy() && resolveStatus() === 'error') {\n <div class=\"ax-text-danger ax-text-sm\">{{ resolveError() }}</div>\n } @else {\n <div class=\"ax-flex ax-max-h-64 ax-flex-col ax-gap-3\">\n @for (item of popoverListItems(); track $index) {\n @let label = getDisplayRaw(item);\n <span\n [class.ax-cursor-pointer]=\"!options['disableDetailPopover']\"\n [class.hover:ax-text-primary]=\"!options['disableDetailPopover']\"\n [class.hover:ax-underline]=\"!options['disableDetailPopover']\"\n (click)=\"handlePopoverItemClick($index)\"\n >\n {{ label }}\n </span>\n } @empty {\n <span class=\"ax-text-muted\">---</span>\n }\n </div>\n }\n </div>\n</ax-popover>\n" }]
16057
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, imports: [AXLoadingModule, AXPopoverModule], inputs: ['rawValue', 'rowData'], template: "@if (isHydratedStrategy()) {\n<div class=\"ax-relative ax-flex ax-min-w-0 ax-items-center ax-gap-1\">\n @for (item of visibleItems(); track $index) {\n @let label = getDisplayRaw(item);\n <span\n [class.ax-cursor-pointer]=\"detailPopoverEnabled()\"\n [class.hover:ax-text-primary]=\"detailPopoverEnabled()\"\n [class.hover:ax-underline]=\"detailPopoverEnabled()\"\n (click)=\"handleItemClick($index)\"\n >\n {{ label }}\n </span>\n @if ($index < visibleItems().length - 1) {\n <span class=\"ax-text-muted\">\u2022</span>\n }\n } @empty {\n <span class=\"ax-text-muted\">---</span>\n }\n @if (hasMoreItems()) {\n <span\n class=\"ax-absolute ax-end-0 ax-flex ax-h-full ax-cursor-pointer ax-items-center ax-px-1 hover:ax-primary-lighter\"\n (click)=\"showMoreItems(); $event.stopPropagation()\"\n #moreButton\n >\n <i class=\"fa-light fa-ellipsis-vertical\"></i>\n </span>\n }\n</div>\n} @else {\n<div class=\"ax-flex ax-min-w-0 ax-items-center ax-gap-1\">\n @if (displayCount() > 0) {\n <span\n class=\"ax-cursor-pointer ax-text-primary hover:ax-underline\"\n (click)=\"openLazyPopover(); $event.stopPropagation()\"\n #lazyTrigger\n >\n {{ summaryLabel() }}\n </span>\n } @else {\n <span class=\"ax-text-muted\">---</span>\n }\n</div>\n}\n\n<ax-popover [openOn]=\"'manual'\" #morePopover (openChange)=\"onPopoverOpenChange($event)\">\n <div class=\"ax-lightest-surface ax-min-w-[280px] ax-rounded-lg ax-border ax-p-4 ax-shadow-lg\">\n <div class=\"ax-mb-4 ax-border-b ax-pb-2\">\n <h3 class=\"ax-text-base ax-font-semibold\">{{ popoverHeader() }}</h3>\n </div>\n\n @if (!isHydratedStrategy() && resolveStatus() === 'loading') {\n <div class=\"ax-flex ax-min-h-[120px] ax-items-center ax-justify-center\">\n <ax-loading></ax-loading>\n </div>\n } @else if (!isHydratedStrategy() && resolveStatus() === 'error') {\n <div class=\"ax-text-danger ax-text-sm\">{{ resolveError() }}</div>\n } @else {\n <div class=\"ax-flex ax-max-h-64 ax-flex-col ax-gap-3\">\n @for (item of popoverListItems(); track $index) {\n @let label = getDisplayRaw(item);\n <span\n [class.ax-cursor-pointer]=\"detailPopoverEnabled()\"\n [class.hover:ax-text-primary]=\"detailPopoverEnabled()\"\n [class.hover:ax-underline]=\"detailPopoverEnabled()\"\n (click)=\"handlePopoverItemClick($index)\"\n >\n {{ label }}\n </span>\n } @empty {\n <span class=\"ax-text-muted\">---</span>\n }\n </div>\n }\n </div>\n</ax-popover>\n" }]
14999
16058
  }], ctorParameters: () => [], propDecorators: { moreButton: [{ type: i0.ViewChild, args: ['moreButton', { isSignal: true }] }], lazyTrigger: [{ type: i0.ViewChild, args: ['lazyTrigger', { isSignal: true }] }], morePopover: [{ type: i0.ViewChild, args: ['morePopover', { isSignal: true }] }] } });
15000
16059
 
15001
16060
  var lookupWidgetColumn_component = /*#__PURE__*/Object.freeze({
@@ -15044,6 +16103,13 @@ const AXPLookupWidget = {
15044
16103
  visible: true,
15045
16104
  },
15046
16105
  AXP_ALLOW_MULTIPLE_PROPERTY,
16106
+ createBooleanProperty({
16107
+ name: 'popoverEnabled',
16108
+ title: 'Entity Detail Popover',
16109
+ path: 'options.popover.enabled',
16110
+ group: AXP_BEHAVIOR_PROPERTY_GROUP,
16111
+ defaultValue: true,
16112
+ }),
15047
16113
  {
15048
16114
  name: 'displayFormat',
15049
16115
  title: 'Display Format',
@@ -16350,105 +17416,1921 @@ class AXPMultiSourceSelectorWidgetColumnComponent extends AXPColumnWidgetCompone
16350
17416
  const def = await this.definitionService.getDefinition(this.providerName);
16351
17417
  this.definition.set(def);
16352
17418
  }
16353
- catch (error) {
16354
- console.error(`[MultiSourceSelectorColumn] Error loading definition '${this.providerName}':`, error);
17419
+ catch (error) {
17420
+ console.error(`[MultiSourceSelectorColumn] Error loading definition '${this.providerName}':`, error);
17421
+ }
17422
+ }
17423
+ extractDisplayItem(ref) {
17424
+ if (!ref) {
17425
+ return null;
17426
+ }
17427
+ const def = this.definition();
17428
+ // If ref is already AXPMultiSourceRef format
17429
+ if (ref.sourceKey && ref.displayName) {
17430
+ return {
17431
+ id: ref.refId || ref.sourceKey,
17432
+ text: ref.displayName,
17433
+ };
17434
+ }
17435
+ // If ref is object with id
17436
+ if (typeof ref === 'object' && ref.id) {
17437
+ const sourceKey = ref.sourceKey;
17438
+ if (sourceKey && def) {
17439
+ const source = def.sources.find((s) => s.key === sourceKey);
17440
+ if (source) {
17441
+ const displayText = this.getDisplayText(ref, source);
17442
+ return {
17443
+ id: ref.id,
17444
+ text: displayText,
17445
+ };
17446
+ }
17447
+ }
17448
+ // Fallback
17449
+ const displayText = ref.displayName || ref.title || ref.id || String(ref);
17450
+ return {
17451
+ id: ref.id,
17452
+ text: displayText,
17453
+ };
17454
+ }
17455
+ // If ref is primitive
17456
+ return {
17457
+ id: String(ref),
17458
+ text: String(ref),
17459
+ };
17460
+ }
17461
+ getDisplayText(item, source) {
17462
+ if (!item) {
17463
+ return '';
17464
+ }
17465
+ const template = source.displayFormat || this.displayFormat();
17466
+ if (template) {
17467
+ const formatted = this.formatService.format(template, 'string', item);
17468
+ if (formatted) {
17469
+ return formatted;
17470
+ }
17471
+ }
17472
+ // Fallback to common display fields
17473
+ return get(item, 'displayName') || get(item, 'title') || get(item, 'name') || String(item.id || item);
17474
+ }
17475
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPMultiSourceSelectorWidgetColumnComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
17476
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPMultiSourceSelectorWidgetColumnComponent, isStandalone: true, selector: "axp-multi-source-selector-widget-column", inputs: { rawValue: "rawValue", rowData: "rowData" }, usesInheritance: true, ngImport: i0, template: `
17477
+ @if (displayItems().length > 0) {
17478
+ <div class="ax-flex ax-flex-wrap ax-gap-1">
17479
+ @for (item of visibleItems(); track item.id) {
17480
+ <ax-badge class="ax-p-0.5 ax-accent1" [text]="item.text"></ax-badge>
17481
+ }
17482
+ @if (hasMoreItems()) {
17483
+ <ax-badge class="ax-p-0.5 ax-accent2" [text]="'+' + remainingItemsCount()"></ax-badge>
17484
+ }
17485
+ </div>
17486
+ } @else {
17487
+ <span class="ax-text-muted">---</span>
17488
+ }
17489
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AXBadgeModule }, { kind: "component", type: i2$1.AXBadgeComponent, selector: "ax-badge", inputs: ["color", "look", "text"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
17490
+ }
17491
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPMultiSourceSelectorWidgetColumnComponent, decorators: [{
17492
+ type: Component,
17493
+ args: [{
17494
+ selector: 'axp-multi-source-selector-widget-column',
17495
+ template: `
17496
+ @if (displayItems().length > 0) {
17497
+ <div class="ax-flex ax-flex-wrap ax-gap-1">
17498
+ @for (item of visibleItems(); track item.id) {
17499
+ <ax-badge class="ax-p-0.5 ax-accent1" [text]="item.text"></ax-badge>
17500
+ }
17501
+ @if (hasMoreItems()) {
17502
+ <ax-badge class="ax-p-0.5 ax-accent2" [text]="'+' + remainingItemsCount()"></ax-badge>
17503
+ }
17504
+ </div>
17505
+ } @else {
17506
+ <span class="ax-text-muted">---</span>
17507
+ }
17508
+ `,
17509
+ changeDetection: ChangeDetectionStrategy.OnPush,
17510
+ imports: [AXBadgeModule],
17511
+ inputs: ['rawValue', 'rowData'],
17512
+ }]
17513
+ }], ctorParameters: () => [] });
17514
+
17515
+ var multiSourceSelectorWidgetColumn_component = /*#__PURE__*/Object.freeze({
17516
+ __proto__: null,
17517
+ AXPMultiSourceSelectorWidgetColumnComponent: AXPMultiSourceSelectorWidgetColumnComponent
17518
+ });
17519
+
17520
+ const FileUploaderWebhookKeys = {
17521
+ Actions: 'file-uploader.actions',
17522
+ Configure: 'file-uploader.configure',
17523
+ BeforeFilesAdded: 'file-uploader.beforeFilesAdded',
17524
+ AfterFilesAdded: 'file-uploader.afterFilesAdded',
17525
+ FileItem: {
17526
+ Actions: 'file-uploader.fileItem.actions',
17527
+ BeforeDownload: 'file-uploader.fileItem.before-download',
17528
+ AfterDownload: 'file-uploader.fileItem.after-download',
17529
+ AfterEdit: 'file-uploader.fileItem.after-edit',
17530
+ AfterRemove: 'file-uploader.fileItem.after-remove',
17531
+ DownloadReference: 'file-uploader.fileItem.download-reference',
17532
+ },
17533
+ EditDialog: {
17534
+ Params: {
17535
+ BeforeRename: 'file-uploader.edit-dialog.params.before-rename',
17536
+ AfterRename: 'file-uploader.edit-dialog.params.after-rename',
17537
+ },
17538
+ Groups: {
17539
+ BeforeForm: 'file-uploader.edit-dialog.groups.before-form',
17540
+ AfterForm: 'file-uploader.edit-dialog.groups.after-form',
17541
+ },
17542
+ },
17543
+ };
17544
+
17545
+ class AXPEditFileUploaderCommand {
17546
+ constructor() {
17547
+ //#region ---- Services & Dependencies ----
17548
+ this.layoutBuilder = inject(AXPLayoutBuilderService);
17549
+ this.translationService = inject(AXTranslationService);
17550
+ this.hooks = inject(AXPHookService);
17551
+ }
17552
+ //#endregion
17553
+ //#region ---- Command Execution ----
17554
+ async execute(input) {
17555
+ if (input.file.readOnly === true && !input.isNewFile) {
17556
+ const text = await this.translationService.translateAsync('@general:terms.common.readonly');
17557
+ return {
17558
+ success: false,
17559
+ message: { text: `${text}.` },
17560
+ };
17561
+ }
17562
+ try {
17563
+ const updatedFile = await this.showEditDialog(input.file, input.plugins ?? [], input.excludePlugins ?? [], input.enableTitleDescription ?? false, input.isNewFile ?? false);
17564
+ return {
17565
+ success: true,
17566
+ data: updatedFile
17567
+ };
17568
+ }
17569
+ catch (error) {
17570
+ return {
17571
+ success: false,
17572
+ message: {
17573
+ text: error instanceof Error ? error.message : 'Failed to edit file'
17574
+ }
17575
+ };
17576
+ }
17577
+ }
17578
+ //#endregion
17579
+ //#region ---- Dialog Logic ----
17580
+ async showEditDialog(file, plugins, excludePlugins, enableTitleDescription, isNewFile) {
17581
+ const titleKey = isNewFile ? '@general:widgets.file-uploader.edit-dialog.add-title' : '@general:widgets.file-uploader.edit-dialog.title';
17582
+ const title = await this.translationService.translateAsync(titleKey);
17583
+ const nameLabel = await this.translationService.translateAsync('@general:terms.common.name');
17584
+ const namePlaceholder = await this.translationService.translateAsync('@general:terms.common.name');
17585
+ const titleLabel = await this.translationService.translateAsync('@general:terms.common.title');
17586
+ const descriptionLabel = await this.translationService.translateAsync('@general:terms.common.description');
17587
+ const submitLabel = await this.translationService.translateAsync('@general:actions.submit.title');
17588
+ // Hooks: parameter insertion points
17589
+ const beforeRenamePayload = await this.hooks.runAsync(FileUploaderWebhookKeys.EditDialog.Params.BeforeRename, {
17590
+ file: file,
17591
+ plugins,
17592
+ excludePlugins,
17593
+ items: [],
17594
+ });
17595
+ const afterRenamePayload = await this.hooks.runAsync(FileUploaderWebhookKeys.EditDialog.Params.AfterRename, {
17596
+ file: file,
17597
+ plugins,
17598
+ excludePlugins,
17599
+ items: [],
17600
+ });
17601
+ // Hooks: group insertion points around 'form'
17602
+ const groupsBeforePayload = await this.hooks.runAsync(FileUploaderWebhookKeys.EditDialog.Groups.BeforeForm, {
17603
+ file: file,
17604
+ plugins,
17605
+ excludePlugins,
17606
+ groups: [],
17607
+ });
17608
+ const groupsAfterPayload = await this.hooks.runAsync(FileUploaderWebhookKeys.EditDialog.Groups.AfterForm, {
17609
+ file: file,
17610
+ plugins,
17611
+ excludePlugins,
17612
+ groups: [],
17613
+ });
17614
+ const dialogRef = await this.layoutBuilder
17615
+ .create()
17616
+ .dialog(dialog => {
17617
+ dialog
17618
+ .setTitle(title)
17619
+ .setContext({ ...file })
17620
+ .content(contentBuilder => {
17621
+ // Add groups before form
17622
+ this.buildGroups(contentBuilder, groupsBeforePayload?.groups ?? []);
17623
+ // Main form container with rename field and hook items
17624
+ contentBuilder.flex(formFlex => {
17625
+ formFlex
17626
+ .setDirection('column')
17627
+ .setGap('16px');
17628
+ // Add items before rename
17629
+ this.buildFormFields(formFlex, beforeRenamePayload?.items ?? []);
17630
+ // Name field (label "Name" when enableTitleDescription, else "Rename")
17631
+ formFlex.formField(nameLabel, field => {
17632
+ field.path('name');
17633
+ field.textBox({
17634
+ placeholder: namePlaceholder,
17635
+ validations: [
17636
+ { rule: 'maxLength', options: { value: 256 } }
17637
+ ]
17638
+ });
17639
+ });
17640
+ // Title and description fields when enableTitleDescription is true
17641
+ if (enableTitleDescription) {
17642
+ formFlex.formField(titleLabel, field => {
17643
+ field.path('title');
17644
+ field.textBox({
17645
+ placeholder: titleLabel,
17646
+ validations: [
17647
+ { rule: 'maxLength', options: { value: 256 } }
17648
+ ]
17649
+ });
17650
+ });
17651
+ formFlex.formField(descriptionLabel, field => {
17652
+ field.path('description');
17653
+ field.largeTextBox({
17654
+ placeholder: descriptionLabel,
17655
+ validations: [
17656
+ { rule: 'maxLength', options: { value: 1024 } }
17657
+ ]
17658
+ });
17659
+ });
17660
+ }
17661
+ // Add items after rename
17662
+ this.buildFormFields(formFlex, afterRenamePayload?.items ?? []);
17663
+ });
17664
+ // Add groups after form
17665
+ this.buildGroups(contentBuilder, groupsAfterPayload?.groups ?? []);
17666
+ })
17667
+ .setCloseButton(true)
17668
+ .setActions(actions => {
17669
+ actions.custom({
17670
+ title: submitLabel,
17671
+ icon: 'fa-check',
17672
+ color: 'primary',
17673
+ command: { name: 'submit', options: { validate: true } }
17674
+ });
17675
+ });
17676
+ })
17677
+ .show();
17678
+ const result = dialogRef.context();
17679
+ dialogRef.close();
17680
+ return result;
17681
+ }
17682
+ //#endregion
17683
+ //#region ---- Helper Methods ----
17684
+ /**
17685
+ * Build form fields from hook items (old format parameters)
17686
+ */
17687
+ buildFormFields(builder, items) {
17688
+ for (const item of items) {
17689
+ if (!item || !item.path) {
17690
+ continue;
17691
+ }
17692
+ const label = item.title || item.path;
17693
+ const widgetType = item.widget?.type || 'text-editor';
17694
+ const widgetOptions = item.widget?.options || {};
17695
+ builder.formField(label, field => {
17696
+ field.path(item.path);
17697
+ // Apply mode if specified
17698
+ if (item.mode) {
17699
+ field.mode(item.mode);
17700
+ }
17701
+ // Apply visibility if specified
17702
+ if (item.visible !== undefined) {
17703
+ field.visible(item.visible);
17704
+ }
17705
+ // Apply readonly if specified
17706
+ if (item.readonly !== undefined) {
17707
+ field.readonly(item.readonly);
17708
+ }
17709
+ // Apply disabled if specified
17710
+ if (item.disabled !== undefined) {
17711
+ field.disabled(item.disabled);
17712
+ }
17713
+ // Convert widget based on type
17714
+ this.applyWidget(field, widgetType, widgetOptions, item.validations);
17715
+ });
17716
+ }
17717
+ }
17718
+ /**
17719
+ * Build groups/containers from hook groups (old format groups)
17720
+ */
17721
+ buildGroups(containerBuilder, groups) {
17722
+ for (const group of groups) {
17723
+ if (!group) {
17724
+ continue;
17725
+ }
17726
+ // Use fieldset for groups with title, otherwise use flex
17727
+ if (group.title) {
17728
+ containerBuilder.fieldset(fieldset => {
17729
+ fieldset.setTitle(group.title);
17730
+ if (group.description) {
17731
+ fieldset.setDescription(group.description);
17732
+ }
17733
+ if (group.mode) {
17734
+ fieldset.mode(group.mode);
17735
+ }
17736
+ if (group.cols) {
17737
+ fieldset.setCols(group.cols);
17738
+ }
17739
+ // Build form fields from group parameters
17740
+ if (group.parameters && Array.isArray(group.parameters)) {
17741
+ this.buildFormFields(fieldset, group.parameters);
17742
+ }
17743
+ });
17744
+ }
17745
+ else {
17746
+ containerBuilder.flex(flex => {
17747
+ flex.setDirection('column');
17748
+ flex.setGap('16px');
17749
+ if (group.mode) {
17750
+ flex.mode(group.mode);
17751
+ }
17752
+ // Build form fields from group parameters
17753
+ if (group.parameters && Array.isArray(group.parameters)) {
17754
+ this.buildFormFields(flex, group.parameters);
17755
+ }
17756
+ });
17757
+ }
17758
+ }
17759
+ }
17760
+ /**
17761
+ * Apply widget configuration to field based on widget type
17762
+ */
17763
+ applyWidget(field, widgetType, options, validations) {
17764
+ // Convert validations from old format if needed
17765
+ const fieldValidations = validations || options.validations || [];
17766
+ // Handle different widget types
17767
+ switch (widgetType) {
17768
+ case 'text-editor':
17769
+ case 'text-box':
17770
+ field.textBox({
17771
+ ...options,
17772
+ validations: fieldValidations
17773
+ });
17774
+ break;
17775
+ case 'large-text-editor':
17776
+ case 'large-text-box':
17777
+ field.largeTextBox({
17778
+ ...options,
17779
+ validations: fieldValidations
17780
+ });
17781
+ break;
17782
+ case 'rich-text-editor':
17783
+ case 'rich-text':
17784
+ field.richText({
17785
+ ...options,
17786
+ validations: fieldValidations
17787
+ });
17788
+ break;
17789
+ case 'number-editor':
17790
+ case 'number-box':
17791
+ field.numberBox({
17792
+ ...options,
17793
+ validations: fieldValidations
17794
+ });
17795
+ break;
17796
+ case 'date-time-editor':
17797
+ case 'date-time-box':
17798
+ field.dateTimeBox({
17799
+ ...options,
17800
+ validations: fieldValidations
17801
+ });
17802
+ break;
17803
+ case 'select-box':
17804
+ case 'select-editor':
17805
+ field.selectBox({
17806
+ ...options,
17807
+ validations: fieldValidations
17808
+ });
17809
+ break;
17810
+ case 'lookup-box':
17811
+ case 'lookup-editor':
17812
+ field.lookupBox({
17813
+ ...options,
17814
+ validations: fieldValidations
17815
+ });
17816
+ break;
17817
+ case 'toggle-switch':
17818
+ case 'boolean-editor':
17819
+ field.toggleSwitch({
17820
+ ...options,
17821
+ validations: fieldValidations
17822
+ });
17823
+ break;
17824
+ default:
17825
+ // For custom widgets or unknown types, use customWidget
17826
+ field.customWidget(widgetType, {
17827
+ ...options,
17828
+ validations: fieldValidations
17829
+ });
17830
+ break;
17831
+ }
17832
+ }
17833
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEditFileUploaderCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
17834
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEditFileUploaderCommand, providedIn: 'root' }); }
17835
+ }
17836
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEditFileUploaderCommand, decorators: [{
17837
+ type: Injectable,
17838
+ args: [{ providedIn: 'root' }]
17839
+ }] });
17840
+
17841
+ class AXPFileListComponent {
17842
+ constructor() {
17843
+ this.fileTypeService = inject(AXPFileTypeProviderService);
17844
+ this.fileStorageService = inject(AXPFileStorageService);
17845
+ this.commandExecutor = inject(AXPCommandExecutor);
17846
+ this.hooks = inject(AXPHookService);
17847
+ this.isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
17848
+ this.fileTypes = signal([], ...(ngDevMode ? [{ debugName: "fileTypes" }] : /* istanbul ignore next */ []));
17849
+ this.onRemove = output();
17850
+ this.onRevert = output();
17851
+ this.onRename = output();
17852
+ this.readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
17853
+ this.fileEditable = input(true, ...(ngDevMode ? [{ debugName: "fileEditable" }] : /* istanbul ignore next */ []));
17854
+ /** When true, edit dialog shows name, title and description fields. Default false. */
17855
+ this.enableTitleDescription = input(false, ...(ngDevMode ? [{ debugName: "enableTitleDescription" }] : /* istanbul ignore next */ []));
17856
+ this.multiple = input(true, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
17857
+ this.files = input([], ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
17858
+ // Plugin context (passed from parent widget)
17859
+ this.plugins = input(undefined, ...(ngDevMode ? [{ debugName: "plugins" }] : /* istanbul ignore next */ []));
17860
+ this.excludePlugins = input(undefined, ...(ngDevMode ? [{ debugName: "excludePlugins" }] : /* istanbul ignore next */ []));
17861
+ // Capabilities API for file operations (passed from parent widget)
17862
+ this.capabilities = input(undefined, ...(ngDevMode ? [{ debugName: "capabilities" }] : /* istanbul ignore next */ []));
17863
+ /**
17864
+ * All files should be displayed, even those with `deleted` status.
17865
+ * The template will handle the visual differences based on the status.
17866
+ */
17867
+ this.displayFiles = computed(() => this.files(), ...(ngDevMode ? [{ debugName: "displayFiles" }] : /* istanbul ignore next */ []));
17868
+ // Cache for per-file actions provided by hooks
17869
+ this.fileIdToActions = signal({}, ...(ngDevMode ? [{ debugName: "fileIdToActions" }] : /* istanbul ignore next */ []));
17870
+ // Global actions from FileUploaderWebhookKeys.Actions hook
17871
+ this.globalActions = signal([], ...(ngDevMode ? [{ debugName: "globalActions" }] : /* istanbul ignore next */ []));
17872
+ // Clear cache when files change to reload actions
17873
+ this.filesChangeEffect = effect(() => {
17874
+ // Track files changes to clear cache
17875
+ this.files();
17876
+ this.fileIdToActions.set({});
17877
+ }, ...(ngDevMode ? [{ debugName: "filesChangeEffect" }] : /* istanbul ignore next */ []));
17878
+ }
17879
+ // Default actions that can be removed via hooks
17880
+ getDefaultActions(file) {
17881
+ const defaultActions = [];
17882
+ // Download action (always available, including in readonly mode)
17883
+ defaultActions.push({
17884
+ id: 'attachments.default.download',
17885
+ plugin: 'attachments',
17886
+ global: true,
17887
+ textKey: '@general:actions.download.title',
17888
+ icon: 'fa-light fa-download',
17889
+ run: async (ctx) => {
17890
+ await this.handleFileDownload({ nativeEvent: { preventDefault: () => { }, stopPropagation: () => { } } }, file);
17891
+ },
17892
+ });
17893
+ // Edit and Remove actions (if editable and not widget/item readonly)
17894
+ if (!this.isItemInteractionLocked(file) &&
17895
+ (this.fileEditable() || (!this.fileEditable() && file.status === 'attached'))) {
17896
+ defaultActions.push({
17897
+ id: 'attachments.default.edit',
17898
+ plugin: 'attachments',
17899
+ global: true,
17900
+ textKey: '@general:actions.edit.title',
17901
+ icon: 'fa-light fa-pencil',
17902
+ run: async (ctx) => {
17903
+ await this.handleFileEdit({ stopPropagation: () => { }, preventDefault: () => { } }, file);
17904
+ },
17905
+ });
17906
+ defaultActions.push({
17907
+ id: 'attachments.default.remove',
17908
+ plugin: 'attachments',
17909
+ global: true,
17910
+ textKey: '@general:actions.delete.title',
17911
+ icon: 'fa-light fa-trash-can',
17912
+ run: async (ctx) => {
17913
+ await this.handleFileRemove({ nativeEvent: { preventDefault: () => { }, stopPropagation: () => { } } }, file);
17914
+ },
17915
+ });
17916
+ }
17917
+ return defaultActions;
17918
+ }
17919
+ /** True when the widget is readonly or this file row is locked (`readOnly`). */
17920
+ isItemInteractionLocked(file) {
17921
+ return this.readonly() || file.readOnly === true;
17922
+ }
17923
+ actionsFor(file, index) {
17924
+ const key = file.id ?? String(index);
17925
+ const map = this.fileIdToActions();
17926
+ if (map[key]) {
17927
+ return map[key];
17928
+ }
17929
+ // Lazy-load actions via hooks
17930
+ this.loadActionsFor(file, index).catch(console.error);
17931
+ return [];
17932
+ }
17933
+ async loadActionsFor(file, index) {
17934
+ const key = file.id ?? String(index);
17935
+ // Start with default actions
17936
+ const defaultActions = this.getDefaultActions(file);
17937
+ // Get per-file actions from FileUploaderWebhookKeys.FileItem.Actions hook
17938
+ // Note: globalActions are only for toolbar, not for per-file actions
17939
+ const fileItemPayload = await this.hooks.runAsync(FileUploaderWebhookKeys.FileItem.Actions, {
17940
+ item: file,
17941
+ index,
17942
+ itemReadOnly: file.readOnly === true,
17943
+ itemInteractionLocked: this.isItemInteractionLocked(file),
17944
+ plugins: this.plugins() ?? [],
17945
+ excludePlugins: this.excludePlugins() ?? [],
17946
+ capabilities: this.capabilities(),
17947
+ actions: [],
17948
+ });
17949
+ // Combine all actions: default + file-item specific (exclude globalActions from per-file items)
17950
+ const allActions = [...defaultActions, ...(fileItemPayload?.actions ?? [])];
17951
+ // Filter actions based on plugins and excludePlugins
17952
+ const enabledPlugins = this.plugins()?.map((p) => p.name) ?? [];
17953
+ const excludedPlugins = this.excludePlugins() ?? [];
17954
+ const filteredActions = allActions.filter((action) => {
17955
+ // If action has an id and is in excluded list, remove it
17956
+ if (action.id && excludedPlugins.some((excluded) => action.id?.includes(excluded))) {
17957
+ return false;
17958
+ }
17959
+ // If plugin is excluded, remove it
17960
+ if (excludedPlugins.includes(action.plugin)) {
17961
+ return false;
17962
+ }
17963
+ // If no enabled plugins, only show global actions
17964
+ if (!enabledPlugins || enabledPlugins.length === 0) {
17965
+ return action.global === true;
17966
+ }
17967
+ // Show if plugin is enabled or action is global
17968
+ return enabledPlugins.includes(action.plugin) || action.global === true;
17969
+ });
17970
+ const next = { ...this.fileIdToActions() };
17971
+ next[key] = filteredActions;
17972
+ this.fileIdToActions.set(next);
17973
+ }
17974
+ async ngOnInit() {
17975
+ this.fileTypes.set(await this.fileTypeService.items());
17976
+ await this.loadGlobalActions();
17977
+ this.isLoading.set(false);
17978
+ }
17979
+ async loadGlobalActions() {
17980
+ try {
17981
+ const payload = await this.hooks.runAsync(FileUploaderWebhookKeys.Actions, {
17982
+ plugins: this.plugins() ?? [],
17983
+ excludePlugins: this.excludePlugins() ?? [],
17984
+ capabilities: this.capabilities(),
17985
+ actions: [],
17986
+ });
17987
+ this.globalActions.set(payload?.actions ?? []);
17988
+ }
17989
+ catch (error) {
17990
+ console.error('Error loading global actions:', error);
17991
+ this.globalActions.set([]);
17992
+ }
17993
+ }
17994
+ getFileInfo(fileName) {
17995
+ const extension = fileName.split('.').pop()?.toLowerCase() || '';
17996
+ const extensions = this.fileTypes().flatMap((t) => t.extensions);
17997
+ const fileType = extensions.find((e) => e.name === extension);
17998
+ return {
17999
+ icon: fileType?.icon || 'fa-light fa-file',
18000
+ type: fileType?.name || 'Unknown',
18001
+ };
18002
+ }
18003
+ async handleFileDownload(event, file) {
18004
+ event.nativeEvent.preventDefault();
18005
+ event.nativeEvent.stopPropagation();
18006
+ try {
18007
+ await this.hooks.runAsync(FileUploaderWebhookKeys.FileItem.BeforeDownload, {
18008
+ file,
18009
+ plugins: this.plugins() ?? [],
18010
+ excludePlugins: this.excludePlugins() ?? [],
18011
+ capabilities: this.capabilities(),
18012
+ });
18013
+ }
18014
+ catch { }
18015
+ if (!file.source) {
18016
+ console.error('File source is undefined, cannot download.', file);
18017
+ return;
18018
+ }
18019
+ const triggerDownload = (blob, fileName) => {
18020
+ const url = URL.createObjectURL(blob);
18021
+ const link = document.createElement('a');
18022
+ link.href = url;
18023
+ link.download = fileName;
18024
+ document.body.appendChild(link);
18025
+ link.click();
18026
+ document.body.removeChild(link);
18027
+ URL.revokeObjectURL(url);
18028
+ };
18029
+ switch (file.source.kind) {
18030
+ case 'blob':
18031
+ if (file.source.value instanceof Blob) {
18032
+ triggerDownload(file.source.value, file.name ?? 'download');
18033
+ }
18034
+ else {
18035
+ console.error('Source kind is blob, but value is not a Blob.', file);
18036
+ }
18037
+ break;
18038
+ case 'fileId':
18039
+ if (typeof file.source.value === 'string') {
18040
+ try {
18041
+ const fileInfo = await this.fileStorageService.getInfo(file.source.value);
18042
+ if (fileInfo && fileInfo.url) {
18043
+ const link = document.createElement('a');
18044
+ link.href = fileInfo.url;
18045
+ link.download = file.name ?? fileInfo.name ?? 'download';
18046
+ link.target = '_blank';
18047
+ document.body.appendChild(link);
18048
+ link.click();
18049
+ document.body.removeChild(link);
18050
+ }
18051
+ else if (fileInfo && fileInfo.binary instanceof Blob) {
18052
+ triggerDownload(fileInfo.binary, file.name ?? fileInfo.name ?? 'download');
18053
+ }
18054
+ else {
18055
+ console.error('Could not retrieve file for download from fileId:', fileInfo);
18056
+ }
18057
+ }
18058
+ catch (error) {
18059
+ console.error('Error downloading file by fileId:', file.source.value, error);
18060
+ }
18061
+ }
18062
+ else {
18063
+ console.error('Source kind is fileId, but value is not a string ID.', file);
18064
+ }
18065
+ break;
18066
+ case 'url':
18067
+ if (typeof file.source.value === 'string') {
18068
+ const link = document.createElement('a');
18069
+ link.href = file.source.value;
18070
+ link.download = file.name ?? 'download';
18071
+ link.target = '_blank';
18072
+ document.body.appendChild(link);
18073
+ link.click();
18074
+ document.body.removeChild(link);
18075
+ }
18076
+ else {
18077
+ console.error('Source kind is url, but value is not a string URL.', file);
18078
+ }
18079
+ break;
18080
+ case 'reference':
18081
+ if (file.source.value &&
18082
+ typeof file.source.value === 'object' &&
18083
+ 'id' in file.source.value &&
18084
+ 'type' in file.source.value) {
18085
+ try {
18086
+ const result = await this.hooks.runAsync(FileUploaderWebhookKeys.FileItem.DownloadReference, {
18087
+ file,
18088
+ reference: file.source.value,
18089
+ plugins: this.plugins() ?? [],
18090
+ excludePlugins: this.excludePlugins() ?? [],
18091
+ capabilities: this.capabilities(),
18092
+ fileInfo: undefined,
18093
+ });
18094
+ if (result?.fileInfo) {
18095
+ if (result.fileInfo.url) {
18096
+ const link = document.createElement('a');
18097
+ link.href = result.fileInfo.url;
18098
+ link.download = file.name ?? result.fileInfo.name ?? 'download';
18099
+ link.target = '_blank';
18100
+ document.body.appendChild(link);
18101
+ link.click();
18102
+ document.body.removeChild(link);
18103
+ }
18104
+ else if (result.fileInfo.binary instanceof Blob) {
18105
+ triggerDownload(result.fileInfo.binary, file.name ?? result.fileInfo.name ?? 'download');
18106
+ }
18107
+ else {
18108
+ console.error('Could not retrieve file for download from reference:', result.fileInfo);
18109
+ }
18110
+ }
18111
+ else {
18112
+ console.error('Hook did not return fileInfo for reference download.', file);
18113
+ }
18114
+ }
18115
+ catch (error) {
18116
+ console.error('Error downloading file by reference:', file.source.value, error);
18117
+ }
18118
+ }
18119
+ else {
18120
+ console.error('Source kind is reference, but value is not a valid AXPEntityReference.', file);
18121
+ }
18122
+ break;
18123
+ case 'preview':
18124
+ case 'none':
18125
+ default:
18126
+ console.error(`Download not supported for source kind: ${file.source.kind}`, file);
18127
+ break;
18128
+ }
18129
+ try {
18130
+ await this.hooks.runAsync(FileUploaderWebhookKeys.FileItem.AfterDownload, {
18131
+ file,
18132
+ plugins: this.plugins() ?? [],
18133
+ excludePlugins: this.excludePlugins() ?? [],
18134
+ capabilities: this.capabilities(),
18135
+ });
18136
+ }
18137
+ catch { }
18138
+ }
18139
+ async handleFileRemove(event, file) {
18140
+ event.nativeEvent.preventDefault();
18141
+ event.nativeEvent.stopPropagation();
18142
+ this.onRemove.emit(file);
18143
+ try {
18144
+ await this.hooks.runAsync(FileUploaderWebhookKeys.FileItem.AfterRemove, {
18145
+ file,
18146
+ plugins: this.plugins() ?? [],
18147
+ excludePlugins: this.excludePlugins() ?? [],
18148
+ capabilities: this.capabilities(),
18149
+ });
18150
+ }
18151
+ catch { }
18152
+ }
18153
+ /**
18154
+ * Handle revert action – emit the file so parent components can update the status.
18155
+ */
18156
+ handleFileRevert(event, file) {
18157
+ if (this.isItemInteractionLocked(file)) {
18158
+ return;
18159
+ }
18160
+ event.nativeEvent.preventDefault();
18161
+ event.nativeEvent.stopPropagation();
18162
+ this.onRevert.emit(file);
18163
+ }
18164
+ /**
18165
+ * Handle file edit action – open rename popup
18166
+ */
18167
+ async handleFileEdit(event, file) {
18168
+ // if (event instanceof MouseEvent) {
18169
+ event.stopPropagation();
18170
+ event.preventDefault();
18171
+ // } else {
18172
+ // event.nativeEvent.preventDefault();
18173
+ // event.nativeEvent.stopPropagation();
18174
+ // }
18175
+ if (this.isItemInteractionLocked(file)) {
18176
+ return;
18177
+ }
18178
+ const commandResult = await this.commandExecutor.execute('FileUploader:Edit', {
18179
+ file,
18180
+ plugins: this.plugins() ?? [],
18181
+ excludePlugins: this.excludePlugins() ?? [],
18182
+ enableTitleDescription: this.enableTitleDescription(),
18183
+ });
18184
+ if (!commandResult?.success || !commandResult.data) {
18185
+ return;
18186
+ }
18187
+ const newFile = commandResult.data;
18188
+ this.onRename.emit(newFile);
18189
+ try {
18190
+ await this.hooks.runAsync(FileUploaderWebhookKeys.FileItem.AfterEdit, {
18191
+ file: newFile ?? file,
18192
+ previous: file,
18193
+ plugins: this.plugins() ?? [],
18194
+ excludePlugins: this.excludePlugins() ?? [],
18195
+ capabilities: this.capabilities(),
18196
+ });
18197
+ }
18198
+ catch { }
18199
+ }
18200
+ ngOnDestroy() { }
18201
+ async runAction(action) {
18202
+ try {
18203
+ await action.run(this.capabilities());
18204
+ }
18205
+ catch (e) {
18206
+ console.error(e);
18207
+ }
18208
+ }
18209
+ getActionColor(action) {
18210
+ // Map default action IDs to colors
18211
+ if (action.id === 'attachments.default.download') {
18212
+ return 'primary';
18213
+ }
18214
+ if (action.id === 'attachments.default.edit') {
18215
+ return 'secondary';
18216
+ }
18217
+ if (action.id === 'attachments.default.remove') {
18218
+ return 'danger';
18219
+ }
18220
+ // Default color for custom actions
18221
+ return action.color ?? 'primary';
18222
+ }
18223
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
18224
+ 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 }, 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" }, providers: [], ngImport: i0, template: "@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 }\"\n >\n <div class=\"__icon\">\n <i class=\"fa-fw {{ getFileInfo(file.name).icon }} fa-solid\"></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\">\n @if (file.status === 'deleted' && multiple() && !isItemInteractionLocked(file)) {\n <!-- Revert button - only show when multiple is true -->\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 <!-- Default + hook actions; readonly still shows download (edit/remove omitted in component logic). -->\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", styles: [":host{display:flex;width:100%;flex-direction:column;gap:.125rem;padding-top:.5rem;padding-bottom:.5rem}:host .__item{display:flex;cursor:pointer;align-items:center;gap:.75rem;border-left-width:4px;border-color:transparent;padding:.5rem}:host .__item:hover{background-color:rgb(var(--ax-sys-color-lighter-surface));color:rgb(var(--ax-sys-color-on-lighter-surface));border-color:rgb(var(--ax-sys-color-border-lighter-surface))}:host .__item.--removed{--tw-border-opacity: 1;border-color:rgba(var(--ax-sys-color-danger-500),var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgba(var(--ax-sys-color-danger-50),var(--tw-bg-opacity, 1))}:host .__item.--attached{--tw-border-opacity: 1;border-color:rgba(var(--ax-sys-color-success-500),var(--tw-border-opacity, 1));animation:attached-flash 1s ease-out forwards}:host .__item.--item-readonly:not(.--removed){opacity:.85}:host .__item .__icon{width:1.5rem;flex-shrink:0}:host .__item .__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:.875rem;line-height:1.25rem;font-weight:500}:host .__item .__content{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:.125rem}:host .__item .__primary{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:.875rem;line-height:1.25rem;font-weight:500}:host .__item .__secondary{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;text-align:start;font-size:.75rem;line-height:1rem;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{cursor:pointer;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s;animation-duration:.2s}.__empty-state:hover{opacity:.8}\n"], dependencies: [{ kind: "ngmodule", type:
18225
+ // Angular
18226
+ CommonModule }, { kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type:
18227
+ // ACoreX
18228
+ 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$1.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type:
18229
+ // Platform
18230
+ AXPStateMessageComponent, selector: "axp-state-message", inputs: ["mode", "icon", "title", "description", "look"] }] }); }
18231
+ }
18232
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileListComponent, decorators: [{
18233
+ type: Component,
18234
+ args: [{ selector: 'axp-file-list', imports: [
18235
+ // Angular
18236
+ CommonModule,
18237
+ // ACoreX
18238
+ AXFormModule,
18239
+ AXTextBoxModule,
18240
+ AXButtonModule,
18241
+ AXLabelModule,
18242
+ AXCheckBoxModule,
18243
+ AXDecoratorModule,
18244
+ AXTranslationModule,
18245
+ // Platform
18246
+ AXPStateMessageComponent,
18247
+ ], providers: [], template: "@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 }\"\n >\n <div class=\"__icon\">\n <i class=\"fa-fw {{ getFileInfo(file.name).icon }} fa-solid\"></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\">\n @if (file.status === 'deleted' && multiple() && !isItemInteractionLocked(file)) {\n <!-- Revert button - only show when multiple is true -->\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 <!-- Default + hook actions; readonly still shows download (edit/remove omitted in component logic). -->\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", styles: [":host{display:flex;width:100%;flex-direction:column;gap:.125rem;padding-top:.5rem;padding-bottom:.5rem}:host .__item{display:flex;cursor:pointer;align-items:center;gap:.75rem;border-left-width:4px;border-color:transparent;padding:.5rem}:host .__item:hover{background-color:rgb(var(--ax-sys-color-lighter-surface));color:rgb(var(--ax-sys-color-on-lighter-surface));border-color:rgb(var(--ax-sys-color-border-lighter-surface))}:host .__item.--removed{--tw-border-opacity: 1;border-color:rgba(var(--ax-sys-color-danger-500),var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgba(var(--ax-sys-color-danger-50),var(--tw-bg-opacity, 1))}:host .__item.--attached{--tw-border-opacity: 1;border-color:rgba(var(--ax-sys-color-success-500),var(--tw-border-opacity, 1));animation:attached-flash 1s ease-out forwards}:host .__item.--item-readonly:not(.--removed){opacity:.85}:host .__item .__icon{width:1.5rem;flex-shrink:0}:host .__item .__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:.875rem;line-height:1.25rem;font-weight:500}:host .__item .__content{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:.125rem}:host .__item .__primary{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:start;font-size:.875rem;line-height:1.25rem;font-weight:500}:host .__item .__secondary{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;text-align:start;font-size:.75rem;line-height:1rem;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{cursor:pointer;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s;animation-duration:.2s}.__empty-state:hover{opacity:.8}\n"] }]
18248
+ }], 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 }] }], 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 }] }] } });
18249
+
18250
+ //#region ---- Attachment fingerprint (no blob comparison) ----
18251
+ /** Stable fingerprint for one file row; ignores Blob binary content. */
18252
+ function fingerprintAttachmentItem(item) {
18253
+ if (!item || typeof item !== 'object') {
18254
+ return '';
18255
+ }
18256
+ const file = item;
18257
+ const source = file['source'];
18258
+ const kind = source?.['kind'];
18259
+ let sourceToken = '';
18260
+ if (kind === 'blob') {
18261
+ const blob = source?.['value'];
18262
+ const size = blob instanceof Blob ? blob.size : file['size'];
18263
+ sourceToken = `blob:${size ?? 0}`;
18264
+ }
18265
+ else if (kind === 'fileId') {
18266
+ sourceToken = `fileId:${String(source?.['value'] ?? '')}`;
18267
+ }
18268
+ else {
18269
+ sourceToken = `${String(kind ?? '')}:${String(source?.['value'] ?? '')}`;
18270
+ }
18271
+ return [
18272
+ String(file['id'] ?? ''),
18273
+ String(file['name'] ?? ''),
18274
+ String(file['status'] ?? ''),
18275
+ String(file['size'] ?? ''),
18276
+ sourceToken,
18277
+ String(file['title'] ?? ''),
18278
+ String(file['description'] ?? ''),
18279
+ ].join('|');
18280
+ }
18281
+ /** Order-independent fingerprint for attachment lists. */
18282
+ function fingerprintAttachments(value) {
18283
+ if (!isArray(value) || value.length === 0) {
18284
+ return '';
18285
+ }
18286
+ return value.map(fingerprintAttachmentItem).sort().join(';');
18287
+ }
18288
+ /** Identity key for semantic compare (ignores blob/source shape differences on load, includes status). */
18289
+ function attachmentIdentityKey(item) {
18290
+ if (!item || typeof item !== 'object') {
18291
+ return '';
18292
+ }
18293
+ const file = item;
18294
+ const source = file['source'];
18295
+ const persistedId = source?.['kind'] === 'fileId' ? String(source['value'] ?? '') : String(file['id'] ?? '');
18296
+ return [persistedId, String(file['name'] ?? ''), String(file['status'] ?? '')].join('|');
18297
+ }
18298
+ /** True when lists represent the same attachments regardless of blob vs fileId shape. */
18299
+ function attachmentsSemanticallyEqual(a, b) {
18300
+ const listA = isArray(a) ? a : [];
18301
+ const listB = isArray(b) ? b : [];
18302
+ if (listA.length !== listB.length) {
18303
+ return false;
18304
+ }
18305
+ const keysA = listA.map(attachmentIdentityKey).sort();
18306
+ const keysB = listB.map(attachmentIdentityKey).sort();
18307
+ return keysA.every((key, index) => key === keysB[index]);
18308
+ }
18309
+ /** Attachment rows that remain after a successful save (drops soft-deleted rows). */
18310
+ function committedAttachments(value) {
18311
+ if (!isArray(value)) {
18312
+ return [];
18313
+ }
18314
+ return value.filter((item) => {
18315
+ if (!item || typeof item !== 'object') {
18316
+ return false;
18317
+ }
18318
+ return item.status !== 'deleted';
18319
+ });
18320
+ }
18321
+ /**
18322
+ * Post-save attachment list: drops soft-deleted rows and promotes pending `attached` rows to `uploaded`
18323
+ * so deferred-save pages get the same delete/revert behavior as after a full reload.
18324
+ */
18325
+ function persistedAttachments(value) {
18326
+ return committedAttachments(value).map((item) => item.status === 'attached' ? { ...item, status: 'uploaded' } : item);
18327
+ }
18328
+ //#endregion
18329
+
18330
+ //#endregion
18331
+ //#region ---- Helpers ----
18332
+ function isFileListItem(value) {
18333
+ return value != null && typeof value === 'object' && typeof get(value, 'name') === 'string';
18334
+ }
18335
+ /** Whether a list-row value represents one attachment (file item, id reference, or fileId source). */
18336
+ function isAttachmentListEntry(value) {
18337
+ if (isFileListItem(value)) {
18338
+ return true;
18339
+ }
18340
+ if (isString(value) && value.trim().length > 0) {
18341
+ return true;
18342
+ }
18343
+ if (value != null && typeof value === 'object') {
18344
+ const id = get(value, 'id');
18345
+ if (typeof id === 'string' && id.trim().length > 0) {
18346
+ return true;
18347
+ }
18348
+ const fileId = get(value, 'source.value');
18349
+ if (get(value, 'source.kind') === 'fileId' && typeof fileId === 'string' && fileId.trim().length > 0) {
18350
+ return true;
18351
+ }
18352
+ const entryName = get(value, 'name');
18353
+ if (typeof entryName === 'string' && entryName.trim().length > 0) {
18354
+ return true;
18355
+ }
18356
+ }
18357
+ return false;
18358
+ }
18359
+ function parseAttachmentFieldEntries(fieldValue) {
18360
+ if (fieldValue == null) {
18361
+ return [];
18362
+ }
18363
+ if (isString(fieldValue)) {
18364
+ const trimmed = fieldValue.trim();
18365
+ if (!trimmed) {
18366
+ return [];
18367
+ }
18368
+ if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
18369
+ try {
18370
+ const parsed = JSON.parse(trimmed);
18371
+ return castArray(parsed);
18372
+ }
18373
+ catch {
18374
+ return [trimmed];
18375
+ }
18376
+ }
18377
+ return [trimmed];
18378
+ }
18379
+ return castArray(fieldValue);
18380
+ }
18381
+ function attachmentFieldCount(fieldValue) {
18382
+ return parseAttachmentFieldEntries(fieldValue).filter(isAttachmentListEntry).length;
18383
+ }
18384
+ function normalizeEntityFieldToFileList(fieldValue) {
18385
+ return parseAttachmentFieldEntries(fieldValue).filter(isFileListItem);
18386
+ }
18387
+ /** Resolve entity scope for column popup / load when widget options carry list-row expressions. */
18388
+ function resolveEntityId(entityIdOption, rowData) {
18389
+ if (isString(entityIdOption) && entityIdOption.trim() && !entityIdOption.includes('{{')) {
18390
+ return entityIdOption.trim();
18391
+ }
18392
+ const rowId = get(rowData, 'id') ?? get(rowData, 'Id');
18393
+ if (rowId != null && String(rowId).trim()) {
18394
+ return String(rowId).trim();
18395
+ }
18396
+ return undefined;
18397
+ }
18398
+ function resolveFileUploaderEntityScope(options, rowData, fieldFallback) {
18399
+ const entityBlock = get(options, 'entity');
18400
+ if (entityBlock != null && typeof entityBlock === 'object') {
18401
+ const name = get(entityBlock, 'name');
18402
+ const field = get(entityBlock, 'field') ?? fieldFallback;
18403
+ const entityId = resolveEntityId(get(entityBlock, 'id'), rowData);
18404
+ if (typeof name === 'string' &&
18405
+ name.trim() &&
18406
+ entityId &&
18407
+ typeof field === 'string' &&
18408
+ field.trim()) {
18409
+ return { name: name.trim(), id: entityId, field: field.trim() };
18410
+ }
18411
+ }
18412
+ const entityName = get(options, 'entityName') ?? get(options, 'entityRef');
18413
+ const field = get(options, 'entityField') ?? fieldFallback;
18414
+ const entityId = resolveEntityId(get(options, 'entityId'), rowData);
18415
+ if (typeof entityName !== 'string' ||
18416
+ !entityName.trim() ||
18417
+ !entityId ||
18418
+ typeof field !== 'string' ||
18419
+ !field.trim()) {
18420
+ return undefined;
18421
+ }
18422
+ return { name: entityName.trim(), id: entityId, field: field.trim() };
18423
+ }
18424
+ //#endregion
18425
+
18426
+ class AXPFileUploaderLoadFilesQuery {
18427
+ constructor() {
18428
+ //#region ---- Services & Dependencies ----
18429
+ this.injector = inject(Injector);
18430
+ }
18431
+ //#endregion
18432
+ //#region ---- Query Execution ----
18433
+ async fetch(input) {
18434
+ const { name: entityRef, id: entityId, field } = input;
18435
+ if (!entityRef?.trim() || !entityId?.trim() || !field?.trim()) {
18436
+ throw new Error('entityRef, entityId, and field are required.');
18437
+ }
18438
+ const record = await runInInjectionContext(this.injector, () => {
18439
+ const dataService = new AXMEntityCrudServiceImpl(entityRef);
18440
+ return dataService.getOne(entityId);
18441
+ });
18442
+ const fieldValue = get(record, field);
18443
+ const files = normalizeEntityFieldToFileList(fieldValue);
18444
+ const count = attachmentFieldCount(fieldValue);
18445
+ return { files, count };
18446
+ }
18447
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderLoadFilesQuery, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
18448
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderLoadFilesQuery, providedIn: 'root' }); }
18449
+ }
18450
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderLoadFilesQuery, decorators: [{
18451
+ type: Injectable,
18452
+ args: [{ providedIn: 'root' }]
18453
+ }] });
18454
+
18455
+ class AXPFileUploaderSaveFilesCommand {
18456
+ constructor() {
18457
+ //#region ---- Services & Dependencies ----
18458
+ this.injector = inject(Injector);
18459
+ //#endregion
18460
+ }
18461
+ //#endregion
18462
+ //#region ---- Command Execution ----
18463
+ async execute(input) {
18464
+ const { name: entityRef, id: entityId, field } = input;
18465
+ const files = (input.files ?? []).filter(isFileListItem);
18466
+ if (!entityRef?.trim() || !entityId?.trim() || !field?.trim()) {
18467
+ return {
18468
+ success: false,
18469
+ message: { text: 'entityRef, entityId, and field are required.' },
18470
+ };
18471
+ }
18472
+ try {
18473
+ const entityData = await runInInjectionContext(this.injector, async () => {
18474
+ const dataService = new AXMEntityCrudServiceImpl(entityRef);
18475
+ return dataService.updateOne(entityId, { [field]: files });
18476
+ });
18477
+ return {
18478
+ success: true,
18479
+ data: { entityData: { ...entityData, [field]: files } },
18480
+ };
18481
+ }
18482
+ catch (error) {
18483
+ return {
18484
+ success: false,
18485
+ message: {
18486
+ text: error instanceof Error ? error.message : 'Failed to save files.',
18487
+ },
18488
+ };
18489
+ }
18490
+ }
18491
+ }
18492
+
18493
+ //#region ---- File uploader widget API types ----
18494
+ function resolveFileUploaderEditDialog(options) {
18495
+ return options?.['editDialog'];
18496
+ }
18497
+ function isFileUploaderEditDialogAuto(editDialog) {
18498
+ return editDialog?.mode === 'auto';
18499
+ }
18500
+ function hasFileUploaderTitleOrDescriptionFields(editDialog) {
18501
+ return !!(editDialog?.fields?.title || editDialog?.fields?.description);
18502
+ }
18503
+ //#endregion
18504
+
18505
+ class AXPFileUploaderWidgetService {
18506
+ constructor() {
18507
+ //#region ---- Services & Dependencies ----
18508
+ this.popupService = inject(AXPopupService);
18509
+ this.translate = inject(AXTranslationService);
18510
+ this.commandExecutor = inject(AXPCommandExecutor);
18511
+ this.queryExecutor = inject(AXPQueryExecutor);
18512
+ }
18513
+ //#endregion
18514
+ //#region ---- Public API ----
18515
+ async showFileList(options) {
18516
+ const entity = options?.entity;
18517
+ let files = options?.files;
18518
+ if (entity) {
18519
+ const filesResult = await this.queryExecutor.fetch('FileUploader:LoadFiles', entity);
18520
+ if (!filesResult) {
18521
+ return undefined;
18522
+ }
18523
+ files = filesResult.files ?? [];
18524
+ }
18525
+ const resolved = {
18526
+ files: files ?? [],
18527
+ readonly: options?.readonly ?? false,
18528
+ multiple: options?.multiple ?? false,
18529
+ accept: options?.accept ?? '*',
18530
+ fileEditable: options?.fileEditable ?? true,
18531
+ maxFileSize: options?.maxFileSize ?? 1024 * 1024 * 10,
18532
+ showAddItemButton: options?.showAddItemButton ?? true,
18533
+ plugins: options?.plugins ?? [],
18534
+ editDialog: options?.editDialog,
18535
+ };
18536
+ const component = await import('./acorex-platform-layout-entity-file-list-popup.component-_yrP5SQe.mjs').then((m) => m.AXPFileListPopupComponent);
18537
+ const result = await this.popupService.open(component, {
18538
+ title: await this.translate.translateAsync('@document-management:terms.common.file'),
18539
+ data: {
18540
+ files: signal(resolved.files),
18541
+ isReadonly: signal(resolved.readonly),
18542
+ multiple: signal(resolved.multiple),
18543
+ accept: signal(resolved.accept),
18544
+ maxFileSize: signal(resolved.maxFileSize),
18545
+ fileEditable: signal(resolved.fileEditable),
18546
+ showAddItemButton: signal(resolved.showAddItemButton),
18547
+ plugins: signal(resolved.plugins),
18548
+ editDialog: signal(resolved.editDialog),
18549
+ },
18550
+ });
18551
+ const updatedFiles = result?.data?.data?.files;
18552
+ if (!updatedFiles) {
18553
+ return undefined;
18554
+ }
18555
+ if (!entity) {
18556
+ return updatedFiles;
18557
+ }
18558
+ const saveResult = await this.commandExecutor.execute('FileUploader:SaveFiles', {
18559
+ ...entity,
18560
+ files: updatedFiles,
18561
+ });
18562
+ if (!saveResult?.success) {
18563
+ return undefined;
18564
+ }
18565
+ return updatedFiles;
18566
+ }
18567
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderWidgetService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
18568
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderWidgetService, providedIn: 'root' }); }
18569
+ }
18570
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderWidgetService, decorators: [{
18571
+ type: Injectable,
18572
+ args: [{
18573
+ providedIn: 'root',
18574
+ }]
18575
+ }] });
18576
+
18577
+ class AXPFileUploaderWidgetColumnComponent extends AXPColumnWidgetComponent {
18578
+ //#endregion
18579
+ //#region ---- Lifecycle ----
18580
+ constructor() {
18581
+ super();
18582
+ //#region ---- State ----
18583
+ this.fileCount = signal(0, ...(ngDevMode ? [{ debugName: "fileCount" }] : /* istanbul ignore next */ []));
18584
+ this.loadRequestId = 0;
18585
+ //#endregion
18586
+ //#region ---- Services & Dependencies ----
18587
+ this.fileService = inject(AXPFileUploaderWidgetService);
18588
+ this.queryExecutor = inject(AXPQueryExecutor);
18589
+ effect(() => {
18590
+ const raw = this.rawValue;
18591
+ const row = this.rowData;
18592
+ const opts = this.options ?? {};
18593
+ const path = this.path;
18594
+ const inlineCount = attachmentFieldCount(raw);
18595
+ if (inlineCount > 0) {
18596
+ untracked(() => this.fileCount.set(inlineCount));
18597
+ return;
18598
+ }
18599
+ const entity = resolveFileUploaderEntityScope(opts, row, path);
18600
+ if (!entity) {
18601
+ untracked(() => this.fileCount.set(0));
18602
+ return;
18603
+ }
18604
+ const requestId = ++this.loadRequestId;
18605
+ untracked(() => {
18606
+ console.log('loadFiles for count', entity);
18607
+ void this.queryExecutor
18608
+ .fetch('FileUploader:LoadFiles', entity)
18609
+ .then((result) => {
18610
+ if (requestId !== this.loadRequestId) {
18611
+ return;
18612
+ }
18613
+ const count = result?.count ?? result?.files?.length ?? 0;
18614
+ this.fileCount.set(count);
18615
+ })
18616
+ .catch(() => {
18617
+ if (requestId === this.loadRequestId) {
18618
+ this.fileCount.set(0);
18619
+ }
18620
+ });
18621
+ });
18622
+ });
18623
+ }
18624
+ //#endregion
18625
+ //#region ---- UI Handlers ----
18626
+ async openFileList() {
18627
+ const entity = resolveFileUploaderEntityScope(this.options ?? {}, this.rowData, this.path);
18628
+ await this.fileService.showFileList({
18629
+ entity,
18630
+ readonly: true,
18631
+ });
18632
+ }
18633
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderWidgetColumnComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
18634
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPFileUploaderWidgetColumnComponent, isStandalone: true, selector: "axp-file-uploader-widget-column", inputs: { rawValue: "rawValue", rowData: "rowData" }, usesInheritance: true, ngImport: i0, template: `
18635
+ @if (fileCount() > 0) {
18636
+ <span
18637
+ class="ax-cursor-pointer ax-text-primary ax-underline"
18638
+ (click)="openFileList()"
18639
+ >
18640
+ {{ fileCount() }} {{ '@document-management:file' | translate | async }}
18641
+ </span>
18642
+ } @else {
18643
+ <span class="ax-text-muted">---</span>
18644
+ }
18645
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AXTranslationModule }, { kind: "pipe", type: i6.AXTranslatorPipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
18646
+ }
18647
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderWidgetColumnComponent, decorators: [{
18648
+ type: Component,
18649
+ args: [{
18650
+ selector: 'axp-file-uploader-widget-column',
18651
+ template: `
18652
+ @if (fileCount() > 0) {
18653
+ <span
18654
+ class="ax-cursor-pointer ax-text-primary ax-underline"
18655
+ (click)="openFileList()"
18656
+ >
18657
+ {{ fileCount() }} {{ '@document-management:file' | translate | async }}
18658
+ </span>
18659
+ } @else {
18660
+ <span class="ax-text-muted">---</span>
18661
+ }
18662
+ `,
18663
+ changeDetection: ChangeDetectionStrategy.OnPush,
18664
+ imports: [AXTranslationModule, AsyncPipe],
18665
+ inputs: ['rawValue', 'rowData'],
18666
+ }]
18667
+ }], ctorParameters: () => [] });
18668
+
18669
+ var fileUploaderWidgetColumn_component = /*#__PURE__*/Object.freeze({
18670
+ __proto__: null,
18671
+ AXPFileUploaderWidgetColumnComponent: AXPFileUploaderWidgetColumnComponent
18672
+ });
18673
+
18674
+ class AXPFileUploaderWidgetEditComponent extends AXPValueWidgetComponent {
18675
+ constructor() {
18676
+ super(...arguments);
18677
+ this.fileService = inject(AXFileService);
18678
+ this.hooks = inject(AXPHookService);
18679
+ this.fileActionsService = inject(AXPFileActionsService);
18680
+ this.commandExecutor = inject(AXPCommandExecutor);
18681
+ //#region ---- Dirty tracking (deferred save pages) ----
18682
+ this.dirtyChangeSubject = new Subject();
18683
+ this.uploadActionsChangeSubject = new Subject();
18684
+ this.savedSnapshot = signal(undefined, ...(ngDevMode ? [{ debugName: "savedSnapshot" }] : /* istanbul ignore next */ []));
18685
+ this.baselineFingerprint = signal('', ...(ngDevMode ? [{ debugName: "baselineFingerprint" }] : /* istanbul ignore next */ []));
18686
+ this.baselinePendingReconcile = signal(true, ...(ngDevMode ? [{ debugName: "baselinePendingReconcile" }] : /* istanbul ignore next */ []));
18687
+ this.isDirtySignal = signal(false, ...(ngDevMode ? [{ debugName: "isDirtySignal" }] : /* istanbul ignore next */ []));
18688
+ this.skipDirtyEvaluation = false;
18689
+ /** Status before soft-delete; used for restore when baseline was not synced (section layout). */
18690
+ this.statusBeforeDelete = new Map();
18691
+ this.autoBaselineSynced = signal(false, ...(ngDevMode ? [{ debugName: "autoBaselineSynced" }] : /* istanbul ignore next */ []));
18692
+ /** Sync baseline from context when not wired by a deferred-save page (section layout). */
18693
+ this.baselineInitEffect = effect(() => {
18694
+ if (this.autoBaselineSynced()) {
18695
+ return;
18696
+ }
18697
+ const value = this.getValue();
18698
+ if (value == null) {
18699
+ return;
18700
+ }
18701
+ this.syncBaseline(value);
18702
+ }, ...(ngDevMode ? [{ debugName: "baselineInitEffect" }] : /* istanbul ignore next */ []));
18703
+ //#endregion
18704
+ this.multiple = computed(() => this.options()['multiple'], ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
18705
+ this.acceptOverride = signal(undefined, ...(ngDevMode ? [{ debugName: "acceptOverride" }] : /* istanbul ignore next */ []));
18706
+ this.accept = computed(() => this.acceptOverride() ?? this.options()['accept'], ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
18707
+ this.plugins = computed(() => this.options()['plugins'] ?? [], ...(ngDevMode ? [{ debugName: "plugins" }] : /* istanbul ignore next */ []));
18708
+ this.pluginNames = computed(() => (this.plugins() ?? []).map((p) => p.name), ...(ngDevMode ? [{ debugName: "pluginNames" }] : /* istanbul ignore next */ []));
18709
+ this.excludePlugins = computed(() => this.options()['excludePlugins'], ...(ngDevMode ? [{ debugName: "excludePlugins" }] : /* istanbul ignore next */ []));
18710
+ this.fileEditable = computed(() => {
18711
+ if (this.options()['readonly']) {
18712
+ return false;
18713
+ }
18714
+ return this.options()['fileEditable'];
18715
+ }, ...(ngDevMode ? [{ debugName: "fileEditable" }] : /* istanbul ignore next */ []));
18716
+ this.editDialog = computed(() => resolveFileUploaderEditDialog(this.options()), ...(ngDevMode ? [{ debugName: "editDialog" }] : /* istanbul ignore next */ []));
18717
+ this.enableTitleDescription = computed(() => hasFileUploaderTitleOrDescriptionFields(this.editDialog()), ...(ngDevMode ? [{ debugName: "enableTitleDescription" }] : /* istanbul ignore next */ []));
18718
+ /** When mode is `auto`, show the edit dialog after each file select. */
18719
+ this.showEditDialogAfterSelect = computed(() => isFileUploaderEditDialogAuto(this.editDialog()), ...(ngDevMode ? [{ debugName: "showEditDialogAfterSelect" }] : /* istanbul ignore next */ []));
18720
+ this.readonly = computed(() => Boolean(this.options()['readonly']), ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
18721
+ this.showBorder = computed(() => this.options()['showBorder'] ?? true, ...(ngDevMode ? [{ debugName: "showBorder" }] : /* istanbul ignore next */ []));
18722
+ this.showAddItemButton = computed(() => this.options()['showAddItemButton'] ?? true, ...(ngDevMode ? [{ debugName: "showAddItemButton" }] : /* istanbul ignore next */ []));
18723
+ this.maxFileSize = computed(() => this.options()['maxFileSize'], ...(ngDevMode ? [{ debugName: "maxFileSize" }] : /* istanbul ignore next */ []));
18724
+ // Drag and drop state
18725
+ this.isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : /* istanbul ignore next */ []));
18726
+ this.innerActions = signal([], ...(ngDevMode ? [{ debugName: "innerActions" }] : /* istanbul ignore next */ []));
18727
+ this.fileActions = computed(() => {
18728
+ const enabledPlugins = this.pluginNames() ?? [];
18729
+ const excludedPlugins = this.excludePlugins() ?? [];
18730
+ const all = this.innerActions() ?? [];
18731
+ return all.filter((a) => {
18732
+ if (excludedPlugins.includes(a.plugin)) {
18733
+ return false;
18734
+ }
18735
+ if (!enabledPlugins || enabledPlugins.length === 0) {
18736
+ return a.global === true;
18737
+ }
18738
+ return enabledPlugins.includes(a.plugin) || a.global === true;
18739
+ });
18740
+ }, ...(ngDevMode ? [{ debugName: "fileActions" }] : /* istanbul ignore next */ []));
18741
+ this.files = computed(() => (this.getValue() ?? []).map((file) => ({
18742
+ ...file,
18743
+ })) ?? [], ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
18744
+ this.capabilities = {
18745
+ getFiles: () => this.files(),
18746
+ setFiles: (files) => this.setValue(files),
18747
+ addFiles: (files) => {
18748
+ if (this.readonly()) {
18749
+ return;
18750
+ }
18751
+ if (this.showEditDialogAfterSelect()) {
18752
+ this.addFilesWithEditDialogAsync(files);
18753
+ }
18754
+ else {
18755
+ this.setValue([...(this.getValue() ?? []), ...files]);
18756
+ }
18757
+ },
18758
+ updateFile: (predicate, updater) => {
18759
+ const current = this.getValue() ?? [];
18760
+ const next = current.map((f) => (predicate(f) ? updater(f) : f));
18761
+ this.setValue(next);
18762
+ },
18763
+ removeFile: (file) => this.removeFile(file),
18764
+ clear: () => this.clear(),
18765
+ getFileById: (id) => (this.getValue() ?? []).find((f) => f.id === id),
18766
+ updateFileById: (id, next) => {
18767
+ const current = this.getValue() ?? [];
18768
+ const idx = current.findIndex((f) => f.id === id);
18769
+ if (idx === -1) {
18770
+ return;
18771
+ }
18772
+ const updated = { ...current[idx], ...next };
18773
+ const cloned = [...current];
18774
+ cloned[idx] = updated;
18775
+ this.setValue(cloned);
18776
+ },
18777
+ };
18778
+ }
18779
+ ngOnInit() {
18780
+ super.ngOnInit();
18781
+ this.configureFromHooks();
18782
+ this.loadActions();
18783
+ }
18784
+ setValue(value) {
18785
+ super.setValue(value);
18786
+ if (this.skipDirtyEvaluation) {
18787
+ return;
18788
+ }
18789
+ this.evaluateDirty();
18790
+ }
18791
+ /** Align dirty baseline with persisted attachments (call when record loads or after discard remount). */
18792
+ syncBaseline(saved) {
18793
+ this.savedSnapshot.set(cloneDeep(saved));
18794
+ this.baselineFingerprint.set(fingerprintAttachments(saved));
18795
+ this.baselinePendingReconcile.set(true);
18796
+ this.statusBeforeDelete.clear();
18797
+ this.autoBaselineSynced.set(true);
18798
+ this.evaluateDirty();
18799
+ }
18800
+ /** Mark current list as saved; clears dirty and drops soft-deleted rows from the UI. */
18801
+ markSaved() {
18802
+ const attachments = persistedAttachments(this.getValue() ?? []);
18803
+ this.skipDirtyEvaluation = true;
18804
+ this.setValue(cloneDeep(attachments));
18805
+ this.skipDirtyEvaluation = false;
18806
+ this.savedSnapshot.set(cloneDeep(attachments));
18807
+ this.baselineFingerprint.set(fingerprintAttachments(attachments));
18808
+ this.baselinePendingReconcile.set(false);
18809
+ this.statusBeforeDelete.clear();
18810
+ this.setDirty(false);
18811
+ }
18812
+ /** Revert widget value to last saved baseline. */
18813
+ discardToBaseline() {
18814
+ const saved = this.savedSnapshot();
18815
+ this.skipDirtyEvaluation = true;
18816
+ this.setValue(cloneDeep(saved ?? []));
18817
+ this.skipDirtyEvaluation = false;
18818
+ this.baselineFingerprint.set(fingerprintAttachments(saved));
18819
+ this.baselinePendingReconcile.set(false);
18820
+ this.statusBeforeDelete.clear();
18821
+ this.setDirty(false);
18822
+ }
18823
+ api() {
18824
+ return {
18825
+ onDirtyChange: this.dirtyChangeSubject,
18826
+ onUploadActionsChanged: this.uploadActionsChangeSubject,
18827
+ getUploadActions: () => this.getUploadActionDescriptors(),
18828
+ runUploadAction: (plugin) => this.runUploadActionByPlugin(plugin),
18829
+ markSaved: () => this.markSaved(),
18830
+ syncBaseline: (saved) => this.syncBaseline(saved),
18831
+ discardToBaseline: () => this.discardToBaseline(),
18832
+ isDirty: () => this.isDirtySignal(),
18833
+ };
18834
+ }
18835
+ /** Current upload actions for host pages (always read via api; list may change after plugins load). */
18836
+ getUploadActionDescriptors() {
18837
+ return this.fileActions().map((action) => ({
18838
+ plugin: action.plugin,
18839
+ text: action.text,
18840
+ textKey: action.textKey,
18841
+ icon: action.icon,
18842
+ }));
18843
+ }
18844
+ async runUploadActionByPlugin(plugin) {
18845
+ if (this.readonly()) {
18846
+ return;
18847
+ }
18848
+ const action = this.fileActions().find((item) => item.plugin === plugin);
18849
+ if (!action) {
18850
+ return;
18851
+ }
18852
+ await action.run(this.capabilities);
18853
+ }
18854
+ notifyUploadActionsChanged() {
18855
+ this.uploadActionsChangeSubject.next();
18856
+ }
18857
+ evaluateDirty() {
18858
+ const attachments = this.getValue() ?? [];
18859
+ const fingerprint = fingerprintAttachments(attachments);
18860
+ const baseline = this.baselineFingerprint();
18861
+ if (this.baselinePendingReconcile()) {
18862
+ this.baselinePendingReconcile.set(false);
18863
+ const normalizedOnly = attachmentsSemanticallyEqual(attachments, this.savedSnapshot());
18864
+ if (normalizedOnly) {
18865
+ this.baselineFingerprint.set(fingerprint);
18866
+ this.setDirty(false);
18867
+ return;
18868
+ }
18869
+ }
18870
+ this.setDirty(fingerprint !== baseline);
18871
+ }
18872
+ setDirty(dirty) {
18873
+ if (this.isDirtySignal() === dirty) {
18874
+ return;
18875
+ }
18876
+ this.isDirtySignal.set(dirty);
18877
+ this.dirtyChangeSubject.next(dirty);
18878
+ }
18879
+ //#endregion
18880
+ async loadActions() {
18881
+ const payload = {
18882
+ plugins: this.plugins() ?? [],
18883
+ excludePlugins: this.excludePlugins() ?? [],
18884
+ capabilities: this.capabilities,
18885
+ actions: [],
18886
+ options: {
18887
+ multiple: this.multiple(),
18888
+ accept: this.accept(),
18889
+ },
18890
+ };
18891
+ const actions = await this.fileActionsService.getActions(payload);
18892
+ this.innerActions.set(actions);
18893
+ this.notifyUploadActionsChanged();
18894
+ }
18895
+ async configureFromHooks() {
18896
+ const payload = await this.hooks.runAsync(FileUploaderWebhookKeys.Configure, {
18897
+ plugins: this.plugins() ?? [],
18898
+ excludePlugins: this.excludePlugins() ?? [],
18899
+ capabilities: this.capabilities,
18900
+ overrides: {},
18901
+ });
18902
+ if (payload?.overrides?.accept) {
18903
+ this.acceptOverride.set(payload.overrides.accept);
18904
+ }
18905
+ }
18906
+ /**
18907
+ * Process files from various sources (file picker, drag-drop, etc.)
18908
+ * When showEditDialogAfterSelect is true, the edit dialog (with plugin hooks) is shown per file before adding; otherwise files are added and user can edit via Edit button.
18909
+ */
18910
+ async processFiles(files) {
18911
+ if (this.readonly()) {
18912
+ return;
18913
+ }
18914
+ if (files.length === 0) {
18915
+ return;
18916
+ }
18917
+ if (!this.multiple()) {
18918
+ this.clear();
18919
+ }
18920
+ const showDialog = this.showEditDialogAfterSelect();
18921
+ const fileUploadRequests = files.map((file) => ({
18922
+ id: AXPDataGenerator.uuid(),
18923
+ name: file.name,
18924
+ size: file.size,
18925
+ type: file.type,
18926
+ status: 'attached',
18927
+ source: {
18928
+ kind: 'blob',
18929
+ value: file,
18930
+ },
18931
+ }));
18932
+ if (showDialog) {
18933
+ await this.addFilesWithEditDialogAsync(fileUploadRequests);
18934
+ return;
18935
+ }
18936
+ // Default: run BeforeFilesAdded (plugins may transform), then add all, then AfterFilesAdded
18937
+ await this.hooks.runAsync(FileUploaderWebhookKeys.BeforeFilesAdded, {
18938
+ plugins: this.plugins() ?? [],
18939
+ excludePlugins: this.excludePlugins() ?? [],
18940
+ capabilities: this.capabilities,
18941
+ newFiles: fileUploadRequests,
18942
+ });
18943
+ this.setValue([...(this.getValue() ?? []), ...fileUploadRequests]);
18944
+ await this.hooks.runAsync(FileUploaderWebhookKeys.AfterFilesAdded, {
18945
+ plugins: this.plugins() ?? [],
18946
+ excludePlugins: this.excludePlugins() ?? [],
18947
+ capabilities: this.capabilities,
18948
+ newFiles: fileUploadRequests,
18949
+ });
18950
+ }
18951
+ /**
18952
+ * Show edit dialog per file and add only those submitted. Used when showEditDialogAfterSelect is true (both from zone and from dropdown actions).
18953
+ */
18954
+ async addFilesWithEditDialogAsync(fileItems) {
18955
+ const added = [];
18956
+ for (const fileItem of fileItems) {
18957
+ const result = await this.commandExecutor.execute('FileUploader:Edit', {
18958
+ file: fileItem,
18959
+ plugins: this.plugins() ?? [],
18960
+ excludePlugins: this.excludePlugins() ?? [],
18961
+ enableTitleDescription: this.enableTitleDescription(),
18962
+ isNewFile: true,
18963
+ });
18964
+ if (result?.success && result.data) {
18965
+ added.push(result.data);
18966
+ }
18967
+ }
18968
+ if (added.length > 0) {
18969
+ this.setValue([...(this.getValue() ?? []), ...added]);
18970
+ await this.hooks.runAsync(FileUploaderWebhookKeys.AfterFilesAdded, {
18971
+ plugins: this.plugins() ?? [],
18972
+ excludePlugins: this.excludePlugins() ?? [],
18973
+ capabilities: this.capabilities,
18974
+ newFiles: added,
18975
+ });
16355
18976
  }
16356
18977
  }
16357
- extractDisplayItem(ref) {
16358
- if (!ref) {
16359
- return null;
18978
+ handleFileRemove(file) {
18979
+ this.removeFile(file);
18980
+ }
18981
+ removeFile(file) {
18982
+ if (file.readOnly === true || !file.id) {
18983
+ return;
16360
18984
  }
16361
- const def = this.definition();
16362
- // If ref is already AXPMultiSourceRef format
16363
- if (ref.sourceKey && ref.displayName) {
16364
- return {
16365
- id: ref.refId || ref.sourceKey,
16366
- text: ref.displayName,
16367
- };
18985
+ // If multiple is false, always remove the file directly (no redo capability)
18986
+ if (!this.multiple()) {
18987
+ this.setValue((this.getValue() ?? []).filter((f) => f.id !== file.id) ?? []);
18988
+ return;
16368
18989
  }
16369
- // If ref is object with id
16370
- if (typeof ref === 'object' && ref.id) {
16371
- const sourceKey = ref.sourceKey;
16372
- if (sourceKey && def) {
16373
- const source = def.sources.find((s) => s.key === sourceKey);
16374
- if (source) {
16375
- const displayText = this.getDisplayText(ref, source);
16376
- return {
16377
- id: ref.id,
16378
- text: displayText,
16379
- };
16380
- }
16381
- }
16382
- // Fallback
16383
- const displayText = ref.displayName || ref.title || ref.id || String(ref);
16384
- return {
16385
- id: ref.id,
16386
- text: displayText,
16387
- };
18990
+ // If multiple is true, use the existing logic (mark as deleted for redo)
18991
+ if (file.status === 'attached') {
18992
+ this.statusBeforeDelete.delete(file.id);
18993
+ this.setValue((this.getValue() ?? []).filter((f) => f.id !== file.id) ?? []);
18994
+ }
18995
+ else {
18996
+ this.statusBeforeDelete.set(file.id, file.status);
18997
+ this.setValue((this.getValue() ?? []).map((f) => (f.id === file.id ? { ...f, status: 'deleted' } : f)) ?? []);
16388
18998
  }
16389
- // If ref is primitive
16390
- return {
16391
- id: String(ref),
16392
- text: String(ref),
16393
- };
16394
18999
  }
16395
- getDisplayText(item, source) {
16396
- if (!item) {
16397
- return '';
19000
+ /**
19001
+ * Revert a deleted file back to its pre-delete status.
19002
+ */
19003
+ handleFileRevert(file) {
19004
+ if (file.readOnly === true || !file.id) {
19005
+ return;
16398
19006
  }
16399
- const template = source.displayFormat || this.displayFormat();
16400
- if (template) {
16401
- const formatted = this.formatService.format(template, 'string', item);
16402
- if (formatted) {
16403
- return formatted;
16404
- }
19007
+ const restoredStatus = this.resolveRestoredStatus(file);
19008
+ if (!restoredStatus) {
19009
+ return;
16405
19010
  }
16406
- // Fallback to common display fields
16407
- return get(item, 'displayName') || get(item, 'title') || get(item, 'name') || String(item.id || item);
19011
+ this.statusBeforeDelete.delete(file.id);
19012
+ this.setValue((this.getValue() ?? []).map((f) => (f.id === file.id ? { ...f, status: restoredStatus } : f)) ?? []);
16408
19013
  }
16409
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPMultiSourceSelectorWidgetColumnComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
16410
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPMultiSourceSelectorWidgetColumnComponent, isStandalone: true, selector: "axp-multi-source-selector-widget-column", inputs: { rawValue: "rawValue", rowData: "rowData" }, usesInheritance: true, ngImport: i0, template: `
16411
- @if (displayItems().length > 0) {
16412
- <div class="ax-flex ax-flex-wrap ax-gap-1">
16413
- @for (item of visibleItems(); track item.id) {
16414
- <ax-badge class="ax-p-0.5 ax-accent1" [text]="item.text"></ax-badge>
19014
+ resolveRestoredStatus(file) {
19015
+ if (!file.id) {
19016
+ return undefined;
16415
19017
  }
16416
- @if (hasMoreItems()) {
16417
- <ax-badge class="ax-p-0.5 ax-accent2" [text]="'+' + remainingItemsCount()"></ax-badge>
19018
+ const fileId = file.id;
19019
+ const baselineFile = (this.savedSnapshot() ?? []).find((f) => f.id === fileId);
19020
+ const fromBaseline = baselineFile?.status;
19021
+ if (fromBaseline && fromBaseline !== 'deleted') {
19022
+ return fromBaseline;
19023
+ }
19024
+ const fromDelete = this.statusBeforeDelete.get(fileId);
19025
+ if (fromDelete && fromDelete !== 'deleted') {
19026
+ return fromDelete;
19027
+ }
19028
+ return 'uploaded';
19029
+ }
19030
+ /**
19031
+ * Handle file rename action. Persists name/title/description to file storage immediately when edited
19032
+ * so they are saved even if the entity payload does not carry these fields.
19033
+ */
19034
+ handleFileRename(file) {
19035
+ if (file.readOnly === true) {
19036
+ return;
16418
19037
  }
19038
+ const currentFiles = this.getValue() ?? [];
19039
+ const previousFile = currentFiles.find((f) => f.id === file.id);
19040
+ const nameChanged = previousFile && previousFile.name !== file.name;
19041
+ const titleChanged = previousFile && previousFile.title !== file.title;
19042
+ const descriptionChanged = previousFile && previousFile.description !== file.description;
19043
+ const anyMetadataChanged = nameChanged || titleChanged || descriptionChanged;
19044
+ const shouldChangeStatus = anyMetadataChanged &&
19045
+ previousFile &&
19046
+ (previousFile.status === 'uploaded' || previousFile.status === 'remote');
19047
+ const updatedFile = {
19048
+ ...file,
19049
+ status: shouldChangeStatus ? 'editing' : file.status,
19050
+ };
19051
+ this.setValue(currentFiles.map((f) => (f.id === file.id ? updatedFile : f)) ?? []);
19052
+ }
19053
+ clear() {
19054
+ const current = this.getValue() ?? [];
19055
+ const locked = current.filter((f) => f.readOnly === true);
19056
+ this.setValue(locked);
19057
+ }
19058
+ //#region ---- Drag & Drop Handlers ----
19059
+ onFileChange(event) {
19060
+ this.processFiles(event.files);
19061
+ }
19062
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderWidgetEditComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
19063
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPFileUploaderWidgetEditComponent, isStandalone: true, selector: "axp-file-uploader-widget-edit", host: { properties: { "class.axp-file-uploader-widget-edit--borderless": "!showBorder()", "class.ax-block": "true", "class.ax-flex-1": "true", "class.ax-border": "showBorder()", "class.ax-rounded-md": "showBorder()", "class.ax-lightest-surface": "showBorder()" } }, usesInheritance: true, ngImport: i0, template: `
19064
+ @if (showAddItemButton() && !readonly()) {
19065
+ <div class="ax-flex ax-justify-end ax-p-2" [class.ax-border-b]="showBorder()" *translate="let t">
19066
+ <!-- Add Item Button -->
19067
+ <ax-button class="ax-sm" [text]="t('@general:actions.add-item.title') | async" [color]="'primary'">
19068
+ <ax-prefix>
19069
+ <ax-icon icon="fa-light fa-plus"></ax-icon>
19070
+ </ax-prefix>
19071
+ <ax-dropdown-panel>
19072
+ <!-- Upload Dropdown -->
19073
+ <ax-button-item-list>
19074
+ @for (action of fileActions(); track action.plugin) {
19075
+ <ax-button-item
19076
+ (onClick)="action.run(capabilities)"
19077
+ [text]="action.text ?? (action.textKey ? (t(action.textKey) | async)! : '')"
19078
+ >
19079
+ @if (action.icon) {
19080
+ <ax-prefix>
19081
+ <ax-icon [icon]="action.icon"></ax-icon>
19082
+ </ax-prefix>
19083
+ }
19084
+ </ax-button-item>
19085
+ }
19086
+ </ax-button-item-list>
19087
+ </ax-dropdown-panel>
19088
+ </ax-button>
16419
19089
  </div>
16420
- } @else {
16421
- <span class="ax-text-muted">---</span>
16422
19090
  }
16423
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: AXBadgeModule }, { kind: "component", type: i2$1.AXBadgeComponent, selector: "ax-badge", inputs: ["color", "look", "text"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
19091
+ <div
19092
+ [class.ax-p-2]="showBorder()"
19093
+ axUploaderZone
19094
+ (fileChange)="onFileChange($event)"
19095
+ [disableBrowse]="readonly() || ((getValue() ?? []).length) > 0"
19096
+ >
19097
+ <axp-file-list
19098
+ [files]="files()"
19099
+ [readonly]="readonly()"
19100
+ [fileEditable]="fileEditable()"
19101
+ [enableTitleDescription]="enableTitleDescription()"
19102
+ [plugins]="plugins()"
19103
+ [excludePlugins]="excludePlugins()"
19104
+ [capabilities]="capabilities"
19105
+ [multiple]="multiple()"
19106
+ (onRemove)="handleFileRemove($event)"
19107
+ (onRevert)="handleFileRevert($event)"
19108
+ (onRename)="handleFileRename($event)"
19109
+ ></axp-file-list>
19110
+ </div>
19111
+ `, isInline: true, styles: [":host{border-color:rgba(var(--ax-comp-editor-border-color))}:host.axp-file-uploader-widget-edit--borderless{border:none!important}.__drag-over{background-color:rgba(var(--ax-sys-color-primary-50),.1);border:2px dashed rgb(var(--ax-sys-color-primary-500));border-radius:.375rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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: "component", type: i1.AXButtonItemComponent, selector: "ax-button-item", inputs: ["color", "disabled", "text", "selected", "divided", "data", "name"], outputs: ["onClick", "onFocus", "onBlur", "disabledChange"] }, { kind: "component", type: i1.AXButtonItemListComponent, selector: "ax-button-item-list", inputs: ["items", "closeParentOnClick", "lockOnLoading"], outputs: ["onItemClick"] }, { kind: "directive", type: AXUploaderZoneDirective, selector: "[axUploaderZone]", inputs: ["multiple", "accept", "overlayTemplate", "disableBrowse", "disableDragDrop"], outputs: ["fileChange", "onChanged", "dragEnter", "dragLeave", "dragOver", "onFileUploadComplete", "onFilesUploadComplete"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3$1.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "component", type: i3$1.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "ngmodule", type: AXLoadingModule }, { kind: "ngmodule", type: AXDropdownModule }, { kind: "component", type: i4$2.AXDropdownPanelComponent, selector: "ax-dropdown-panel", inputs: ["isOpen", "fitParent", "dropdownWidth", "position", "placement", "_target", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "ngmodule", type: AXPComponentSlotModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "directive", type: i6.AXTranslatorDirective, selector: "[translate]" }, { kind: "component", type: AXPFileListComponent, selector: "axp-file-list", inputs: ["readonly", "fileEditable", "enableTitleDescription", "multiple", "files", "plugins", "excludePlugins", "capabilities"], outputs: ["onRemove", "onRevert", "onRename"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
16424
19112
  }
16425
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPMultiSourceSelectorWidgetColumnComponent, decorators: [{
19113
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderWidgetEditComponent, decorators: [{
16426
19114
  type: Component,
16427
- args: [{
16428
- selector: 'axp-multi-source-selector-widget-column',
16429
- template: `
16430
- @if (displayItems().length > 0) {
16431
- <div class="ax-flex ax-flex-wrap ax-gap-1">
16432
- @for (item of visibleItems(); track item.id) {
16433
- <ax-badge class="ax-p-0.5 ax-accent1" [text]="item.text"></ax-badge>
16434
- }
16435
- @if (hasMoreItems()) {
16436
- <ax-badge class="ax-p-0.5 ax-accent2" [text]="'+' + remainingItemsCount()"></ax-badge>
16437
- }
19115
+ args: [{ selector: 'axp-file-uploader-widget-edit', template: `
19116
+ @if (showAddItemButton() && !readonly()) {
19117
+ <div class="ax-flex ax-justify-end ax-p-2" [class.ax-border-b]="showBorder()" *translate="let t">
19118
+ <!-- Add Item Button -->
19119
+ <ax-button class="ax-sm" [text]="t('@general:actions.add-item.title') | async" [color]="'primary'">
19120
+ <ax-prefix>
19121
+ <ax-icon icon="fa-light fa-plus"></ax-icon>
19122
+ </ax-prefix>
19123
+ <ax-dropdown-panel>
19124
+ <!-- Upload Dropdown -->
19125
+ <ax-button-item-list>
19126
+ @for (action of fileActions(); track action.plugin) {
19127
+ <ax-button-item
19128
+ (onClick)="action.run(capabilities)"
19129
+ [text]="action.text ?? (action.textKey ? (t(action.textKey) | async)! : '')"
19130
+ >
19131
+ @if (action.icon) {
19132
+ <ax-prefix>
19133
+ <ax-icon [icon]="action.icon"></ax-icon>
19134
+ </ax-prefix>
19135
+ }
19136
+ </ax-button-item>
19137
+ }
19138
+ </ax-button-item-list>
19139
+ </ax-dropdown-panel>
19140
+ </ax-button>
16438
19141
  </div>
16439
- } @else {
16440
- <span class="ax-text-muted">---</span>
16441
19142
  }
19143
+ <div
19144
+ [class.ax-p-2]="showBorder()"
19145
+ axUploaderZone
19146
+ (fileChange)="onFileChange($event)"
19147
+ [disableBrowse]="readonly() || ((getValue() ?? []).length) > 0"
19148
+ >
19149
+ <axp-file-list
19150
+ [files]="files()"
19151
+ [readonly]="readonly()"
19152
+ [fileEditable]="fileEditable()"
19153
+ [enableTitleDescription]="enableTitleDescription()"
19154
+ [plugins]="plugins()"
19155
+ [excludePlugins]="excludePlugins()"
19156
+ [capabilities]="capabilities"
19157
+ [multiple]="multiple()"
19158
+ (onRemove)="handleFileRemove($event)"
19159
+ (onRevert)="handleFileRevert($event)"
19160
+ (onRename)="handleFileRename($event)"
19161
+ ></axp-file-list>
19162
+ </div>
19163
+ `, host: {
19164
+ '[class.axp-file-uploader-widget-edit--borderless]': '!showBorder()',
19165
+ '[class.ax-block]': 'true',
19166
+ '[class.ax-flex-1]': 'true',
19167
+ '[class.ax-border]': 'showBorder()',
19168
+ '[class.ax-rounded-md]': 'showBorder()',
19169
+ '[class.ax-lightest-surface]': 'showBorder()',
19170
+ }, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
19171
+ CommonModule,
19172
+ FormsModule,
19173
+ AXButtonModule,
19174
+ AXUploaderZoneDirective,
19175
+ AXDecoratorModule,
19176
+ AXLoadingModule,
19177
+ AXDropdownModule,
19178
+ AXPComponentSlotModule,
19179
+ AXTranslationModule,
19180
+ AXPFileListComponent,
19181
+ ], styles: [":host{border-color:rgba(var(--ax-comp-editor-border-color))}:host.axp-file-uploader-widget-edit--borderless{border:none!important}.__drag-over{background-color:rgba(var(--ax-sys-color-primary-50),.1);border:2px dashed rgb(var(--ax-sys-color-primary-500));border-radius:.375rem}\n"] }]
19182
+ }] });
19183
+
19184
+ var fileUploaderWidgetEdit_component = /*#__PURE__*/Object.freeze({
19185
+ __proto__: null,
19186
+ AXPFileUploaderWidgetEditComponent: AXPFileUploaderWidgetEditComponent
19187
+ });
19188
+
19189
+ class AXPFileUploaderWidgetViewComponent extends AXPValueWidgetComponent {
19190
+ constructor() {
19191
+ super(...arguments);
19192
+ this.files = computed(() => (this.getValue() ?? []).filter(file => file.status !== 'deleted') ?? [], ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
19193
+ }
19194
+ get __class() {
19195
+ const cls = {};
19196
+ cls[`ax-block`] = true;
19197
+ cls[`ax-flex-1`] = true;
19198
+ return cls;
19199
+ }
19200
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderWidgetViewComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
19201
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: AXPFileUploaderWidgetViewComponent, isStandalone: true, selector: "axp-file-uploader-widget-view", host: { properties: { "class": "this.__class" } }, usesInheritance: true, ngImport: i0, template: `
19202
+ <axp-file-list [files]="files()" [readonly]="true"></axp-file-list>
19203
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "ngmodule", type: AXUploaderModule }, { kind: "ngmodule", type: AXLoadingModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type: AXPFileListComponent, selector: "axp-file-list", inputs: ["readonly", "fileEditable", "enableTitleDescription", "multiple", "files", "plugins", "excludePlugins", "capabilities"], outputs: ["onRemove", "onRevert", "onRename"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
19204
+ }
19205
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPFileUploaderWidgetViewComponent, decorators: [{
19206
+ type: Component,
19207
+ args: [{
19208
+ selector: 'axp-file-uploader-widget-view',
19209
+ template: `
19210
+ <axp-file-list [files]="files()" [readonly]="true"></axp-file-list>
16442
19211
  `,
16443
19212
  changeDetection: ChangeDetectionStrategy.OnPush,
16444
- imports: [AXBadgeModule],
16445
- inputs: ['rawValue', 'rowData'],
19213
+ standalone: true,
19214
+ imports: [
19215
+ FormsModule,
19216
+ AXButtonModule,
19217
+ AXDecoratorModule,
19218
+ AXUploaderModule,
19219
+ AXLoadingModule,
19220
+ AXTranslationModule,
19221
+ AXPFileListComponent
19222
+ ],
19223
+ inputs: [],
16446
19224
  }]
16447
- }], ctorParameters: () => [] });
19225
+ }], propDecorators: { __class: [{
19226
+ type: HostBinding,
19227
+ args: ['class']
19228
+ }] } });
16448
19229
 
16449
- var multiSourceSelectorWidgetColumn_component = /*#__PURE__*/Object.freeze({
19230
+ var fileUploaderWidgetView_component = /*#__PURE__*/Object.freeze({
16450
19231
  __proto__: null,
16451
- AXPMultiSourceSelectorWidgetColumnComponent: AXPMultiSourceSelectorWidgetColumnComponent
19232
+ AXPFileUploaderWidgetViewComponent: AXPFileUploaderWidgetViewComponent
19233
+ });
19234
+
19235
+ const AXPFileUploaderWidget = {
19236
+ name: 'attachments',
19237
+ title: '@platform-layout-widgets:widgets.attachments.title',
19238
+ description: '@platform-layout-widgets:widgets.attachments.description',
19239
+ icon: 'fa-light fa-files',
19240
+ categories: [AXP_WIDGETS_ADVANCE_CATEGORY],
19241
+ subCategory: AXP_WIDGETS_ADVANCE_SUB_MEDIA,
19242
+ groups: [AXPWidgetGroupEnum.EntityWidget, 'form-element'],
19243
+ type: 'editor',
19244
+ properties: [
19245
+ AXP_NAME_PROPERTY,
19246
+ AXP_DATA_PATH_PROPERTY,
19247
+ AXP_ALLOW_MULTIPLE_PROPERTY,
19248
+ AXP_DOWNLOADABLE_PROPERTY,
19249
+ AXP_READONLY_PROPERTY,
19250
+ AXP_DESCRIPTION_PROPERTY,
19251
+ createStringProperty({
19252
+ name: 'accept',
19253
+ title: 'Accept',
19254
+ path: 'options.accept',
19255
+ group: AXP_BEHAVIOR_PROPERTY_GROUP,
19256
+ }),
19257
+ createBooleanProperty({
19258
+ name: 'fileEditable',
19259
+ title: 'File Editable',
19260
+ path: 'options.fileEditable',
19261
+ group: AXP_BEHAVIOR_PROPERTY_GROUP,
19262
+ }),
19263
+ createBooleanProperty({
19264
+ name: 'enableTitleDescription',
19265
+ title: 'Enable Title & Description',
19266
+ path: 'options.enableTitleDescription',
19267
+ group: AXP_BEHAVIOR_PROPERTY_GROUP,
19268
+ }),
19269
+ createBooleanProperty({
19270
+ name: 'showEditDialogAfterSelect',
19271
+ title: 'Show Edit Dialog After File Select',
19272
+ path: 'options.showEditDialogAfterSelect',
19273
+ group: AXP_BEHAVIOR_PROPERTY_GROUP,
19274
+ }),
19275
+ createBooleanProperty({
19276
+ name: 'showBorder',
19277
+ title: 'Show Border',
19278
+ path: 'options.showBorder',
19279
+ group: AXP_BEHAVIOR_PROPERTY_GROUP,
19280
+ }),
19281
+ createBooleanProperty({
19282
+ name: 'showAddItemButton',
19283
+ title: 'Show Add Item Button',
19284
+ path: 'options.showAddItemButton',
19285
+ group: AXP_BEHAVIOR_PROPERTY_GROUP,
19286
+ }),
19287
+ createNumberProperty({
19288
+ name: 'maxFileSize',
19289
+ title: 'Max File Size (bytes)',
19290
+ path: 'options.maxFileSize',
19291
+ group: AXP_BEHAVIOR_PROPERTY_GROUP,
19292
+ }),
19293
+ ],
19294
+ components: {
19295
+ view: {
19296
+ component: () => Promise.resolve().then(function () { return fileUploaderWidgetView_component; }).then((c) => c.AXPFileUploaderWidgetViewComponent),
19297
+ },
19298
+ edit: {
19299
+ component: () => Promise.resolve().then(function () { return fileUploaderWidgetEdit_component; }).then((c) => c.AXPFileUploaderWidgetEditComponent),
19300
+ },
19301
+ column: {
19302
+ component: () => Promise.resolve().then(function () { return fileUploaderWidgetColumn_component; }).then((c) => c.AXPFileUploaderWidgetColumnComponent),
19303
+ },
19304
+ designer: {
19305
+ component: () => Promise.resolve().then(function () { return fileUploaderWidgetEdit_component; }).then((c) => c.AXPFileUploaderWidgetEditComponent),
19306
+ },
19307
+ },
19308
+ };
19309
+
19310
+ var index = /*#__PURE__*/Object.freeze({
19311
+ __proto__: null,
19312
+ AXPEditFileUploaderCommand: AXPEditFileUploaderCommand,
19313
+ AXPFileListComponent: AXPFileListComponent,
19314
+ AXPFileUploaderLoadFilesQuery: AXPFileUploaderLoadFilesQuery,
19315
+ AXPFileUploaderSaveFilesCommand: AXPFileUploaderSaveFilesCommand,
19316
+ AXPFileUploaderWidget: AXPFileUploaderWidget,
19317
+ AXPFileUploaderWidgetColumnComponent: AXPFileUploaderWidgetColumnComponent,
19318
+ AXPFileUploaderWidgetEditComponent: AXPFileUploaderWidgetEditComponent,
19319
+ AXPFileUploaderWidgetService: AXPFileUploaderWidgetService,
19320
+ AXPFileUploaderWidgetViewComponent: AXPFileUploaderWidgetViewComponent,
19321
+ attachmentFieldCount: attachmentFieldCount,
19322
+ attachmentsSemanticallyEqual: attachmentsSemanticallyEqual,
19323
+ committedAttachments: committedAttachments,
19324
+ fingerprintAttachmentItem: fingerprintAttachmentItem,
19325
+ fingerprintAttachments: fingerprintAttachments,
19326
+ hasFileUploaderTitleOrDescriptionFields: hasFileUploaderTitleOrDescriptionFields,
19327
+ isAttachmentListEntry: isAttachmentListEntry,
19328
+ isFileListItem: isFileListItem,
19329
+ isFileUploaderEditDialogAuto: isFileUploaderEditDialogAuto,
19330
+ normalizeEntityFieldToFileList: normalizeEntityFieldToFileList,
19331
+ persistedAttachments: persistedAttachments,
19332
+ resolveFileUploaderEditDialog: resolveFileUploaderEditDialog,
19333
+ resolveFileUploaderEntityScope: resolveFileUploaderEntityScope
16452
19334
  });
16453
19335
 
16454
19336
  //#region ---- Imports ----
@@ -17697,6 +20579,7 @@ const ENTITY_WIDGETS = [
17697
20579
  AXPEntityCategoryWidget,
17698
20580
  AXPMultiSourceSelectorWidget,
17699
20581
  AXPEntityDefinitionProviderWidget,
20582
+ AXPFileUploaderWidget,
17700
20583
  ];
17701
20584
  const ENTITY_EXTENDED_WIDGETS = [
17702
20585
  {
@@ -20402,6 +23285,127 @@ const defaultMultiLanguageMiddlewareProvider = {
20402
23285
  };
20403
23286
  //#endregion
20404
23287
 
23288
+ //#region ---- Smart card layout builders ----
23289
+ const TITLE_FIELD_CANDIDATES = ['title', 'name'];
23290
+ const DESCRIPTION_FIELD_CANDIDATES = ['description'];
23291
+ const ICON_FIELD_CANDIDATES = ['icon'];
23292
+ const STATUS_FIELD_CANDIDATES = ['status', 'statusId'];
23293
+ function findPropertyName(properties, candidates) {
23294
+ const names = new Set(properties.map((p) => p.name));
23295
+ return candidates.find((candidate) => names.has(candidate));
23296
+ }
23297
+ function findColumnName(columnNames, candidates) {
23298
+ return candidates.find((candidate) => columnNames.includes(candidate));
23299
+ }
23300
+ function findNestedColumnName(columnNames, suffix) {
23301
+ return columnNames.find((name) => name === suffix || name.endsWith(`.${suffix}`));
23302
+ }
23303
+ function resolveTitleField(properties, columnNames) {
23304
+ return (findPropertyName(properties, TITLE_FIELD_CANDIDATES) ??
23305
+ findColumnName(columnNames, TITLE_FIELD_CANDIDATES) ??
23306
+ findNestedColumnName(columnNames, 'fullName') ??
23307
+ findNestedColumnName(columnNames, 'displayName') ??
23308
+ columnNames[0] ??
23309
+ 'name');
23310
+ }
23311
+ function resolveDescriptionField(properties, columnNames) {
23312
+ return (findPropertyName(properties, DESCRIPTION_FIELD_CANDIDATES) ??
23313
+ findColumnName(columnNames, DESCRIPTION_FIELD_CANDIDATES) ??
23314
+ findNestedColumnName(columnNames, 'description'));
23315
+ }
23316
+ function resolveIconField(properties, columnNames) {
23317
+ return findPropertyName(properties, ICON_FIELD_CANDIDATES) ?? findColumnName(columnNames, ICON_FIELD_CANDIDATES);
23318
+ }
23319
+ function resolveStatusField(properties, columnNames) {
23320
+ return (findPropertyName(properties, STATUS_FIELD_CANDIDATES) ??
23321
+ findColumnName(columnNames, STATUS_FIELD_CANDIDATES) ??
23322
+ findNestedColumnName(columnNames, 'status'));
23323
+ }
23324
+ function resolveListColumnNames(entityColumns, tableColumns) {
23325
+ const names = new Set();
23326
+ const ordered = [];
23327
+ const append = (columns) => {
23328
+ for (const column of columns ?? []) {
23329
+ if (!column.name || names.has(column.name)) {
23330
+ continue;
23331
+ }
23332
+ if (column.options?.visible === false) {
23333
+ continue;
23334
+ }
23335
+ names.add(column.name);
23336
+ ordered.push(column.name);
23337
+ }
23338
+ };
23339
+ append(entityColumns);
23340
+ append(tableColumns);
23341
+ return ordered;
23342
+ }
23343
+ function buildCardFields(columnNames, headerFields, statusField) {
23344
+ return columnNames
23345
+ .filter((name) => !headerFields.has(name))
23346
+ .map((name) => {
23347
+ const field = { name };
23348
+ if (statusField && name === statusField) {
23349
+ field.display = 'badge';
23350
+ }
23351
+ return field;
23352
+ });
23353
+ }
23354
+ function buildDefaultCardLayout(entity, entityColumns) {
23355
+ const properties = entity.properties ?? [];
23356
+ const tableColumns = entity.interfaces?.master?.list?.layouts?.table?.columns;
23357
+ const columnNames = resolveListColumnNames(entityColumns, tableColumns);
23358
+ if (columnNames.length === 0) {
23359
+ return null;
23360
+ }
23361
+ const titleField = resolveTitleField(properties, columnNames);
23362
+ const descriptionField = resolveDescriptionField(properties, columnNames);
23363
+ const iconField = resolveIconField(properties, columnNames);
23364
+ const statusField = resolveStatusField(properties, columnNames);
23365
+ const headerFields = new Set([titleField, descriptionField, iconField].filter((field) => !!field));
23366
+ const fields = buildCardFields(columnNames, headerFields, statusField);
23367
+ if (fields.length === 0 && !titleField) {
23368
+ return null;
23369
+ }
23370
+ return {
23371
+ enabled: true,
23372
+ header: {
23373
+ title: titleField,
23374
+ ...(descriptionField ? { description: descriptionField } : {}),
23375
+ ...(iconField ? { icon: iconField } : {}),
23376
+ },
23377
+ fields,
23378
+ };
23379
+ }
23380
+ //#endregion
23381
+ //#region ---- Default card layout middleware ----
23382
+ /**
23383
+ * When an entity has no `interfaces.master.list.layouts.card`, creates a sensible default
23384
+ * from list columns and property names (title, description, remaining columns as card fields).
23385
+ */
23386
+ const defaultCardLayoutMiddleware = (context) => {
23387
+ const existingCard = context.interfaces.master.list.card.get();
23388
+ if (existingCard !== undefined) {
23389
+ return;
23390
+ }
23391
+ if (!context.interfaces.master.list.get()) {
23392
+ return;
23393
+ }
23394
+ const layout = buildDefaultCardLayout(context.entity, context.columns.list());
23395
+ if (!layout) {
23396
+ return;
23397
+ }
23398
+ context.interfaces.master.list.card.set(layout);
23399
+ };
23400
+ //#endregion
23401
+ //#region ---- Provider registration ----
23402
+ const defaultCardLayoutMiddlewareProvider = {
23403
+ entityName: '*',
23404
+ modifier: defaultCardLayoutMiddleware,
23405
+ order: 600,
23406
+ };
23407
+ //#endregion
23408
+
20405
23409
  /**
20406
23410
  * Factory to create a column width middleware using provided config map.
20407
23411
  * Sets width for columns defined in the map if not already defined on the column.
@@ -20456,8 +23460,9 @@ const AXPCrudModifier = {
20456
23460
  if (!queries?.list) {
20457
23461
  queries.list = {
20458
23462
  execute: async (e) => {
20459
- console.log('query', ctx.module.get() + '.' + ctx.name.get(), e);
20460
- return await dataService.query(e);
23463
+ const res = await dataService.query(e);
23464
+ console.log('query', res, ctx.module.get() + '.' + ctx.name.get(), e);
23465
+ return res;
20461
23466
  },
20462
23467
  type: AXPEntityQueryType.List,
20463
23468
  };
@@ -21074,54 +24079,50 @@ const AXPShowDetailsViewWorkflow = {
21074
24079
  class AXPShowFileUploaderPopupAction extends AXPWorkflowAction {
21075
24080
  constructor() {
21076
24081
  super(...arguments);
24082
+ //#region ---- Services & Dependencies ----
21077
24083
  this.fileUploaderWidgetService = inject(AXPFileUploaderWidgetService);
21078
- this.entityRegistryService = inject(AXPEntityDefinitionRegistryService);
21079
24084
  }
24085
+ //#endregion
24086
+ //#region ---- Workflow Action ----
21080
24087
  async execute(context) {
21081
- // *********** get options ***********
21082
- const key = context.getVariable('options.key');
24088
+ const field = context.getVariable('options.key');
21083
24089
  const rawReadonly = context.getVariable('options.readonly') ??
21084
24090
  context.getVariable('options.readOnly');
21085
24091
  const isReadonly = rawReadonly === true || rawReadonly === 'true';
21086
24092
  const multiple = context.getVariable('options.multiple');
21087
24093
  const accept = context.getVariable('options.accept');
21088
24094
  const maxFileSize = context.getVariable('options.maxFileSize');
21089
- const files = context.getVariable('options.files');
21090
24095
  const fileEditable = context.getVariable('options.fileEditable');
21091
- const enableTitleDescription = context.getVariable('options.enableTitleDescription');
21092
- const showEditDialogAfterSelect = context.getVariable('options.showEditDialogAfterSelect');
21093
- const correctFiles = (files ?? []).filter((item) => item != null && typeof item === 'object' && typeof item.name === 'string');
21094
- // *********** show file list ***********
21095
- const res = await this.fileUploaderWidgetService.showFileList({
21096
- files: correctFiles,
24096
+ const showAddItemButton = context.getVariable('options.showAddItemButton');
24097
+ const editDialog = context.getVariable('options.editDialog');
24098
+ const plugins = context.getVariable('options.plugins');
24099
+ const entityOption = context.getVariable('options.entity');
24100
+ const entityRef = entityOption?.name ??
24101
+ context.getVariable('entity') ??
24102
+ context.getVariable('options.entityName');
24103
+ const entityId = entityOption?.id ?? context.getVariable('options.id');
24104
+ const resolvedField = entityOption?.field ?? field;
24105
+ if (!entityRef || !entityId || !resolvedField) {
24106
+ context.setOutput('result', false);
24107
+ return;
24108
+ }
24109
+ const files = await this.fileUploaderWidgetService.showFileList({
24110
+ entity: { name: entityRef, id: entityId, field: resolvedField },
21097
24111
  readonly: isReadonly,
21098
- multiple: multiple,
21099
- accept: accept,
21100
- fileEditable: fileEditable,
21101
- enableTitleDescription: enableTitleDescription ?? false,
21102
- showEditDialogAfterSelect: showEditDialogAfterSelect ?? false,
21103
- // maxFileSize: maxFileSize,
24112
+ multiple,
24113
+ accept,
24114
+ fileEditable,
24115
+ maxFileSize,
24116
+ showAddItemButton: showAddItemButton ?? true,
24117
+ plugins: plugins ?? [],
24118
+ editDialog,
21104
24119
  });
21105
- // Handle case when result is undefined or empty array
21106
- if (!res) {
24120
+ if (!files) {
21107
24121
  context.setOutput('result', false);
21108
24122
  return;
21109
24123
  }
21110
- // *********** save files to entity ***********
21111
- const [module, entity] = context.getVariable('entity').split('.');
21112
- const entityDefinition = await this.entityRegistryService.resolve(module, entity);
21113
- if (entityDefinition) {
21114
- const updateExec = entityDefinition.commands?.update?.execute;
21115
- const entityData = await updateExec({
21116
- id: context.getVariable('options.id'),
21117
- [key]: res,
21118
- });
21119
- context.setOutput('result', true);
21120
- // Merge full file list (name, title, description) from popup into returned data so
21121
- // workflow output includes title/description when enableTitleDescription is used
21122
- const dataWithFiles = { ...entityData, [key]: res };
21123
- context.setVariable('data', cloneDeep(dataWithFiles));
21124
- }
24124
+ context.setOutput('result', true);
24125
+ context.setVariable('data', cloneDeep({ id: entityId, [resolvedField]: files }));
21125
24126
  }
21126
24127
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPShowFileUploaderPopupAction, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
21127
24128
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPShowFileUploaderPopupAction }); }
@@ -21291,7 +24292,8 @@ function routesFacory() {
21291
24292
  }
21292
24293
  class AXPEntityModule {
21293
24294
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
21294
- static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityModule, imports: [RouterModule, i1$3.AXPWorkflowModule, AXPWidgetCoreModule,
24295
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityModule, imports: [RouterModule,
24296
+ AXPAttachmentsPluginModule, i1$3.AXPWorkflowModule, AXPWidgetCoreModule,
21295
24297
  LayoutBuilderModule] }); }
21296
24298
  static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPEntityModule, providers: [
21297
24299
  { provide: AXP_WIDGET_DEFINITION_PROVIDER, useClass: AXPEntityWidgetsProvider, multi: true },
@@ -21322,6 +24324,11 @@ class AXPEntityModule {
21322
24324
  useValue: columnOrderingMiddlewareProvider,
21323
24325
  multi: true,
21324
24326
  },
24327
+ {
24328
+ provide: AXP_ENTITY_MODIFIER,
24329
+ useValue: defaultCardLayoutMiddlewareProvider,
24330
+ multi: true,
24331
+ },
21325
24332
  {
21326
24333
  provide: AXP_ENTITY_MODIFIER,
21327
24334
  useValue: AXPCrudModifier,
@@ -21386,14 +24393,27 @@ class AXPEntityModule {
21386
24393
  key: 'Entity:View',
21387
24394
  command: () => Promise.resolve().then(function () { return viewEntityDetails_command; }).then((c) => c.AXPViewEntityDetailsCommand),
21388
24395
  },
24396
+ {
24397
+ key: 'FileUploader:Edit',
24398
+ command: () => Promise.resolve().then(function () { return index; }).then((c) => c.AXPEditFileUploaderCommand),
24399
+ },
24400
+ {
24401
+ key: 'FileUploader:SaveFiles',
24402
+ command: () => Promise.resolve().then(function () { return index; }).then((c) => c.AXPFileUploaderSaveFilesCommand),
24403
+ },
21389
24404
  ]),
21390
24405
  provideQuerySetups([
21391
24406
  {
21392
24407
  key: 'Entity:GetDetails',
21393
24408
  loader: () => Promise.resolve().then(function () { return getEntityDetails_query; }).then((c) => c.AXPGetEntityDetailsQuery),
21394
24409
  },
24410
+ {
24411
+ key: 'FileUploader:LoadFiles',
24412
+ loader: () => Promise.resolve().then(function () { return index; }).then((c) => c.AXPFileUploaderLoadFilesQuery),
24413
+ },
21395
24414
  ]),
21396
24415
  ], imports: [RouterModule,
24416
+ AXPAttachmentsPluginModule,
21397
24417
  AXPWorkflowModule.forChild({
21398
24418
  actions: {
21399
24419
  AXPEntityCreatePopupAction,
@@ -21429,6 +24449,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
21429
24449
  args: [{
21430
24450
  imports: [
21431
24451
  RouterModule,
24452
+ AXPAttachmentsPluginModule,
21432
24453
  AXPWorkflowModule.forChild({
21433
24454
  actions: {
21434
24455
  AXPEntityCreatePopupAction,
@@ -21490,6 +24511,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
21490
24511
  useValue: columnOrderingMiddlewareProvider,
21491
24512
  multi: true,
21492
24513
  },
24514
+ {
24515
+ provide: AXP_ENTITY_MODIFIER,
24516
+ useValue: defaultCardLayoutMiddlewareProvider,
24517
+ multi: true,
24518
+ },
21493
24519
  {
21494
24520
  provide: AXP_ENTITY_MODIFIER,
21495
24521
  useValue: AXPCrudModifier,
@@ -21554,12 +24580,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
21554
24580
  key: 'Entity:View',
21555
24581
  command: () => Promise.resolve().then(function () { return viewEntityDetails_command; }).then((c) => c.AXPViewEntityDetailsCommand),
21556
24582
  },
24583
+ {
24584
+ key: 'FileUploader:Edit',
24585
+ command: () => Promise.resolve().then(function () { return index; }).then((c) => c.AXPEditFileUploaderCommand),
24586
+ },
24587
+ {
24588
+ key: 'FileUploader:SaveFiles',
24589
+ command: () => Promise.resolve().then(function () { return index; }).then((c) => c.AXPFileUploaderSaveFilesCommand),
24590
+ },
21557
24591
  ]),
21558
24592
  provideQuerySetups([
21559
24593
  {
21560
24594
  key: 'Entity:GetDetails',
21561
24595
  loader: () => Promise.resolve().then(function () { return getEntityDetails_query; }).then((c) => c.AXPGetEntityDetailsQuery),
21562
24596
  },
24597
+ {
24598
+ key: 'FileUploader:LoadFiles',
24599
+ loader: () => Promise.resolve().then(function () { return index; }).then((c) => c.AXPFileUploaderLoadFilesQuery),
24600
+ },
21563
24601
  ]),
21564
24602
  ],
21565
24603
  }]
@@ -21800,5 +24838,5 @@ var getEntityDetails_query = /*#__PURE__*/Object.freeze({
21800
24838
  * Generated bundle index. Do not edit.
21801
24839
  */
21802
24840
 
21803
- export { AXMEntityCrudService, AXMEntityCrudServiceImpl, AXPCategoryTreeService, AXPCreateEntityCommand, AXPCreateEntityWorkflow, AXPDataSeederService, AXPDeleteEntityWorkflow, AXPEntitiesListDataSourceDefinition, AXPEntityApplyUpdatesAction, AXPEntityCategoryTreeSelectorComponent, AXPEntityCategoryWidget, AXPEntityCategoryWidgetColumnComponent, AXPEntityCategoryWidgetEditComponent, AXPEntityCategoryWidgetViewComponent, AXPEntityCommandTriggerViewModel, AXPEntityCreateEvent, AXPEntityCreatePopupAction, AXPEntityCreateSubmittedAction, AXPEntityCreateViewElementViewModel, AXPEntityCreateViewModelFactory, AXPEntityCreateViewSectionViewModel, AXPEntityDataProvider, AXPEntityDataProviderImpl, AXPEntityDataSelectorService, AXPEntityDefinitionProviderWidget, AXPEntityDefinitionProviderWidgetEditComponent, AXPEntityDefinitionRegistryService, AXPEntityDeletedEvent, AXPEntityDetailListViewModel, AXPEntityDetailPopoverComponent, AXPEntityDetailPopoverService, AXPEntityDetailViewModelFactory, AXPEntityDetailViewModelResolver, AXPEntityEventDispatcherService, AXPEntityEventsKeys, AXPEntityFormBuilderService, AXPEntityListPersistenceModeDefault, AXPEntityListTableService, AXPEntityListToolbarService, AXPEntityListViewColumnViewModel, AXPEntityListViewModelFactory, AXPEntityListViewModelResolver, AXPEntityListWidget, AXPEntityListWidgetViewComponent, AXPEntityMasterCreateViewModel, AXPEntityMasterListViewModel, AXPEntityMasterListViewQueryViewModel, AXPEntityMasterSingleElementViewModel, AXPEntityMasterSingleViewGroupViewModel, AXPEntityMasterSingleViewModel, AXPEntityMasterUpdateElementViewModel, AXPEntityMasterUpdateViewModel, AXPEntityMasterUpdateViewModelFactory, AXPEntityMiddleware, AXPEntityModifyConfirmedAction, AXPEntityModifyEvent, AXPEntityModifySectionPopupAction, AXPEntityModule, AXPEntityPerformDeleteAction, AXPEntityPreloadFiltersContainerComponent, AXPEntityPreloadFiltersViewModel, AXPEntityPreloadFiltersViewModelResolver, AXPEntityResolver, AXPEntityService, AXPEntityStorageService, AXPEntityUpdateViewSectionViewModel, AXPGetEntityDetailsQuery, AXPLayoutOrderingConfigService, AXPLookupWidget, AXPLookupWidgetColumnComponent, AXPLookupWidgetEditComponent, AXPLookupWidgetViewComponent, AXPMiddlewareAbortError, AXPMiddlewareEntityStorageService, AXPModifyEntitySectionWorkflow, AXPMultiSourceDefinitionProviderContext, AXPMultiSourceDefinitionProviderService, AXPMultiSourceFederatedSearchService, AXPMultiSourceSelectorComponent, AXPMultiSourceSelectorService, AXPMultiSourceSelectorWidget, AXPMultiSourceSelectorWidgetColumnComponent, AXPMultiSourceSelectorWidgetEditComponent, AXPMultiSourceSelectorWidgetViewComponent, AXPMultiSourceType, AXPOpenEntityDetailsCommand, AXPQuickEntityModifyPopupAction, AXPQuickModifyEntityWorkflow, AXPRelatedColumnEnrichmentService, AXPRelatedColumnMetadataResolver, AXPSelectorStructureWidget, AXPSelectorStructureWidgetColumnComponent, AXPSelectorStructureWidgetEditComponent, AXPSelectorStructureWidgetViewComponent, AXPShowDetailViewAction, AXPShowDetailsViewWorkflow, AXPShowListViewAction, AXPShowListViewWorkflow, AXPTruncatedBreadcrumbComponent, AXPUpdateEntityCommand, AXPViewEntityDetailsCommand, AXP_CATEGORY_TREE_ROOT_TITLE_I18N_KEY, 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, AXP_RECORD_WORKFLOW_INFO_CORRELATION_ID_FIELD, AXP_RECORD_WORKFLOW_INFO_DEFINITION_ID_FIELD, AXP_RECORD_WORKFLOW_INFO_INSTANCE_ID_FIELD, DEFAULT_COLUMN_ORDER, DEFAULT_PAIR_SPAN_RULES, DEFAULT_PROPERTY_ORDER, DEFAULT_SECTION_ORDER, ENTITY_LIST_ROUTE_CONTEXT_SESSION_KEY, EntityBuilder, EntityDataAccessor, actionExists, applyDataSourcePagingWithoutLoad, axpCreateEntityAiToolInputDefaults, axpCreateEntityCommandDefinition, buildAXPRecordWorkflowInfo, canPersistEntityListState, cloneLayoutArrays, collectEntityQuickSearchFieldPaths, collectNestedCreateHiddenProperties, collectNestedFieldPathsFromEntityColumns, collectQuickSearchPathsFromSingleEntityDefinition, columnOrderingMiddleware, columnOrderingMiddlewareProvider, columnWidthMiddleware, columnWidthMiddlewareProvider, computeEntityAggregates, createColumnOrderingMiddlewareProvider, createLayoutOrderingMiddlewareProvider, createModifierContext, defaultMultiLanguageMiddleware, defaultMultiLanguageMiddlewareProvider, detectEntityChanges, ensureLayoutPropertyView, ensureLayoutSection, ensureListActions, entityDetailsCreateActions, entityDetailsCreateActionsDeferredParent, entityDetailsCrudActions, entityDetailsEditAction, entityDetailsNewEditAction, entityDetailsReferenceCondition, entityDetailsReferenceCreateActions, entityDetailsSimpleCondition, entityMasterBulkDeleteAction, entityMasterCreateAction, entityMasterCrudActions, entityMasterDeleteAction, entityMasterEditAction, entityMasterRecordActions, entityMasterViewAction, entityOverrideDetailsViewAction, eventDispatchMiddleware, filterSortEntityRows, findEntityListRowDataInTree, formatLookupItemDisplay, getDataSourcePageIndex, getEntityListRowId, getMasterInterfacePropertySortKey, getRecordWorkflowCorrelationId, getRecordWorkflowInstanceId, isAXPMiddlewareAbortError, isCategoryEntity, isCategoryFilter, isUnresolvedLookupDisplayTemplate, layoutOrderingMiddlewareFactory, layoutOrderingMiddlewareProvider, mergeForeignKeyFieldIntoCreateActions, normalizeEntityListPersistenceMode, normalizeListPaging, normalizeLookupDisplayTemplate, provideEntity, resolveEntityPluginDetailPageOrder, resolveLookupDisplayField, resolveLookupDisplayTemplate, restoreEntityListExpandedRows, runEntityQuery, searchResultDescriptionMiddleware, searchResultDescriptionMiddlewareProvider, shouldLoadEntityListStateFromStorage, shouldResetEntityListStateOnRouteEntry };
24841
+ export { ATTACHMENTS_PAGE_COMPONENT_KEY, AXMEntityCrudService, AXMEntityCrudServiceImpl, AXPCategoryTreeService, AXPCreateEntityCommand, AXPCreateEntityWorkflow, AXPDataSeederService, AXPDeleteEntityWorkflow, AXPEditFileUploaderCommand, AXPEntitiesListDataSourceDefinition, AXPEntityApplyUpdatesAction, AXPEntityCategoryTreeSelectorComponent, AXPEntityCategoryWidget, AXPEntityCategoryWidgetColumnComponent, AXPEntityCategoryWidgetEditComponent, AXPEntityCategoryWidgetViewComponent, AXPEntityCommandTriggerViewModel, AXPEntityCreateEvent, AXPEntityCreatePopupAction, AXPEntityCreateSubmittedAction, AXPEntityCreateViewElementViewModel, AXPEntityCreateViewModelFactory, AXPEntityCreateViewSectionViewModel, AXPEntityDataProvider, AXPEntityDataProviderImpl, AXPEntityDataSelectorRowActionsService, AXPEntityDataSelectorService, AXPEntityDefinitionProviderWidget, AXPEntityDefinitionProviderWidgetEditComponent, AXPEntityDefinitionRegistryService, AXPEntityDeletedEvent, AXPEntityDetailListViewModel, AXPEntityDetailPopoverComponent, AXPEntityDetailPopoverService, AXPEntityDetailViewModelFactory, AXPEntityDetailViewModelResolver, AXPEntityEventDispatcherService, AXPEntityEventsKeys, AXPEntityFormBuilderService, AXPEntityListPersistenceModeDefault, AXPEntityListTableService, AXPEntityListToolbarService, AXPEntityListViewCardFieldViewModel, AXPEntityListViewColumnViewModel, AXPEntityListViewModelFactory, AXPEntityListViewModelResolver, AXPEntityListWidget, AXPEntityListWidgetViewComponent, AXPEntityMasterCreateViewModel, AXPEntityMasterListCardSelectActionName, AXPEntityMasterListViewModel, AXPEntityMasterListViewQueryViewModel, AXPEntityMasterSingleElementViewModel, AXPEntityMasterSingleViewGroupViewModel, AXPEntityMasterSingleViewModel, AXPEntityMasterUpdateElementViewModel, AXPEntityMasterUpdateViewModel, AXPEntityMasterUpdateViewModelFactory, AXPEntityMiddleware, AXPEntityModifyConfirmedAction, AXPEntityModifyEvent, AXPEntityModifySectionPopupAction, AXPEntityModule, AXPEntityPerformDeleteAction, AXPEntityPreloadFiltersContainerComponent, AXPEntityPreloadFiltersViewModel, AXPEntityPreloadFiltersViewModelResolver, AXPEntityResolver, AXPEntityService, AXPEntityStorageService, AXPEntityUpdateViewSectionViewModel, AXPFileListComponent, AXPFileUploaderLoadFilesQuery, AXPFileUploaderSaveFilesCommand, AXPFileUploaderWidget, AXPFileUploaderWidgetColumnComponent, AXPFileUploaderWidgetEditComponent, AXPFileUploaderWidgetService, AXPFileUploaderWidgetViewComponent, AXPGetEntityDetailsQuery, AXPLayoutOrderingConfigService, AXPLookupWidget, AXPLookupWidgetColumnComponent, AXPLookupWidgetEditComponent, AXPLookupWidgetViewComponent, AXPMiddlewareAbortError, AXPMiddlewareEntityStorageService, AXPModifyEntitySectionWorkflow, AXPMultiSourceDefinitionProviderContext, AXPMultiSourceDefinitionProviderService, AXPMultiSourceFederatedSearchService, AXPMultiSourceSelectorComponent, AXPMultiSourceSelectorService, AXPMultiSourceSelectorWidget, AXPMultiSourceSelectorWidgetColumnComponent, AXPMultiSourceSelectorWidgetEditComponent, AXPMultiSourceSelectorWidgetViewComponent, AXPMultiSourceType, AXPOpenEntityDetailsCommand, AXPQuickEntityModifyPopupAction, AXPQuickModifyEntityWorkflow, AXPRelatedColumnEnrichmentService, AXPRelatedColumnMetadataResolver, AXPSelectorStructureWidget, AXPSelectorStructureWidgetColumnComponent, AXPSelectorStructureWidgetEditComponent, AXPSelectorStructureWidgetViewComponent, AXPShowDetailViewAction, AXPShowDetailsViewWorkflow, AXPShowListViewAction, AXPShowListViewWorkflow, AXPTruncatedBreadcrumbComponent, AXPUpdateEntityCommand, AXPViewEntityDetailsCommand, AXP_CATEGORY_TREE_ROOT_TITLE_I18N_KEY, 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, AXP_RECORD_WORKFLOW_INFO_CORRELATION_ID_FIELD, AXP_RECORD_WORKFLOW_INFO_DEFINITION_ID_FIELD, AXP_RECORD_WORKFLOW_INFO_INSTANCE_ID_FIELD, DEFAULT_COLUMN_ORDER, DEFAULT_PAIR_SPAN_RULES, DEFAULT_PROPERTY_ORDER, DEFAULT_SECTION_ORDER, ENTITY_LIST_ROUTE_CONTEXT_SESSION_KEY, EntityBuilder, EntityDataAccessor, actionExists, applyDataSourcePagingWithoutLoad, attachmentFieldCount, attachmentsPlugin, attachmentsSemanticallyEqual, axpCreateEntityAiToolInputDefaults, axpCreateEntityCommandDefinition, buildAXPRecordWorkflowInfo, canPersistEntityListState, cloneLayoutArrays, collectEntityQuickSearchFieldPaths, collectNestedCreateHiddenProperties, collectNestedFieldPathsFromEntityColumns, collectQuickSearchPathsFromSingleEntityDefinition, columnOrderingMiddleware, columnOrderingMiddlewareProvider, columnWidthMiddleware, columnWidthMiddlewareProvider, committedAttachments, computeEntityAggregates, createColumnOrderingMiddlewareProvider, createLayoutOrderingMiddlewareProvider, createModifierContext, defaultCardLayoutMiddleware, defaultCardLayoutMiddlewareProvider, defaultMultiLanguageMiddleware, defaultMultiLanguageMiddlewareProvider, detectEntityChanges, ensureLayoutPropertyView, ensureLayoutSection, ensureListActions, entityDetailsCreateActions, entityDetailsCreateActionsDeferredParent, entityDetailsCrudActions, entityDetailsEditAction, entityDetailsNewEditAction, entityDetailsReferenceCondition, entityDetailsReferenceCreateActions, entityDetailsSimpleCondition, entityMasterBulkDeleteAction, entityMasterCreateAction, entityMasterCrudActions, entityMasterDeleteAction, entityMasterEditAction, entityMasterRecordActions, entityMasterViewAction, entityOverrideDetailsViewAction, eventDispatchMiddleware, filterSortEntityRows, findEntityListRowDataInTree, fingerprintAttachmentItem, fingerprintAttachments, formatLookupItemDisplay, getDataSourcePageIndex, getEntityListRowId, getMasterInterfacePropertySortKey, getRecordWorkflowCorrelationId, getRecordWorkflowInstanceId, hasFileUploaderTitleOrDescriptionFields, isAXPMiddlewareAbortError, isAttachmentListEntry, isCategoryEntity, isCategoryFilter, isFileListItem, isFileUploaderEditDialogAuto, isLegacyEntityDataSelectorOptions, isUnresolvedLookupDisplayTemplate, layoutOrderingMiddlewareFactory, layoutOrderingMiddlewareProvider, mapLegacyEntityDataSelectorOptions, mergeForeignKeyFieldIntoCreateActions, normalizeEntityDataSelectorOptions, normalizeEntityFieldToFileList, normalizeEntityListPersistenceMode, normalizeListPaging, normalizeLookupDisplayTemplate, persistedAttachments, provideEntity, resolveEntityPluginDetailPageOrder, resolveFileUploaderEditDialog, resolveFileUploaderEntityScope, resolveLookupDisplayField, resolveLookupDisplayTemplate, restoreEntityListExpandedRows, runEntityQuery, searchResultDescriptionMiddleware, searchResultDescriptionMiddlewareProvider, shouldLoadEntityListStateFromStorage, shouldResetEntityListStateOnRouteEntry };
21804
24842
  //# sourceMappingURL=acorex-platform-layout-entity.mjs.map