@memberjunction/ng-explorer-core 2.121.0 → 2.122.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +0 -1
  2. package/dist/app-routing.module.d.ts +6 -5
  3. package/dist/app-routing.module.d.ts.map +1 -1
  4. package/dist/app-routing.module.js +266 -132
  5. package/dist/app-routing.module.js.map +1 -1
  6. package/dist/lib/app-view/application-view.component.js +10 -10
  7. package/dist/lib/app-view/application-view.component.js.map +1 -1
  8. package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.js +11 -12
  9. package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.js.map +1 -1
  10. package/dist/lib/data-browser-component/data-browser.component.js +8 -8
  11. package/dist/lib/data-browser-component/data-browser.component.js.map +1 -1
  12. package/dist/lib/generic-browse-list/generic-browse-list.component.js +10 -7
  13. package/dist/lib/generic-browse-list/generic-browse-list.component.js.map +1 -1
  14. package/dist/lib/generic-browser-list/generic-browser-list.component.js +14 -11
  15. package/dist/lib/generic-browser-list/generic-browser-list.component.js.map +1 -1
  16. package/dist/lib/list-view/list-view.component.js +24 -21
  17. package/dist/lib/list-view/list-view.component.js.map +1 -1
  18. package/dist/lib/navigation/navigation.component.d.ts +0 -1
  19. package/dist/lib/navigation/navigation.component.d.ts.map +1 -1
  20. package/dist/lib/navigation/navigation.component.js +11 -19
  21. package/dist/lib/navigation/navigation.component.js.map +1 -1
  22. package/dist/lib/resource-browser/resource-browser.component.js +14 -11
  23. package/dist/lib/resource-browser/resource-browser.component.js.map +1 -1
  24. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +20 -0
  25. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -0
  26. package/dist/lib/resource-wrappers/artifact-resource.component.js +92 -0
  27. package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -0
  28. package/dist/lib/resource-wrappers/chat-collections-resource.component.d.ts +119 -0
  29. package/dist/lib/resource-wrappers/chat-collections-resource.component.d.ts.map +1 -0
  30. package/dist/lib/resource-wrappers/chat-collections-resource.component.js +500 -0
  31. package/dist/lib/resource-wrappers/chat-collections-resource.component.js.map +1 -0
  32. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +87 -0
  33. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -0
  34. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +345 -0
  35. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -0
  36. package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts +67 -0
  37. package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts.map +1 -0
  38. package/dist/lib/resource-wrappers/chat-tasks-resource.component.js +244 -0
  39. package/dist/lib/resource-wrappers/chat-tasks-resource.component.js.map +1 -0
  40. package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts +46 -3
  41. package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts.map +1 -1
  42. package/dist/lib/resource-wrappers/dashboard-resource.component.js +298 -21
  43. package/dist/lib/resource-wrappers/dashboard-resource.component.js.map +1 -1
  44. package/dist/lib/resource-wrappers/list-detail-resource.component.js +1 -1
  45. package/dist/lib/resource-wrappers/list-detail-resource.component.js.map +1 -1
  46. package/dist/lib/resource-wrappers/query-resource.component.js +1 -1
  47. package/dist/lib/resource-wrappers/query-resource.component.js.map +1 -1
  48. package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
  49. package/dist/lib/resource-wrappers/record-resource.component.js +22 -5
  50. package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
  51. package/dist/lib/resource-wrappers/report-resource.component.js +1 -1
  52. package/dist/lib/resource-wrappers/report-resource.component.js.map +1 -1
  53. package/dist/lib/resource-wrappers/resource-wrappers-loader.d.ts.map +1 -1
  54. package/dist/lib/resource-wrappers/resource-wrappers-loader.js +15 -0
  55. package/dist/lib/resource-wrappers/resource-wrappers-loader.js.map +1 -1
  56. package/dist/lib/resource-wrappers/search-results-resource.component.js +1 -1
  57. package/dist/lib/resource-wrappers/search-results-resource.component.js.map +1 -1
  58. package/dist/lib/resource-wrappers/view-resource.component.js +1 -1
  59. package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
  60. package/dist/lib/shell/components/header/app-nav.component.d.ts +45 -0
  61. package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -0
  62. package/dist/lib/shell/components/header/app-nav.component.js +127 -0
  63. package/dist/lib/shell/components/header/app-nav.component.js.map +1 -0
  64. package/dist/lib/shell/components/header/app-switcher.component.d.ts +53 -0
  65. package/dist/lib/shell/components/header/app-switcher.component.d.ts.map +1 -0
  66. package/dist/lib/shell/components/header/app-switcher.component.js +190 -0
  67. package/dist/lib/shell/components/header/app-switcher.component.js.map +1 -0
  68. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +83 -0
  69. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -0
  70. package/dist/lib/shell/components/tabs/component-cache-manager.js +175 -0
  71. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -0
  72. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +138 -0
  73. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -0
  74. package/dist/lib/shell/components/tabs/tab-container.component.js +920 -0
  75. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -0
  76. package/dist/lib/shell/services/settings-dialog.service.d.ts +28 -0
  77. package/dist/lib/shell/services/settings-dialog.service.d.ts.map +1 -0
  78. package/dist/lib/shell/services/settings-dialog.service.js +67 -0
  79. package/dist/lib/shell/services/settings-dialog.service.js.map +1 -0
  80. package/dist/lib/shell/shell.component.d.ts +166 -0
  81. package/dist/lib/shell/shell.component.d.ts.map +1 -0
  82. package/dist/lib/shell/shell.component.js +1173 -0
  83. package/dist/lib/shell/shell.component.js.map +1 -0
  84. package/dist/lib/shell/shell.module.d.ts +14 -0
  85. package/dist/lib/shell/shell.module.d.ts.map +1 -0
  86. package/dist/lib/shell/shell.module.js +42 -0
  87. package/dist/lib/shell/shell.module.js.map +1 -0
  88. package/dist/lib/single-dashboard/Components/add-item/add-item.component.js +17 -13
  89. package/dist/lib/single-dashboard/Components/add-item/add-item.component.js.map +1 -1
  90. package/dist/lib/single-dashboard/single-dashboard.component.d.ts +2 -1
  91. package/dist/lib/single-dashboard/single-dashboard.component.d.ts.map +1 -1
  92. package/dist/lib/single-dashboard/single-dashboard.component.js +6 -1
  93. package/dist/lib/single-dashboard/single-dashboard.component.js.map +1 -1
  94. package/dist/lib/single-entity/single-entity.component.js +21 -17
  95. package/dist/lib/single-entity/single-entity.component.js.map +1 -1
  96. package/dist/lib/single-list-detail/single-list-detail.component.js +36 -30
  97. package/dist/lib/single-list-detail/single-list-detail.component.js.map +1 -1
  98. package/dist/lib/single-record/single-record.component.d.ts +1 -0
  99. package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
  100. package/dist/lib/single-record/single-record.component.js +16 -9
  101. package/dist/lib/single-record/single-record.component.js.map +1 -1
  102. package/dist/lib/single-report/single-report.component.d.ts +1 -7
  103. package/dist/lib/single-report/single-report.component.d.ts.map +1 -1
  104. package/dist/lib/single-report/single-report.component.js +16 -37
  105. package/dist/lib/single-report/single-report.component.js.map +1 -1
  106. package/dist/lib/single-view/single-view.component.d.ts +1 -2
  107. package/dist/lib/single-view/single-view.component.d.ts.map +1 -1
  108. package/dist/lib/single-view/single-view.component.js +35 -43
  109. package/dist/lib/single-view/single-view.component.js.map +1 -1
  110. package/dist/lib/style-guide-test/style-guide-test.component.js +20 -13
  111. package/dist/lib/style-guide-test/style-guide-test.component.js.map +1 -1
  112. package/dist/lib/tabbed-dashboard/tabbed-dashboard.component.d.ts +1 -1
  113. package/dist/lib/tabbed-dashboard/tabbed-dashboard.component.d.ts.map +1 -1
  114. package/dist/lib/tabbed-dashboard/tabbed-dashboard.component.js +8 -11
  115. package/dist/lib/tabbed-dashboard/tabbed-dashboard.component.js.map +1 -1
  116. package/dist/lib/user-notifications/user-notifications.component.d.ts.map +1 -1
  117. package/dist/lib/user-notifications/user-notifications.component.js +0 -5
  118. package/dist/lib/user-notifications/user-notifications.component.js.map +1 -1
  119. package/dist/module.d.ts +50 -46
  120. package/dist/module.d.ts.map +1 -1
  121. package/dist/module.js +23 -11
  122. package/dist/module.js.map +1 -1
  123. package/dist/public-api.d.ts +4 -0
  124. package/dist/public-api.d.ts.map +1 -1
  125. package/dist/public-api.js +5 -0
  126. package/dist/public-api.js.map +1 -1
  127. package/package.json +36 -35
@@ -0,0 +1,920 @@
1
+ import { Component, ViewChild, createComponent, ViewEncapsulation } from '@angular/core';
2
+ import { MJGlobal } from '@memberjunction/global';
3
+ import { BaseResourceComponent } from '@memberjunction/ng-shared';
4
+ import { ResourceData } from '@memberjunction/core-entities';
5
+ import { LogError, Metadata } from '@memberjunction/core';
6
+ import { ComponentCacheManager } from './component-cache-manager';
7
+ import * as i0 from "@angular/core";
8
+ import * as i1 from "@memberjunction/ng-base-application";
9
+ import * as i2 from "@angular/common";
10
+ const _c0 = ["glContainer"];
11
+ const _c1 = ["directContentContainer"];
12
+ function TabContainerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
13
+ i0.ɵɵelement(0, "div", 3, 0);
14
+ } }
15
+ function TabContainerComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
16
+ i0.ɵɵelement(0, "div", 4, 1);
17
+ } }
18
+ function TabContainerComponent_div_3_Template(rf, ctx) { if (rf & 1) {
19
+ const _r1 = i0.ɵɵgetCurrentView();
20
+ i0.ɵɵelementStart(0, "div", 6)(1, "div", 7);
21
+ i0.ɵɵlistener("click", function TabContainerComponent_div_3_Template_div_click_1_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onContextPin()); });
22
+ i0.ɵɵelement(2, "i", 8);
23
+ i0.ɵɵelementStart(3, "span");
24
+ i0.ɵɵtext(4);
25
+ i0.ɵɵelementEnd()();
26
+ i0.ɵɵelement(5, "div", 9);
27
+ i0.ɵɵelementStart(6, "div", 7);
28
+ i0.ɵɵlistener("click", function TabContainerComponent_div_3_Template_div_click_6_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onContextClose()); });
29
+ i0.ɵɵelement(7, "i", 10);
30
+ i0.ɵɵelementStart(8, "span");
31
+ i0.ɵɵtext(9, "Close Tab");
32
+ i0.ɵɵelementEnd()();
33
+ i0.ɵɵelementStart(10, "div", 7);
34
+ i0.ɵɵlistener("click", function TabContainerComponent_div_3_Template_div_click_10_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onContextCloseOthers()); });
35
+ i0.ɵɵelement(11, "i", 11);
36
+ i0.ɵɵelementStart(12, "span");
37
+ i0.ɵɵtext(13, "Close Others");
38
+ i0.ɵɵelementEnd()();
39
+ i0.ɵɵelementStart(14, "div", 7);
40
+ i0.ɵɵlistener("click", function TabContainerComponent_div_3_Template_div_click_14_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onContextCloseToRight()); });
41
+ i0.ɵɵelement(15, "i", 12);
42
+ i0.ɵɵelementStart(16, "span");
43
+ i0.ɵɵtext(17, "Close to Right");
44
+ i0.ɵɵelementEnd()()();
45
+ } if (rf & 2) {
46
+ const ctx_r1 = i0.ɵɵnextContext();
47
+ i0.ɵɵstyleProp("left", ctx_r1.contextMenuX, "px")("top", ctx_r1.contextMenuY, "px");
48
+ i0.ɵɵadvance(4);
49
+ i0.ɵɵtextInterpolate(ctx_r1.isContextTabPinned ? "Unpin Tab" : "Pin Tab");
50
+ } }
51
+ /**
52
+ * Container for Golden Layout tabs with app-colored styling.
53
+ *
54
+ * Handles:
55
+ * - Golden Layout initialization
56
+ * - Tab creation and styling
57
+ * - Lazy loading of tab content
58
+ * - Context menu for pin/close
59
+ * - Layout persistence
60
+ */
61
+ export class TabContainerComponent {
62
+ layoutManager;
63
+ workspaceManager;
64
+ appManager;
65
+ appRef;
66
+ environmentInjector;
67
+ cdr;
68
+ glContainer;
69
+ directContentContainer;
70
+ subscriptions = [];
71
+ layoutInitialized = false;
72
+ // Track component references for cleanup (legacy - keep for backward compat during transition)
73
+ componentRefs = new Map();
74
+ // NEW: Smart component cache for preserving state across tab switches
75
+ cacheManager;
76
+ // Single-resource mode: render component directly without Golden Layout
77
+ // This avoids the 20px height issue when GL header is hidden
78
+ useSingleResourceMode = false;
79
+ singleResourceComponentRef = null;
80
+ previousTabBarVisible = null;
81
+ currentSingleResourceSignature = null; // Track loaded content signature to avoid unnecessary reloads
82
+ isCreatingInitialTabs = false; // Flag to prevent syncTabsWithConfiguration during initial tab creation
83
+ // Context menu state
84
+ contextMenuVisible = false;
85
+ contextMenuX = 0;
86
+ contextMenuY = 0;
87
+ contextMenuTabId = null;
88
+ constructor(layoutManager, workspaceManager, appManager, appRef, environmentInjector, cdr) {
89
+ this.layoutManager = layoutManager;
90
+ this.workspaceManager = workspaceManager;
91
+ this.appManager = appManager;
92
+ this.appRef = appRef;
93
+ this.environmentInjector = environmentInjector;
94
+ this.cdr = cdr;
95
+ // Initialize component cache manager
96
+ this.cacheManager = new ComponentCacheManager(this.appRef);
97
+ }
98
+ ngOnInit() {
99
+ // Subscribe to tab events
100
+ this.subscriptions.push(this.layoutManager.TabShown.subscribe(event => {
101
+ this.onTabShown(event);
102
+ }), this.layoutManager.TabClosed.subscribe(tabId => {
103
+ this.cleanupTabComponent(tabId);
104
+ this.workspaceManager.CloseTab(tabId);
105
+ }), this.layoutManager.LayoutChanged.subscribe(event => {
106
+ const layout = this.layoutManager.SaveLayout();
107
+ this.workspaceManager.UpdateLayout(layout);
108
+ }), this.layoutManager.ActiveTab.subscribe(tabId => {
109
+ if (tabId) {
110
+ this.workspaceManager.SetActiveTab(tabId);
111
+ }
112
+ }), this.layoutManager.TabDoubleClicked.subscribe(tabId => {
113
+ this.workspaceManager.TogglePin(tabId);
114
+ }), this.layoutManager.TabRightClicked.subscribe(event => {
115
+ this.showContextMenu(event.x, event.y, event.tabId);
116
+ }));
117
+ // Subscribe to configuration changes to sync tabs
118
+ this.subscriptions.push(this.workspaceManager.Configuration.subscribe(config => {
119
+ if (config) {
120
+ if (this.useSingleResourceMode) {
121
+ // In single-resource mode, reload content if the tab content changed
122
+ // The same tab ID can have different content (tab gets reused)
123
+ const activeTab = config.tabs.find(t => t.id === config.activeTabId) || config.tabs[0];
124
+ if (activeTab) {
125
+ const signature = this.getTabContentSignature(activeTab);
126
+ if (signature !== this.currentSingleResourceSignature) {
127
+ this.loadSingleResourceContent();
128
+ }
129
+ }
130
+ }
131
+ else if (this.layoutInitialized && !this.isCreatingInitialTabs) {
132
+ // In multi-tab mode, sync with Golden Layout
133
+ // Skip during initial tab creation to avoid race condition (tabs would be created twice)
134
+ this.syncTabsWithConfiguration(config.tabs);
135
+ }
136
+ }
137
+ }));
138
+ // Subscribe to tab bar visibility changes for single-resource mode
139
+ this.subscriptions.push(this.workspaceManager.TabBarVisible.subscribe(tabBarVisible => {
140
+ this.handleTabBarVisibilityChange(tabBarVisible);
141
+ }));
142
+ }
143
+ ngAfterViewInit() {
144
+ // Initialize Golden Layout only if we're not in single-resource mode
145
+ if (!this.useSingleResourceMode) {
146
+ this.initializeGoldenLayout();
147
+ }
148
+ else {
149
+ // In single-resource mode, load content directly
150
+ this.loadSingleResourceContent();
151
+ }
152
+ }
153
+ /**
154
+ * Initialize Golden Layout and load tabs
155
+ * @param forceCreateTabs - If true, always creates tabs fresh from config.tabs instead of restoring saved layout
156
+ */
157
+ initializeGoldenLayout(forceCreateTabs = false) {
158
+ if (!this.glContainer?.nativeElement) {
159
+ console.warn('Golden Layout container not available, waiting...');
160
+ setTimeout(() => this.initializeGoldenLayout(forceCreateTabs), 50);
161
+ return;
162
+ }
163
+ if (this.layoutInitialized) {
164
+ return; // Already initialized
165
+ }
166
+ // Initialize Golden Layout
167
+ this.layoutManager.Initialize(this.glContainer.nativeElement);
168
+ // Mark layout as initialized
169
+ this.layoutInitialized = true;
170
+ // Load tabs from configuration
171
+ const config = this.workspaceManager.GetConfiguration();
172
+ if (!config || config.tabs.length === 0) {
173
+ return;
174
+ }
175
+ // Check if we have a saved layout structure with actual content
176
+ const hasSavedLayout = config.layout?.root?.content && config.layout.root.content.length > 0;
177
+ if (hasSavedLayout && !forceCreateTabs) {
178
+ // RESTORE SAVED LAYOUT - preserves drag/drop arrangements (stacks, columns, rows)
179
+ // This is the single source of truth for visual arrangement
180
+ console.log('[TabContainer.initializeGoldenLayout] Restoring saved layout structure');
181
+ this.layoutManager.LoadLayout(config.layout);
182
+ // Focus active tab and ensure proper sizing
183
+ setTimeout(() => {
184
+ if (config.activeTabId) {
185
+ this.layoutManager.FocusTab(config.activeTabId);
186
+ }
187
+ }, 50);
188
+ }
189
+ else {
190
+ // CREATE FRESH - no saved layout or forceCreateTabs=true
191
+ // Use config.tabs sorted by sequence to build a simple single-stack layout
192
+ console.log(`[TabContainer.initializeGoldenLayout] Creating ${config.tabs.length} tabs from config (sorted by sequence)`);
193
+ const sortedTabs = [...config.tabs].sort((a, b) => a.sequence - b.sequence);
194
+ this.isCreatingInitialTabs = true;
195
+ try {
196
+ sortedTabs.forEach(tab => {
197
+ this.createTab(tab);
198
+ });
199
+ }
200
+ finally {
201
+ this.isCreatingInitialTabs = false;
202
+ }
203
+ setTimeout(() => {
204
+ if (config.activeTabId) {
205
+ this.layoutManager.FocusTab(config.activeTabId);
206
+ }
207
+ }, 50);
208
+ }
209
+ }
210
+ ngOnDestroy() {
211
+ this.subscriptions.forEach(sub => sub.unsubscribe());
212
+ // Cleanup single-resource mode component if exists
213
+ this.cleanupSingleResourceComponent();
214
+ // Clear the component cache (destroys all components)
215
+ this.cacheManager.clearCache();
216
+ // Cleanup any legacy componentRefs
217
+ this.componentRefs.forEach((ref, tabId) => {
218
+ this.appRef.detachView(ref.hostView);
219
+ ref.destroy();
220
+ });
221
+ this.componentRefs.clear();
222
+ }
223
+ /**
224
+ * Handle changes to tab bar visibility - switches between single-resource and multi-tab modes
225
+ */
226
+ handleTabBarVisibilityChange(tabBarVisible) {
227
+ // Skip if no change
228
+ if (this.previousTabBarVisible === tabBarVisible) {
229
+ return;
230
+ }
231
+ this.previousTabBarVisible = tabBarVisible;
232
+ // Determine if we should use single-resource mode
233
+ const shouldUseSingleResourceMode = !tabBarVisible;
234
+ if (shouldUseSingleResourceMode !== this.useSingleResourceMode) {
235
+ console.log(`🔄 Switching to ${shouldUseSingleResourceMode ? 'single-resource' : 'multi-tab'} mode`);
236
+ this.useSingleResourceMode = shouldUseSingleResourceMode;
237
+ this.cdr.detectChanges();
238
+ if (this.useSingleResourceMode) {
239
+ // Transitioning to single-resource mode
240
+ // **CRITICAL FIX**: Wait for the template to render directContentContainer
241
+ // before trying to load content. detectChanges() only marks dirty, doesn't render immediately.
242
+ setTimeout(() => {
243
+ // First, destroy Golden Layout if it was initialized (prevents stale state)
244
+ if (this.layoutInitialized) {
245
+ console.log('[TabContainer] Destroying Golden Layout when transitioning to single-resource mode');
246
+ this.layoutManager.Destroy();
247
+ this.layoutInitialized = false;
248
+ }
249
+ // Load the active tab's content directly (now container will exist)
250
+ this.loadSingleResourceContent();
251
+ }, 0);
252
+ }
253
+ else {
254
+ // Transitioning to multi-tab mode
255
+ // Pin the previously displayed tab (it was the "current" content in single-resource mode)
256
+ // This ensures we only have ONE temporary tab at a time
257
+ const config = this.workspaceManager.GetConfiguration();
258
+ if (config && config.tabs.length > 0) {
259
+ // The new tab (just added via OpenTabForced) is now the activeTabId
260
+ // All OTHER unpinned tabs should be pinned since they represent content
261
+ // the user explicitly kept open
262
+ const updatedTabs = config.tabs.map(tab => {
263
+ // Pin all tabs except the newly active one (which is the temporary tab)
264
+ if (tab.id !== config.activeTabId && !tab.isPinned) {
265
+ return { ...tab, isPinned: true };
266
+ }
267
+ return tab;
268
+ });
269
+ // Only update if we actually changed something
270
+ const hasChanges = updatedTabs.some((tab, i) => tab.isPinned !== config.tabs[i].isPinned);
271
+ if (hasChanges) {
272
+ this.workspaceManager.UpdateConfiguration({
273
+ ...config,
274
+ tabs: updatedTabs
275
+ });
276
+ }
277
+ }
278
+ // Clean up direct component, Golden Layout will handle tabs
279
+ this.cleanupSingleResourceComponent();
280
+ this.currentSingleResourceSignature = null; // Reset tracking
281
+ // Reset layout initialized flag since we're switching from single-resource mode
282
+ // The gl-container is a new DOM element (due to @if), so we need fresh initialization
283
+ this.layoutInitialized = false;
284
+ // Initialize Golden Layout - use setTimeout to allow the template to update first
285
+ // and ensure the gl-container div exists in the DOM
286
+ // IMPORTANT: Use forceCreateTabs=true to create tabs fresh from config.tabs
287
+ // instead of restoring potentially stale saved layout structure
288
+ setTimeout(() => {
289
+ this.initializeGoldenLayout(true /* forceCreateTabs */);
290
+ }, 0);
291
+ }
292
+ }
293
+ }
294
+ /**
295
+ * Load content directly for single-resource mode (bypasses Golden Layout)
296
+ */
297
+ async loadSingleResourceContent() {
298
+ // Wait for next tick to ensure the container is rendered
299
+ await Promise.resolve();
300
+ const config = this.workspaceManager.GetConfiguration();
301
+ if (!config || config.tabs.length === 0) {
302
+ return;
303
+ }
304
+ // Get the active tab (or first tab)
305
+ const activeTab = config.tabs.find(t => t.id === config.activeTabId) || config.tabs[0];
306
+ if (!activeTab) {
307
+ return;
308
+ }
309
+ // Track which content we're loading (signature includes resource type and record ID)
310
+ const newSignature = this.getTabContentSignature(activeTab);
311
+ if (this.currentSingleResourceSignature === newSignature) {
312
+ // Content already loaded, no action needed
313
+ return;
314
+ }
315
+ this.currentSingleResourceSignature = newSignature;
316
+ // Get the container element
317
+ const container = this.directContentContainer?.nativeElement;
318
+ if (!container) {
319
+ console.warn('Direct content container not available yet, retrying...');
320
+ // Retry after view is updated
321
+ setTimeout(() => this.loadSingleResourceContent(), 50);
322
+ return;
323
+ }
324
+ // Create ResourceData from tab
325
+ const resourceData = await this.getResourceDataFromTab(activeTab);
326
+ if (!resourceData) {
327
+ LogError(`Unable to create ResourceData for tab: ${activeTab.title}`);
328
+ return;
329
+ }
330
+ // Get driver class for component lookup
331
+ const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
332
+ // **OPTIMIZATION: Check cache first to reuse existing loaded component**
333
+ const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId);
334
+ if (cached) {
335
+ console.log(`♻️ Reusing cached component for single-resource mode: ${driverClass}`);
336
+ // Clean up previous single-resource component (if different)
337
+ this.cleanupSingleResourceComponent();
338
+ // Detach from tab tracking (it was attached to a tab in Golden Layout)
339
+ this.cacheManager.markAsDetached(activeTab.id);
340
+ // Reattach the cached wrapper element to single-resource container
341
+ cached.wrapperElement.style.height = "100%"; // Ensure full height
342
+ container.appendChild(cached.wrapperElement);
343
+ // Store reference for cleanup
344
+ this.singleResourceComponentRef = cached.componentRef;
345
+ console.log('✅ Single-resource component transferred from cache (instant!)');
346
+ return;
347
+ }
348
+ // **Fallback: Create new component if not in cache**
349
+ console.log(`📦 Creating new component for single-resource mode: ${driverClass}`);
350
+ // Get the component registration
351
+ const resourceReg = MJGlobal.Instance.ClassFactory.GetRegistration(BaseResourceComponent, driverClass);
352
+ if (!resourceReg) {
353
+ LogError(`Unable to find resource registration for driver class: ${driverClass}`);
354
+ return;
355
+ }
356
+ // Clean up previous component if any
357
+ this.cleanupSingleResourceComponent();
358
+ // Create the component dynamically
359
+ const componentRef = createComponent(resourceReg.SubClass, {
360
+ environmentInjector: this.environmentInjector
361
+ });
362
+ // Attach to Angular's change detection
363
+ this.appRef.attachView(componentRef.hostView);
364
+ // Set the resource data on the component
365
+ const instance = componentRef.instance;
366
+ instance.Data = resourceData;
367
+ // Wire up events
368
+ instance.LoadCompleteEvent = () => {
369
+ console.log('✅ Single-resource component loaded');
370
+ };
371
+ // Get the native element and append to container
372
+ const nativeElement = componentRef.hostView.rootNodes[0];
373
+ container.appendChild(nativeElement);
374
+ // now make sure that the container's direct child is 100% height
375
+ if (container.children?.length > 0) {
376
+ container.children[0].style.height = "100%";
377
+ }
378
+ // Store reference for cleanup
379
+ this.singleResourceComponentRef = componentRef;
380
+ }
381
+ /**
382
+ * Clean up single-resource mode component
383
+ */
384
+ cleanupSingleResourceComponent() {
385
+ if (this.singleResourceComponentRef) {
386
+ this.appRef.detachView(this.singleResourceComponentRef.hostView);
387
+ this.singleResourceComponentRef.destroy();
388
+ this.singleResourceComponentRef = null;
389
+ }
390
+ // Clear the container
391
+ const container = this.directContentContainer?.nativeElement;
392
+ if (container) {
393
+ container.innerHTML = '';
394
+ }
395
+ }
396
+ /**
397
+ * Generate a signature for tab content to detect when content changes
398
+ * This is needed because in single-resource mode, the same tab ID can have different content
399
+ */
400
+ getTabContentSignature(tab) {
401
+ // Include key identifying fields that determine what component/content is shown
402
+ const parts = [
403
+ tab.applicationId,
404
+ tab.configuration?.resourceType || '',
405
+ tab.configuration?.driverClass || '',
406
+ tab.resourceRecordId || '',
407
+ tab.configuration?.route || ''
408
+ ];
409
+ return parts.join('|');
410
+ }
411
+ /**
412
+ * Create a tab in Golden Layout from workspace tab data
413
+ */
414
+ createTab(tab) {
415
+ const app = this.appManager.GetAppById(tab.applicationId);
416
+ const appColor = app?.GetColor() || '#757575';
417
+ const state = {
418
+ tabId: tab.id,
419
+ appId: tab.applicationId,
420
+ appColor,
421
+ title: tab.title,
422
+ route: tab.configuration['route'] || '',
423
+ isPinned: tab.isPinned,
424
+ isLoaded: false
425
+ };
426
+ this.layoutManager.AddTab(state);
427
+ // Load display name in background without loading full component
428
+ this.updateTabDisplayName(tab);
429
+ }
430
+ /**
431
+ * Handle tab shown event for lazy loading
432
+ */
433
+ async onTabShown(event) {
434
+ if (event.isFirstShow) {
435
+ // Load content for this tab
436
+ await this.loadTabContent(event.tabId, event.container);
437
+ this.layoutManager.MarkTabLoaded(event.tabId);
438
+ }
439
+ }
440
+ /**
441
+ * Load content into a tab container
442
+ * Uses component cache to reuse components for same resources
443
+ */
444
+ async loadTabContent(tabId, container) {
445
+ try {
446
+ const tab = this.workspaceManager.GetTab(tabId);
447
+ if (!tab) {
448
+ LogError(`Tab not found: ${tabId}`);
449
+ return;
450
+ }
451
+ // Get the container element from Golden Layout
452
+ const glContainer = container;
453
+ if (!glContainer?.element) {
454
+ LogError('Golden Layout container element not found');
455
+ return;
456
+ }
457
+ // Extract resource data from tab configuration
458
+ const resourceData = await this.getResourceDataFromTab(tab);
459
+ if (!resourceData) {
460
+ LogError(`Unable to create ResourceData for tab: ${tab.title}`);
461
+ return;
462
+ }
463
+ // Clear any existing content from the container (important for tab reuse)
464
+ glContainer.element.innerHTML = '';
465
+ // Get driver class for cache lookup (resolves to actual component class name)
466
+ const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
467
+ // Check if we have a cached component for this resource
468
+ const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', tab.applicationId);
469
+ if (cached) {
470
+ console.log(`♻️ Reusing cached component for ${resourceData.ResourceType} (driver: ${driverClass})`);
471
+ // Reattach the cached wrapper element
472
+ glContainer.element.appendChild(cached.wrapperElement);
473
+ // Mark as attached to this tab
474
+ this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, tabId);
475
+ // Keep legacy componentRefs map updated
476
+ this.componentRefs.set(tabId, cached.componentRef);
477
+ // If resource is already loaded, update tab title immediately
478
+ const instance = cached.componentRef.instance;
479
+ if (instance.LoadComplete) {
480
+ this.updateTabTitleFromResource(tabId, instance, resourceData);
481
+ }
482
+ return;
483
+ }
484
+ // No cached component found - create new one
485
+ console.log(`🆕 Creating new component for ${resourceData.ResourceType} using driver class: ${driverClass}`);
486
+ // Get the component registration using the driver class
487
+ const resourceReg = MJGlobal.Instance.ClassFactory.GetRegistration(BaseResourceComponent, driverClass);
488
+ if (!resourceReg) {
489
+ LogError(`Unable to find resource registration for driver class: ${driverClass}`);
490
+ return;
491
+ }
492
+ // Create the component dynamically
493
+ const componentRef = createComponent(resourceReg.SubClass, {
494
+ environmentInjector: this.environmentInjector
495
+ });
496
+ // Attach to Angular's change detection
497
+ this.appRef.attachView(componentRef.hostView);
498
+ // Set the resource data on the component
499
+ const instance = componentRef.instance;
500
+ instance.Data = resourceData;
501
+ // Wire up events
502
+ instance.LoadCompleteEvent = () => {
503
+ // Tab content loaded - update tab title with resource display name
504
+ this.updateTabTitleFromResource(tabId, instance, resourceData);
505
+ };
506
+ instance.ResourceRecordSavedEvent = (entity) => {
507
+ // Update tab title if needed
508
+ if (entity && entity.Get && entity.Get('Name')) {
509
+ // TODO: Implement UpdateTabTitle in WorkspaceStateManager
510
+ }
511
+ };
512
+ // Create a container div for the component
513
+ const componentElement = document.createElement('div');
514
+ componentElement.className = 'tab-content-wrapper';
515
+ componentElement.style.cssText = 'width: 100%; height: 100%;';
516
+ // Append the component's native element
517
+ const nativeElement = componentRef.hostView.rootNodes[0];
518
+ componentElement.appendChild(nativeElement);
519
+ // Add to Golden Layout container
520
+ glContainer.element.appendChild(componentElement);
521
+ // Cache the component for future reuse
522
+ this.cacheManager.cacheComponent(componentRef, componentElement, resourceData, tabId);
523
+ // Store reference for cleanup (legacy)
524
+ this.componentRefs.set(tabId, componentRef);
525
+ }
526
+ catch (e) {
527
+ LogError(e);
528
+ }
529
+ }
530
+ /**
531
+ * Update tab display name in background without loading full component
532
+ * This ensures all tabs show proper names immediately, not just when clicked
533
+ */
534
+ async updateTabDisplayName(tab) {
535
+ try {
536
+ // Only update display names for resource-based tabs
537
+ const resourceType = tab.configuration['resourceType'];
538
+ if (!resourceType) {
539
+ return;
540
+ }
541
+ // Get ResourceData from tab
542
+ const resourceData = await this.getResourceDataFromTab(tab);
543
+ if (!resourceData) {
544
+ return;
545
+ }
546
+ // Get the resource registration to access GetResourceDisplayName without loading full component
547
+ const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
548
+ const resourceReg = MJGlobal.Instance.ClassFactory.GetRegistration(BaseResourceComponent, driverClass);
549
+ if (!resourceReg) {
550
+ return;
551
+ }
552
+ // Create a lightweight instance just to call GetResourceDisplayName
553
+ const tempInstance = new resourceReg.SubClass();
554
+ const displayName = await tempInstance.GetResourceDisplayName(resourceData);
555
+ if (displayName && displayName !== tab.title) {
556
+ console.log('[TabContainer.updateTabDisplayName] Updating tab title:', {
557
+ tabId: tab.id,
558
+ from: tab.title,
559
+ to: displayName
560
+ });
561
+ // Update the tab title in Golden Layout
562
+ this.layoutManager.UpdateTabStyle(tab.id, { title: displayName });
563
+ // Update the tab title in workspace configuration for persistence
564
+ this.workspaceManager.UpdateTabTitle(tab.id, displayName);
565
+ }
566
+ }
567
+ catch (error) {
568
+ console.error('[TabContainer.updateTabDisplayName] Error updating tab display name:', error);
569
+ }
570
+ }
571
+ /**
572
+ * Update tab title with resource display name after resource loads
573
+ */
574
+ async updateTabTitleFromResource(tabId, resourceComponent, resourceData) {
575
+ try {
576
+ console.log('[TabContainer.updateTabTitleFromResource] Getting display name for tab:', tabId);
577
+ // Get the display name from the resource component
578
+ const displayName = await resourceComponent.GetResourceDisplayName(resourceData);
579
+ console.log('[TabContainer.updateTabTitleFromResource] Got display name:', displayName);
580
+ if (!displayName) {
581
+ console.log('[TabContainer.updateTabTitleFromResource] No display name returned, keeping current title');
582
+ return;
583
+ }
584
+ // Update the tab title in Golden Layout
585
+ this.layoutManager.UpdateTabStyle(tabId, { title: displayName });
586
+ console.log('[TabContainer.updateTabTitleFromResource] Updated Golden Layout tab title to:', displayName);
587
+ // Update the tab title in workspace configuration for persistence
588
+ this.workspaceManager.UpdateTabTitle(tabId, displayName);
589
+ console.log('[TabContainer.updateTabTitleFromResource] Updated workspace configuration tab title');
590
+ }
591
+ catch (error) {
592
+ console.error('[TabContainer.updateTabTitleFromResource] Error updating tab title:', error);
593
+ }
594
+ }
595
+ /**
596
+ * Convert tab configuration to ResourceData
597
+ */
598
+ async getResourceDataFromTab(tab) {
599
+ const config = tab.configuration;
600
+ // Extract resource type from configuration or route
601
+ let resourceType = config['resourceType'];
602
+ if (!resourceType && config['route']) {
603
+ // Parse route to determine resource type
604
+ resourceType = this.getResourceTypeFromRoute(config['route']);
605
+ }
606
+ if (!resourceType) {
607
+ console.error('[TabContainer.getResourceDataFromTab] No resourceType found in config or route');
608
+ return null;
609
+ }
610
+ // Determine the driver class to use for component instantiation
611
+ let driverClass = resourceType; // Default: use resourceType as driver class
612
+ // For Custom resource type, get DriverClass from configuration or ResourceType metadata
613
+ if (resourceType.toLowerCase() === 'custom') {
614
+ // Custom resource type uses NavItem's DriverClass
615
+ driverClass = config['driverClass'];
616
+ if (!driverClass) {
617
+ LogError('Custom resource type requires driverClass in configuration');
618
+ console.error('[TabContainer.getResourceDataFromTab] Missing driverClass for Custom resource type');
619
+ return null;
620
+ }
621
+ }
622
+ else {
623
+ // For standard resource types, look up DriverClass from metadata
624
+ const resourceTypeEntity = await this.getResourceTypeEntity(resourceType);
625
+ if (resourceTypeEntity?.DriverClass) {
626
+ driverClass = resourceTypeEntity.DriverClass;
627
+ }
628
+ // If no DriverClass in metadata, fall back to resourceType (backward compatibility)
629
+ }
630
+ // Include applicationId and driverClass in configuration
631
+ const resourceConfig = {
632
+ ...config,
633
+ applicationId: tab.applicationId,
634
+ resourceTypeDriverClass: driverClass // Store resolved driver class for component lookup
635
+ };
636
+ // Get ResourceRecordID from config or fall back to tab.resourceRecordId
637
+ // Important: Some tabs store the record ID in config['recordId'], others in tab.resourceRecordId
638
+ const resourceRecordId = config['recordId'] || tab.resourceRecordId || '';
639
+ const resourceData = new ResourceData({
640
+ ResourceTypeID: await this.getResourceTypeId(resourceType),
641
+ ResourceRecordID: resourceRecordId,
642
+ Configuration: resourceConfig
643
+ });
644
+ return resourceData;
645
+ }
646
+ static _resourceTypesDataset = null;
647
+ /**
648
+ * Get ResourceType entity by name (includes DriverClass field)
649
+ */
650
+ async getResourceTypeEntity(resourceType) {
651
+ const md = new Metadata();
652
+ const ds = TabContainerComponent._resourceTypesDataset || await md.GetDatasetByName("ResourceTypes");
653
+ if (!ds || !ds.Success || ds.Results.length === 0) {
654
+ return null;
655
+ }
656
+ if (!TabContainerComponent._resourceTypesDataset) {
657
+ TabContainerComponent._resourceTypesDataset = ds; // cache for next time
658
+ }
659
+ const result = ds.Results.find(r => r.Code.trim().toLowerCase() === 'resourcetypes');
660
+ if (result && result.Results?.length > 0) {
661
+ const rt = result.Results.find(rt => rt.Name.trim().toLowerCase() === resourceType.trim().toLowerCase());
662
+ return rt || null;
663
+ }
664
+ return null;
665
+ }
666
+ async getResourceTypeId(resourceType) {
667
+ const rt = await this.getResourceTypeEntity(resourceType);
668
+ if (rt) {
669
+ return rt.ID;
670
+ }
671
+ throw new Error(`ResourceType ID not found for type: ${resourceType}`);
672
+ }
673
+ /**
674
+ * Determine resource type from route
675
+ */
676
+ getResourceTypeFromRoute(route) {
677
+ // Parse route segments to determine resource type
678
+ const segments = route.split('/').filter(s => s);
679
+ if (segments.length === 0) {
680
+ return 'home';
681
+ }
682
+ // Common route patterns
683
+ if (route.includes('/record/')) {
684
+ return 'record';
685
+ }
686
+ if (route.includes('/view/')) {
687
+ return 'view';
688
+ }
689
+ if (route.includes('/dashboard/')) {
690
+ return 'dashboard';
691
+ }
692
+ if (route.includes('/report/')) {
693
+ return 'report';
694
+ }
695
+ if (route.includes('/search')) {
696
+ return 'search';
697
+ }
698
+ if (route.includes('/query/')) {
699
+ return 'query';
700
+ }
701
+ // Default based on first segment
702
+ return segments[0] || 'home';
703
+ }
704
+ /**
705
+ * Cleanup a tab's component
706
+ * Detaches from DOM but keeps in cache for potential reuse
707
+ */
708
+ cleanupTabComponent(tabId) {
709
+ // First, try to detach from cache (preserves component for reuse)
710
+ const cachedInfo = this.cacheManager.markAsDetached(tabId);
711
+ if (cachedInfo) {
712
+ console.log(`📎 Detached component from tab ${tabId}, available for reuse`);
713
+ // Remove from legacy componentRefs but keep in cache
714
+ this.componentRefs.delete(tabId);
715
+ }
716
+ else {
717
+ // Fallback: destroy if not in cache (shouldn't happen in normal flow)
718
+ const componentRef = this.componentRefs.get(tabId);
719
+ if (componentRef) {
720
+ this.appRef.detachView(componentRef.hostView);
721
+ componentRef.destroy();
722
+ this.componentRefs.delete(tabId);
723
+ }
724
+ }
725
+ }
726
+ /**
727
+ * Sync tabs with configuration changes
728
+ */
729
+ syncTabsWithConfiguration(tabs) {
730
+ // Get existing tab IDs from Golden Layout
731
+ const existingTabIds = this.layoutManager.GetAllTabIds();
732
+ // Get tab IDs from configuration
733
+ const configTabIds = tabs.map(tab => tab.id);
734
+ // Remove tabs that are no longer in configuration
735
+ existingTabIds.forEach(tabId => {
736
+ if (!configTabIds.includes(tabId)) {
737
+ console.log('[TabContainer.syncTabsWithConfiguration] Removing tab not in config:', tabId);
738
+ this.layoutManager.RemoveTab(tabId);
739
+ }
740
+ });
741
+ // Create tabs that don't exist yet
742
+ tabs.forEach(tab => {
743
+ if (!existingTabIds.includes(tab.id)) {
744
+ this.createTab(tab);
745
+ }
746
+ else {
747
+ // Check if tab content needs to be reloaded (app or resource type changed)
748
+ const existingComponentRef = this.componentRefs.get(tab.id);
749
+ if (existingComponentRef) {
750
+ const existingResourceData = existingComponentRef.instance.Data;
751
+ // For Custom resource types, also check driverClass to distinguish between different custom resources
752
+ const existingDriverClass = existingResourceData?.Configuration?.driverClass || existingResourceData?.Configuration?.resourceTypeDriverClass;
753
+ const newDriverClass = tab.configuration['driverClass'] || tab.configuration['resourceTypeDriverClass'];
754
+ // Normalize record IDs for comparison (treat null/undefined as empty string)
755
+ // IMPORTANT: Check both tab.resourceRecordId AND tab.configuration['recordId']
756
+ // because for nav items, the recordId is stored in configuration, not resourceRecordId
757
+ const existingRecordId = existingResourceData?.ResourceRecordID || '';
758
+ const newRecordId = tab.resourceRecordId || tab.configuration['recordId'] || '';
759
+ const needsReload = existingResourceData?.ResourceType !== tab.configuration['resourceType'] ||
760
+ existingResourceData?.Configuration?.applicationId !== tab.applicationId ||
761
+ existingRecordId !== newRecordId ||
762
+ (tab.configuration['resourceType'] === 'Custom' && existingDriverClass !== newDriverClass);
763
+ if (needsReload) {
764
+ console.log('[TabContainer.syncTabsWithConfiguration] Tab content changed, reloading:', {
765
+ tabId: tab.id,
766
+ title: tab.title,
767
+ existingRecordId,
768
+ newRecordId,
769
+ configRecordId: tab.configuration['recordId'],
770
+ recordIdChanged: existingRecordId !== newRecordId
771
+ });
772
+ // Clean up old component
773
+ this.cleanupTabComponent(tab.id);
774
+ // Mark tab as not loaded so it will reload when shown
775
+ this.layoutManager.MarkTabNotLoaded(tab.id);
776
+ // Update display name in background
777
+ this.updateTabDisplayName(tab);
778
+ // If this tab is currently active, reload it immediately
779
+ const config = this.workspaceManager.GetConfiguration();
780
+ if (config?.activeTabId === tab.id) {
781
+ const glContainer = this.layoutManager.GetContainer(tab.id);
782
+ if (glContainer) {
783
+ this.loadTabContent(tab.id, glContainer);
784
+ }
785
+ }
786
+ }
787
+ }
788
+ // Update styling for existing tabs
789
+ const app = this.appManager.GetAppById(tab.applicationId);
790
+ this.layoutManager.UpdateTabStyle(tab.id, {
791
+ isPinned: tab.isPinned,
792
+ title: tab.title,
793
+ appColor: app?.GetColor() || '#757575'
794
+ });
795
+ }
796
+ });
797
+ // Focus the active tab
798
+ const config = this.workspaceManager.GetConfiguration();
799
+ if (config?.activeTabId) {
800
+ this.layoutManager.FocusTab(config.activeTabId);
801
+ }
802
+ }
803
+ /**
804
+ * Show context menu
805
+ */
806
+ showContextMenu(x, y, tabId) {
807
+ this.contextMenuX = x;
808
+ this.contextMenuY = y;
809
+ this.contextMenuTabId = tabId;
810
+ this.contextMenuVisible = true;
811
+ // Close menu when clicking outside - use setTimeout to avoid immediate trigger
812
+ setTimeout(() => {
813
+ const clickHandler = (event) => {
814
+ const target = event.target;
815
+ if (!target.closest('.context-menu')) {
816
+ this.hideContextMenu();
817
+ document.removeEventListener('click', clickHandler);
818
+ document.removeEventListener('keydown', keyHandler);
819
+ }
820
+ };
821
+ const keyHandler = (event) => {
822
+ if (event.key === 'Escape') {
823
+ this.hideContextMenu();
824
+ document.removeEventListener('click', clickHandler);
825
+ document.removeEventListener('keydown', keyHandler);
826
+ }
827
+ };
828
+ document.addEventListener('click', clickHandler);
829
+ document.addEventListener('keydown', keyHandler);
830
+ }, 0);
831
+ }
832
+ /**
833
+ * Hide context menu
834
+ */
835
+ hideContextMenu() {
836
+ this.contextMenuVisible = false;
837
+ this.contextMenuTabId = null;
838
+ }
839
+ /**
840
+ * Check if context menu tab is pinned
841
+ */
842
+ get isContextTabPinned() {
843
+ if (!this.contextMenuTabId)
844
+ return false;
845
+ const tab = this.workspaceManager.GetTab(this.contextMenuTabId);
846
+ return tab?.isPinned || false;
847
+ }
848
+ /**
849
+ * Toggle pin from context menu
850
+ */
851
+ onContextPin() {
852
+ if (this.contextMenuTabId) {
853
+ this.workspaceManager.TogglePin(this.contextMenuTabId);
854
+ }
855
+ this.hideContextMenu();
856
+ }
857
+ /**
858
+ * Close tab from context menu
859
+ */
860
+ onContextClose() {
861
+ if (this.contextMenuTabId) {
862
+ this.layoutManager.RemoveTab(this.contextMenuTabId);
863
+ }
864
+ this.hideContextMenu();
865
+ }
866
+ /**
867
+ * Close all other tabs from context menu
868
+ */
869
+ onContextCloseOthers() {
870
+ console.log('[TabContainer.onContextCloseOthers] Called with tabId:', this.contextMenuTabId);
871
+ if (this.contextMenuTabId) {
872
+ const config = this.workspaceManager.GetConfiguration();
873
+ console.log('[TabContainer.onContextCloseOthers] Current tabs:', config?.tabs.length);
874
+ this.workspaceManager.CloseOtherTabs(this.contextMenuTabId);
875
+ }
876
+ this.hideContextMenu();
877
+ }
878
+ /**
879
+ * Close tabs to the right from context menu
880
+ */
881
+ onContextCloseToRight() {
882
+ console.log('[TabContainer.onContextCloseToRight] Called with tabId:', this.contextMenuTabId);
883
+ if (this.contextMenuTabId) {
884
+ const config = this.workspaceManager.GetConfiguration();
885
+ console.log('[TabContainer.onContextCloseToRight] Current tabs:', config?.tabs.length);
886
+ this.workspaceManager.CloseTabsToRight(this.contextMenuTabId);
887
+ }
888
+ this.hideContextMenu();
889
+ }
890
+ static ɵfac = function TabContainerComponent_Factory(t) { return new (t || TabContainerComponent)(i0.ɵɵdirectiveInject(i1.GoldenLayoutManager), i0.ɵɵdirectiveInject(i1.WorkspaceStateManager), i0.ɵɵdirectiveInject(i1.ApplicationManager), i0.ɵɵdirectiveInject(i0.ApplicationRef), i0.ɵɵdirectiveInject(i0.EnvironmentInjector), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
891
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: TabContainerComponent, selectors: [["mj-tab-container"]], viewQuery: function TabContainerComponent_Query(rf, ctx) { if (rf & 1) {
892
+ i0.ɵɵviewQuery(_c0, 5);
893
+ i0.ɵɵviewQuery(_c1, 5);
894
+ } if (rf & 2) {
895
+ let _t;
896
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.glContainer = _t.first);
897
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.directContentContainer = _t.first);
898
+ } }, decls: 4, vars: 2, consts: [["directContentContainer", ""], ["glContainer", ""], [1, "tab-container"], [1, "direct-content-container"], [1, "gl-container"], ["class", "context-menu", 3, "left", "top", 4, "ngIf"], [1, "context-menu"], [1, "context-menu-item", 3, "click"], [1, "fa-solid", "fa-thumbtack"], [1, "context-menu-divider"], [1, "fa-solid", "fa-xmark"], [1, "fa-solid", "fa-layer-group"], [1, "fa-solid", "fa-angles-right"]], template: function TabContainerComponent_Template(rf, ctx) { if (rf & 1) {
899
+ i0.ɵɵelementStart(0, "div", 2);
900
+ i0.ɵɵtemplate(1, TabContainerComponent_Conditional_1_Template, 2, 0, "div", 3)(2, TabContainerComponent_Conditional_2_Template, 2, 0, "div", 4)(3, TabContainerComponent_div_3_Template, 18, 5, "div", 5);
901
+ i0.ɵɵelementEnd();
902
+ } if (rf & 2) {
903
+ i0.ɵɵadvance();
904
+ i0.ɵɵconditional(ctx.useSingleResourceMode ? 1 : 2);
905
+ i0.ɵɵadvance(2);
906
+ i0.ɵɵproperty("ngIf", ctx.contextMenuVisible);
907
+ } }, dependencies: [i2.NgIf], styles: [":host {\n display: flex;\n flex: 1;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n.tab-container {\n display: flex;\n flex-direction: column;\n flex: 1;\n width: 100%;\n overflow: hidden;\n}\n\n.gl-container {\n flex: 1;\n width: 100%;\n height: 100%;\n position: relative;\n background: white;\n}\n\n/* Direct content container for single-resource mode */\n/* Renders components directly without Golden Layout overhead */\n.direct-content-container {\n flex: 1;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n background: white;\n overflow: hidden;\n}\n\n.tab-content-container {\n background: white;\n color: #333;\n padding: 20px;\n}\n\n/* Context Menu */\n.context-menu {\n position: fixed;\n background: white;\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n min-width: 150px;\n z-index: 10001;\n overflow: hidden;\n}\n\n.context-menu .context-menu-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 14px;\n cursor: pointer;\n font-size: 13px;\n color: #424242;\n transition: background 0.15s;\n}\n\n.context-menu .context-menu-item i {\n width: 16px;\n text-align: center;\n color: #757575;\n}\n\n.context-menu .context-menu-item:hover {\n background: #f5f5f5;\n}\n\n.context-menu .context-menu-divider {\n height: 1px;\n background: #e0e0e0;\n margin: 4px 0;\n}\n\n/* Override Golden Layout styles */\n/* Global overrides */\nmj-tab-container .lm_content {\n background: white !important;\n}\n\nmj-tab-container .lm_item_container {\n background: white !important;\n}\n\n/* Tab content wrapper - allow scrolling */\n.tab-content-wrapper {\n overflow: auto !important;\n}\n\n/* Make tabs larger and easier to click */\nmj-tab-container .lm_header {\n height: 38px !important;\n padding-top: 2px !important;\n padding-left: 4px !important;\n background: #f5f5f5 !important;\n border-bottom: 1px solid #ebebeb !important;\n overflow: visible !important;\n box-sizing: border-box !important;\n}\n\nmj-tab-container .lm_tabs {\n height: 36px !important;\n}\n\n/* Hide Golden Layout window controls */\nmj-tab-container .lm_controls {\n display: none !important;\n}\n\nmj-tab-container .lm_header .lm_tab {\n padding: 0 16px !important;\n font-size: 13px !important;\n height: 35px !important;\n line-height: 35px !important;\n box-sizing: border-box !important;\n cursor: pointer !important;\n user-select: none !important;\n background: transparent !important;\n border: none !important;\n border-bottom: 1px solid #ebebeb !important;\n transition: all 0.15s ease !important;\n position: relative !important;\n z-index: 1 !important;\n margin-right: 1px !important;\n}\n\n/* App color accent - left edge indicator */\nmj-tab-container .lm_header .lm_tab::before {\n content: '' !important;\n position: absolute !important;\n left: 0 !important;\n top: 4px !important;\n bottom: 4px !important;\n width: 3px !important;\n border-radius: 0 2px 2px 0 !important;\n background-color: var(--app-color, transparent) !important;\n opacity: 0.6 !important;\n transition: all 0.15s ease !important;\n}\n\nmj-tab-container .lm_header .lm_tab:hover {\n background: #e8e8e8 !important;\n}\n\nmj-tab-container .lm_header .lm_tab:hover .lm_close_tab {\n opacity: 0.7 !important;\n}\n\nmj-tab-container .lm_header .lm_tab:hover::before {\n opacity: 0.8 !important;\n}\n\nmj-tab-container .lm_header .lm_tab.lm_active {\n background: white !important;\n height: 37px !important;\n margin-bottom: -1px !important;\n margin-right: 0 !important;\n border: 1px solid #ebebeb !important;\n border-bottom-color: white !important;\n border-radius: 4px 4px 0 0 !important;\n z-index: 2 !important;\n}\n\n/* Enhanced app color accent for active tab */\nmj-tab-container .lm_header .lm_tab.lm_active::before {\n opacity: 1 !important;\n width: 3px !important;\n top: 2px !important;\n bottom: 2px !important;\n box-shadow: 0 0 6px var(--app-color, transparent) !important;\n}\n\nmj-tab-container .lm_title {\n cursor: pointer !important;\n user-select: none !important;\n}\n\nmj-tab-container .lm_close_tab {\n position: absolute !important;\n right: 4px !important;\n top: 50% !important;\n transform: translateY(-50%) !important;\n width: 16px !important;\n height: 16px !important;\n cursor: pointer !important;\n opacity: 0 !important;\n transition: all 0.15s ease !important;\n flex-shrink: 0 !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n font-size: 10px !important;\n color: #757575 !important;\n}\n\nmj-tab-container .lm_close_tab:hover {\n opacity: 1 !important;\n color: #c62828 !important;\n}\n\n/* Show close button on hover (except pinned) */\nmj-tab-container .lm_header .lm_tab:hover:not(.pinned) .lm_close_tab {\n opacity: 0.7 !important;\n}\n\n/* Pinned tabs never show close button */\nmj-tab-container .lm_header .lm_tab.pinned .lm_close_tab {\n display: none !important;\n}\n\n/* Hide close button on active tab by default */\nmj-tab-container .lm_active .lm_close_tab {\n opacity: 0 !important;\n}\n\n/* Show close button when hovering active tab */\nmj-tab-container .lm_active:hover .lm_close_tab {\n opacity: 0.7 !important;\n}\n\n/* Adjust padding for close button */\nmj-tab-container .lm_header .lm_tab {\n padding-right: 24px !important;\n}\n"], encapsulation: 2 });
908
+ }
909
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(TabContainerComponent, [{
910
+ type: Component,
911
+ args: [{ selector: 'mj-tab-container', encapsulation: ViewEncapsulation.None, template: "<div class=\"tab-container\">\n\n <!-- Single-Resource Mode: Direct component rendering without Golden Layout -->\n <!-- This avoids the 20px height issue when GL header is hidden -->\n @if (useSingleResourceMode) {\n <div #directContentContainer class=\"direct-content-container\"></div>\n } @else {\n <!-- Multi-Tab Mode: Golden Layout Container -->\n <div #glContainer class=\"gl-container\"></div>\n }\n\n <!-- Context Menu (only relevant in multi-tab mode) -->\n <div\n class=\"context-menu\"\n *ngIf=\"contextMenuVisible\"\n [style.left.px]=\"contextMenuX\"\n [style.top.px]=\"contextMenuY\">\n <div class=\"context-menu-item\" (click)=\"onContextPin()\">\n <i class=\"fa-solid fa-thumbtack\"></i>\n <span>{{ isContextTabPinned ? 'Unpin Tab' : 'Pin Tab' }}</span>\n </div>\n <div class=\"context-menu-divider\"></div>\n <div class=\"context-menu-item\" (click)=\"onContextClose()\">\n <i class=\"fa-solid fa-xmark\"></i>\n <span>Close Tab</span>\n </div>\n <div class=\"context-menu-item\" (click)=\"onContextCloseOthers()\">\n <i class=\"fa-solid fa-layer-group\"></i>\n <span>Close Others</span>\n </div>\n <div class=\"context-menu-item\" (click)=\"onContextCloseToRight()\">\n <i class=\"fa-solid fa-angles-right\"></i>\n <span>Close to Right</span>\n </div>\n </div>\n</div>\n", styles: [":host {\n display: flex;\n flex: 1;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n.tab-container {\n display: flex;\n flex-direction: column;\n flex: 1;\n width: 100%;\n overflow: hidden;\n}\n\n.gl-container {\n flex: 1;\n width: 100%;\n height: 100%;\n position: relative;\n background: white;\n}\n\n/* Direct content container for single-resource mode */\n/* Renders components directly without Golden Layout overhead */\n.direct-content-container {\n flex: 1;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n background: white;\n overflow: hidden;\n}\n\n.tab-content-container {\n background: white;\n color: #333;\n padding: 20px;\n}\n\n/* Context Menu */\n.context-menu {\n position: fixed;\n background: white;\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n min-width: 150px;\n z-index: 10001;\n overflow: hidden;\n}\n\n.context-menu .context-menu-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 14px;\n cursor: pointer;\n font-size: 13px;\n color: #424242;\n transition: background 0.15s;\n}\n\n.context-menu .context-menu-item i {\n width: 16px;\n text-align: center;\n color: #757575;\n}\n\n.context-menu .context-menu-item:hover {\n background: #f5f5f5;\n}\n\n.context-menu .context-menu-divider {\n height: 1px;\n background: #e0e0e0;\n margin: 4px 0;\n}\n\n/* Override Golden Layout styles */\n/* Global overrides */\nmj-tab-container .lm_content {\n background: white !important;\n}\n\nmj-tab-container .lm_item_container {\n background: white !important;\n}\n\n/* Tab content wrapper - allow scrolling */\n.tab-content-wrapper {\n overflow: auto !important;\n}\n\n/* Make tabs larger and easier to click */\nmj-tab-container .lm_header {\n height: 38px !important;\n padding-top: 2px !important;\n padding-left: 4px !important;\n background: #f5f5f5 !important;\n border-bottom: 1px solid #ebebeb !important;\n overflow: visible !important;\n box-sizing: border-box !important;\n}\n\nmj-tab-container .lm_tabs {\n height: 36px !important;\n}\n\n/* Hide Golden Layout window controls */\nmj-tab-container .lm_controls {\n display: none !important;\n}\n\nmj-tab-container .lm_header .lm_tab {\n padding: 0 16px !important;\n font-size: 13px !important;\n height: 35px !important;\n line-height: 35px !important;\n box-sizing: border-box !important;\n cursor: pointer !important;\n user-select: none !important;\n background: transparent !important;\n border: none !important;\n border-bottom: 1px solid #ebebeb !important;\n transition: all 0.15s ease !important;\n position: relative !important;\n z-index: 1 !important;\n margin-right: 1px !important;\n}\n\n/* App color accent - left edge indicator */\nmj-tab-container .lm_header .lm_tab::before {\n content: '' !important;\n position: absolute !important;\n left: 0 !important;\n top: 4px !important;\n bottom: 4px !important;\n width: 3px !important;\n border-radius: 0 2px 2px 0 !important;\n background-color: var(--app-color, transparent) !important;\n opacity: 0.6 !important;\n transition: all 0.15s ease !important;\n}\n\nmj-tab-container .lm_header .lm_tab:hover {\n background: #e8e8e8 !important;\n}\n\nmj-tab-container .lm_header .lm_tab:hover .lm_close_tab {\n opacity: 0.7 !important;\n}\n\nmj-tab-container .lm_header .lm_tab:hover::before {\n opacity: 0.8 !important;\n}\n\nmj-tab-container .lm_header .lm_tab.lm_active {\n background: white !important;\n height: 37px !important;\n margin-bottom: -1px !important;\n margin-right: 0 !important;\n border: 1px solid #ebebeb !important;\n border-bottom-color: white !important;\n border-radius: 4px 4px 0 0 !important;\n z-index: 2 !important;\n}\n\n/* Enhanced app color accent for active tab */\nmj-tab-container .lm_header .lm_tab.lm_active::before {\n opacity: 1 !important;\n width: 3px !important;\n top: 2px !important;\n bottom: 2px !important;\n box-shadow: 0 0 6px var(--app-color, transparent) !important;\n}\n\nmj-tab-container .lm_title {\n cursor: pointer !important;\n user-select: none !important;\n}\n\nmj-tab-container .lm_close_tab {\n position: absolute !important;\n right: 4px !important;\n top: 50% !important;\n transform: translateY(-50%) !important;\n width: 16px !important;\n height: 16px !important;\n cursor: pointer !important;\n opacity: 0 !important;\n transition: all 0.15s ease !important;\n flex-shrink: 0 !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n font-size: 10px !important;\n color: #757575 !important;\n}\n\nmj-tab-container .lm_close_tab:hover {\n opacity: 1 !important;\n color: #c62828 !important;\n}\n\n/* Show close button on hover (except pinned) */\nmj-tab-container .lm_header .lm_tab:hover:not(.pinned) .lm_close_tab {\n opacity: 0.7 !important;\n}\n\n/* Pinned tabs never show close button */\nmj-tab-container .lm_header .lm_tab.pinned .lm_close_tab {\n display: none !important;\n}\n\n/* Hide close button on active tab by default */\nmj-tab-container .lm_active .lm_close_tab {\n opacity: 0 !important;\n}\n\n/* Show close button when hovering active tab */\nmj-tab-container .lm_active:hover .lm_close_tab {\n opacity: 0.7 !important;\n}\n\n/* Adjust padding for close button */\nmj-tab-container .lm_header .lm_tab {\n padding-right: 24px !important;\n}\n"] }]
912
+ }], () => [{ type: i1.GoldenLayoutManager }, { type: i1.WorkspaceStateManager }, { type: i1.ApplicationManager }, { type: i0.ApplicationRef }, { type: i0.EnvironmentInjector }, { type: i0.ChangeDetectorRef }], { glContainer: [{
913
+ type: ViewChild,
914
+ args: ['glContainer', { static: false }]
915
+ }], directContentContainer: [{
916
+ type: ViewChild,
917
+ args: ['directContentContainer', { static: false }]
918
+ }] }); })();
919
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(TabContainerComponent, { className: "TabContainerComponent", filePath: "src/lib/shell/components/tabs/tab-container.component.ts", lineNumber: 46 }); })();
920
+ //# sourceMappingURL=tab-container.component.js.map