@acorex/platform 21.0.0-next.75 → 21.0.0-next.76

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 (40) hide show
  1. package/fesm2022/{acorex-platform-common-common-settings.provider-Ytey9uhY.mjs → acorex-platform-common-common-settings.provider-ChoT4m4k.mjs} +42 -1
  2. package/fesm2022/acorex-platform-common-common-settings.provider-ChoT4m4k.mjs.map +1 -0
  3. package/fesm2022/acorex-platform-common.mjs +1471 -606
  4. package/fesm2022/acorex-platform-common.mjs.map +1 -1
  5. package/fesm2022/acorex-platform-contracts.mjs +1330 -1
  6. package/fesm2022/acorex-platform-contracts.mjs.map +1 -1
  7. package/fesm2022/acorex-platform-core.mjs +208 -5
  8. package/fesm2022/acorex-platform-core.mjs.map +1 -1
  9. package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
  10. package/fesm2022/acorex-platform-layout-entity.mjs +595 -439
  11. package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
  12. package/fesm2022/acorex-platform-layout-views.mjs +372 -171
  13. package/fesm2022/acorex-platform-layout-views.mjs.map +1 -1
  14. package/fesm2022/acorex-platform-layout-widget-core.mjs +3 -103
  15. package/fesm2022/acorex-platform-layout-widget-core.mjs.map +1 -1
  16. package/fesm2022/acorex-platform-layout-widgets-repeater-widget-column.component-CFyfix31.mjs +513 -0
  17. package/fesm2022/acorex-platform-layout-widgets-repeater-widget-column.component-CFyfix31.mjs.map +1 -0
  18. package/fesm2022/acorex-platform-layout-widgets.mjs +11 -6
  19. package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
  20. package/fesm2022/{acorex-platform-themes-default-entity-master-single-view.component-Br9p5aXT.mjs → acorex-platform-themes-default-entity-master-single-view.component-DrUwqKKA.mjs} +2 -3
  21. package/fesm2022/acorex-platform-themes-default-entity-master-single-view.component-DrUwqKKA.mjs.map +1 -0
  22. package/fesm2022/acorex-platform-themes-default.mjs +643 -52
  23. package/fesm2022/acorex-platform-themes-default.mjs.map +1 -1
  24. package/fesm2022/acorex-platform-workflow.mjs +5 -8
  25. package/fesm2022/acorex-platform-workflow.mjs.map +1 -1
  26. package/package.json +1 -1
  27. package/types/acorex-platform-common.d.ts +337 -1056
  28. package/types/acorex-platform-contracts.d.ts +1679 -4
  29. package/types/acorex-platform-core.d.ts +92 -181
  30. package/types/acorex-platform-layout-components.d.ts +2 -1
  31. package/types/acorex-platform-layout-entity.d.ts +68 -176
  32. package/types/acorex-platform-layout-views.d.ts +39 -5
  33. package/types/acorex-platform-layout-widget-core.d.ts +2 -3
  34. package/types/acorex-platform-layout-widgets.d.ts +11 -4
  35. package/types/acorex-platform-themes-default.d.ts +120 -57
  36. package/types/acorex-platform-workflow.d.ts +5 -18
  37. package/fesm2022/acorex-platform-common-common-settings.provider-Ytey9uhY.mjs.map +0 -1
  38. package/fesm2022/acorex-platform-layout-widgets-repeater-widget-column.component-J0zcGKBX.mjs +0 -116
  39. package/fesm2022/acorex-platform-layout-widgets-repeater-widget-column.component-J0zcGKBX.mjs.map +0 -1
  40. package/fesm2022/acorex-platform-themes-default-entity-master-single-view.component-Br9p5aXT.mjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import * as i2 from '@angular/common';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { inject, viewChild, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component, signal, computed, Injectable, input, output, ViewContainerRef, Directive, DestroyRef, ElementRef, untracked } from '@angular/core';
4
+ import { inject, viewChild, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component, signal, untracked, computed, Injectable, input, output, ViewContainerRef, Directive, DestroyRef, ElementRef } from '@angular/core';
5
5
  import { AXCommonModule } from '@acorex/cdk/common';
6
6
  import * as i4 from '@acorex/components/breadcrumbs';
7
7
  import { AXBreadcrumbsModule } from '@acorex/components/breadcrumbs';
@@ -12,9 +12,9 @@ import { AXDecoratorModule } from '@acorex/components/decorators';
12
12
  import * as i5 from '@acorex/components/dropdown';
13
13
  import { AXDropdownModule } from '@acorex/components/dropdown';
14
14
  import { AXDropdownButtonModule } from '@acorex/components/dropdown-button';
15
- import { AXPStickyDirective, AXPCommonSettings, AXPHomePageService, AXPSettingsService, axpNavigateAppPath, AXPEntityCommandScope, axpIsEntityDetailsViewRoute, AXPUnsavedChangesConfirmService, AXPUnsavedChangesPopstateService, createUnsavedChangesCanDeactivateGuard } from '@acorex/platform/common';
15
+ import { AXPTabNavigationService, AXPStickyDirective, AXPCommonSettings, AXPHomePageService, AXPSettingsService, AXPTabPreviewService, AXPUnsavedChangesConfirmService, normalizeRouterUrl, AXP_HOME_TAB_KEY, axpResolveTabKey, axpIsEntityDetailsViewRoute, axpNavigateAppPath, AXPEntityCommandScope, AXPUnsavedChangesPopstateService, createUnsavedChangesCanDeactivateGuard } from '@acorex/platform/common';
16
16
  import * as i8 from '@acorex/platform/core';
17
- import { AXPDeviceService, AXPComponentSlotModule, getSmart, AXPExpressionEvaluatorService, AXPContextStore, normalizeKeyboardShortcuts, AXPBroadcastEventService, AXPKeyboardShortcutRegistry, AXPKeyboardShortcutPriority } from '@acorex/platform/core';
17
+ import { AXPDeviceService, AXPComponentSlotModule, getSmart, AXPExpressionEvaluatorService, AXPContextEvalFactory, normalizeKeyboardShortcuts, AXPContextStore, AXPBroadcastEventService, AXPKeyboardShortcutRegistry, AXPKeyboardShortcutPriority } from '@acorex/platform/core';
18
18
  import * as i1 from '@acorex/cdk/drawer';
19
19
  import { AXDrawerDirectiveModule, AXDrawerContainerDirective } from '@acorex/cdk/drawer';
20
20
  import { AXDrawerModule } from '@acorex/components/drawer';
@@ -28,11 +28,11 @@ import { AXPWorkflowService } from '@acorex/platform/workflow';
28
28
  import { AXToastService } from '@acorex/components/toast';
29
29
  import { AXFormatService, AXFormatModule } from '@acorex/core/format';
30
30
  import { AXPSessionService } from '@acorex/platform/auth';
31
+ import { Router, ActivatedRoute } from '@angular/router';
31
32
  import { isEmpty, cloneDeep, get } from 'lodash-es';
32
33
  import { AXBasePageComponent } from '@acorex/components/page';
33
34
  import * as i6$1 from '@acorex/platform/layout/widget-core';
34
35
  import { AXPPageStatus, AXPWidgetContainerComponent, AXPWidgetCoreModule } from '@acorex/platform/layout/widget-core';
35
- import { Router, ActivatedRoute } from '@angular/router';
36
36
  import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
37
37
  import { AXBadgeModule } from '@acorex/components/badge';
38
38
  import { AXButtonGroupModule } from '@acorex/components/button-group';
@@ -55,6 +55,7 @@ class AXPPageLayoutComponent {
55
55
  this.page = inject(AXPPageLayoutBase, { optional: true });
56
56
  this.layoutService = inject(AXPLayoutThemeService);
57
57
  this.deviceService = inject(AXPDeviceService);
58
+ this.tabNavigation = inject(AXPTabNavigationService);
58
59
  this.startSideDrawer = viewChild('startSideDrawer', ...(ngDevMode ? [{ debugName: "startSideDrawer" }] : /* istanbul ignore next */ []));
59
60
  this.workflow = inject(AXPWorkflowService);
60
61
  this.commandService = inject(AXPCommandService);
@@ -218,11 +219,55 @@ class AXPPageLayoutBaseComponent {
218
219
  this.homePageService = inject(AXPHomePageService);
219
220
  this.policyService = inject(AXPPolicyEngineService);
220
221
  this.settingsService = inject(AXPSettingsService);
222
+ this.tabNavigation = inject(AXPTabNavigationService);
223
+ this.tabPreviewService = inject(AXPTabPreviewService);
224
+ this.unsavedChangesConfirm = inject(AXPUnsavedChangesConfirmService);
225
+ this.router = inject(Router);
221
226
  //#endregion
222
227
  //#region ---------------- Signal to force recomputation ----------------
223
228
  this._updateTrigger = signal(0, ...(ngDevMode ? [{ debugName: "_updateTrigger" }] : /* istanbul ignore next */ []));
229
+ /** Tab key captured at page init — stable while the route instance is cached. */
230
+ this.pageTabKey = signal(null, ...(ngDevMode ? [{ debugName: "pageTabKey" }] : /* istanbul ignore next */ []));
224
231
  this.updateTrigger = this._updateTrigger.asReadonly();
225
232
  //#endregion
233
+ //#region ---------------- Tab Title Sync ----------------
234
+ /** Refreshes tab title when this cached page becomes the active tab again. */
235
+ this.#syncTabTitleOnActivate = effect(() => {
236
+ const activeKey = this.tabNavigation.activeKey();
237
+ const pageKey = this.pageTabKey();
238
+ if (!activeKey || !pageKey || activeKey !== pageKey) {
239
+ return;
240
+ }
241
+ if (this.tabNavigation.effectiveMode() !== 'tabular') {
242
+ return;
243
+ }
244
+ untracked(() => {
245
+ this.recompute();
246
+ });
247
+ }, ...(ngDevMode ? [{ debugName: "#syncTabTitleOnActivate" }] : /* istanbul ignore next */ []));
248
+ /** Publishes structured tab preview (hover card + dirty badge) for this cached page. */
249
+ this.#syncTabStructuredPreview = effect(() => {
250
+ this.updateTrigger();
251
+ const tabKey = this.pageTabKey();
252
+ const titleValue = this.title();
253
+ const descriptionValue = this.description();
254
+ const icon = this.titleIcon();
255
+ const breadcrumbItems = this.breadcrumbs();
256
+ if (!tabKey || this.tabNavigation.effectiveMode() !== 'tabular') {
257
+ return;
258
+ }
259
+ const isDirty = this.hasUnsavedChanges();
260
+ untracked(() => {
261
+ void this.publishTabStructuredPreview(tabKey, {
262
+ titleValue,
263
+ descriptionValue,
264
+ icon,
265
+ breadcrumbItems,
266
+ isDirty,
267
+ });
268
+ });
269
+ }, ...(ngDevMode ? [{ debugName: "#syncTabStructuredPreview" }] : /* istanbul ignore next */ []));
270
+ //#endregion
226
271
  //#region ---------------- Title ----------------
227
272
  this.title = signal('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
228
273
  this.titleIcon = signal(null, ...(ngDevMode ? [{ debugName: "titleIcon" }] : /* istanbul ignore next */ []));
@@ -231,6 +276,12 @@ class AXPPageLayoutBaseComponent {
231
276
  this.title.set(await this.getPageTitle());
232
277
  const icon = await this.getPageIcon();
233
278
  this.titleIcon.set(icon);
279
+ if (this.tabNavigation.effectiveMode() === 'tabular') {
280
+ const tabKey = this.pageTabKey();
281
+ if (tabKey && icon) {
282
+ this.tabNavigation.syncTabIcon(tabKey, icon);
283
+ }
284
+ }
234
285
  }, ...(ngDevMode ? [{ debugName: "#titleEffect" }] : /* istanbul ignore next */ []));
235
286
  this.hasTitle = computed(() => {
236
287
  const value = this.title();
@@ -284,9 +335,20 @@ class AXPPageLayoutBaseComponent {
284
335
  this.#breadcrumbsEffect = effect(async () => {
285
336
  this.updateTrigger();
286
337
  const breadCrumbs = await this.getPageBreadcrumbs();
287
- this.breadcrumbs.set([...this.defaultBreadCrumbs, ...breadCrumbs]);
338
+ const trail = [...this.defaultBreadCrumbs, ...breadCrumbs];
339
+ this.breadcrumbs.set(trail);
340
+ if (this.tabNavigation.effectiveMode() === 'tabular') {
341
+ const tabKey = this.pageTabKey();
342
+ let tabTitle = await this.resolveBreadcrumbTabTitle(breadCrumbs);
343
+ if (!tabTitle) {
344
+ tabTitle = await this.resolvePageTitleForTab();
345
+ }
346
+ if (tabKey && tabTitle) {
347
+ this.tabNavigation.syncTabTitle(tabKey, tabTitle);
348
+ }
349
+ }
288
350
  }, ...(ngDevMode ? [{ debugName: "#breadcrumbsEffect" }] : /* istanbul ignore next */ []));
289
- this.hasBreadcrumbs = computed(() => this.breadcrumbs().length > 1, ...(ngDevMode ? [{ debugName: "hasBreadcrumbs" }] : /* istanbul ignore next */ []));
351
+ this.hasBreadcrumbs = computed(() => this.breadcrumbs().length > 1 && this.tabNavigation.effectiveMode() !== 'tabular', ...(ngDevMode ? [{ debugName: "hasBreadcrumbs" }] : /* istanbul ignore next */ []));
290
352
  //#endregion
291
353
  //#region ---------------- Badge ----------------
292
354
  this.badge = signal(null, ...(ngDevMode ? [{ debugName: "badge" }] : /* istanbul ignore next */ []));
@@ -342,10 +404,75 @@ class AXPPageLayoutBaseComponent {
342
404
  //#endregion
343
405
  //#region ---------------- Lifecycle hooks ----------------
344
406
  async ngOnInit() {
407
+ this.pageTabKey.set(this.resolvePageTabKey());
345
408
  this.translateService.langChanges$.subscribe(() => {
346
409
  this.recompute();
347
410
  });
348
411
  }
412
+ /** Maps the home-page route to the pinned home tab key instead of a path-based key. */
413
+ resolvePageTabKey() {
414
+ const homePath = this.homePageService.getCurrent().path;
415
+ if (homePath && normalizeRouterUrl(homePath) === normalizeRouterUrl(this.router.url)) {
416
+ return AXP_HOME_TAB_KEY;
417
+ }
418
+ return axpResolveTabKey(this.router);
419
+ }
420
+ //#endregion
421
+ //#region ---------------- Tab Title Sync ----------------
422
+ /** Refreshes tab title when this cached page becomes the active tab again. */
423
+ #syncTabTitleOnActivate;
424
+ /** Publishes structured tab preview (hover card + dirty badge) for this cached page. */
425
+ #syncTabStructuredPreview;
426
+ async publishTabStructuredPreview(tabKey, input) {
427
+ const tab = this.tabNavigation.tabList().find((item) => item.key === tabKey);
428
+ const breadcrumbTrail = [];
429
+ for (const item of input.breadcrumbItems.slice(1)) {
430
+ const segment = await this.resolveBreadcrumbItemTitle(item);
431
+ if (segment && segment !== breadcrumbTrail[breadcrumbTrail.length - 1]) {
432
+ breadcrumbTrail.push(segment);
433
+ }
434
+ }
435
+ const fallbackTitle = await this.translateTextIfKey(breadcrumbTrail.join(' - ') || tab?.title || '');
436
+ const title = await this.resolveMultiLanguageText(input.titleValue, fallbackTitle);
437
+ const subtitle = await this.resolveMultiLanguageText(input.descriptionValue, '');
438
+ this.tabPreviewService.setPreview(tabKey, {
439
+ title,
440
+ subtitle: subtitle || undefined,
441
+ url: tab?.url ?? '',
442
+ icon: input.icon ?? tab?.icon,
443
+ isDirty: input.isDirty,
444
+ breadcrumbTrail,
445
+ });
446
+ }
447
+ async resolveMultiLanguageText(value, fallback) {
448
+ const resolvedFallback = await this.translateTextIfKey(fallback);
449
+ if (value == null || value === '') {
450
+ return resolvedFallback;
451
+ }
452
+ if (typeof value === 'string') {
453
+ const trimmed = value.trim();
454
+ if (!trimmed) {
455
+ return resolvedFallback;
456
+ }
457
+ if (trimmed.startsWith('@')) {
458
+ return (await this.translateService.translateAsync(trimmed)) || resolvedFallback;
459
+ }
460
+ return trimmed;
461
+ }
462
+ const resolved = this.translateService.resolve(value);
463
+ return resolved || resolvedFallback;
464
+ }
465
+ /** Resolves i18n keys (`@scope:key`); returns plain text unchanged. */
466
+ async translateTextIfKey(text) {
467
+ const trimmed = text.trim();
468
+ if (!trimmed) {
469
+ return '';
470
+ }
471
+ if (trimmed.startsWith('@')) {
472
+ return (await this.translateService.translateAsync(trimmed)) || trimmed;
473
+ }
474
+ return trimmed;
475
+ }
349
476
  #titleEffect;
350
477
  getPageTitle() {
351
478
  return '';
@@ -361,10 +488,78 @@ class AXPPageLayoutBaseComponent {
361
488
  getPageBreadcrumbs() {
362
489
  return [];
363
490
  }
491
+ /** Joins all page breadcrumbs (Home is excluded upstream) as the tab label. */
492
+ async resolveBreadcrumbTabTitle(pageBreadcrumbs) {
493
+ const segments = [];
494
+ for (const item of pageBreadcrumbs) {
495
+ const title = await this.resolveBreadcrumbItemTitle(item);
496
+ if (title && title !== segments[segments.length - 1]) {
497
+ segments.push(title);
498
+ }
499
+ }
500
+ if (!segments.length) {
501
+ return null;
502
+ }
503
+ return segments.join(' - ');
504
+ }
505
+ /** Resolves a breadcrumb label the same way as page chrome (`t(item.title)`). */
506
+ async resolveBreadcrumbItemTitle(item) {
507
+ const raw = item?.title;
508
+ if (raw == null || raw === '') {
509
+ return null;
510
+ }
511
+ if (typeof raw === 'string') {
512
+ const trimmed = raw.trim();
513
+ if (!trimmed) {
514
+ return null;
515
+ }
516
+ if (trimmed.startsWith('@')) {
517
+ return (await this.translateService.translateAsync(trimmed)) || trimmed;
518
+ }
519
+ return trimmed;
520
+ }
521
+ if (typeof raw === 'object' && !Array.isArray(raw)) {
522
+ if (this.translateService.isValidMultiLanguageObject(raw)) {
523
+ const resolved = this.translateService.resolve(raw);
524
+ return resolved || null;
525
+ }
526
+ const resolved = this.translateService.resolve(raw);
527
+ return resolved || null;
528
+ }
529
+ return String(raw);
530
+ }
531
+ /** Fallback tab label from the page title when breadcrumbs are not ready yet. */
532
+ async resolvePageTitleForTab() {
533
+ const raw = await Promise.resolve(this.getPageTitle());
534
+ if (raw == null || raw === '') {
535
+ return null;
536
+ }
537
+ if (typeof raw === 'string') {
538
+ const trimmed = raw.trim();
539
+ if (trimmed.startsWith('@')) {
540
+ return (await this.translateService.translateAsync(trimmed)) || trimmed;
541
+ }
542
+ return trimmed;
543
+ }
544
+ if (typeof raw === 'object' && !Array.isArray(raw)) {
545
+ const resolved = this.translateService.resolve(raw);
546
+ return resolved || null;
547
+ }
548
+ return null;
549
+ }
364
550
  #badgeEffect;
365
551
  hasUnsavedChanges() {
366
552
  return false;
367
553
  }
554
+ /**
555
+ * Confirms closing this page as a background tab (no router navigation).
556
+ * Override when dirty detection differs from {@link hasUnsavedChanges}.
557
+ */
558
+ onBeforeTabClose() {
559
+ return this.unsavedChangesConfirm.confirmIfDirty(this.hasUnsavedChanges(), {
560
+ approveLeaveOnConfirm: false,
561
+ });
562
+ }
368
563
  async getPageBadge() {
369
564
  if (!this.hasUnsavedChanges()) {
370
565
  return null;
@@ -675,14 +870,63 @@ const AXPLayoutDetailsViewViewModel = signalStore(withState(() => {
675
870
  const evaluatedByTab = store.evaluatedTabContentsById();
676
871
  return evaluatedByTab[tab.id] ?? [];
677
872
  }),
678
- })), withMethods((store, evaluatorService = inject(AXPExpressionEvaluatorService), router = inject(Router), route = inject(ActivatedRoute), deviceService = inject(AXPDeviceService), formatService = inject(AXFormatService), toastService = inject(AXToastService), translateService = inject(AXTranslationService), settingsService = inject(AXPSettingsService)) => {
679
- const refreshEvaluatedPageContents = async (page) => {
873
+ })), withMethods((store, evaluatorService = inject(AXPExpressionEvaluatorService), contextEvalFactory = inject(AXPContextEvalFactory), router = inject(Router), route = inject(ActivatedRoute), deviceService = inject(AXPDeviceService), formatService = inject(AXFormatService), toastService = inject(AXToastService), translateService = inject(AXTranslationService), settingsService = inject(AXPSettingsService)) => {
874
+ const resolvePageFromId = (adapter, pageId) => {
875
+ if (pageId) {
876
+ return adapter.pages.find((p) => p.id === pageId) ?? null;
877
+ }
878
+ if (deviceService.isLarge() || adapter.pages.length === 1) {
879
+ return adapter.pages.find((p) => p.isPrimary) ?? adapter.pages[0] ?? null;
880
+ }
881
+ return null;
882
+ };
883
+ /** Loads page data, context, and evaluated widgets for the target sidebar page. */
884
+ const activatePageById = async (pageId, forceRefresh) => {
885
+ const adapter = store.adapter();
886
+ if (!adapter) {
887
+ return;
888
+ }
889
+ const queryParams = router.routerState.snapshot.root.queryParams;
890
+ const targetPageId = pageId ?? queryParams['page'];
891
+ const currentPage = resolvePageFromId(adapter, targetPageId);
892
+ if (!forceRefresh &&
893
+ currentPage?.id === store.currentPage()?.id &&
894
+ store.status() === AXPPageStatus.Rendered) {
895
+ return;
896
+ }
897
+ patchState(store, {
898
+ status: AXPPageStatus.Processing,
899
+ });
900
+ let context = {};
901
+ if (currentPage) {
902
+ const currentPageIndex = adapter.pages.findIndex((p) => p.id === currentPage.id);
903
+ const loadResult = await adapter.pages[currentPageIndex]?.load?.({ forceRefresh });
904
+ context = loadResult?.data ?? {};
905
+ }
906
+ const currentTab = currentPage?.tabs?.[0] ?? null;
680
907
  const rootCtx = store.rootContext();
681
- const { evaluatedPageContent, evaluatedTabContentsById } = await buildEvaluatedPageContentsForRootContext(page, rootCtx, evaluatorService);
908
+ const { evaluatedPageContent, evaluatedTabContentsById } = await buildEvaluatedPageContentsForRootContext(currentPage, rootCtx, evaluatorService);
682
909
  patchState(store, {
910
+ previousContext: cloneDeep(context),
911
+ context: cloneDeep(context),
912
+ status: AXPPageStatus.Rendered,
913
+ currentPage,
914
+ currentTab,
683
915
  evaluatedPageContent,
684
916
  evaluatedTabContentsById,
917
+ formDirty: false,
685
918
  });
919
+ if (currentPage && axpIsEntityDetailsViewRoute(router)) {
920
+ const pageParam = route.snapshot.queryParamMap.get('page');
921
+ if (pageParam !== currentPage.id) {
922
+ router.navigate([], {
923
+ relativeTo: route,
924
+ queryParams: { page: currentPage.id },
925
+ queryParamsHandling: 'merge',
926
+ replaceUrl: true,
927
+ });
928
+ }
929
+ }
686
930
  };
687
931
  // Sync sidebar page from route query params
688
932
  effect((onCleanup) => {
@@ -690,30 +934,20 @@ const AXPLayoutDetailsViewViewModel = signalStore(withState(() => {
690
934
  return;
691
935
  }
692
936
  const subscription = route.queryParams.subscribe((params) => {
693
- const pageId = params['page'];
694
937
  const adapter = store.adapter();
695
938
  if (!adapter) {
696
939
  return;
697
940
  }
698
- let currentPage = null;
699
- if (pageId) {
700
- currentPage = adapter.pages.find((p) => p.id === pageId) ?? null;
701
- }
702
- else if (deviceService.isLarge() || adapter.pages.length === 1) {
703
- currentPage = adapter.pages.find((p) => p.isPrimary) ?? adapter.pages[0];
941
+ const pageId = params['page'];
942
+ const targetPage = resolvePageFromId(adapter, pageId);
943
+ if (store.currentPage()?.id === targetPage?.id) {
944
+ return;
704
945
  }
705
- const currentTab = currentPage?.tabs?.[0] ?? null;
706
- if (store.currentPage()?.id !== currentPage?.id) {
707
- // `load()` → `loadPage` owns initial hydration when both sides are still null.
708
- if (store.currentPage() == null && currentPage == null) {
709
- return;
710
- }
711
- patchState(store, {
712
- currentPage,
713
- currentTab,
714
- });
715
- void refreshEvaluatedPageContents(currentPage);
946
+ // `load()` `activatePageById` owns initial hydration when both sides are still null.
947
+ if (store.currentPage() == null && targetPage == null) {
948
+ return;
716
949
  }
950
+ void activatePageById(targetPage?.id);
717
951
  });
718
952
  onCleanup(() => subscription.unsubscribe());
719
953
  });
@@ -774,7 +1008,12 @@ const AXPLayoutDetailsViewViewModel = signalStore(withState(() => {
774
1008
  evaluated = await evaluatorService.evaluate(raw, evalScope);
775
1009
  }
776
1010
  else if (typeof sourceAfterPreLocale === 'string') {
777
- evaluated = await formatService.format(sourceAfterPreLocale, 'string', formatContext);
1011
+ if (sourceAfterPreLocale.trim().startsWith('@')) {
1012
+ evaluated = await translateService.translateAsync(sourceAfterPreLocale);
1013
+ }
1014
+ else {
1015
+ evaluated = await formatService.format(sourceAfterPreLocale, 'string', formatContext);
1016
+ }
778
1017
  }
779
1018
  else {
780
1019
  evaluated = await formatService.format(raw, 'string', formatContext);
@@ -808,71 +1047,12 @@ const AXPLayoutDetailsViewViewModel = signalStore(withState(() => {
808
1047
  });
809
1048
  },
810
1049
  async loadPage(pageId, forceRefresh) {
811
- const adapter = store.adapter();
812
- if (!adapter) {
813
- throw new Error('Adapter must be loaded before loading page');
814
- }
815
- patchState(store, {
816
- status: AXPPageStatus.Processing,
817
- });
818
- // Get page ID from parameter or query params
819
- const queryParams = router.routerState.snapshot.root.queryParams;
820
- const targetPageId = pageId || queryParams['page'];
821
- // Find current page from page ID or conditionally default to first page
822
- let currentPage = null;
823
- if (targetPageId) {
824
- const foundPage = adapter.pages.find((p) => p.id === targetPageId);
825
- if (foundPage) {
826
- currentPage = foundPage;
827
- }
828
- }
829
- else {
830
- // Only auto-select primary page if layout is large
831
- if (deviceService.isLarge() || adapter.pages.length == 1) {
832
- const primaryPage = adapter.pages.find((p) => p.isPrimary) ?? adapter.pages[0];
833
- currentPage = primaryPage;
834
- }
835
- }
836
- // Load page data if current page exists
837
- let context = {};
838
- if (currentPage) {
839
- const currentPageIndex = adapter.pages.findIndex((p) => p.id === currentPage.id);
840
- const loadResult = await adapter.pages[currentPageIndex]?.load?.({ forceRefresh });
841
- context = loadResult?.data ?? {};
842
- }
843
- // Set current tab to first tab of the current page (internal state only)
844
- let currentTab = currentPage?.tabs?.[0] ?? null;
845
- const rootCtx = store.rootContext();
846
- const { evaluatedPageContent, evaluatedTabContentsById } = await buildEvaluatedPageContentsForRootContext(currentPage, rootCtx, evaluatorService);
847
- patchState(store, {
848
- previousContext: cloneDeep(context),
849
- context: cloneDeep(context),
850
- status: AXPPageStatus.Rendered,
851
- currentPage,
852
- currentTab,
853
- evaluatedPageContent,
854
- evaluatedTabContentsById,
855
- formDirty: false,
856
- });
857
- if (currentPage && axpIsEntityDetailsViewRoute(router)) {
858
- const pageParam = route.snapshot.queryParamMap.get('page');
859
- if (pageParam !== currentPage.id) {
860
- router.navigate([], {
861
- relativeTo: route,
862
- queryParams: { page: currentPage.id },
863
- queryParamsHandling: 'merge',
864
- replaceUrl: true,
865
- });
866
- }
867
- }
1050
+ await activatePageById(pageId, forceRefresh);
868
1051
  },
869
1052
  async load(adapter) {
870
- // Setup expression evaluator scope
871
1053
  const scope = {
872
1054
  context: {
873
- eval: (path) => {
874
- return getSmart(store.context(), path);
875
- },
1055
+ ...contextEvalFactory.createLiveScope(() => store.context()),
876
1056
  isDirty: () => store.isDirty(),
877
1057
  },
878
1058
  };
@@ -951,14 +1131,8 @@ const AXPLayoutDetailsViewViewModel = signalStore(withState(() => {
951
1131
  setCurrentPage(page) {
952
1132
  const previousPage = store.currentPage();
953
1133
  if (page) {
954
- const firstTab = page.tabs?.[0] ?? null;
955
1134
  const pageParam = route.snapshot.queryParamMap.get('page');
956
1135
  const isSamePage = previousPage?.id === page.id && pageParam === page.id;
957
- patchState(store, {
958
- currentPage: page,
959
- currentTab: firstTab,
960
- ...(page.isReadonly ? { formDirty: false } : {}),
961
- });
962
1136
  if (!isSamePage && axpIsEntityDetailsViewRoute(router)) {
963
1137
  const queryParams = { page: page.id };
964
1138
  if (previousPage?.id && previousPage.id !== page.id) {
@@ -972,7 +1146,7 @@ const AXPLayoutDetailsViewViewModel = signalStore(withState(() => {
972
1146
  replaceUrl: false,
973
1147
  });
974
1148
  }
975
- void refreshEvaluatedPageContents(page);
1149
+ void activatePageById(page.id);
976
1150
  }
977
1151
  else {
978
1152
  patchState(store, {
@@ -1107,6 +1281,7 @@ const AXPLayoutDetailsViewViewModel = signalStore(withState(() => {
1107
1281
  },
1108
1282
  async discard() {
1109
1283
  patchState(store, {
1284
+ context: cloneDeep(store.previousContext()),
1110
1285
  formDirty: false,
1111
1286
  });
1112
1287
  },
@@ -1207,6 +1382,66 @@ const AXPLayoutDetailsViewViewModel = signalStore(withState(() => {
1207
1382
  };
1208
1383
  }));
1209
1384
 
1385
+ //#region ---- Details View Action Shortcut Utilities ----
1386
+ /** Platform defaults for known entity detail commands when `shortcuts` is omitted on the action. */
1387
+ const DEFAULT_ENTITY_DETAIL_ACTION_SHORTCUTS = {
1388
+ 'delete-entity': ['ctrl+shift+delete'],
1389
+ };
1390
+ const DEFAULT_FOOTER_COMMAND_SHORTCUTS = {
1391
+ 'update-entity': ['ctrl+s'],
1392
+ discard: ['escape'],
1393
+ };
1394
+ /**
1395
+ * Returns the base workflow/command name from a composite entity action command id (`cmd&actionName`).
1396
+ */
1397
+ function getDetailsViewActionCommandBaseName(commandName) {
1398
+ if (!commandName) {
1399
+ return '';
1400
+ }
1401
+ return commandName.split('&')[0];
1402
+ }
1403
+ /**
1404
+ * Resolves keyboard shortcuts for a header entity action.
1405
+ * Explicit `shortcuts` wins; pass `[]` to disable. Otherwise known commands use platform defaults.
1406
+ */
1407
+ function resolveDetailsViewActionShortcuts(commandBaseName, explicit) {
1408
+ if (explicit !== undefined) {
1409
+ const chords = flattenDetailsViewActionShortcuts(explicit);
1410
+ return chords.length ? chords : undefined;
1411
+ }
1412
+ const defaults = DEFAULT_ENTITY_DETAIL_ACTION_SHORTCUTS[commandBaseName];
1413
+ return defaults?.length ? [...defaults] : undefined;
1414
+ }
1415
+ /**
1416
+ * Resolves save/discard footer command shortcuts from page settings.
1417
+ */
1418
+ function resolveDetailsViewFooterCommandShortcuts(commandName, explicit) {
1419
+ if (explicit !== undefined) {
1420
+ const chords = flattenDetailsViewActionShortcuts(explicit);
1421
+ return chords.length ? chords : undefined;
1422
+ }
1423
+ const defaults = DEFAULT_FOOTER_COMMAND_SHORTCUTS[commandName];
1424
+ return defaults?.length ? [...defaults] : undefined;
1425
+ }
1426
+ function flattenDetailsViewActionShortcuts(shortcuts) {
1427
+ const chords = normalizeKeyboardShortcuts(shortcuts).flatMap((shortcut) => shortcut.keys);
1428
+ return dedupeDetailsViewActionShortcuts(chords);
1429
+ }
1430
+ function dedupeDetailsViewActionShortcuts(shortcuts) {
1431
+ const seen = new Set();
1432
+ const result = [];
1433
+ for (const shortcut of shortcuts) {
1434
+ const token = shortcut.trim().toLowerCase();
1435
+ if (!token || seen.has(token)) {
1436
+ continue;
1437
+ }
1438
+ seen.add(token);
1439
+ result.push(shortcut.trim());
1440
+ }
1441
+ return result;
1442
+ }
1443
+ //#endregion
1444
+
1210
1445
  /**
1211
1446
  * Registry of rendered page component instances by component key.
1212
1447
  * Used so page execute can delegate to the actual component instance in the view.
@@ -1345,77 +1580,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1345
1580
  }]
1346
1581
  }], propDecorators: { componentKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "componentKey", required: true }] }], rootContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootContext", required: false }] }], pageConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageConfig", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], componentReady: [{ type: i0.Output, args: ["componentReady"] }] } });
1347
1582
 
1348
- //#region ---- Details View Action Shortcut Utilities ----
1349
- /** Platform defaults for known entity detail commands when `shortcuts` is omitted on the action. */
1350
- const DEFAULT_ENTITY_DETAIL_ACTION_SHORTCUTS = {
1351
- 'delete-entity': ['ctrl+shift+delete'],
1352
- };
1353
- const DEFAULT_FOOTER_COMMAND_SHORTCUTS = {
1354
- 'update-entity': ['ctrl+s'],
1355
- discard: ['escape'],
1356
- };
1357
- /**
1358
- * Returns the base workflow/command name from a composite entity action command id (`cmd&actionName`).
1359
- */
1360
- function getDetailsViewActionCommandBaseName(commandName) {
1361
- if (!commandName) {
1362
- return '';
1363
- }
1364
- return commandName.split('&')[0];
1365
- }
1366
- /**
1367
- * Resolves keyboard shortcuts for a header entity action.
1368
- * Explicit `shortcuts` wins; pass `[]` to disable. Otherwise known commands use platform defaults.
1369
- */
1370
- function resolveDetailsViewActionShortcuts(commandBaseName, explicit) {
1371
- if (explicit !== undefined) {
1372
- const chords = flattenDetailsViewActionShortcuts(explicit);
1373
- return chords.length ? chords : undefined;
1374
- }
1375
- const defaults = DEFAULT_ENTITY_DETAIL_ACTION_SHORTCUTS[commandBaseName];
1376
- return defaults?.length ? [...defaults] : undefined;
1377
- }
1378
- /**
1379
- * Resolves save/discard footer command shortcuts from page settings.
1380
- */
1381
- function resolveDetailsViewFooterCommandShortcuts(commandName, explicit) {
1382
- if (explicit !== undefined) {
1383
- const chords = flattenDetailsViewActionShortcuts(explicit);
1384
- return chords.length ? chords : undefined;
1385
- }
1386
- const defaults = DEFAULT_FOOTER_COMMAND_SHORTCUTS[commandName];
1387
- return defaults?.length ? [...defaults] : undefined;
1388
- }
1389
- function flattenDetailsViewActionShortcuts(shortcuts) {
1390
- const chords = normalizeKeyboardShortcuts(shortcuts).flatMap((shortcut) => shortcut.keys);
1391
- return dedupeDetailsViewActionShortcuts(chords);
1392
- }
1393
- function dedupeDetailsViewActionShortcuts(shortcuts) {
1394
- const seen = new Set();
1395
- const result = [];
1396
- for (const shortcut of shortcuts) {
1397
- const token = shortcut.trim().toLowerCase();
1398
- if (!token || seen.has(token)) {
1399
- continue;
1400
- }
1401
- seen.add(token);
1402
- result.push(shortcut.trim());
1403
- }
1404
- return result;
1405
- }
1406
- //#endregion
1407
-
1408
1583
  class AXPLayoutDetailsViewComponent extends AXPPageLayoutBaseComponent {
1409
1584
  constructor() {
1410
1585
  super(...arguments);
1411
1586
  this.adapter = input.required(...(ngDevMode ? [{ debugName: "adapter" }] : /* istanbul ignore next */ []));
1412
1587
  //
1413
1588
  this.vm = inject(AXPLayoutDetailsViewViewModel);
1414
- this.router = inject(Router);
1415
1589
  this.route = inject(ActivatedRoute);
1416
1590
  this.eventService = inject(AXPBroadcastEventService);
1417
1591
  this.deviceService = inject(AXPDeviceService);
1418
- this.unsavedChangesConfirm = inject(AXPUnsavedChangesConfirmService);
1419
1592
  this.unsavedChangesPopstate = inject(AXPUnsavedChangesPopstateService);
1420
1593
  this.pageComponentInstanceRegistry = inject(AXPPageComponentInstanceRegistryService);
1421
1594
  this.shortcutRegistry = inject(AXPKeyboardShortcutRegistry);
@@ -1664,11 +1837,17 @@ class AXPLayoutDetailsViewComponent extends AXPPageLayoutBaseComponent {
1664
1837
  }
1665
1838
  finalizeDiscard() {
1666
1839
  const container = this.widgetContainer();
1667
- if (!container) {
1840
+ if (container) {
1841
+ container.revertAndSettle();
1842
+ this.vm.applySavedState(container.contextService.data());
1668
1843
  return;
1669
1844
  }
1670
- container.revertAndSettle();
1671
- this.vm.applySavedState(container.contextService.data());
1845
+ void this.vm.discard();
1846
+ }
1847
+ /** Reverts widget form state and view-model context to the last saved baseline. */
1848
+ async discardUnsavedChanges() {
1849
+ this.finalizeDiscard();
1850
+ this.recompute();
1672
1851
  }
1673
1852
  /**
1674
1853
  * `ax-tabs` selects on click before we can confirm. Revert synchronously, then confirm + switch.
@@ -1764,6 +1943,15 @@ class AXPLayoutDetailsViewComponent extends AXPPageLayoutBaseComponent {
1764
1943
  async getBackButton() {
1765
1944
  return this.vm.getBackButton();
1766
1945
  }
1946
+ hasUnsavedChanges() {
1947
+ if (this.vm.currentPage()?.isReadonly) {
1948
+ return false;
1949
+ }
1950
+ if (this.isActivePageComponentDirty()) {
1951
+ return true;
1952
+ }
1953
+ return this.liveFormIsDirty();
1954
+ }
1767
1955
  async getPageBadge() {
1768
1956
  if (this.vm.currentPage()?.isReadonly) {
1769
1957
  return null;
@@ -1808,6 +1996,12 @@ class AXPLayoutDetailsViewComponent extends AXPPageLayoutBaseComponent {
1808
1996
  }
1809
1997
  return this.confirmLeaveIfDirty();
1810
1998
  }
1999
+ /** Background tab close — confirm without discarding (the cached instance is destroyed). */
2000
+ onBeforeTabClose() {
2001
+ return this.unsavedChangesConfirm.confirmIfDirty(this.shouldPromptLeave(), {
2002
+ approveLeaveOnConfirm: false,
2003
+ });
2004
+ }
1811
2005
  /** Active entity component page instance, when the current details sub-page is component-based. */
1812
2006
  getActivePageComponentInstance() {
1813
2007
  const componentKey = this.getActivePageComponentKey();
@@ -1832,6 +2026,12 @@ class AXPLayoutDetailsViewComponent extends AXPPageLayoutBaseComponent {
1832
2026
  static isComponentPageContent(item) {
1833
2027
  return 'type' in item && item.type === 'component';
1834
2028
  }
2029
+ pageContentTrackKey(item, index) {
2030
+ if (AXPLayoutDetailsViewComponent.isComponentPageContent(item)) {
2031
+ return `component:${item.componentKey}`;
2032
+ }
2033
+ return item.name ?? `widget:${index}`;
2034
+ }
1835
2035
  /** Whether the active component-based entity page has unsaved edits. */
1836
2036
  isActivePageComponentDirty() {
1837
2037
  return this.getActivePageComponentInstance()?.hasUnsavedChanges() ?? false;
@@ -1847,8 +2047,13 @@ class AXPLayoutDetailsViewComponent extends AXPPageLayoutBaseComponent {
1847
2047
  return this.liveFormIsDirty();
1848
2048
  }
1849
2049
  /** Single gate for leaving the page or switching details sub-pages when dirty. */
1850
- confirmLeaveIfDirty() {
1851
- return this.unsavedChangesConfirm.confirmIfDirty(this.shouldPromptLeave());
2050
+ async confirmLeaveIfDirty() {
2051
+ const shouldPrompt = this.shouldPromptLeave();
2052
+ const confirmed = await this.unsavedChangesConfirm.confirmIfDirty(shouldPrompt);
2053
+ if (confirmed && shouldPrompt) {
2054
+ await this.discardUnsavedChanges();
2055
+ }
2056
+ return confirmed;
1852
2057
  }
1853
2058
  //#endregion
1854
2059
  //#region ---- Command Execution ----
@@ -1867,9 +2072,7 @@ class AXPLayoutDetailsViewComponent extends AXPPageLayoutBaseComponent {
1867
2072
  return;
1868
2073
  }
1869
2074
  if (command.name == rejectCommand) {
1870
- await this.vm.discard();
1871
- this.finalizeDiscard();
1872
- this.recompute();
2075
+ await this.discardUnsavedChanges();
1873
2076
  return;
1874
2077
  }
1875
2078
  // Handle status update command
@@ -1932,9 +2135,7 @@ class AXPLayoutDetailsViewComponent extends AXPPageLayoutBaseComponent {
1932
2135
  return;
1933
2136
  }
1934
2137
  if (isDiscardCommand(command.name, rejectCommand)) {
1935
- await this.vm.discard();
1936
- this.finalizeDiscard();
1937
- this.recompute();
2138
+ await this.discardUnsavedChanges();
1938
2139
  return;
1939
2140
  }
1940
2141
  if (!this.isActivePageComponentDirty()) {
@@ -2143,7 +2344,7 @@ class AXPLayoutDetailsViewComponent extends AXPPageLayoutBaseComponent {
2143
2344
  useExisting: AXPLayoutDetailsViewComponent,
2144
2345
  },
2145
2346
  AXPLayoutDetailsViewViewModel,
2146
- ], viewQueries: [{ propertyName: "form", first: true, predicate: ["form"], descendants: true, isSignal: true }, { propertyName: "widgetContainer", first: true, predicate: AXPWidgetContainerComponent, descendants: true, isSignal: true }, { propertyName: "sidebarTabs", first: true, predicate: ["sidebarTabs"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<axp-page-layout *translate=\"let t\">\n @if (vm.adapter()?.pages?.length! > 1 && vm.isLarge()) {\n <axp-layout-start-side>\n <axp-layout-header>\n <axp-layout-title>{{\n (formatService.format(vm.showPages() ? (vm.adapter()?.title ?? '') : (vm.adapter()?.label ?? ''), 'string', vm.rootContext())\n | translate\n | async)\n }}</axp-layout-title>\n <axp-layout-description>{{\n (formatService.format(vm.adapter()?.description ?? '', 'string', vm.rootContext()) | translate | async)\n }}</axp-layout-description>\n </axp-layout-header>\n <axp-layout-content>\n <ax-tabs\n #sidebarTabs\n class=\"axp-vertical-tabs\"\n [look]=\"'with-line-color'\"\n [location]=\"'end'\"\n [fitParent]=\"true\"\n (onActiveTabChanged)=\"handleSidebarTabChanged($event)\"\n >\n @for (item of vm.adapter()?.pages; track item.id) {\n <ax-tab-item\n [key]=\"item.id\"\n [text]=\"(formatService.format(item.label, 'string', vm.rootContext()) | translate | async)!\"\n [active]=\"vm.currentPage()?.id === item.id\"\n >\n <ax-prefix>\n <ax-icon icon=\"far ${{ item.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n }\n </ax-tabs>\n </axp-layout-content>\n </axp-layout-start-side>\n }\n <!-- Content Section -->\n <axp-page-content class=\"ax-flex ax-flex-row ax-gap-4\">\n @if (vm.showPages()) {\n <axp-layout-list class=\"ax-w-full\">\n @for (item of vm.adapter()?.pages; track $index) {\n <axp-layout-list-item (click)=\"handleSelectPage(item)\">\n <axp-layout-prefix>\n <ax-icon icon=\"far ${{ item.icon }}\" class=\"ax-text-gray-500\"></ax-icon>\n </axp-layout-prefix>\n <axp-layout-content>\n <a class=\"ax-font-semibold\">{{\n formatService.format(item.label, 'string', vm.rootContext()) | translate | async\n }}</a>\n </axp-layout-content>\n <axp-layout-suffix>\n <ax-icon icon=\" far fa-chevron-right\" class=\"ax-text-gray-400\"></ax-icon>\n </axp-layout-suffix>\n </axp-layout-list-item>\n }\n </axp-layout-list>\n } @else {\n <ax-form class=\"ax-h-full ax-w-full\" #form>\n <axp-widgets-container\n #widgetsContainerRef\n [context]=\"vm.context()\"\n (onContextChanged)=\"handleOnContextChanged($event)\"\n >\n <div class=\"ax-flex ax-flex-col ax-w-full ax-gap-4 ax-h-full\">\n @for (item of vm.currentPageContent(); track $index) {\n @if ('type' in item && item.type === 'component') {\n <!-- Component-based content -->\n <div\n axp-page-component-renderer\n [componentKey]=\"$any(item).componentKey\"\n [rootContext]=\"vm.rootContext()\"\n [pageConfig]=\"$any(item).pageConfig\"\n [options]=\"$any(item).options\"\n (componentReady)=\"recompute()\"\n ></div>\n } @else {\n <!-- Widget-based content -->\n <ng-container axp-widget-renderer [node]=\"$any(item)\" [mode]=\"$any(item).mode ?? 'view'\"></ng-container>\n }\n }\n @if (vm.currentPage()?.tabs?.length) {\n <div class=\"axp-horizontal-tabs\">\n <ax-tabs [location]=\"'bottom'\" [fitParent]=\"false\" look=\"classic\">\n @for (tab of vm.currentPageTabs(); track $index) {\n <ax-tab-item\n (onClick)=\"vm.setCurrentTab(tab)\"\n [text]=\"(tab.title | translate | async)!\"\n [active]=\"vm.currentTab()?.id === tab.id\"\n >\n <ax-prefix>\n <ax-icon icon=\"far ${{ tab.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n }\n </ax-tabs>\n <div class=\"content\">\n @if (vm.currentTab()) {\n @for (content of vm.activeTabContent(); track content.name) {\n <ng-container axp-widget-renderer [node]=\"content\" [mode]=\"content.mode ?? 'view'\">\n </ng-container>\n }\n }\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n </ax-form>\n }\n </axp-page-content>\n\n <!-- Footer Section -->\n @if (hasFooter()) {\n <axp-page-footer class=\"--animated\">\n <axp-layout-suffix>\n <!-- secondary footer actions -->\n @if (hasVisibleFooterSecondaryActions()) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [iconOnly]=\"deviceService.isSmall()\"\n [text]=\"'@general:terms.interface.actions' | translate | async\"\n [look]=\"deviceService.isSmall() ? 'blank' : 'solid'\"\n [color]=\"'default'\"\n >\n <ax-prefix>\n <i class=\"fa-solid fa-ellipsis-vertical\"></i>\n </ax-prefix>\n <ax-dropdown-panel #panel>\n <ax-button-item-list>\n @for (action of footerSecondaryActions(); track $index) {\n @if (isFooterActionVisible(action)) {\n <ax-button-item\n [text]=\"(t(action.title) | async)!\"\n [color]=\"action.color\"\n [disabled]=\"isFooterActionDisabled(action)\"\n (onClick)=\"execute(action.command!)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light {{ action.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n @if (action.break) {\n <ax-divider></ax-divider>\n }\n }\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n </ax-button>\n }\n\n @if (hasVisibleFooterPrimaryActions()) {\n @if (showSaveDiscardFooter()) {\n @if (rejectFooterCommand(); as reject) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"vm.isSaving()\"\n [text]=\"(t(reject.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"reject.color\"\n (onClick)=\"execute(reject.command)\"\n >\n <ax-prefix>\n <i class=\"{{ reject.icon }}\"></i>\n </ax-prefix>\n </ax-button>\n }\n @if (acceptFooterCommand(); as accept) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"vm.isSaving()\"\n [text]=\"(t(accept.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"accept.color\"\n (onClick)=\"execute(accept.command)\"\n >\n <ax-prefix>\n <i class=\"{{ accept.icon }}\"></i>\n </ax-prefix>\n </ax-button>\n }\n }\n @for (action of footerPrimaryActions(); track $index) {\n @if (isFooterActionVisible(action)) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"isFooterActionDisabled(action)\"\n [text]=\"(t(action.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"action.color\"\n (onClick)=\"execute(action.command!)\"\n >\n <ax-prefix>\n <i class=\"{{ action.icon }}\"></i>\n </ax-prefix>\n @if (action?.items) {\n <ax-dropdown-panel #panel>\n <ax-button-item-list>\n @for (sub of action?.items; track $index) {\n @if (isFooterActionVisible(sub)) {\n <ax-button-item\n [text]=\"(t(sub.title) | async)!\"\n [color]=\"sub.color\"\n [disabled]=\"isFooterActionDisabled(sub)\"\n (onClick)=\"execute(sub.command!)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light {{ sub.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n @if (sub.break) {\n <ax-divider></ax-divider>\n }\n }\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n }\n </ax-button>\n }\n }\n }\n </axp-layout-suffix>\n </axp-page-footer>\n }\n</axp-page-layout>\n", styles: ["axp-layout-details-view .axp-vertical-tabs{--ax-comp-tabs-default-border-radius: 0}axp-layout-details-view .axp-vertical-tabs ax-tab-item{margin-top:0!important;margin-bottom:0!important;padding-top:.75rem!important;padding-bottom:.75rem!important;font-weight:600!important}axp-layout-details-view .axp-vertical-tabs ax-tab-item>div{display:flex!important;align-items:center!important}axp-layout-details-view .axp-vertical-tabs ax-tab-item>div .ax-tab-item-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}axp-layout-details-view .axp-horizontal-tabs{display:flex;width:100%;flex-direction:column}axp-layout-details-view .axp-horizontal-tabs .content{margin-top:1rem;margin-bottom:1rem;display:block;width:100%}axp-layout-details-view axp-layout-section axp-layout-content{display:flex;width:100%;flex-direction:column}axp-layout-details-view axp-layout-section axp-layout-content>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse));border-style:dashed;--tw-divide-opacity: 1;border-color:rgba(var(--ax-sys-color-border-lightest-surface),var(--tw-divide-opacity, 1))}axp-layout-details-view axp-layout-section axp-layout-content .--item-container{align-items:center;gap:1rem;padding:1rem}@media(min-width:1024px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:1.5rem;padding-right:1.5rem}}@media(min-width:1280px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:2rem;padding-right:2rem}}@media(min-width:1536px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:2.5rem;padding-right:2.5rem}}axp-layout-details-view axp-layout-section axp-layout-content .--item-container{display:grid;grid-template-columns:repeat(12,minmax(0,1fr))}axp-layout-details-view axp-layout-section axp-layout-content .--item-container axp-layout-description{margin-top:.5rem!important;font-size:.75rem!important;line-height:1rem!important}axp-layout-details-view axp-page-content ax-form form{height:100%}axp-layout-details-view axp-layout-start-side{border-inline-end-width:1px}\n"], dependencies: [{ kind: "ngmodule", type:
2347
+ ], viewQueries: [{ propertyName: "form", first: true, predicate: ["form"], descendants: true, isSignal: true }, { propertyName: "widgetContainer", first: true, predicate: AXPWidgetContainerComponent, descendants: true, isSignal: true }, { propertyName: "sidebarTabs", first: true, predicate: ["sidebarTabs"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<axp-page-layout *translate=\"let t\">\n @if (vm.adapter()?.pages?.length! > 1 && vm.isLarge()) {\n <axp-layout-start-side>\n <axp-layout-header>\n <axp-layout-title>{{\n formatService.format(\n vm.showPages() ? (vm.adapter()?.title ?? '') : (vm.adapter()?.label ?? ''),\n 'string',\n vm.rootContext()\n )\n | translate\n | async\n }}</axp-layout-title>\n <axp-layout-description>{{\n formatService.format(vm.adapter()?.description ?? '', 'string', vm.rootContext()) | translate | async\n }}</axp-layout-description>\n </axp-layout-header>\n <axp-layout-content>\n <ax-tabs\n #sidebarTabs\n class=\"axp-vertical-tabs\"\n [look]=\"'with-line-color'\"\n [location]=\"'end'\"\n [fitParent]=\"true\"\n (onActiveTabChanged)=\"handleSidebarTabChanged($event)\"\n >\n @for (item of vm.adapter()?.pages; track item.id) {\n <ax-tab-item\n [key]=\"item.id\"\n [text]=\"(formatService.format(item.label, 'string', vm.rootContext()) | translate | async)!\"\n [active]=\"vm.currentPage()?.id === item.id\"\n >\n <ax-prefix>\n <ax-icon icon=\"far ${{ item.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n }\n </ax-tabs>\n </axp-layout-content>\n </axp-layout-start-side>\n }\n <!-- Content Section -->\n <axp-page-content class=\"ax-flex ax-flex-row ax-gap-4\">\n @if (vm.showPages()) {\n <axp-layout-list class=\"ax-w-full\">\n @for (item of vm.adapter()?.pages; track $index) {\n <axp-layout-list-item (click)=\"handleSelectPage(item)\">\n <axp-layout-prefix>\n <ax-icon icon=\"far ${{ item.icon }}\" class=\"ax-text-gray-500\"></ax-icon>\n </axp-layout-prefix>\n <axp-layout-content>\n <a class=\"ax-font-semibold\">{{\n formatService.format(item.label, 'string', vm.rootContext()) | translate | async\n }}</a>\n </axp-layout-content>\n <axp-layout-suffix>\n <ax-icon icon=\" far fa-chevron-right\" class=\"ax-text-gray-400\"></ax-icon>\n </axp-layout-suffix>\n </axp-layout-list-item>\n }\n </axp-layout-list>\n } @else {\n <ax-form class=\"ax-h-full ax-w-full\" #form>\n <axp-widgets-container\n #widgetsContainerRef\n [context]=\"vm.context()\"\n (onContextChanged)=\"handleOnContextChanged($event)\"\n >\n <div class=\"ax-flex ax-flex-col ax-w-full ax-gap-4 ax-h-full\">\n @for (item of vm.currentPageContent(); track pageContentTrackKey($any(item), $index)) {\n @if ('type' in item && item.type === 'component') {\n <!-- Component-based content -->\n <div\n axp-page-component-renderer\n [componentKey]=\"$any(item).componentKey\"\n [rootContext]=\"vm.rootContext()\"\n [pageConfig]=\"$any(item).pageConfig\"\n [options]=\"$any(item).options\"\n (componentReady)=\"recompute()\"\n ></div>\n } @else {\n <!-- Widget-based content -->\n <ng-container axp-widget-renderer [node]=\"$any(item)\" [mode]=\"$any(item).mode ?? 'view'\"></ng-container>\n }\n }\n @if (vm.currentPage()?.tabs?.length) {\n <div class=\"axp-horizontal-tabs\">\n <ax-tabs [location]=\"'bottom'\" [fitParent]=\"false\" look=\"classic\">\n @for (tab of vm.currentPageTabs(); track $index) {\n <ax-tab-item\n (onClick)=\"vm.setCurrentTab(tab)\"\n [text]=\"(tab.title | translate | async)!\"\n [active]=\"vm.currentTab()?.id === tab.id\"\n >\n <ax-prefix>\n <ax-icon icon=\"far ${{ tab.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n }\n </ax-tabs>\n <div class=\"content\">\n @if (vm.currentTab()) {\n @for (content of vm.activeTabContent(); track content.name) {\n <ng-container axp-widget-renderer [node]=\"content\" [mode]=\"content.mode ?? 'view'\">\n </ng-container>\n }\n }\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n </ax-form>\n }\n </axp-page-content>\n\n <!-- Footer Section -->\n @if (hasFooter()) {\n <axp-page-footer class=\"--animated\">\n <axp-layout-suffix>\n <!-- secondary footer actions -->\n @if (hasVisibleFooterSecondaryActions()) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [iconOnly]=\"deviceService.isSmall()\"\n [text]=\"'@general:terms.interface.actions' | translate | async\"\n [look]=\"deviceService.isSmall() ? 'blank' : 'solid'\"\n [color]=\"'default'\"\n >\n <ax-prefix>\n <i class=\"fa-solid fa-ellipsis-vertical\"></i>\n </ax-prefix>\n <ax-dropdown-panel #panel>\n <ax-button-item-list>\n @for (action of footerSecondaryActions(); track $index) {\n @if (isFooterActionVisible(action)) {\n <ax-button-item\n [text]=\"(t(action.title) | async)!\"\n [color]=\"action.color\"\n [disabled]=\"isFooterActionDisabled(action)\"\n (onClick)=\"execute(action.command!)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light {{ action.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n @if (action.break) {\n <ax-divider></ax-divider>\n }\n }\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n </ax-button>\n }\n\n @if (hasVisibleFooterPrimaryActions()) {\n @if (showSaveDiscardFooter()) {\n @if (rejectFooterCommand(); as reject) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"vm.isSaving()\"\n [text]=\"(t(reject.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"reject.color\"\n (onClick)=\"execute(reject.command)\"\n >\n <ax-prefix>\n <i class=\"{{ reject.icon }}\"></i>\n </ax-prefix>\n </ax-button>\n }\n @if (acceptFooterCommand(); as accept) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"vm.isSaving()\"\n [text]=\"(t(accept.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"accept.color\"\n (onClick)=\"execute(accept.command)\"\n >\n <ax-prefix>\n <i class=\"{{ accept.icon }}\"></i>\n </ax-prefix>\n </ax-button>\n }\n }\n @for (action of footerPrimaryActions(); track $index) {\n @if (isFooterActionVisible(action)) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"isFooterActionDisabled(action)\"\n [text]=\"(t(action.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"action.color\"\n (onClick)=\"execute(action.command!)\"\n >\n <ax-prefix>\n <i class=\"{{ action.icon }}\"></i>\n </ax-prefix>\n @if (action?.items) {\n <ax-dropdown-panel #panel>\n <ax-button-item-list>\n @for (sub of action?.items; track $index) {\n @if (isFooterActionVisible(sub)) {\n <ax-button-item\n [text]=\"(t(sub.title) | async)!\"\n [color]=\"sub.color\"\n [disabled]=\"isFooterActionDisabled(sub)\"\n (onClick)=\"execute(sub.command!)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light {{ sub.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n @if (sub.break) {\n <ax-divider></ax-divider>\n }\n }\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n }\n </ax-button>\n }\n }\n }\n </axp-layout-suffix>\n </axp-page-footer>\n }\n</axp-page-layout>\n", styles: ["axp-layout-details-view .axp-vertical-tabs{--ax-comp-tabs-default-border-radius: 0}axp-layout-details-view .axp-vertical-tabs ax-tab-item{margin-top:0!important;margin-bottom:0!important;padding-top:.75rem!important;padding-bottom:.75rem!important;font-weight:600!important}axp-layout-details-view .axp-vertical-tabs ax-tab-item>div{display:flex!important;align-items:center!important}axp-layout-details-view .axp-vertical-tabs ax-tab-item>div .ax-tab-item-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}axp-layout-details-view .axp-horizontal-tabs{display:flex;width:100%;flex-direction:column}axp-layout-details-view .axp-horizontal-tabs .content{margin-top:1rem;margin-bottom:1rem;display:block;width:100%}axp-layout-details-view axp-layout-section axp-layout-content{display:flex;width:100%;flex-direction:column}axp-layout-details-view axp-layout-section axp-layout-content>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse));border-style:dashed;--tw-divide-opacity: 1;border-color:rgba(var(--ax-sys-color-border-lightest-surface),var(--tw-divide-opacity, 1))}axp-layout-details-view axp-layout-section axp-layout-content .--item-container{align-items:center;gap:1rem;padding:1rem}@media(min-width:1024px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:1.5rem;padding-right:1.5rem}}@media(min-width:1280px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:2rem;padding-right:2rem}}@media(min-width:1536px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:2.5rem;padding-right:2.5rem}}axp-layout-details-view axp-layout-section axp-layout-content .--item-container{display:grid;grid-template-columns:repeat(12,minmax(0,1fr))}axp-layout-details-view axp-layout-section axp-layout-content .--item-container axp-layout-description{margin-top:.5rem!important;font-size:.75rem!important;line-height:1rem!important}axp-layout-details-view axp-page-content ax-form form{height:100%}axp-layout-details-view axp-layout-start-side{border-inline-end-width:1px}\n"], dependencies: [{ kind: "ngmodule", type:
2147
2348
  // Angular
2148
2349
  CommonModule }, { kind: "ngmodule", type:
2149
2350
  // ACoreX
@@ -2189,7 +2390,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2189
2390
  useExisting: AXPLayoutDetailsViewComponent,
2190
2391
  },
2191
2392
  AXPLayoutDetailsViewViewModel,
2192
- ], template: "<axp-page-layout *translate=\"let t\">\n @if (vm.adapter()?.pages?.length! > 1 && vm.isLarge()) {\n <axp-layout-start-side>\n <axp-layout-header>\n <axp-layout-title>{{\n (formatService.format(vm.showPages() ? (vm.adapter()?.title ?? '') : (vm.adapter()?.label ?? ''), 'string', vm.rootContext())\n | translate\n | async)\n }}</axp-layout-title>\n <axp-layout-description>{{\n (formatService.format(vm.adapter()?.description ?? '', 'string', vm.rootContext()) | translate | async)\n }}</axp-layout-description>\n </axp-layout-header>\n <axp-layout-content>\n <ax-tabs\n #sidebarTabs\n class=\"axp-vertical-tabs\"\n [look]=\"'with-line-color'\"\n [location]=\"'end'\"\n [fitParent]=\"true\"\n (onActiveTabChanged)=\"handleSidebarTabChanged($event)\"\n >\n @for (item of vm.adapter()?.pages; track item.id) {\n <ax-tab-item\n [key]=\"item.id\"\n [text]=\"(formatService.format(item.label, 'string', vm.rootContext()) | translate | async)!\"\n [active]=\"vm.currentPage()?.id === item.id\"\n >\n <ax-prefix>\n <ax-icon icon=\"far ${{ item.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n }\n </ax-tabs>\n </axp-layout-content>\n </axp-layout-start-side>\n }\n <!-- Content Section -->\n <axp-page-content class=\"ax-flex ax-flex-row ax-gap-4\">\n @if (vm.showPages()) {\n <axp-layout-list class=\"ax-w-full\">\n @for (item of vm.adapter()?.pages; track $index) {\n <axp-layout-list-item (click)=\"handleSelectPage(item)\">\n <axp-layout-prefix>\n <ax-icon icon=\"far ${{ item.icon }}\" class=\"ax-text-gray-500\"></ax-icon>\n </axp-layout-prefix>\n <axp-layout-content>\n <a class=\"ax-font-semibold\">{{\n formatService.format(item.label, 'string', vm.rootContext()) | translate | async\n }}</a>\n </axp-layout-content>\n <axp-layout-suffix>\n <ax-icon icon=\" far fa-chevron-right\" class=\"ax-text-gray-400\"></ax-icon>\n </axp-layout-suffix>\n </axp-layout-list-item>\n }\n </axp-layout-list>\n } @else {\n <ax-form class=\"ax-h-full ax-w-full\" #form>\n <axp-widgets-container\n #widgetsContainerRef\n [context]=\"vm.context()\"\n (onContextChanged)=\"handleOnContextChanged($event)\"\n >\n <div class=\"ax-flex ax-flex-col ax-w-full ax-gap-4 ax-h-full\">\n @for (item of vm.currentPageContent(); track $index) {\n @if ('type' in item && item.type === 'component') {\n <!-- Component-based content -->\n <div\n axp-page-component-renderer\n [componentKey]=\"$any(item).componentKey\"\n [rootContext]=\"vm.rootContext()\"\n [pageConfig]=\"$any(item).pageConfig\"\n [options]=\"$any(item).options\"\n (componentReady)=\"recompute()\"\n ></div>\n } @else {\n <!-- Widget-based content -->\n <ng-container axp-widget-renderer [node]=\"$any(item)\" [mode]=\"$any(item).mode ?? 'view'\"></ng-container>\n }\n }\n @if (vm.currentPage()?.tabs?.length) {\n <div class=\"axp-horizontal-tabs\">\n <ax-tabs [location]=\"'bottom'\" [fitParent]=\"false\" look=\"classic\">\n @for (tab of vm.currentPageTabs(); track $index) {\n <ax-tab-item\n (onClick)=\"vm.setCurrentTab(tab)\"\n [text]=\"(tab.title | translate | async)!\"\n [active]=\"vm.currentTab()?.id === tab.id\"\n >\n <ax-prefix>\n <ax-icon icon=\"far ${{ tab.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n }\n </ax-tabs>\n <div class=\"content\">\n @if (vm.currentTab()) {\n @for (content of vm.activeTabContent(); track content.name) {\n <ng-container axp-widget-renderer [node]=\"content\" [mode]=\"content.mode ?? 'view'\">\n </ng-container>\n }\n }\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n </ax-form>\n }\n </axp-page-content>\n\n <!-- Footer Section -->\n @if (hasFooter()) {\n <axp-page-footer class=\"--animated\">\n <axp-layout-suffix>\n <!-- secondary footer actions -->\n @if (hasVisibleFooterSecondaryActions()) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [iconOnly]=\"deviceService.isSmall()\"\n [text]=\"'@general:terms.interface.actions' | translate | async\"\n [look]=\"deviceService.isSmall() ? 'blank' : 'solid'\"\n [color]=\"'default'\"\n >\n <ax-prefix>\n <i class=\"fa-solid fa-ellipsis-vertical\"></i>\n </ax-prefix>\n <ax-dropdown-panel #panel>\n <ax-button-item-list>\n @for (action of footerSecondaryActions(); track $index) {\n @if (isFooterActionVisible(action)) {\n <ax-button-item\n [text]=\"(t(action.title) | async)!\"\n [color]=\"action.color\"\n [disabled]=\"isFooterActionDisabled(action)\"\n (onClick)=\"execute(action.command!)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light {{ action.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n @if (action.break) {\n <ax-divider></ax-divider>\n }\n }\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n </ax-button>\n }\n\n @if (hasVisibleFooterPrimaryActions()) {\n @if (showSaveDiscardFooter()) {\n @if (rejectFooterCommand(); as reject) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"vm.isSaving()\"\n [text]=\"(t(reject.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"reject.color\"\n (onClick)=\"execute(reject.command)\"\n >\n <ax-prefix>\n <i class=\"{{ reject.icon }}\"></i>\n </ax-prefix>\n </ax-button>\n }\n @if (acceptFooterCommand(); as accept) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"vm.isSaving()\"\n [text]=\"(t(accept.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"accept.color\"\n (onClick)=\"execute(accept.command)\"\n >\n <ax-prefix>\n <i class=\"{{ accept.icon }}\"></i>\n </ax-prefix>\n </ax-button>\n }\n }\n @for (action of footerPrimaryActions(); track $index) {\n @if (isFooterActionVisible(action)) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"isFooterActionDisabled(action)\"\n [text]=\"(t(action.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"action.color\"\n (onClick)=\"execute(action.command!)\"\n >\n <ax-prefix>\n <i class=\"{{ action.icon }}\"></i>\n </ax-prefix>\n @if (action?.items) {\n <ax-dropdown-panel #panel>\n <ax-button-item-list>\n @for (sub of action?.items; track $index) {\n @if (isFooterActionVisible(sub)) {\n <ax-button-item\n [text]=\"(t(sub.title) | async)!\"\n [color]=\"sub.color\"\n [disabled]=\"isFooterActionDisabled(sub)\"\n (onClick)=\"execute(sub.command!)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light {{ sub.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n @if (sub.break) {\n <ax-divider></ax-divider>\n }\n }\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n }\n </ax-button>\n }\n }\n }\n </axp-layout-suffix>\n </axp-page-footer>\n }\n</axp-page-layout>\n", styles: ["axp-layout-details-view .axp-vertical-tabs{--ax-comp-tabs-default-border-radius: 0}axp-layout-details-view .axp-vertical-tabs ax-tab-item{margin-top:0!important;margin-bottom:0!important;padding-top:.75rem!important;padding-bottom:.75rem!important;font-weight:600!important}axp-layout-details-view .axp-vertical-tabs ax-tab-item>div{display:flex!important;align-items:center!important}axp-layout-details-view .axp-vertical-tabs ax-tab-item>div .ax-tab-item-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}axp-layout-details-view .axp-horizontal-tabs{display:flex;width:100%;flex-direction:column}axp-layout-details-view .axp-horizontal-tabs .content{margin-top:1rem;margin-bottom:1rem;display:block;width:100%}axp-layout-details-view axp-layout-section axp-layout-content{display:flex;width:100%;flex-direction:column}axp-layout-details-view axp-layout-section axp-layout-content>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse));border-style:dashed;--tw-divide-opacity: 1;border-color:rgba(var(--ax-sys-color-border-lightest-surface),var(--tw-divide-opacity, 1))}axp-layout-details-view axp-layout-section axp-layout-content .--item-container{align-items:center;gap:1rem;padding:1rem}@media(min-width:1024px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:1.5rem;padding-right:1.5rem}}@media(min-width:1280px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:2rem;padding-right:2rem}}@media(min-width:1536px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:2.5rem;padding-right:2.5rem}}axp-layout-details-view axp-layout-section axp-layout-content .--item-container{display:grid;grid-template-columns:repeat(12,minmax(0,1fr))}axp-layout-details-view axp-layout-section axp-layout-content .--item-container axp-layout-description{margin-top:.5rem!important;font-size:.75rem!important;line-height:1rem!important}axp-layout-details-view axp-page-content ax-form form{height:100%}axp-layout-details-view axp-layout-start-side{border-inline-end-width:1px}\n"] }]
2393
+ ], template: "<axp-page-layout *translate=\"let t\">\n @if (vm.adapter()?.pages?.length! > 1 && vm.isLarge()) {\n <axp-layout-start-side>\n <axp-layout-header>\n <axp-layout-title>{{\n formatService.format(\n vm.showPages() ? (vm.adapter()?.title ?? '') : (vm.adapter()?.label ?? ''),\n 'string',\n vm.rootContext()\n )\n | translate\n | async\n }}</axp-layout-title>\n <axp-layout-description>{{\n formatService.format(vm.adapter()?.description ?? '', 'string', vm.rootContext()) | translate | async\n }}</axp-layout-description>\n </axp-layout-header>\n <axp-layout-content>\n <ax-tabs\n #sidebarTabs\n class=\"axp-vertical-tabs\"\n [look]=\"'with-line-color'\"\n [location]=\"'end'\"\n [fitParent]=\"true\"\n (onActiveTabChanged)=\"handleSidebarTabChanged($event)\"\n >\n @for (item of vm.adapter()?.pages; track item.id) {\n <ax-tab-item\n [key]=\"item.id\"\n [text]=\"(formatService.format(item.label, 'string', vm.rootContext()) | translate | async)!\"\n [active]=\"vm.currentPage()?.id === item.id\"\n >\n <ax-prefix>\n <ax-icon icon=\"far ${{ item.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n }\n </ax-tabs>\n </axp-layout-content>\n </axp-layout-start-side>\n }\n <!-- Content Section -->\n <axp-page-content class=\"ax-flex ax-flex-row ax-gap-4\">\n @if (vm.showPages()) {\n <axp-layout-list class=\"ax-w-full\">\n @for (item of vm.adapter()?.pages; track $index) {\n <axp-layout-list-item (click)=\"handleSelectPage(item)\">\n <axp-layout-prefix>\n <ax-icon icon=\"far ${{ item.icon }}\" class=\"ax-text-gray-500\"></ax-icon>\n </axp-layout-prefix>\n <axp-layout-content>\n <a class=\"ax-font-semibold\">{{\n formatService.format(item.label, 'string', vm.rootContext()) | translate | async\n }}</a>\n </axp-layout-content>\n <axp-layout-suffix>\n <ax-icon icon=\" far fa-chevron-right\" class=\"ax-text-gray-400\"></ax-icon>\n </axp-layout-suffix>\n </axp-layout-list-item>\n }\n </axp-layout-list>\n } @else {\n <ax-form class=\"ax-h-full ax-w-full\" #form>\n <axp-widgets-container\n #widgetsContainerRef\n [context]=\"vm.context()\"\n (onContextChanged)=\"handleOnContextChanged($event)\"\n >\n <div class=\"ax-flex ax-flex-col ax-w-full ax-gap-4 ax-h-full\">\n @for (item of vm.currentPageContent(); track pageContentTrackKey($any(item), $index)) {\n @if ('type' in item && item.type === 'component') {\n <!-- Component-based content -->\n <div\n axp-page-component-renderer\n [componentKey]=\"$any(item).componentKey\"\n [rootContext]=\"vm.rootContext()\"\n [pageConfig]=\"$any(item).pageConfig\"\n [options]=\"$any(item).options\"\n (componentReady)=\"recompute()\"\n ></div>\n } @else {\n <!-- Widget-based content -->\n <ng-container axp-widget-renderer [node]=\"$any(item)\" [mode]=\"$any(item).mode ?? 'view'\"></ng-container>\n }\n }\n @if (vm.currentPage()?.tabs?.length) {\n <div class=\"axp-horizontal-tabs\">\n <ax-tabs [location]=\"'bottom'\" [fitParent]=\"false\" look=\"classic\">\n @for (tab of vm.currentPageTabs(); track $index) {\n <ax-tab-item\n (onClick)=\"vm.setCurrentTab(tab)\"\n [text]=\"(tab.title | translate | async)!\"\n [active]=\"vm.currentTab()?.id === tab.id\"\n >\n <ax-prefix>\n <ax-icon icon=\"far ${{ tab.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-tab-item>\n }\n </ax-tabs>\n <div class=\"content\">\n @if (vm.currentTab()) {\n @for (content of vm.activeTabContent(); track content.name) {\n <ng-container axp-widget-renderer [node]=\"content\" [mode]=\"content.mode ?? 'view'\">\n </ng-container>\n }\n }\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n </ax-form>\n }\n </axp-page-content>\n\n <!-- Footer Section -->\n @if (hasFooter()) {\n <axp-page-footer class=\"--animated\">\n <axp-layout-suffix>\n <!-- secondary footer actions -->\n @if (hasVisibleFooterSecondaryActions()) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [iconOnly]=\"deviceService.isSmall()\"\n [text]=\"'@general:terms.interface.actions' | translate | async\"\n [look]=\"deviceService.isSmall() ? 'blank' : 'solid'\"\n [color]=\"'default'\"\n >\n <ax-prefix>\n <i class=\"fa-solid fa-ellipsis-vertical\"></i>\n </ax-prefix>\n <ax-dropdown-panel #panel>\n <ax-button-item-list>\n @for (action of footerSecondaryActions(); track $index) {\n @if (isFooterActionVisible(action)) {\n <ax-button-item\n [text]=\"(t(action.title) | async)!\"\n [color]=\"action.color\"\n [disabled]=\"isFooterActionDisabled(action)\"\n (onClick)=\"execute(action.command!)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light {{ action.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n @if (action.break) {\n <ax-divider></ax-divider>\n }\n }\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n </ax-button>\n }\n\n @if (hasVisibleFooterPrimaryActions()) {\n @if (showSaveDiscardFooter()) {\n @if (rejectFooterCommand(); as reject) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"vm.isSaving()\"\n [text]=\"(t(reject.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"reject.color\"\n (onClick)=\"execute(reject.command)\"\n >\n <ax-prefix>\n <i class=\"{{ reject.icon }}\"></i>\n </ax-prefix>\n </ax-button>\n }\n @if (acceptFooterCommand(); as accept) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"vm.isSaving()\"\n [text]=\"(t(accept.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"accept.color\"\n (onClick)=\"execute(accept.command)\"\n >\n <ax-prefix>\n <i class=\"{{ accept.icon }}\"></i>\n </ax-prefix>\n </ax-button>\n }\n }\n @for (action of footerPrimaryActions(); track $index) {\n @if (isFooterActionVisible(action)) {\n <ax-button\n [class.ax-sm]=\"deviceService.isSmall()\"\n [disabled]=\"isFooterActionDisabled(action)\"\n [text]=\"(t(action.title) | async)!\"\n [look]=\"'solid'\"\n [color]=\"action.color\"\n (onClick)=\"execute(action.command!)\"\n >\n <ax-prefix>\n <i class=\"{{ action.icon }}\"></i>\n </ax-prefix>\n @if (action?.items) {\n <ax-dropdown-panel #panel>\n <ax-button-item-list>\n @for (sub of action?.items; track $index) {\n @if (isFooterActionVisible(sub)) {\n <ax-button-item\n [text]=\"(t(sub.title) | async)!\"\n [color]=\"sub.color\"\n [disabled]=\"isFooterActionDisabled(sub)\"\n (onClick)=\"execute(sub.command!)\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light {{ sub.icon }}\"></ax-icon>\n </ax-prefix>\n </ax-button-item>\n @if (sub.break) {\n <ax-divider></ax-divider>\n }\n }\n }\n </ax-button-item-list>\n </ax-dropdown-panel>\n }\n </ax-button>\n }\n }\n }\n </axp-layout-suffix>\n </axp-page-footer>\n }\n</axp-page-layout>\n", styles: ["axp-layout-details-view .axp-vertical-tabs{--ax-comp-tabs-default-border-radius: 0}axp-layout-details-view .axp-vertical-tabs ax-tab-item{margin-top:0!important;margin-bottom:0!important;padding-top:.75rem!important;padding-bottom:.75rem!important;font-weight:600!important}axp-layout-details-view .axp-vertical-tabs ax-tab-item>div{display:flex!important;align-items:center!important}axp-layout-details-view .axp-vertical-tabs ax-tab-item>div .ax-tab-item-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}axp-layout-details-view .axp-horizontal-tabs{display:flex;width:100%;flex-direction:column}axp-layout-details-view .axp-horizontal-tabs .content{margin-top:1rem;margin-bottom:1rem;display:block;width:100%}axp-layout-details-view axp-layout-section axp-layout-content{display:flex;width:100%;flex-direction:column}axp-layout-details-view axp-layout-section axp-layout-content>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse));border-style:dashed;--tw-divide-opacity: 1;border-color:rgba(var(--ax-sys-color-border-lightest-surface),var(--tw-divide-opacity, 1))}axp-layout-details-view axp-layout-section axp-layout-content .--item-container{align-items:center;gap:1rem;padding:1rem}@media(min-width:1024px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:1.5rem;padding-right:1.5rem}}@media(min-width:1280px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:2rem;padding-right:2rem}}@media(min-width:1536px){axp-layout-details-view axp-layout-section axp-layout-content .--item-container{padding-left:2.5rem;padding-right:2.5rem}}axp-layout-details-view axp-layout-section axp-layout-content .--item-container{display:grid;grid-template-columns:repeat(12,minmax(0,1fr))}axp-layout-details-view axp-layout-section axp-layout-content .--item-container axp-layout-description{margin-top:.5rem!important;font-size:.75rem!important;line-height:1rem!important}axp-layout-details-view axp-page-content ax-form form{height:100%}axp-layout-details-view axp-layout-start-side{border-inline-end-width:1px}\n"] }]
2193
2394
  }], propDecorators: { adapter: [{ type: i0.Input, args: [{ isSignal: true, alias: "adapter", required: true }] }], form: [{ type: i0.ViewChild, args: ['form', { isSignal: true }] }], widgetContainer: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AXPWidgetContainerComponent), { isSignal: true }] }], sidebarTabs: [{ type: i0.ViewChild, args: ['sidebarTabs', { isSignal: true }] }] } });
2194
2395
 
2195
2396
  //#region ---- Details View Can Deactivate Guard ----