@memberjunction/ng-explorer-core 5.51.0 → 6.1.0-edge.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.
- package/LICENSE +7 -0
- package/dist/app-routing.module.d.ts.map +1 -1
- package/dist/app-routing.module.js +5 -5
- package/dist/app-routing.module.js.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-resource.component.js +28 -0
- package/dist/lib/resource-wrappers/dashboard-resource.component.js.map +1 -1
- package/dist/lib/shell/components/header/app-nav.component.d.ts +2 -1
- package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
- package/dist/lib/shell/components/header/app-nav.component.js +58 -42
- package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
- package/dist/lib/shell/components/record-open/record-bar.component.d.ts +29 -0
- package/dist/lib/shell/components/record-open/record-bar.component.d.ts.map +1 -0
- package/dist/lib/shell/components/record-open/record-bar.component.js +72 -0
- package/dist/lib/shell/components/record-open/record-bar.component.js.map +1 -0
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.d.ts +26 -0
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.d.ts.map +1 -0
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.js +140 -0
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.js.map +1 -0
- package/dist/lib/shell/components/record-open/record-switcher-sheet.component.d.ts +43 -0
- package/dist/lib/shell/components/record-open/record-switcher-sheet.component.d.ts.map +1 -0
- package/dist/lib/shell/components/record-open/record-switcher-sheet.component.js +102 -0
- package/dist/lib/shell/components/record-open/record-switcher-sheet.component.js.map +1 -0
- package/dist/lib/shell/components/record-open/record-switcher.service.d.ts +21 -0
- package/dist/lib/shell/components/record-open/record-switcher.service.d.ts.map +1 -0
- package/dist/lib/shell/components/record-open/record-switcher.service.js +30 -0
- package/dist/lib/shell/components/record-open/record-switcher.service.js.map +1 -0
- package/dist/lib/shell/components/record-open/records-hub-pill.component.d.ts +63 -0
- package/dist/lib/shell/components/record-open/records-hub-pill.component.d.ts.map +1 -0
- package/dist/lib/shell/components/record-open/records-hub-pill.component.js +143 -0
- package/dist/lib/shell/components/record-open/records-hub-pill.component.js.map +1 -0
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts +228 -2
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.js +1073 -113
- package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
- package/dist/lib/shell/services/sharing-center-dialog-host.component.d.ts.map +1 -1
- package/dist/lib/shell/services/sharing-center-dialog-host.component.js +2 -0
- package/dist/lib/shell/services/sharing-center-dialog-host.component.js.map +1 -1
- package/dist/lib/shell/shell.component.d.ts +41 -4
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +400 -240
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/lib/shell/shell.module.d.ts +4 -1
- package/dist/lib/shell/shell.module.d.ts.map +1 -1
- package/dist/lib/shell/shell.module.js +14 -3
- package/dist/lib/shell/shell.module.js.map +1 -1
- package/package.json +62 -59
|
@@ -1,77 +1,146 @@
|
|
|
1
1
|
import { Component, ViewChild, runInInjectionContext, createComponent, ViewEncapsulation, HostListener, Output, EventEmitter, inject } from '@angular/core';
|
|
2
|
+
import { GoldenLayoutManager, FlattenLayoutToSingleStack } from '@memberjunction/ng-base-application';
|
|
2
3
|
import { MJGlobal } from '@memberjunction/global';
|
|
3
|
-
import { BaseResourceComponent, HomeAppPinService, NavigationService } from '@memberjunction/ng-shared';
|
|
4
|
+
import { BaseResourceComponent, HomeAppPinService, NavigationService, IsRecordTabsStyle, IsRecordsTabConfiguration, IsRecordsRegionTab, IsRecordDockedToWorkspace, RECORD_DOCKED_TO_WORKSPACE_KEY, GetRecordSourceContext, SafeDetectChanges, ExplorerBreakpointService, ResolveRecordTypeIcon } from '@memberjunction/ng-shared';
|
|
4
5
|
import { ResourceData, ResourcePermissionEngine } from '@memberjunction/core-entities';
|
|
6
|
+
import { RecordOriginCrumbComponent } from '../record-open/record-origin-crumb.component';
|
|
7
|
+
import { RecordSwitcherService } from '../record-open/record-switcher.service';
|
|
5
8
|
import { MJNotificationService } from '@memberjunction/ng-notifications';
|
|
6
9
|
import { LogError } from '@memberjunction/core';
|
|
7
10
|
import { ComponentCacheManager } from './component-cache-manager';
|
|
8
11
|
import { BaseAngularComponent } from '@memberjunction/ng-base-types';
|
|
9
12
|
import * as i0 from "@angular/core";
|
|
10
13
|
import * as i1 from "@memberjunction/ng-base-application";
|
|
14
|
+
import * as i2 from "../record-open/record-bar.component";
|
|
15
|
+
import * as i3 from "../record-open/record-switcher-sheet.component";
|
|
11
16
|
const _c0 = ["glContainer"];
|
|
12
17
|
const _c1 = ["directContentContainer"];
|
|
13
|
-
|
|
14
|
-
i0.ɵɵelement(0, "div", 3, 0);
|
|
15
|
-
} }
|
|
18
|
+
const _c2 = ["recordsGlContainer"];
|
|
16
19
|
function TabContainerComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
17
|
-
i0.ɵɵelement(0, "div",
|
|
20
|
+
i0.ɵɵelement(0, "div", 5, 0);
|
|
21
|
+
} }
|
|
22
|
+
function TabContainerComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
23
|
+
i0.ɵɵelement(0, "div", 6, 1);
|
|
24
|
+
} }
|
|
25
|
+
function TabContainerComponent_Conditional_4_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
26
|
+
const _r2 = i0.ɵɵgetCurrentView();
|
|
27
|
+
i0.ɵɵelementStart(0, "mj-record-bar", 12);
|
|
28
|
+
i0.ɵɵlistener("SwitcherRequested", function TabContainerComponent_Conditional_4_Conditional_1_Template_mj_record_bar_SwitcherRequested_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.OpenRecordSwitcher()); });
|
|
29
|
+
i0.ɵɵelementEnd();
|
|
30
|
+
} if (rf & 2) {
|
|
31
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
32
|
+
i0.ɵɵproperty("Title", ctx_r2.RecordBarTitle)("Icon", ctx_r2.RecordBarIcon)("Color", ctx_r2.RecordBarColor)("Count", ctx_r2.OpenRecordCount);
|
|
33
|
+
} }
|
|
34
|
+
function TabContainerComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
35
|
+
const _r1 = i0.ɵɵgetCurrentView();
|
|
36
|
+
i0.ɵɵelementStart(0, "div", 8);
|
|
37
|
+
i0.ɵɵconditionalCreate(1, TabContainerComponent_Conditional_4_Conditional_1_Template, 1, 4, "mj-record-bar", 9);
|
|
38
|
+
i0.ɵɵelement(2, "div", 10, 2);
|
|
39
|
+
i0.ɵɵelementEnd();
|
|
40
|
+
i0.ɵɵelementStart(4, "mj-record-switcher-sheet", 11);
|
|
41
|
+
i0.ɵɵlistener("VisibleChange", function TabContainerComponent_Conditional_4_Template_mj_record_switcher_sheet_VisibleChange_4_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnSwitcherVisibleChange($event)); })("ActivateRequested", function TabContainerComponent_Conditional_4_Template_mj_record_switcher_sheet_ActivateRequested_4_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnSwitcherActivate($event)); })("CloseRequested", function TabContainerComponent_Conditional_4_Template_mj_record_switcher_sheet_CloseRequested_4_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnSwitcherClose($event)); });
|
|
42
|
+
i0.ɵɵelementEnd();
|
|
43
|
+
} if (rf & 2) {
|
|
44
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
45
|
+
i0.ɵɵclassProp("region-hidden", !ctx_r2.ShowRecordsRegion);
|
|
46
|
+
i0.ɵɵadvance();
|
|
47
|
+
i0.ɵɵconditional(ctx_r2.IsMobileRecords ? 1 : -1);
|
|
48
|
+
i0.ɵɵadvance(3);
|
|
49
|
+
i0.ɵɵproperty("Entries", ctx_r2.SwitcherEntries)("Visible", ctx_r2.RecordSwitcherVisible);
|
|
18
50
|
} }
|
|
19
|
-
function
|
|
51
|
+
function TabContainerComponent_Conditional_5_Conditional_8_Template(rf, ctx) { if (rf & 1) {
|
|
20
52
|
i0.ɵɵelementStart(0, "span");
|
|
21
53
|
i0.ɵɵtext(1, "Pinned to Home");
|
|
22
54
|
i0.ɵɵelementEnd();
|
|
23
|
-
i0.ɵɵelementStart(2, "span",
|
|
24
|
-
i0.ɵɵelement(3, "i",
|
|
55
|
+
i0.ɵɵelementStart(2, "span", 22);
|
|
56
|
+
i0.ɵɵelement(3, "i", 23);
|
|
25
57
|
i0.ɵɵelementEnd();
|
|
26
58
|
} }
|
|
27
|
-
function
|
|
59
|
+
function TabContainerComponent_Conditional_5_Conditional_9_Template(rf, ctx) { if (rf & 1) {
|
|
28
60
|
i0.ɵɵelementStart(0, "span");
|
|
29
61
|
i0.ɵɵtext(1, "Pin to Home");
|
|
30
62
|
i0.ɵɵelementEnd();
|
|
31
63
|
} }
|
|
32
|
-
function
|
|
33
|
-
const
|
|
34
|
-
i0.ɵɵelementStart(0, "
|
|
35
|
-
i0.ɵɵlistener("click", function
|
|
36
|
-
i0.ɵɵelement(
|
|
64
|
+
function TabContainerComponent_Conditional_5_Conditional_10_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
65
|
+
const _r5 = i0.ɵɵgetCurrentView();
|
|
66
|
+
i0.ɵɵelementStart(0, "button", 14);
|
|
67
|
+
i0.ɵɵlistener("click", function TabContainerComponent_Conditional_5_Conditional_10_Conditional_1_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r5); const ctx_r2 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r2.onContextMoveToWorkspace()); });
|
|
68
|
+
i0.ɵɵelement(1, "i", 25);
|
|
69
|
+
i0.ɵɵelementStart(2, "span");
|
|
70
|
+
i0.ɵɵtext(3, "Move to Workspace");
|
|
71
|
+
i0.ɵɵelementEnd()();
|
|
72
|
+
} }
|
|
73
|
+
function TabContainerComponent_Conditional_5_Conditional_10_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
74
|
+
const _r6 = i0.ɵɵgetCurrentView();
|
|
75
|
+
i0.ɵɵelementStart(0, "button", 14);
|
|
76
|
+
i0.ɵɵlistener("click", function TabContainerComponent_Conditional_5_Conditional_10_Conditional_2_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r6); const ctx_r2 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r2.onContextMoveToRecords()); });
|
|
77
|
+
i0.ɵɵelement(1, "i", 26);
|
|
78
|
+
i0.ɵɵelementStart(2, "span");
|
|
79
|
+
i0.ɵɵtext(3, "Move to Records");
|
|
80
|
+
i0.ɵɵelementEnd()();
|
|
81
|
+
} }
|
|
82
|
+
function TabContainerComponent_Conditional_5_Conditional_10_Template(rf, ctx) { if (rf & 1) {
|
|
83
|
+
i0.ɵɵelement(0, "div", 16);
|
|
84
|
+
i0.ɵɵconditionalCreate(1, TabContainerComponent_Conditional_5_Conditional_10_Conditional_1_Template, 4, 0, "button", 24);
|
|
85
|
+
i0.ɵɵconditionalCreate(2, TabContainerComponent_Conditional_5_Conditional_10_Conditional_2_Template, 4, 0, "button", 24);
|
|
86
|
+
} if (rf & 2) {
|
|
87
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
88
|
+
i0.ɵɵadvance();
|
|
89
|
+
i0.ɵɵconditional(ctx_r2.canContextMoveToWorkspace ? 1 : -1);
|
|
90
|
+
i0.ɵɵadvance();
|
|
91
|
+
i0.ɵɵconditional(ctx_r2.canContextMoveToRecords ? 2 : -1);
|
|
92
|
+
} }
|
|
93
|
+
function TabContainerComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
94
|
+
const _r4 = i0.ɵɵgetCurrentView();
|
|
95
|
+
i0.ɵɵelementStart(0, "div", 13);
|
|
96
|
+
i0.ɵɵlistener("keydown", function TabContainerComponent_Conditional_5_Template_div_keydown_0_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onMenuKeydown($event)); });
|
|
97
|
+
i0.ɵɵelementStart(1, "button", 14);
|
|
98
|
+
i0.ɵɵlistener("click", function TabContainerComponent_Conditional_5_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onContextPin()); });
|
|
99
|
+
i0.ɵɵelement(2, "i", 15);
|
|
37
100
|
i0.ɵɵelementStart(3, "span");
|
|
38
101
|
i0.ɵɵtext(4);
|
|
39
102
|
i0.ɵɵelementEnd()();
|
|
40
|
-
i0.ɵɵelement(5, "div",
|
|
41
|
-
i0.ɵɵelementStart(6, "
|
|
42
|
-
i0.ɵɵlistener("click", function
|
|
43
|
-
i0.ɵɵelement(7, "i",
|
|
44
|
-
i0.ɵɵconditionalCreate(8,
|
|
103
|
+
i0.ɵɵelement(5, "div", 16);
|
|
104
|
+
i0.ɵɵelementStart(6, "button", 17);
|
|
105
|
+
i0.ɵɵlistener("click", function TabContainerComponent_Conditional_5_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onContextPinToHome()); });
|
|
106
|
+
i0.ɵɵelement(7, "i", 18);
|
|
107
|
+
i0.ɵɵconditionalCreate(8, TabContainerComponent_Conditional_5_Conditional_8_Template, 4, 0)(9, TabContainerComponent_Conditional_5_Conditional_9_Template, 2, 0, "span");
|
|
45
108
|
i0.ɵɵelementEnd();
|
|
46
|
-
i0.ɵɵ
|
|
47
|
-
i0.ɵɵ
|
|
48
|
-
i0.ɵɵ
|
|
49
|
-
i0.ɵɵ
|
|
50
|
-
i0.ɵɵ
|
|
51
|
-
i0.ɵɵ
|
|
109
|
+
i0.ɵɵconditionalCreate(10, TabContainerComponent_Conditional_5_Conditional_10_Template, 3, 2);
|
|
110
|
+
i0.ɵɵelement(11, "div", 16);
|
|
111
|
+
i0.ɵɵelementStart(12, "button", 14);
|
|
112
|
+
i0.ɵɵlistener("click", function TabContainerComponent_Conditional_5_Template_button_click_12_listener() { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onContextClose()); });
|
|
113
|
+
i0.ɵɵelement(13, "i", 19);
|
|
114
|
+
i0.ɵɵelementStart(14, "span");
|
|
115
|
+
i0.ɵɵtext(15, "Close Tab");
|
|
52
116
|
i0.ɵɵelementEnd()();
|
|
53
|
-
i0.ɵɵelementStart(
|
|
54
|
-
i0.ɵɵlistener("click", function
|
|
55
|
-
i0.ɵɵelement(
|
|
56
|
-
i0.ɵɵelementStart(
|
|
57
|
-
i0.ɵɵtext(
|
|
117
|
+
i0.ɵɵelementStart(16, "button", 14);
|
|
118
|
+
i0.ɵɵlistener("click", function TabContainerComponent_Conditional_5_Template_button_click_16_listener() { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onContextCloseOthers()); });
|
|
119
|
+
i0.ɵɵelement(17, "i", 20);
|
|
120
|
+
i0.ɵɵelementStart(18, "span");
|
|
121
|
+
i0.ɵɵtext(19, "Close Others");
|
|
58
122
|
i0.ɵɵelementEnd()();
|
|
59
|
-
i0.ɵɵelementStart(
|
|
60
|
-
i0.ɵɵlistener("click", function
|
|
61
|
-
i0.ɵɵelement(
|
|
62
|
-
i0.ɵɵelementStart(
|
|
63
|
-
i0.ɵɵtext(
|
|
123
|
+
i0.ɵɵelementStart(20, "button", 14);
|
|
124
|
+
i0.ɵɵlistener("click", function TabContainerComponent_Conditional_5_Template_button_click_20_listener() { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onContextCloseToRight()); });
|
|
125
|
+
i0.ɵɵelement(21, "i", 21);
|
|
126
|
+
i0.ɵɵelementStart(22, "span");
|
|
127
|
+
i0.ɵɵtext(23, "Close to Right");
|
|
64
128
|
i0.ɵɵelementEnd()()();
|
|
65
129
|
} if (rf & 2) {
|
|
66
|
-
const
|
|
67
|
-
i0.ɵɵstyleProp("left",
|
|
130
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
131
|
+
i0.ɵɵstyleProp("left", ctx_r2.contextMenuX, "px")("top", ctx_r2.contextMenuY, "px");
|
|
68
132
|
i0.ɵɵadvance(4);
|
|
69
|
-
i0.ɵɵtextInterpolate(
|
|
133
|
+
i0.ɵɵtextInterpolate(ctx_r2.isContextTabPinned ? "Unpin Tab" : "Pin Tab");
|
|
134
|
+
i0.ɵɵadvance(2);
|
|
135
|
+
i0.ɵɵclassProp("new-item", !ctx_r2.isContextTabPinnedToHome)("disabled", ctx_r2.isContextTabPinnedToHome);
|
|
136
|
+
i0.ɵɵproperty("disabled", ctx_r2.isContextTabPinnedToHome);
|
|
70
137
|
i0.ɵɵadvance(2);
|
|
71
|
-
i0.ɵɵ
|
|
138
|
+
i0.ɵɵconditional(ctx_r2.isContextTabPinnedToHome ? 8 : 9);
|
|
72
139
|
i0.ɵɵadvance(2);
|
|
73
|
-
i0.ɵɵconditional(
|
|
140
|
+
i0.ɵɵconditional(ctx_r2.canContextMoveToWorkspace || ctx_r2.canContextMoveToRecords ? 10 : -1);
|
|
74
141
|
} }
|
|
142
|
+
/** Fallback tab accent when an app has no color (matches pre-existing usage) */
|
|
143
|
+
const DEFAULT_APP_COLOR = '#757575';
|
|
75
144
|
/**
|
|
76
145
|
* Container for Golden Layout tabs with app-colored styling.
|
|
77
146
|
*
|
|
@@ -91,6 +160,7 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
91
160
|
cdr;
|
|
92
161
|
glContainer;
|
|
93
162
|
directContentContainer;
|
|
163
|
+
recordsGlContainer;
|
|
94
164
|
/**
|
|
95
165
|
* Emitted when the first resource component finishes loading.
|
|
96
166
|
* This allows the shell to keep showing its loading indicator until the first
|
|
@@ -104,6 +174,7 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
104
174
|
layoutInitError = new EventEmitter();
|
|
105
175
|
pinService = inject(HomeAppPinService);
|
|
106
176
|
navigationService = inject(NavigationService);
|
|
177
|
+
recordSwitcher = inject(RecordSwitcherService);
|
|
107
178
|
subscriptions = [];
|
|
108
179
|
layoutInitRetryCount = 0;
|
|
109
180
|
MAX_LAYOUT_INIT_RETRIES = 5;
|
|
@@ -111,6 +182,8 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
111
182
|
layoutRestorationComplete = false; // True only AFTER layout is fully restored/created
|
|
112
183
|
// Track component references for cleanup (legacy - keep for backward compat during transition)
|
|
113
184
|
componentRefs = new Map();
|
|
185
|
+
/** Pane-level origin crumbs, one per record pane (see RecordOriginCrumbComponent) */
|
|
186
|
+
originCrumbRefs = new Map();
|
|
114
187
|
// Guard against concurrent loadTabContent calls for the same tab.
|
|
115
188
|
// When a tab's content changes while active, both the reload path (workspace config subscription)
|
|
116
189
|
// and onTabShown can race to call loadTabContent, resulting in duplicate component rendering.
|
|
@@ -131,6 +204,379 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
131
204
|
contextMenuX = 0;
|
|
132
205
|
contextMenuY = 0;
|
|
133
206
|
contextMenuTabId = null;
|
|
207
|
+
/** The control that opened the context menu — focus returns here on close */
|
|
208
|
+
contextMenuAnchor = null;
|
|
209
|
+
// ---- RECORDS region (records-style record opens) ----
|
|
210
|
+
/**
|
|
211
|
+
* A SEPARATE Golden Layout instance hosting only record tabs. Instantiated
|
|
212
|
+
* directly (not DI) — GoldenLayoutManager has no constructor dependencies
|
|
213
|
+
* and the root-provided instance belongs to the MAIN region. Records get
|
|
214
|
+
* native GL tabs (close/drag-to-split/pin) without ever appearing in the
|
|
215
|
+
* main workspace's tab bar.
|
|
216
|
+
*/
|
|
217
|
+
recordsLayoutManager = new GoldenLayoutManager();
|
|
218
|
+
recordsLayoutInitialized = false;
|
|
219
|
+
/** Last active tab id reported by the records GL (guards focus feedback loops) */
|
|
220
|
+
recordsRegionActiveTabId = null;
|
|
221
|
+
/**
|
|
222
|
+
* True while records-GL tabs are being created/restored in a batch. GL
|
|
223
|
+
* fires an activation event for EVERY tab it creates; letting those write
|
|
224
|
+
* back into the workspace during init steals focus from the tab the user
|
|
225
|
+
* actually asked for (and persists junk layouts mid-build). Suppress both
|
|
226
|
+
* write-backs while set; the batch ends with one explicit focus.
|
|
227
|
+
*/
|
|
228
|
+
recordsCreatingTabs = false;
|
|
229
|
+
/** Shell mobile breakpoint (768px) — drives the records mobile surface */
|
|
230
|
+
breakpoint = inject(ExplorerBreakpointService);
|
|
231
|
+
/**
|
|
232
|
+
* True while the records surface renders MOBILE chrome: strip hidden
|
|
233
|
+
* (headerless GL), record bar shown, splits flattened at render, layout
|
|
234
|
+
* persistence suppressed (see the LayoutChanged guard), move actions
|
|
235
|
+
* hidden. Mirrors the breakpoint; flips via rebuildRecordsLayoutForBreakpoint.
|
|
236
|
+
*/
|
|
237
|
+
mobileRecordsActive = this.breakpoint.IsMobile;
|
|
238
|
+
/**
|
|
239
|
+
* True only synchronously around Destroy() during a breakpoint-crossing
|
|
240
|
+
* rebuild. Destroy fires TabClosed for EVERY live pane; without this guard
|
|
241
|
+
* the records TabClosed handler would CloseTab each one — a crossing would
|
|
242
|
+
* close every open record. cleanupTabComponent still runs (the cache
|
|
243
|
+
* detach is what lets the re-init reattach content).
|
|
244
|
+
*/
|
|
245
|
+
recordsRebuilding = false;
|
|
246
|
+
/** True when the deployment runs the records-style record-open model */
|
|
247
|
+
get RecordsStyleActive() {
|
|
248
|
+
return IsRecordTabsStyle();
|
|
249
|
+
}
|
|
250
|
+
/** Records surface is in mobile chrome (record bar instead of tab strip) */
|
|
251
|
+
get IsMobileRecords() {
|
|
252
|
+
return this.RecordsStyleActive && this.mobileRecordsActive;
|
|
253
|
+
}
|
|
254
|
+
// ---- Mobile record bar state (computed by updateRecordSurfaceState) ----
|
|
255
|
+
/** Active record's title, entity icon, and app color for the mobile bar */
|
|
256
|
+
RecordBarTitle = '';
|
|
257
|
+
RecordBarIcon = 'fa-regular fa-file-lines';
|
|
258
|
+
RecordBarColor = DEFAULT_APP_COLOR;
|
|
259
|
+
/**
|
|
260
|
+
* ALL open records — region AND docked — matching the switcher sheet's
|
|
261
|
+
* row count (docked/region composition is a desktop concept; on mobile
|
|
262
|
+
* they're one list).
|
|
263
|
+
*/
|
|
264
|
+
OpenRecordCount = 0;
|
|
265
|
+
/** Record switcher sheet visibility (mobile) */
|
|
266
|
+
RecordSwitcherVisible = false;
|
|
267
|
+
/** Rows for the record switcher sheet (region + docked records, by sequence) */
|
|
268
|
+
SwitcherEntries = [];
|
|
269
|
+
/** Open the record switcher sheet (record bar tap / drawer pill) */
|
|
270
|
+
OpenRecordSwitcher() {
|
|
271
|
+
if (this.OpenRecordCount === 0) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.RecordSwitcherVisible = true;
|
|
275
|
+
this.flushRegionCd();
|
|
276
|
+
}
|
|
277
|
+
/** Sheet row tapped: activate — same contract as the pill (sync drives GL focus) */
|
|
278
|
+
OnSwitcherActivate(tabId) {
|
|
279
|
+
this.RecordSwitcherVisible = false;
|
|
280
|
+
this.flushRegionCd();
|
|
281
|
+
// Guard against a stale row (SetActiveTab doesn't validate existence —
|
|
282
|
+
// stamping a closed tab's id would leave no resolvable active tab)
|
|
283
|
+
if (this.workspaceManager.GetTab(tabId)) {
|
|
284
|
+
this.workspaceManager.SetActiveTab(tabId);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Sheet row ✕: close the record through the SAME path as the tab context
|
|
289
|
+
* menu — GL RemoveTab → TabClosed handler (workspace bookkeeping, close
|
|
290
|
+
* guards, cache eviction all apply). The workspace-level fallback covers
|
|
291
|
+
* the edge where GL never held the tab (zero-height init refusal).
|
|
292
|
+
* The sheet stays open; entries recompute on the config emission.
|
|
293
|
+
*/
|
|
294
|
+
OnSwitcherClose(tabId) {
|
|
295
|
+
const tab = this.workspaceManager.GetTab(tabId);
|
|
296
|
+
const manager = tab && this.isRecordTab(tab) ? this.recordsLayoutManager : this.layoutManager;
|
|
297
|
+
if (manager.GetContainer(tabId)) {
|
|
298
|
+
manager.RemoveTab(tabId);
|
|
299
|
+
}
|
|
300
|
+
else if (tab) {
|
|
301
|
+
// GL never held this tab (e.g. docked record in single-resource mode) —
|
|
302
|
+
// run the same cleanup the GL close path does (cache detach, origin
|
|
303
|
+
// crumb destroy) or the crumb ComponentRef leaks in originCrumbRefs.
|
|
304
|
+
this.cleanupTabComponent(tabId);
|
|
305
|
+
this.workspaceManager.CloseTab(tabId);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
OnSwitcherVisibleChange(visible) {
|
|
309
|
+
this.RecordSwitcherVisible = visible;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Recompute the mobile record-surface state (bar inputs + count) from a
|
|
313
|
+
* workspace configuration emission. Cheap and unconditional — the values
|
|
314
|
+
* only render while IsMobileRecords, but keeping them current at all
|
|
315
|
+
* widths makes the breakpoint flip instant.
|
|
316
|
+
*/
|
|
317
|
+
updateRecordSurfaceState(config) {
|
|
318
|
+
if (!this.RecordsStyleActive) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const previous = `${this.RecordBarTitle}|${this.RecordBarIcon}|${this.RecordBarColor}|${this.OpenRecordCount}`;
|
|
322
|
+
const allRecords = config.tabs
|
|
323
|
+
.filter(t => IsRecordsTabConfiguration(t.configuration))
|
|
324
|
+
.sort((a, b) => a.sequence - b.sequence);
|
|
325
|
+
this.OpenRecordCount = allRecords.length;
|
|
326
|
+
this.SwitcherEntries = allRecords.map(tab => this.buildSwitcherEntry(tab, config.activeTabId));
|
|
327
|
+
const activeTab = config.tabs.find(t => t.id === config.activeTabId);
|
|
328
|
+
if (activeTab && this.isRecordTab(activeTab)) {
|
|
329
|
+
const app = this.appManager.GetAppById(activeTab.applicationId);
|
|
330
|
+
this.RecordBarTitle = activeTab.title;
|
|
331
|
+
this.RecordBarIcon = ResolveRecordTypeIcon(activeTab.configuration, this.ProviderToUse);
|
|
332
|
+
this.RecordBarColor = app?.GetColor() || DEFAULT_APP_COLOR;
|
|
333
|
+
}
|
|
334
|
+
// If the active tab is NOT a region record the bar isn't visible (the
|
|
335
|
+
// records region is hidden) — stale bar values are harmless.
|
|
336
|
+
if (this.RecordSwitcherVisible && this.OpenRecordCount === 0) {
|
|
337
|
+
// Last record closed from the sheet — nothing left to switch to
|
|
338
|
+
this.RecordSwitcherVisible = false;
|
|
339
|
+
}
|
|
340
|
+
const current = `${this.RecordBarTitle}|${this.RecordBarIcon}|${this.RecordBarColor}|${this.OpenRecordCount}`;
|
|
341
|
+
// Always flush while the sheet is OPEN: its rows carry state the bar hash
|
|
342
|
+
// can't see (row titles, origins, IsActive) — an activation or async
|
|
343
|
+
// title change with identical bar values would otherwise render stale.
|
|
344
|
+
if (this.RecordSwitcherVisible || (this.mobileRecordsActive && current !== previous)) {
|
|
345
|
+
this.flushRegionCd();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** One switcher sheet row: identity + origin subtitle (crumb semantics) */
|
|
349
|
+
buildSwitcherEntry(tab, activeTabId) {
|
|
350
|
+
const app = this.appManager.GetAppById(tab.applicationId);
|
|
351
|
+
const origin = GetRecordSourceContext(tab.configuration);
|
|
352
|
+
const originLabel = origin
|
|
353
|
+
? (origin.sourceLabel ?? ([origin.sourceAppName, origin.sourceNavLabel].filter(Boolean).join(' › ') || null))
|
|
354
|
+
: null;
|
|
355
|
+
return {
|
|
356
|
+
TabId: tab.id,
|
|
357
|
+
Title: tab.title,
|
|
358
|
+
Icon: ResolveRecordTypeIcon(tab.configuration, this.ProviderToUse),
|
|
359
|
+
Color: app?.GetColor() || DEFAULT_APP_COLOR,
|
|
360
|
+
OriginLabel: originLabel,
|
|
361
|
+
IsActive: tab.id === activeTabId
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/** True while the RECORDS region is the visible surface (active tab is a record) */
|
|
365
|
+
ShowRecordsRegion = false;
|
|
366
|
+
/**
|
|
367
|
+
* A tab belongs to the records REGION (not the main workspace layout).
|
|
368
|
+
* Records docked to the workspace ("Move to Workspace") fail this check —
|
|
369
|
+
* they are main-layout tabs everywhere this predicate gates.
|
|
370
|
+
*/
|
|
371
|
+
isRecordTab(tab) {
|
|
372
|
+
return this.RecordsStyleActive && IsRecordsRegionTab(tab.configuration);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Synchronous, exception-safe change-detection flush. Zoneless + OnPush
|
|
376
|
+
* ancestors make RxJS-driven mutations invisible until SOMETHING runs CD;
|
|
377
|
+
* detectChanges throws when a pass is already in flight, and an unguarded
|
|
378
|
+
* throw inside a promise/subscription silently kills the update (the
|
|
379
|
+
* original "clicked the pill, nothing happened for seconds" bug). markForCheck
|
|
380
|
+
* first so even the re-entrant case gets picked up by the in-flight pass.
|
|
381
|
+
*/
|
|
382
|
+
flushRegionCd() {
|
|
383
|
+
SafeDetectChanges(this.cdr);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Keep the RECORDS region in lockstep with the workspace configuration:
|
|
387
|
+
* resolve region visibility (active tab is a record), lazily initialize the
|
|
388
|
+
* records layout on first show (restoring the persisted records layout when
|
|
389
|
+
* it matches the tab set), add/remove record tabs, and focus the active one.
|
|
390
|
+
*/
|
|
391
|
+
syncRecordsRegion(config) {
|
|
392
|
+
if (!this.RecordsStyleActive) {
|
|
393
|
+
// Defense in depth: if the style ever resolves to classic AFTER a
|
|
394
|
+
// records-style pass set the flag (e.g. late instance-config load),
|
|
395
|
+
// un-hide the main region — otherwise it stays invisible forever.
|
|
396
|
+
if (this.ShowRecordsRegion) {
|
|
397
|
+
this.ShowRecordsRegion = false;
|
|
398
|
+
this.flushRegionCd();
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const recordTabs = config.tabs.filter(t => this.isRecordTab(t));
|
|
403
|
+
const activeTab = config.tabs.find(t => t.id === config.activeTabId);
|
|
404
|
+
const showing = !!activeTab && this.isRecordTab(activeTab);
|
|
405
|
+
// EAGER init + sync on EVERY emission. The region container always keeps
|
|
406
|
+
// its full layout box (visibility-hidden, never display:none — see the
|
|
407
|
+
// component CSS), so the records GL can initialize and reconcile even
|
|
408
|
+
// while the region isn't the visible surface. This is what keeps the
|
|
409
|
+
// strip, the pill, and the workspace from ever drifting apart: closed
|
|
410
|
+
// records leave the strip immediately, new ones appear immediately,
|
|
411
|
+
// whether or not the user is looking at the region.
|
|
412
|
+
if (!this.recordsLayoutInitialized && recordTabs.length > 0) {
|
|
413
|
+
this.ensureRecordsLayoutInitialized(recordTabs);
|
|
414
|
+
}
|
|
415
|
+
if (this.recordsLayoutInitialized) {
|
|
416
|
+
this.syncRecordsTabs(recordTabs);
|
|
417
|
+
}
|
|
418
|
+
if (showing !== this.ShowRecordsRegion) {
|
|
419
|
+
this.ShowRecordsRegion = showing;
|
|
420
|
+
// Flush NOW — the visibility class must land in this pass, not
|
|
421
|
+
// whenever the next unrelated emission happens to run CD.
|
|
422
|
+
this.flushRegionCd();
|
|
423
|
+
}
|
|
424
|
+
if (showing && activeTab && this.recordsLayoutInitialized) {
|
|
425
|
+
this.focusRecordsTab(activeTab.id);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** Add/remove/style record tabs in the records GL to match the configuration */
|
|
429
|
+
syncRecordsTabs(recordTabs) {
|
|
430
|
+
if (!this.recordsLayoutInitialized) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const existingIds = this.recordsLayoutManager.GetAllTabIds();
|
|
434
|
+
const configIds = recordTabs.map(t => t.id);
|
|
435
|
+
existingIds.forEach(id => {
|
|
436
|
+
if (!configIds.includes(id)) {
|
|
437
|
+
this.recordsLayoutManager.RemoveTab(id);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
const toCreate = recordTabs.filter(t => !existingIds.includes(t.id));
|
|
441
|
+
if (toCreate.length > 0) {
|
|
442
|
+
this.recordsCreatingTabs = true;
|
|
443
|
+
try {
|
|
444
|
+
toCreate.forEach(tab => this.createRecordsRegionTab(tab));
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
this.recordsCreatingTabs = false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
recordTabs.forEach(tab => {
|
|
451
|
+
if (existingIds.includes(tab.id)) {
|
|
452
|
+
const app = this.appManager.GetAppById(tab.applicationId);
|
|
453
|
+
this.recordsLayoutManager.UpdateTabStyle(tab.id, {
|
|
454
|
+
isPinned: tab.isPinned,
|
|
455
|
+
title: tab.title,
|
|
456
|
+
appColor: app?.GetColor() || DEFAULT_APP_COLOR,
|
|
457
|
+
typeIcon: this.resolveTabTypeIcon(tab)
|
|
458
|
+
});
|
|
459
|
+
// Origin can change on re-open re-capture — keep the pane crumb live
|
|
460
|
+
this.updateOriginCrumb(tab);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
/** Focus a records-region tab without feeding back into SetActiveTab loops */
|
|
465
|
+
focusRecordsTab(tabId) {
|
|
466
|
+
if (this.recordsRegionActiveTabId !== tabId) {
|
|
467
|
+
this.recordsLayoutManager.FocusTab(tabId);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Initialize the records Golden Layout as soon as record tabs exist —
|
|
472
|
+
* EAGERLY, whether or not the region is visible (its container always has
|
|
473
|
+
* a full layout box). Restores the persisted records layout when its
|
|
474
|
+
* component count matches the current record-tab set; otherwise builds
|
|
475
|
+
* tabs fresh. Creation/restore runs with activation write-backs
|
|
476
|
+
* suppressed (see recordsCreatingTabs).
|
|
477
|
+
*/
|
|
478
|
+
ensureRecordsLayoutInitialized(recordTabs) {
|
|
479
|
+
if (this.recordsLayoutInitialized) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const container = this.recordsGlContainer?.nativeElement;
|
|
483
|
+
if (!container) {
|
|
484
|
+
return; // Template not settled yet — the next sync pass retries
|
|
485
|
+
}
|
|
486
|
+
// NEVER initialize GL inside a zero-size (hidden) container — it bakes a
|
|
487
|
+
// 0x0 internal size into every stack it later creates and the region
|
|
488
|
+
// renders collapsed forever. Happens when the region flip is reverted
|
|
489
|
+
// (activation stolen) before this deferred init runs; staying
|
|
490
|
+
// uninitialized is safe — the next real show retries.
|
|
491
|
+
if (container.getBoundingClientRect().height === 0) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// Mobile: headerless GL — the record bar replaces the strip
|
|
495
|
+
this.recordsLayoutManager.Initialize(container, { HideHeaders: this.mobileRecordsActive });
|
|
496
|
+
this.recordsLayoutInitialized = true;
|
|
497
|
+
this.recordsCreatingTabs = true;
|
|
498
|
+
try {
|
|
499
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
500
|
+
const savedLayout = config?.recordsLayout;
|
|
501
|
+
// Mobile renders the persisted layout FLATTENED to one stack — a clone;
|
|
502
|
+
// the persisted recordsLayout is never touched (and never written back
|
|
503
|
+
// while mobile — see the LayoutChanged guard). Flattening preserves
|
|
504
|
+
// components, so the restore gate below is unaffected.
|
|
505
|
+
const layoutToLoad = this.mobileRecordsActive ? FlattenLayoutToSingleStack(savedLayout) : savedLayout;
|
|
506
|
+
// Restore gate: the persisted layout must cover EXACTLY the current tab
|
|
507
|
+
// set — identity, not count. Counts can match while identities diverge
|
|
508
|
+
// (open one + close one during a persistence-suppressed mobile session);
|
|
509
|
+
// loading such a layout would render a ghost pane for a closed record.
|
|
510
|
+
if (layoutToLoad?.root && recordTabs.length > 0 && this.layoutCoversExactTabSet(layoutToLoad.root, recordTabs)) {
|
|
511
|
+
if (this.recordsLayoutManager.LoadLayout(layoutToLoad)) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// No (or mismatched) saved layout — create record tabs fresh
|
|
516
|
+
[...recordTabs]
|
|
517
|
+
.sort((a, b) => a.sequence - b.sequence)
|
|
518
|
+
.forEach(tab => this.createRecordsRegionTab(tab));
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
this.recordsCreatingTabs = false;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Breakpoint crossing: tear the records GL down and let the next sync pass
|
|
526
|
+
* rebuild it in the other chrome mode (mobile: headerless + flattened;
|
|
527
|
+
* desktop: full strip + restored splits). The teardown runs under
|
|
528
|
+
* recordsRebuilding — Destroy() fires TabClosed per pane, and without the
|
|
529
|
+
* guard those events would CloseTab every open record (see the guard's
|
|
530
|
+
* field comment). Content survives via the component cache: cleanup
|
|
531
|
+
* detaches each pane's component, and the re-init's first-show reattaches.
|
|
532
|
+
*/
|
|
533
|
+
rebuildRecordsLayoutForBreakpoint() {
|
|
534
|
+
// The switcher sheet is MOBILE chrome — never let it survive a crossing
|
|
535
|
+
// (it would overlay the desktop UI and its entries stop re-rendering)
|
|
536
|
+
this.RecordSwitcherVisible = false;
|
|
537
|
+
// Template state first (bar/strip @if flips, region height settles)
|
|
538
|
+
this.flushRegionCd();
|
|
539
|
+
if (!this.recordsLayoutInitialized) {
|
|
540
|
+
return; // GL not built yet — the next sync initializes in the new mode
|
|
541
|
+
}
|
|
542
|
+
this.recordsRebuilding = true;
|
|
543
|
+
try {
|
|
544
|
+
this.recordsLayoutManager.Destroy();
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
// ALL teardown state resets live in the finally: if Destroy() throws
|
|
548
|
+
// mid-walk, leaving recordsLayoutInitialized=true over a half-destroyed
|
|
549
|
+
// GL would corrupt every subsequent sync pass.
|
|
550
|
+
this.recordsRebuilding = false;
|
|
551
|
+
this.recordsLayoutInitialized = false;
|
|
552
|
+
this.recordsRegionActiveTabId = null;
|
|
553
|
+
}
|
|
554
|
+
// Defer re-init one macrotask so the flipped template has painted and
|
|
555
|
+
// the GL container has its final (bar-adjusted) height before Initialize
|
|
556
|
+
// measures it — same reason handleTabBarVisibilityChange defers.
|
|
557
|
+
setTimeout(() => {
|
|
558
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
559
|
+
if (config) {
|
|
560
|
+
this.syncRecordsRegion(config);
|
|
561
|
+
}
|
|
562
|
+
}, 0);
|
|
563
|
+
}
|
|
564
|
+
/** Create a record tab in the RECORDS layout (mirror of createTab for main) */
|
|
565
|
+
createRecordsRegionTab(tab) {
|
|
566
|
+
const app = this.appManager.GetAppById(tab.applicationId);
|
|
567
|
+
const state = {
|
|
568
|
+
tabId: tab.id,
|
|
569
|
+
appId: tab.applicationId,
|
|
570
|
+
appColor: app?.GetColor() || DEFAULT_APP_COLOR,
|
|
571
|
+
title: tab.title,
|
|
572
|
+
route: tab.configuration['route'] || '',
|
|
573
|
+
isPinned: tab.isPinned,
|
|
574
|
+
isLoaded: false,
|
|
575
|
+
typeIcon: this.resolveTabTypeIcon(tab)
|
|
576
|
+
};
|
|
577
|
+
this.recordsLayoutManager.AddTab(state);
|
|
578
|
+
this.updateTabDisplayName(tab);
|
|
579
|
+
}
|
|
134
580
|
constructor(layoutManager, workspaceManager, appManager, appRef, environmentInjector, cdr) {
|
|
135
581
|
super();
|
|
136
582
|
this.layoutManager = layoutManager;
|
|
@@ -148,6 +594,16 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
148
594
|
this.onTabShown(event);
|
|
149
595
|
}), this.layoutManager.TabClosed.subscribe(tabId => {
|
|
150
596
|
this.cleanupTabComponent(tabId);
|
|
597
|
+
// DEMOTE guard ("Move to Records"): the tab was removed from the
|
|
598
|
+
// main GL because its region membership changed — it's still in the
|
|
599
|
+
// workspace config and the records region is about to pick it up.
|
|
600
|
+
// Closing it here would destroy the tab the user asked to keep.
|
|
601
|
+
// cleanupTabComponent above still runs: the cache detach is what
|
|
602
|
+
// lets the records GL reattach the component with state intact.
|
|
603
|
+
const movedTab = this.workspaceManager.GetTab(tabId);
|
|
604
|
+
if (movedTab && this.isRecordTab(movedTab)) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
151
607
|
this.workspaceManager.CloseTab(tabId);
|
|
152
608
|
}), this.layoutManager.LayoutChanged.subscribe(event => {
|
|
153
609
|
const layout = this.layoutManager.SaveLayout();
|
|
@@ -159,44 +615,162 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
159
615
|
}), this.layoutManager.TabDoubleClicked.subscribe(tabId => {
|
|
160
616
|
this.workspaceManager.TogglePin(tabId);
|
|
161
617
|
}), this.layoutManager.TabRightClicked.subscribe(event => {
|
|
162
|
-
this.showContextMenu(event.x, event.y, event.tabId);
|
|
618
|
+
this.showContextMenu(event.x, event.y, event.tabId, event.anchorEl);
|
|
619
|
+
}));
|
|
620
|
+
this.wireRecordsLayoutEvents();
|
|
621
|
+
// Breakpoint crossings rebuild the records GL in the other chrome mode
|
|
622
|
+
// (headerless+flattened vs full strip+splits). Skip-if-equal: the
|
|
623
|
+
// BehaviorSubject replays the current value on subscribe.
|
|
624
|
+
this.subscriptions.push(this.breakpoint.IsMobile$.subscribe(isMobile => {
|
|
625
|
+
// Exception-guarded like the Configuration subscription: a throw from
|
|
626
|
+
// one crossing (e.g. a cached component's ngOnDestroy during the GL
|
|
627
|
+
// teardown) would otherwise unsubscribe the stream permanently and
|
|
628
|
+
// brick breakpoint handling until reload.
|
|
629
|
+
try {
|
|
630
|
+
if (isMobile === this.mobileRecordsActive) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
this.mobileRecordsActive = isMobile;
|
|
634
|
+
this.rebuildRecordsLayoutForBreakpoint();
|
|
635
|
+
}
|
|
636
|
+
catch (err) {
|
|
637
|
+
LogError(err);
|
|
638
|
+
}
|
|
639
|
+
}),
|
|
640
|
+
// Open requests from surfaces outside this component (drawer pill)
|
|
641
|
+
this.recordSwitcher.OpenRequested.subscribe(() => {
|
|
642
|
+
this.OpenRecordSwitcher();
|
|
163
643
|
}));
|
|
164
|
-
// Subscribe to configuration changes to sync tabs
|
|
644
|
+
// Subscribe to configuration changes to sync tabs.
|
|
645
|
+
// The callback is exception-guarded: an error thrown from ONE emission
|
|
646
|
+
// would otherwise unsubscribe the stream permanently — after which the
|
|
647
|
+
// regions silently stop tracking the workspace (tabs drift, content
|
|
648
|
+
// stops loading) with no visible failure.
|
|
165
649
|
this.subscriptions.push(this.workspaceManager.Configuration.subscribe(config => {
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
650
|
+
try {
|
|
651
|
+
if (config) {
|
|
652
|
+
// Keep the RECORDS region in sync first — it also resolves whether
|
|
653
|
+
// the region is the visible surface for this configuration.
|
|
654
|
+
this.syncRecordsRegion(config);
|
|
655
|
+
this.updateRecordSurfaceState(config);
|
|
656
|
+
if (this.useSingleResourceMode) {
|
|
657
|
+
// In single-resource mode, reload content if the tab content changed
|
|
658
|
+
// The same tab ID can have different content (tab gets reused)
|
|
659
|
+
const activeTab = config.tabs.find(t => t.id === config.activeTabId) || config.tabs[0];
|
|
660
|
+
if (activeTab && this.isRecordTab(activeTab)) {
|
|
661
|
+
// Active tab is a RECORD — the records region owns it. Leave the
|
|
662
|
+
// main region's content (the last nav page) untouched behind it.
|
|
179
663
|
}
|
|
180
|
-
else {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
664
|
+
else if (activeTab) {
|
|
665
|
+
const signature = this.getTabContentSignature(activeTab);
|
|
666
|
+
if (signature !== this.currentSingleResourceSignature) {
|
|
667
|
+
// DO NOT call saveCurrentComponentQueryParams() here — by the time this
|
|
668
|
+
// subscription fires, OpenTab has already replaced the tab config with the
|
|
669
|
+
// new nav item's config, so queryParams are gone. The cache entry already
|
|
670
|
+
// has the correct queryParams from the most recent unchanged-signature save.
|
|
671
|
+
this.loadSingleResourceContent();
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Signature unchanged — sync queryParams to cache entry so it stays current.
|
|
675
|
+
// This catches incremental queryParam updates (e.g., user selects a conversation).
|
|
676
|
+
this.saveCurrentComponentQueryParams();
|
|
677
|
+
}
|
|
184
678
|
}
|
|
185
679
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
680
|
+
else if (this.layoutRestorationComplete && !this.isCreatingInitialTabs) {
|
|
681
|
+
// In multi-tab mode, sync with Golden Layout.
|
|
682
|
+
// Record tabs live in the records region — never in the main layout.
|
|
683
|
+
// IMPORTANT: Only sync AFTER layout restoration is complete to avoid creating duplicate tabs
|
|
684
|
+
// layoutRestorationComplete is set to true only after initializeGoldenLayout finishes
|
|
685
|
+
this.syncTabsWithConfiguration(config.tabs.filter(t => !this.isRecordTab(t)));
|
|
686
|
+
}
|
|
192
687
|
}
|
|
193
688
|
}
|
|
689
|
+
catch (err) {
|
|
690
|
+
// Never let one bad emission kill the stream — see comment above.
|
|
691
|
+
LogError(err);
|
|
692
|
+
}
|
|
194
693
|
}));
|
|
195
694
|
// Subscribe to tab bar visibility changes for single-resource mode
|
|
196
695
|
this.subscriptions.push(this.workspaceManager.TabBarVisible.subscribe(tabBarVisible => {
|
|
197
696
|
this.handleTabBarVisibilityChange(tabBarVisible);
|
|
198
697
|
}));
|
|
199
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* RECORDS region event wiring: the separate layout's events route through
|
|
701
|
+
* the SAME handlers as the main layout — content loading, close, pin, and
|
|
702
|
+
* context menu are layout-agnostic; only layout persistence targets its
|
|
703
|
+
* own slot. Activation + persistence write-backs are suppressed during
|
|
704
|
+
* batch creation/restore (recordsCreatingTabs) so GL's per-tab-created
|
|
705
|
+
* events can't steal focus or persist half-built layouts.
|
|
706
|
+
*/
|
|
707
|
+
wireRecordsLayoutEvents() {
|
|
708
|
+
this.subscriptions.push(this.recordsLayoutManager.TabShown.subscribe(async (event) => {
|
|
709
|
+
if (event.isFirstShow) {
|
|
710
|
+
await this.loadTabContent(event.tabId, event.container);
|
|
711
|
+
// Mark loaded ONLY when content actually attached to the LIVE
|
|
712
|
+
// container. During layout restore, GL can re-render item elements
|
|
713
|
+
// while an async load is in flight — the load appends into a
|
|
714
|
+
// detached element and the visible pane stays blank. Leaving the
|
|
715
|
+
// tab unmarked lets the next show retry, which hits the component
|
|
716
|
+
// cache and reattaches instantly into the live element.
|
|
717
|
+
const live = this.recordsLayoutManager.GetContainer(event.tabId);
|
|
718
|
+
if (live?.element && live.element.childElementCount > 0) {
|
|
719
|
+
this.recordsLayoutManager.MarkTabLoaded(event.tabId);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}), this.recordsLayoutManager.TabClosed.subscribe(async (tabId) => {
|
|
723
|
+
this.cleanupTabComponent(tabId);
|
|
724
|
+
// REBUILD guard (breakpoint crossing): Destroy() fires TabClosed for
|
|
725
|
+
// every pane. The tabs are NOT closing — the layout is being rebuilt
|
|
726
|
+
// in the other chrome mode. Cache detach above is exactly what the
|
|
727
|
+
// re-init needs; everything below (CloseTab, backfill) must not run.
|
|
728
|
+
if (this.recordsRebuilding) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const closedTab = this.workspaceManager.GetTab(tabId);
|
|
732
|
+
// PROMOTE guard ("Move to Workspace"): the tab left the records GL
|
|
733
|
+
// because its region membership changed — it's still in the
|
|
734
|
+
// workspace config and the MAIN layout is about to pick it up.
|
|
735
|
+
// Closing it here would destroy the tab the user just promoted.
|
|
736
|
+
// cleanupTabComponent above still runs: the cache detach is what
|
|
737
|
+
// lets the main GL reattach the component with state intact.
|
|
738
|
+
if (closedTab && !this.isRecordTab(closedTab)) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
// Guard: a configuration sync that removed this tab already updated
|
|
742
|
+
// the workspace — closing again would re-emit an unchanged config
|
|
743
|
+
// from inside a subscription pass.
|
|
744
|
+
if (closedTab) {
|
|
745
|
+
this.workspaceManager.CloseTab(tabId);
|
|
746
|
+
}
|
|
747
|
+
// Records close OUTRIGHT (no keep-last-tab-alive rule) — if that
|
|
748
|
+
// emptied the workspace, land the user on the active app's default
|
|
749
|
+
// tab instead of a blank shell.
|
|
750
|
+
await this.backfillEmptyWorkspace();
|
|
751
|
+
}), this.recordsLayoutManager.ActiveTab.subscribe(tabId => {
|
|
752
|
+
this.recordsRegionActiveTabId = tabId;
|
|
753
|
+
if (tabId && this.ShowRecordsRegion && !this.recordsCreatingTabs && !this.recordsRebuilding) {
|
|
754
|
+
this.workspaceManager.SetActiveTab(tabId);
|
|
755
|
+
}
|
|
756
|
+
}), this.recordsLayoutManager.LayoutChanged.subscribe(() => {
|
|
757
|
+
// MOBILE suppression: the mobile surface renders a FLATTENED clone of
|
|
758
|
+
// the persisted layout — persisting it would destroy the user's
|
|
759
|
+
// desktop splits (GL fires stateChanged on load/resize/tab ops, so
|
|
760
|
+
// the clobber would land within the 500ms persist debounce of the
|
|
761
|
+
// first mobile render). While suppressed, tab opens/closes still
|
|
762
|
+
// persist via the tab LIST; on the next desktop init a changed tab
|
|
763
|
+
// count hits the existing count-mismatch restore path (fresh
|
|
764
|
+
// sequential creation — splits lost gracefully, records kept).
|
|
765
|
+
if (this.recordsLayoutInitialized && !this.recordsCreatingTabs && !this.recordsRebuilding && !this.mobileRecordsActive) {
|
|
766
|
+
this.workspaceManager.UpdateRecordsLayout(this.recordsLayoutManager.SaveLayout());
|
|
767
|
+
}
|
|
768
|
+
}), this.recordsLayoutManager.TabDoubleClicked.subscribe(tabId => {
|
|
769
|
+
this.workspaceManager.TogglePin(tabId);
|
|
770
|
+
}), this.recordsLayoutManager.TabRightClicked.subscribe(event => {
|
|
771
|
+
this.showContextMenu(event.x, event.y, event.tabId, event.anchorEl);
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
200
774
|
ngAfterViewInit() {
|
|
201
775
|
// Initialize Golden Layout only if we're not in single-resource mode
|
|
202
776
|
if (!this.useSingleResourceMode) {
|
|
@@ -235,7 +809,13 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
235
809
|
// If not, wait for it to be loaded before proceeding
|
|
236
810
|
const config = this.workspaceManager.GetConfiguration();
|
|
237
811
|
if (!config) {
|
|
238
|
-
// Configuration not loaded yet
|
|
812
|
+
// Configuration not loaded yet — wait for it. TRACKED in
|
|
813
|
+
// this.subscriptions: the sub normally retires itself on the first
|
|
814
|
+
// non-null config, but if the component is destroyed while config is
|
|
815
|
+
// still null (failed load, fast navigation away), an untracked sub
|
|
816
|
+
// would outlive us and re-init a destroyed component when config
|
|
817
|
+
// finally lands. Unsubscribe is idempotent — self-retire + ngOnDestroy
|
|
818
|
+
// both firing is safe.
|
|
239
819
|
const configSub = this.workspaceManager.Configuration.subscribe(loadedConfig => {
|
|
240
820
|
if (loadedConfig) {
|
|
241
821
|
configSub.unsubscribe();
|
|
@@ -243,14 +823,18 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
243
823
|
this.initializeGoldenLayout(forceCreateTabs);
|
|
244
824
|
}
|
|
245
825
|
});
|
|
826
|
+
this.subscriptions.push(configSub);
|
|
246
827
|
return;
|
|
247
828
|
}
|
|
248
829
|
// Initialize Golden Layout (we have config now)
|
|
249
830
|
this.layoutManager.Initialize(this.glContainer.nativeElement);
|
|
250
831
|
// Mark layout as initialized
|
|
251
832
|
this.layoutInitialized = true;
|
|
833
|
+
// The MAIN layout hosts only non-record tabs — record tabs live in the
|
|
834
|
+
// separate records region (no-op filter under classic style).
|
|
835
|
+
const mainTabs = config.tabs.filter(t => !this.isRecordTab(t));
|
|
252
836
|
// Check if config has no tabs
|
|
253
|
-
if (
|
|
837
|
+
if (mainTabs.length === 0) {
|
|
254
838
|
// No tabs to load, but mark restoration as complete
|
|
255
839
|
this.layoutRestorationComplete = true;
|
|
256
840
|
return;
|
|
@@ -260,8 +844,8 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
260
844
|
if (hasSavedLayout && !forceCreateTabs && config.layout) {
|
|
261
845
|
// VALIDATE: Check that layout component count matches tabs array count
|
|
262
846
|
const layoutComponentCount = this.countLayoutComponents(config.layout.root);
|
|
263
|
-
if (layoutComponentCount !==
|
|
264
|
-
console.warn(`[TabContainer.initializeGoldenLayout] Layout/tabs mismatch: layout has ${layoutComponentCount} components but tabs array has ${
|
|
847
|
+
if (layoutComponentCount !== mainTabs.length) {
|
|
848
|
+
console.warn(`[TabContainer.initializeGoldenLayout] Layout/tabs mismatch: layout has ${layoutComponentCount} components but tabs array has ${mainTabs.length} tabs. Clearing layout.`);
|
|
265
849
|
this.workspaceManager.ClearLayout();
|
|
266
850
|
// Fall through to create fresh tabs
|
|
267
851
|
}
|
|
@@ -290,8 +874,8 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
290
874
|
}
|
|
291
875
|
}
|
|
292
876
|
// CREATE FRESH - no saved layout, forceCreateTabs=true, or layout load failed
|
|
293
|
-
// Use
|
|
294
|
-
const sortedTabs = [...
|
|
877
|
+
// Use the main (non-record) tabs sorted by sequence for a single-stack layout
|
|
878
|
+
const sortedTabs = [...mainTabs].sort((a, b) => a.sequence - b.sequence);
|
|
295
879
|
this.isCreatingInitialTabs = true;
|
|
296
880
|
try {
|
|
297
881
|
sortedTabs.forEach(tab => {
|
|
@@ -366,9 +950,59 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
366
950
|
}
|
|
367
951
|
}
|
|
368
952
|
}
|
|
953
|
+
// The RECORDS region is a separate layout with the same destroyed-cache
|
|
954
|
+
// problem — sweep it in BOTH main modes or every record pane stays
|
|
955
|
+
// permanently blank after a tenant switch (marked loaded, component gone).
|
|
956
|
+
await this.reloadRecordsRegionTabs();
|
|
957
|
+
}
|
|
958
|
+
/** Mark all records-region tabs not-loaded and reload the active one (tenant switch) */
|
|
959
|
+
async reloadRecordsRegionTabs() {
|
|
960
|
+
if (!this.recordsLayoutInitialized) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
for (const tabId of this.recordsLayoutManager.GetAllTabIds()) {
|
|
964
|
+
this.recordsLayoutManager.MarkTabNotLoaded(tabId);
|
|
965
|
+
}
|
|
966
|
+
const activeTabId = this.workspaceManager.GetActiveTabId();
|
|
967
|
+
const activeTab = activeTabId ? this.workspaceManager.GetTab(activeTabId) : undefined;
|
|
968
|
+
if (activeTab && this.isRecordTab(activeTab)) {
|
|
969
|
+
const container = this.recordsLayoutManager.GetContainer(activeTab.id);
|
|
970
|
+
if (container) {
|
|
971
|
+
await this.loadTabContent(activeTab.id, container);
|
|
972
|
+
this.recordsLayoutManager.MarkTabLoaded(activeTab.id);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* If the workspace has no tabs left (closing the last record empties it —
|
|
978
|
+
* records skip the manager's keep-last-tab-alive rule), open the active
|
|
979
|
+
* app's default tab so the user lands on a meaningful surface.
|
|
980
|
+
*/
|
|
981
|
+
async backfillEmptyWorkspace() {
|
|
982
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
983
|
+
if (!config || config.tabs.length > 0) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const app = this.appManager.GetActiveApp();
|
|
987
|
+
if (!app) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const request = await app.CreateDefaultTab();
|
|
991
|
+
if (request) {
|
|
992
|
+
this.workspaceManager.OpenTab(request, app.GetColor());
|
|
993
|
+
}
|
|
369
994
|
}
|
|
370
995
|
ngOnDestroy() {
|
|
996
|
+
// Pane crumbs are appRef-attached views — destroy them or they stay in
|
|
997
|
+
// every CD pass after the container is gone (logout/tenant teardown).
|
|
998
|
+
this.originCrumbRefs.forEach(ref => ref.destroy());
|
|
999
|
+
this.originCrumbRefs.clear();
|
|
371
1000
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
|
1001
|
+
// Tear down the records region's layout
|
|
1002
|
+
if (this.recordsLayoutInitialized) {
|
|
1003
|
+
this.recordsLayoutManager.Destroy();
|
|
1004
|
+
this.recordsLayoutInitialized = false;
|
|
1005
|
+
}
|
|
372
1006
|
// Cleanup single-resource mode component if exists
|
|
373
1007
|
this.cleanupSingleResourceComponent();
|
|
374
1008
|
// Clear the component cache (destroys all components)
|
|
@@ -435,8 +1069,11 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
435
1069
|
// All OTHER unpinned tabs should be pinned since they represent content
|
|
436
1070
|
// the user explicitly kept open
|
|
437
1071
|
const updatedTabs = config.tabs.map(tab => {
|
|
438
|
-
// Pin all tabs except the newly active one (which is
|
|
439
|
-
|
|
1072
|
+
// Pin all MAIN-layout tabs except the newly active one (which is
|
|
1073
|
+
// the temporary tab). Record tabs are exempt — their pin state is
|
|
1074
|
+
// user-owned (PreservePinState), and force-pinning them makes the
|
|
1075
|
+
// temp-tab semantics path-dependent.
|
|
1076
|
+
if (tab.id !== config.activeTabId && !tab.isPinned && !this.isRecordTab(tab)) {
|
|
440
1077
|
return { ...tab, isPinned: true };
|
|
441
1078
|
}
|
|
442
1079
|
return tab;
|
|
@@ -479,7 +1116,18 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
479
1116
|
return;
|
|
480
1117
|
}
|
|
481
1118
|
// Get the active tab (or first tab)
|
|
482
|
-
|
|
1119
|
+
let activeTab = config.tabs.find(t => t.id === config.activeTabId) || config.tabs[0];
|
|
1120
|
+
// Records style: record tabs render in the RECORDS region, never here.
|
|
1121
|
+
// When the active tab is a record (e.g. reloading the app while viewing
|
|
1122
|
+
// one), the main region shows the most recently used NAV tab behind it.
|
|
1123
|
+
if (activeTab && this.isRecordTab(activeTab)) {
|
|
1124
|
+
const navTabs = config.tabs.filter(t => !this.isRecordTab(t));
|
|
1125
|
+
activeTab = [...navTabs].sort((a, b) => (b.lastAccessedAt || '').localeCompare(a.lastAccessedAt || ''))[0];
|
|
1126
|
+
if (!activeTab) {
|
|
1127
|
+
// Only record tabs exist — the records region owns first-load
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
483
1131
|
if (!activeTab) {
|
|
484
1132
|
// Config has tabs but none match activeTabId and the array fallback failed.
|
|
485
1133
|
// This shouldn't happen, but if it does, unblock the loading screen.
|
|
@@ -523,8 +1171,13 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
523
1171
|
// IMPORTANT: We use markAsAttached here, NOT markAsDetached — the component is being
|
|
524
1172
|
// reattached to the DOM and should NOT be eligible for LRU eviction.
|
|
525
1173
|
this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, activeTab.id, cacheDiscriminator);
|
|
1174
|
+
// Record panes lead with their origin crumb
|
|
1175
|
+
this.ensureRecordOriginCrumb(activeTab, container);
|
|
1176
|
+
// Re-home the component's tab binding (see loadTabContent's cached
|
|
1177
|
+
// branch — same cross-tab reattach hazard).
|
|
1178
|
+
cached.componentRef.instance.RebindTabId(activeTab.id);
|
|
526
1179
|
// Reattach the cached wrapper element to single-resource container
|
|
527
|
-
|
|
1180
|
+
// (sizing via the pane-layout CSS: crumb fixed, content flex-fills)
|
|
528
1181
|
container.appendChild(cached.wrapperElement);
|
|
529
1182
|
// Store reference and identity for cleanup/detachment
|
|
530
1183
|
this.singleResourceComponentRef = cached.componentRef;
|
|
@@ -594,12 +1247,15 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
594
1247
|
// Guard: only update the title if THIS component is the currently displayed one.
|
|
595
1248
|
// Without this guard, cached components (detached but alive) can fire this callback
|
|
596
1249
|
// and overwrite the active tab's title with a stale name.
|
|
1250
|
+
// CRITICAL: rename the tab this component was RENDERED FOR — never
|
|
1251
|
+
// GetActiveTabId(). Under the records style the active tab can be a
|
|
1252
|
+
// record (region showing) while the main region keeps a SUBSTITUTE nav
|
|
1253
|
+
// component alive underneath; its emission was renaming the user's
|
|
1254
|
+
// record tab (an Action Params record ended up titled "Data").
|
|
1255
|
+
const renderedTabId = activeTab.id;
|
|
597
1256
|
instance.DisplayNameChangedEvent = (newName) => {
|
|
598
1257
|
if (this.singleResourceComponentRef?.instance === instance) {
|
|
599
|
-
|
|
600
|
-
if (tabId) {
|
|
601
|
-
this.workspaceManager.UpdateTabTitle(tabId, newName);
|
|
602
|
-
}
|
|
1258
|
+
this.workspaceManager.UpdateTabTitle(renderedTabId, newName);
|
|
603
1259
|
}
|
|
604
1260
|
};
|
|
605
1261
|
// When a record is saved, re-key the tab and cache (new-record → saved transition)
|
|
@@ -613,13 +1269,12 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
613
1269
|
instance.ResourceCloseRequestedEvent = () => {
|
|
614
1270
|
this.handleResourceCloseRequested(activeTab.id, instance);
|
|
615
1271
|
};
|
|
1272
|
+
// Record panes lead with their origin crumb
|
|
1273
|
+
this.ensureRecordOriginCrumb(activeTab, container);
|
|
616
1274
|
// Get the native element and append to container
|
|
1275
|
+
// (sizing via the pane-layout CSS: crumb fixed, content flex-fills)
|
|
617
1276
|
const nativeElement = componentRef.hostView.rootNodes[0];
|
|
618
1277
|
container.appendChild(nativeElement);
|
|
619
|
-
// now make sure that the container's direct child is 100% height
|
|
620
|
-
if (container.children?.length > 0) {
|
|
621
|
-
container.children[0].style.height = "100%";
|
|
622
|
-
}
|
|
623
1278
|
// Cache the component for reuse when switching between nav items within the same app.
|
|
624
1279
|
// Without this, every nav switch creates a brand new component from scratch.
|
|
625
1280
|
const wrapperElement = nativeElement;
|
|
@@ -880,12 +1535,56 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
880
1535
|
title: tab.title,
|
|
881
1536
|
route: tab.configuration['route'] || '',
|
|
882
1537
|
isPinned: tab.isPinned,
|
|
883
|
-
isLoaded: false
|
|
1538
|
+
isLoaded: false,
|
|
1539
|
+
typeIcon: this.resolveTabTypeIcon(tab)
|
|
884
1540
|
};
|
|
885
1541
|
this.layoutManager.AddTab(state);
|
|
1542
|
+
// Nav-item icons live behind an async lookup — upgrade in the background
|
|
1543
|
+
void this.upgradeNavTabIcon(tab, this.layoutManager);
|
|
886
1544
|
// Load display name in background without loading full component
|
|
887
1545
|
this.updateTabDisplayName(tab);
|
|
888
1546
|
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Synchronous best-effort TYPE icon for a tab (the app-colored icon in
|
|
1549
|
+
* the tab's type slot — see TabComponentState.typeIcon). Record tabs
|
|
1550
|
+
* resolve fully here (entity metadata is loaded); nav tabs start with the
|
|
1551
|
+
* app's icon and get upgraded to the nav item's own icon asynchronously
|
|
1552
|
+
* (upgradeNavTabIcon).
|
|
1553
|
+
*/
|
|
1554
|
+
resolveTabTypeIcon(tab) {
|
|
1555
|
+
if (IsRecordsTabConfiguration(tab.configuration)) {
|
|
1556
|
+
// Shared helper — the record bar and switcher sheet resolve through the
|
|
1557
|
+
// same function so a record shows one icon everywhere
|
|
1558
|
+
return ResolveRecordTypeIcon(tab.configuration, this.ProviderToUse);
|
|
1559
|
+
}
|
|
1560
|
+
const app = this.appManager.GetAppById(tab.applicationId);
|
|
1561
|
+
return app?.Icon || 'fa-regular fa-file';
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Resolve and apply a NAV tab's type icon — the async owner of nav-tab
|
|
1565
|
+
* icons (GetNavItems is a cached JSON parse: near-instant, but async by
|
|
1566
|
+
* contract). Ladder: nav item's own icon → app icon → generic. No-op for
|
|
1567
|
+
* record tabs (their icons resolve synchronously). Idempotent: the slot
|
|
1568
|
+
* only touches the DOM when the class actually changes.
|
|
1569
|
+
*/
|
|
1570
|
+
async upgradeNavTabIcon(tab, manager) {
|
|
1571
|
+
if (IsRecordsTabConfiguration(tab.configuration)) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const app = this.appManager.GetAppById(tab.applicationId);
|
|
1575
|
+
let navItemIcon;
|
|
1576
|
+
const navItemName = tab.configuration?.['navItemName'];
|
|
1577
|
+
if (app && typeof navItemName === 'string' && navItemName) {
|
|
1578
|
+
try {
|
|
1579
|
+
const navItems = await app.GetNavItems();
|
|
1580
|
+
navItemIcon = navItems.find(i => i.Label === navItemName)?.Icon;
|
|
1581
|
+
}
|
|
1582
|
+
catch {
|
|
1583
|
+
// Icon resolution is cosmetic — fall through to the app fallback.
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
manager.UpdateTabStyle(tab.id, { typeIcon: navItemIcon || app?.Icon || 'fa-regular fa-file' });
|
|
1587
|
+
}
|
|
889
1588
|
/**
|
|
890
1589
|
* Handle tab shown event for lazy loading
|
|
891
1590
|
*/
|
|
@@ -931,6 +1630,9 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
931
1630
|
}
|
|
932
1631
|
// Clear any existing content from the container (important for tab reuse)
|
|
933
1632
|
glContainer.element.innerHTML = '';
|
|
1633
|
+
// Record panes lead with their origin crumb — BEFORE content attaches
|
|
1634
|
+
// (fresh or cached), so it is always the pane's first element.
|
|
1635
|
+
this.ensureRecordOriginCrumb(tab, glContainer.element);
|
|
934
1636
|
// Get driver class for cache lookup (resolves to actual component class name)
|
|
935
1637
|
const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
|
|
936
1638
|
// Discriminate distinct "new record" tabs of different entities (all have empty
|
|
@@ -943,6 +1645,12 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
943
1645
|
glContainer.element.appendChild(cached.wrapperElement);
|
|
944
1646
|
// Mark as attached to this tab
|
|
945
1647
|
this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, tabId, cacheDiscriminator);
|
|
1648
|
+
// RE-HOME the component's tab binding: the cache keys on
|
|
1649
|
+
// driver+record+app, so this component may have been born under a
|
|
1650
|
+
// DIFFERENT tab id — without the rebind its query-param
|
|
1651
|
+
// subscription listens to the dead tab forever and deliveries to
|
|
1652
|
+
// this tab are lost.
|
|
1653
|
+
cached.componentRef.instance.RebindTabId(tabId);
|
|
946
1654
|
// Keep legacy componentRefs map updated
|
|
947
1655
|
this.componentRefs.set(tabId, cached.componentRef);
|
|
948
1656
|
// If resource is already loaded, update tab title immediately and signal
|
|
@@ -991,9 +1699,12 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
991
1699
|
instance.ResourceCloseRequestedEvent = () => {
|
|
992
1700
|
this.handleResourceCloseRequested(tabId, instance);
|
|
993
1701
|
};
|
|
994
|
-
// Wire up display name change notifications
|
|
1702
|
+
// Wire up display name change notifications (routed to whichever
|
|
1703
|
+
// Golden Layout hosts this tab)
|
|
995
1704
|
instance.DisplayNameChangedEvent = (newName) => {
|
|
996
|
-
this.
|
|
1705
|
+
const t = this.workspaceManager.GetTab(tabId);
|
|
1706
|
+
const manager = t && this.isRecordTab(t) ? this.recordsLayoutManager : this.layoutManager;
|
|
1707
|
+
manager.UpdateTabStyle(tabId, { title: newName });
|
|
997
1708
|
this.workspaceManager.UpdateTabTitle(tabId, newName);
|
|
998
1709
|
};
|
|
999
1710
|
// Create a container div for the component
|
|
@@ -1056,8 +1767,9 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
1056
1767
|
const tempInstance = runInInjectionContext(this.environmentInjector, () => new resourceReg.SubClass());
|
|
1057
1768
|
const displayName = await tempInstance.GetResourceDisplayName(resourceData);
|
|
1058
1769
|
if (displayName && displayName !== tab.title) {
|
|
1059
|
-
// Update the tab title in Golden Layout
|
|
1060
|
-
this.
|
|
1770
|
+
// Update the tab title in whichever Golden Layout hosts this tab
|
|
1771
|
+
const targetManager = this.isRecordTab(tab) ? this.recordsLayoutManager : this.layoutManager;
|
|
1772
|
+
targetManager.UpdateTabStyle(tab.id, { title: displayName });
|
|
1061
1773
|
// Update the tab title in workspace configuration for persistence
|
|
1062
1774
|
this.workspaceManager.UpdateTabTitle(tab.id, displayName);
|
|
1063
1775
|
}
|
|
@@ -1233,11 +1945,97 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
1233
1945
|
}
|
|
1234
1946
|
return 0;
|
|
1235
1947
|
}
|
|
1948
|
+
/**
|
|
1949
|
+
* A saved records layout is restorable only when its component tabIds are
|
|
1950
|
+
* EXACTLY the current record-tab set. Count equality is not enough: a
|
|
1951
|
+
* persistence-suppressed mobile session can open one record and close
|
|
1952
|
+
* another, leaving a same-sized layout that references a closed tab —
|
|
1953
|
+
* restoring it renders a ghost pane (content load fails) until the next
|
|
1954
|
+
* sync sweeps it.
|
|
1955
|
+
*/
|
|
1956
|
+
layoutCoversExactTabSet(root, recordTabs) {
|
|
1957
|
+
const layoutIds = new Set();
|
|
1958
|
+
const collect = (node) => {
|
|
1959
|
+
if (!node) {
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (node.type === 'component') {
|
|
1963
|
+
const id = node.componentState?.['tabId'];
|
|
1964
|
+
if (typeof id === 'string') {
|
|
1965
|
+
layoutIds.add(id);
|
|
1966
|
+
}
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
node.content?.forEach(collect);
|
|
1970
|
+
};
|
|
1971
|
+
collect(root);
|
|
1972
|
+
return layoutIds.size === recordTabs.length && recordTabs.every(t => layoutIds.has(t.id));
|
|
1973
|
+
}
|
|
1236
1974
|
/**
|
|
1237
1975
|
* Cleanup a tab's component
|
|
1238
1976
|
* Detaches from DOM but keeps in cache for potential reuse
|
|
1239
1977
|
*/
|
|
1978
|
+
/**
|
|
1979
|
+
* Ensure a record pane's FIRST element is its origin crumb (Matt's
|
|
1980
|
+
* pane-level placement — correct in splits, docked panes, and
|
|
1981
|
+
* single-resource, unlike the old region-level bar). Recreated on every
|
|
1982
|
+
* content attach: the pane container is cleared/re-homed on loads,
|
|
1983
|
+
* cache reattaches, and promote/demote moves.
|
|
1984
|
+
*/
|
|
1985
|
+
ensureRecordOriginCrumb(tab, containerEl) {
|
|
1986
|
+
this.destroyOriginCrumb(tab.id);
|
|
1987
|
+
// The single-resource container is REUSED across tabs — sweep any crumb
|
|
1988
|
+
// element a previous tab left behind before (maybe) adding ours.
|
|
1989
|
+
containerEl.querySelectorAll('mj-record-origin-crumb').forEach(e => e.remove());
|
|
1990
|
+
if (!this.RecordsStyleActive || !IsRecordsTabConfiguration(tab.configuration)) {
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
const origin = GetRecordSourceContext(tab.configuration);
|
|
1994
|
+
if (!origin) {
|
|
1995
|
+
return; // No captured origin (deep link, history re-open) — no crumb.
|
|
1996
|
+
}
|
|
1997
|
+
const ref = createComponent(RecordOriginCrumbComponent, {
|
|
1998
|
+
environmentInjector: this.environmentInjector
|
|
1999
|
+
});
|
|
2000
|
+
ref.setInput('Origin', origin);
|
|
2001
|
+
this.appRef.attachView(ref.hostView);
|
|
2002
|
+
const el = ref.hostView.rootNodes[0];
|
|
2003
|
+
containerEl.insertBefore(el, containerEl.firstChild);
|
|
2004
|
+
this.originCrumbRefs.set(tab.id, ref);
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Refresh a pane crumb's origin after a config change (re-open
|
|
2008
|
+
* re-capture). Also handles the CREATE case: a record first opened
|
|
2009
|
+
* WITHOUT an origin (deep link, history recreate) whose re-open just
|
|
2010
|
+
* captured one — the pane is already attached, so ensureRecordOriginCrumb
|
|
2011
|
+
* never runs again; build the crumb into the live pane here.
|
|
2012
|
+
*/
|
|
2013
|
+
updateOriginCrumb(tab) {
|
|
2014
|
+
const ref = this.originCrumbRefs.get(tab.id);
|
|
2015
|
+
const origin = GetRecordSourceContext(tab.configuration);
|
|
2016
|
+
if (ref) {
|
|
2017
|
+
ref.setInput('Origin', origin);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
if (origin && this.RecordsStyleActive && IsRecordsTabConfiguration(tab.configuration)) {
|
|
2021
|
+
const paneEl = this.recordsLayoutManager.GetContainer(tab.id)?.element
|
|
2022
|
+
?? this.layoutManager.GetContainer(tab.id)?.element;
|
|
2023
|
+
if (paneEl && paneEl.childElementCount > 0) {
|
|
2024
|
+
this.ensureRecordOriginCrumb(tab, paneEl);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
destroyOriginCrumb(tabId) {
|
|
2029
|
+
const ref = this.originCrumbRefs.get(tabId);
|
|
2030
|
+
if (ref) {
|
|
2031
|
+
ref.destroy();
|
|
2032
|
+
this.originCrumbRefs.delete(tabId);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1240
2035
|
cleanupTabComponent(tabId) {
|
|
2036
|
+
// The pane crumb belongs to the pane, not the cached component — always
|
|
2037
|
+
// destroyed here; the next attach recreates it in the new pane.
|
|
2038
|
+
this.destroyOriginCrumb(tabId);
|
|
1241
2039
|
// First, try to detach from cache (preserves component for reuse)
|
|
1242
2040
|
const cachedInfo = this.cacheManager.findAndDetachByTabId(tabId);
|
|
1243
2041
|
if (cachedInfo) {
|
|
@@ -1315,13 +2113,24 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
1315
2113
|
}
|
|
1316
2114
|
}
|
|
1317
2115
|
}
|
|
1318
|
-
// Update styling for existing tabs
|
|
2116
|
+
// Update styling for existing tabs. typeIcon ownership is split:
|
|
2117
|
+
// record tabs resolve synchronously here; NAV tab icons are owned
|
|
2118
|
+
// by upgradeNavTabIcon (async ladder) — setting the sync fallback
|
|
2119
|
+
// here too would downgrade an upgraded icon every emission and
|
|
2120
|
+
// flicker. Restored nav tabs never pass through createTab, so the
|
|
2121
|
+
// upgrade call here is also their FIRST icon application.
|
|
1319
2122
|
const app = this.appManager.GetAppById(tab.applicationId);
|
|
1320
|
-
|
|
2123
|
+
const styleUpdate = {
|
|
1321
2124
|
isPinned: tab.isPinned,
|
|
1322
2125
|
title: tab.title,
|
|
1323
|
-
appColor: app?.GetColor() ||
|
|
1324
|
-
}
|
|
2126
|
+
appColor: app?.GetColor() || DEFAULT_APP_COLOR
|
|
2127
|
+
};
|
|
2128
|
+
if (IsRecordsTabConfiguration(tab.configuration)) {
|
|
2129
|
+
styleUpdate.typeIcon = this.resolveTabTypeIcon(tab);
|
|
2130
|
+
}
|
|
2131
|
+
this.layoutManager.UpdateTabStyle(tab.id, styleUpdate);
|
|
2132
|
+
void this.upgradeNavTabIcon(tab, this.layoutManager);
|
|
2133
|
+
this.updateOriginCrumb(tab);
|
|
1325
2134
|
}
|
|
1326
2135
|
});
|
|
1327
2136
|
// Focus the active tab
|
|
@@ -1333,38 +2142,113 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
1333
2142
|
/**
|
|
1334
2143
|
* Show context menu
|
|
1335
2144
|
*/
|
|
1336
|
-
showContextMenu(x, y, tabId) {
|
|
2145
|
+
showContextMenu(x, y, tabId, anchorEl) {
|
|
1337
2146
|
this.contextMenuX = x;
|
|
1338
2147
|
this.contextMenuY = y;
|
|
1339
2148
|
this.contextMenuTabId = tabId;
|
|
1340
2149
|
this.contextMenuVisible = true;
|
|
1341
|
-
//
|
|
2150
|
+
// Remember the invoking control so closing can hand focus back (the
|
|
2151
|
+
// slot button passes itself; right-click falls back to whatever was
|
|
2152
|
+
// focused). aria-expanded reflects the open menu on the anchor.
|
|
2153
|
+
this.contextMenuAnchor = anchorEl ?? (document.activeElement instanceof HTMLElement ? document.activeElement : null);
|
|
2154
|
+
if (this.contextMenuAnchor?.classList.contains('mj-tab-type-slot')) {
|
|
2155
|
+
this.contextMenuAnchor.setAttribute('aria-expanded', 'true');
|
|
2156
|
+
}
|
|
2157
|
+
// GL tab events (right-click, type-slot click) originate OUTSIDE any
|
|
2158
|
+
// Angular CD trigger — zoneless: without an explicit flush the flag
|
|
2159
|
+
// flips but the menu never renders.
|
|
2160
|
+
SafeDetectChanges(this.cdr);
|
|
2161
|
+
// Menu-pattern focus: land on the first enabled item so arrow keys work
|
|
2162
|
+
// immediately (rAF — the flush above just created the DOM).
|
|
2163
|
+
requestAnimationFrame(() => {
|
|
2164
|
+
const first = document.querySelector('.context-menu [role="menuitem"]:not([disabled])');
|
|
2165
|
+
first?.focus();
|
|
2166
|
+
});
|
|
2167
|
+
// Close menu when clicking outside - use setTimeout to avoid immediate trigger.
|
|
2168
|
+
// CAPTURE phase: bubble-phase closers are blind to clicks whose
|
|
2169
|
+
// propagation something stopped (the origin crumb stops its clicks so
|
|
2170
|
+
// GL pane-focus can't stomp navigation) — the menu stayed open when the
|
|
2171
|
+
// outside click landed on such an element.
|
|
1342
2172
|
setTimeout(() => {
|
|
1343
2173
|
const clickHandler = (event) => {
|
|
1344
2174
|
const target = event.target;
|
|
1345
2175
|
if (!target.closest('.context-menu')) {
|
|
1346
2176
|
this.hideContextMenu();
|
|
1347
|
-
document.removeEventListener('click', clickHandler);
|
|
1348
|
-
document.removeEventListener('keydown', keyHandler);
|
|
2177
|
+
document.removeEventListener('click', clickHandler, true);
|
|
2178
|
+
document.removeEventListener('keydown', keyHandler, true);
|
|
1349
2179
|
}
|
|
1350
2180
|
};
|
|
1351
2181
|
const keyHandler = (event) => {
|
|
1352
2182
|
if (event.key === 'Escape') {
|
|
1353
2183
|
this.hideContextMenu();
|
|
1354
|
-
document.removeEventListener('click', clickHandler);
|
|
1355
|
-
document.removeEventListener('keydown', keyHandler);
|
|
2184
|
+
document.removeEventListener('click', clickHandler, true);
|
|
2185
|
+
document.removeEventListener('keydown', keyHandler, true);
|
|
1356
2186
|
}
|
|
1357
2187
|
};
|
|
1358
|
-
document.addEventListener('click', clickHandler);
|
|
1359
|
-
document.addEventListener('keydown', keyHandler);
|
|
2188
|
+
document.addEventListener('click', clickHandler, true);
|
|
2189
|
+
document.addEventListener('keydown', keyHandler, true);
|
|
1360
2190
|
}, 0);
|
|
1361
2191
|
}
|
|
1362
2192
|
/**
|
|
1363
2193
|
* Hide context menu
|
|
1364
2194
|
*/
|
|
1365
|
-
hideContextMenu() {
|
|
2195
|
+
hideContextMenu(restoreFocus = true) {
|
|
1366
2196
|
this.contextMenuVisible = false;
|
|
1367
2197
|
this.contextMenuTabId = null;
|
|
2198
|
+
const anchor = this.contextMenuAnchor;
|
|
2199
|
+
this.contextMenuAnchor = null;
|
|
2200
|
+
if (anchor?.classList.contains('mj-tab-type-slot')) {
|
|
2201
|
+
anchor.setAttribute('aria-expanded', 'false');
|
|
2202
|
+
}
|
|
2203
|
+
// Outside-click/Escape teardown also runs outside CD — flush so the
|
|
2204
|
+
// menu actually disappears (see showContextMenu).
|
|
2205
|
+
SafeDetectChanges(this.cdr);
|
|
2206
|
+
// Menu-pattern focus return (app-switcher precedent). Actions that
|
|
2207
|
+
// deliberately move the user to another surface pass restoreFocus=false
|
|
2208
|
+
// and let the destination own focus.
|
|
2209
|
+
if (restoreFocus && anchor?.isConnected) {
|
|
2210
|
+
requestAnimationFrame(() => anchor.focus());
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* role="menu" keyboard model: ArrowUp/Down rove (wrapping) over enabled
|
|
2215
|
+
* items, Home/End jump, Escape closes with focus return, Tab dismisses
|
|
2216
|
+
* (native menus don't trap Tab).
|
|
2217
|
+
*/
|
|
2218
|
+
onMenuKeydown(event) {
|
|
2219
|
+
const items = Array.from(document.querySelectorAll('.context-menu [role="menuitem"]:not([disabled])'));
|
|
2220
|
+
if (items.length === 0) {
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
const idx = items.indexOf(document.activeElement);
|
|
2224
|
+
switch (event.key) {
|
|
2225
|
+
case 'ArrowDown':
|
|
2226
|
+
event.preventDefault();
|
|
2227
|
+
items[(idx + 1) % items.length].focus();
|
|
2228
|
+
break;
|
|
2229
|
+
case 'ArrowUp':
|
|
2230
|
+
event.preventDefault();
|
|
2231
|
+
items[(idx - 1 + items.length) % items.length].focus();
|
|
2232
|
+
break;
|
|
2233
|
+
case 'Home':
|
|
2234
|
+
event.preventDefault();
|
|
2235
|
+
items[0].focus();
|
|
2236
|
+
break;
|
|
2237
|
+
case 'End':
|
|
2238
|
+
event.preventDefault();
|
|
2239
|
+
items[items.length - 1].focus();
|
|
2240
|
+
break;
|
|
2241
|
+
case 'Escape':
|
|
2242
|
+
// Handle here (with stopPropagation) so Escape works even before the
|
|
2243
|
+
// deferred document-level teardown listener attaches.
|
|
2244
|
+
event.preventDefault();
|
|
2245
|
+
event.stopPropagation();
|
|
2246
|
+
this.hideContextMenu();
|
|
2247
|
+
break;
|
|
2248
|
+
case 'Tab':
|
|
2249
|
+
this.hideContextMenu(false);
|
|
2250
|
+
break;
|
|
2251
|
+
}
|
|
1368
2252
|
}
|
|
1369
2253
|
/**
|
|
1370
2254
|
* Check if context menu tab is pinned
|
|
@@ -1375,6 +2259,69 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
1375
2259
|
const tab = this.workspaceManager.GetTab(this.contextMenuTabId);
|
|
1376
2260
|
return tab?.isPinned || false;
|
|
1377
2261
|
}
|
|
2262
|
+
/** Context tab is a records-REGION record — eligible for "Move to Workspace" */
|
|
2263
|
+
get canContextMoveToWorkspace() {
|
|
2264
|
+
// Docked/region composition is a DESKTOP concept — no move actions on mobile
|
|
2265
|
+
if (!this.contextMenuTabId || !this.RecordsStyleActive || this.mobileRecordsActive)
|
|
2266
|
+
return false;
|
|
2267
|
+
const tab = this.workspaceManager.GetTab(this.contextMenuTabId);
|
|
2268
|
+
return !!tab && IsRecordsRegionTab(tab.configuration);
|
|
2269
|
+
}
|
|
2270
|
+
/** Context tab is a DOCKED record — eligible for "Move to Records" */
|
|
2271
|
+
get canContextMoveToRecords() {
|
|
2272
|
+
if (!this.contextMenuTabId || !this.RecordsStyleActive || this.mobileRecordsActive)
|
|
2273
|
+
return false;
|
|
2274
|
+
const tab = this.workspaceManager.GetTab(this.contextMenuTabId);
|
|
2275
|
+
return !!tab && IsRecordsTabConfiguration(tab.configuration) && IsRecordDockedToWorkspace(tab.configuration);
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Promote: records region → main workspace layout ("Move to Workspace").
|
|
2279
|
+
* ORDER MATTERS: activate FIRST, then flip the flag. With the tab already
|
|
2280
|
+
* active, (a) the moved tab stays the user's focus through the membership
|
|
2281
|
+
* flip (syncTabsWithConfiguration's tail focuses config.activeTabId), and
|
|
2282
|
+
* (b) a single-resource→multi-tab transition triggered by the flip exempts
|
|
2283
|
+
* it from the force-pin sweep (it IS the activeTabId).
|
|
2284
|
+
*/
|
|
2285
|
+
onContextMoveToWorkspace() {
|
|
2286
|
+
const tabId = this.contextMenuTabId;
|
|
2287
|
+
this.hideContextMenu(false);
|
|
2288
|
+
if (!tabId)
|
|
2289
|
+
return;
|
|
2290
|
+
this.workspaceManager.SetActiveTab(tabId);
|
|
2291
|
+
this.workspaceManager.UpdateTabConfiguration(tabId, { [RECORD_DOCKED_TO_WORKSPACE_KEY]: true });
|
|
2292
|
+
this.assertMovedTabActivation(tabId);
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Demote: main workspace layout → records region ("Move to Records").
|
|
2296
|
+
* Same activate-first ordering: the flip's emission then computes
|
|
2297
|
+
* showing=true in syncRecordsRegion, so the region surfaces focused on
|
|
2298
|
+
* the returned tab. `false` (not undefined) so the choice is explicit in
|
|
2299
|
+
* the persisted configuration.
|
|
2300
|
+
*/
|
|
2301
|
+
onContextMoveToRecords() {
|
|
2302
|
+
const tabId = this.contextMenuTabId;
|
|
2303
|
+
this.hideContextMenu(false);
|
|
2304
|
+
if (!tabId)
|
|
2305
|
+
return;
|
|
2306
|
+
this.workspaceManager.SetActiveTab(tabId);
|
|
2307
|
+
this.workspaceManager.UpdateTabConfiguration(tabId, { [RECORD_DOCKED_TO_WORKSPACE_KEY]: false });
|
|
2308
|
+
this.assertMovedTabActivation(tabId);
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* A move's SOURCE layout auto-activates its next tab when the moved tab is
|
|
2312
|
+
* removed, and that GL activation WRITES BACK into the workspace — stomping
|
|
2313
|
+
* the moved tab's activation (demote left the user staring at the main
|
|
2314
|
+
* layout while the record surfaced in the records region). Re-assert after
|
|
2315
|
+
* the removal cascade settles — the assertRecordActivation pattern.
|
|
2316
|
+
*/
|
|
2317
|
+
assertMovedTabActivation(tabId) {
|
|
2318
|
+
setTimeout(() => {
|
|
2319
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
2320
|
+
if (config?.tabs.some(t => t.id === tabId) && config.activeTabId !== tabId) {
|
|
2321
|
+
this.workspaceManager.SetActiveTab(tabId);
|
|
2322
|
+
}
|
|
2323
|
+
}, 0);
|
|
2324
|
+
}
|
|
1378
2325
|
/**
|
|
1379
2326
|
* Toggle pin from context menu
|
|
1380
2327
|
*/
|
|
@@ -1389,9 +2336,12 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
1389
2336
|
*/
|
|
1390
2337
|
onContextClose() {
|
|
1391
2338
|
if (this.contextMenuTabId) {
|
|
1392
|
-
this.
|
|
2339
|
+
const tab = this.workspaceManager.GetTab(this.contextMenuTabId);
|
|
2340
|
+
const manager = tab && this.isRecordTab(tab) ? this.recordsLayoutManager : this.layoutManager;
|
|
2341
|
+
manager.RemoveTab(this.contextMenuTabId);
|
|
1393
2342
|
}
|
|
1394
|
-
|
|
2343
|
+
// The anchor lives on the closed tab — nothing to return focus to.
|
|
2344
|
+
this.hideContextMenu(false);
|
|
1395
2345
|
}
|
|
1396
2346
|
/**
|
|
1397
2347
|
* Close all other tabs from context menu
|
|
@@ -1604,34 +2554,44 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
1604
2554
|
}
|
|
1605
2555
|
static ɵfac = function TabContainerComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || 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)); };
|
|
1606
2556
|
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: TabContainerComponent, selectors: [["mj-tab-container"]], viewQuery: function TabContainerComponent_Query(rf, ctx) { if (rf & 1) {
|
|
1607
|
-
i0.ɵɵviewQuery(_c0, 5)(_c1, 5);
|
|
2557
|
+
i0.ɵɵviewQuery(_c0, 5)(_c1, 5)(_c2, 5);
|
|
1608
2558
|
} if (rf & 2) {
|
|
1609
2559
|
let _t;
|
|
1610
2560
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.glContainer = _t.first);
|
|
1611
2561
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.directContentContainer = _t.first);
|
|
2562
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.recordsGlContainer = _t.first);
|
|
1612
2563
|
} }, hostBindings: function TabContainerComponent_HostBindings(rf, ctx) { if (rf & 1) {
|
|
1613
2564
|
i0.ɵɵlistener("resize", function TabContainerComponent_resize_HostBindingHandler() { return ctx.onWindowResize(); }, i0.ɵɵresolveWindow);
|
|
1614
|
-
} }, outputs: { firstResourceLoadComplete: "firstResourceLoadComplete", layoutInitError: "layoutInitError" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls:
|
|
1615
|
-
i0.ɵɵelementStart(0, "div",
|
|
1616
|
-
i0.ɵɵconditionalCreate(
|
|
1617
|
-
i0.ɵɵ
|
|
2565
|
+
} }, outputs: { firstResourceLoadComplete: "firstResourceLoadComplete", layoutInitError: "layoutInitError" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 6, vars: 5, consts: [["directContentContainer", ""], ["glContainer", ""], ["recordsGlContainer", ""], [1, "tab-container"], [1, "main-region"], [1, "direct-content-container"], [1, "gl-container"], ["role", "menu", "aria-label", "Tab actions", 1, "context-menu", 3, "left", "top"], [1, "records-region"], [3, "Title", "Icon", "Color", "Count"], [1, "records-gl-container"], [3, "VisibleChange", "ActivateRequested", "CloseRequested", "Entries", "Visible"], [3, "SwitcherRequested", "Title", "Icon", "Color", "Count"], ["role", "menu", "aria-label", "Tab actions", 1, "context-menu", 3, "keydown"], ["type", "button", "role", "menuitem", 1, "context-menu-item", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-thumbtack"], [1, "context-menu-divider"], ["type", "button", "role", "menuitem", 1, "context-menu-item", 3, "click", "disabled"], ["aria-hidden", "true", 1, "fa-solid", "fa-house-chimney"], ["aria-hidden", "true", 1, "fa-solid", "fa-xmark"], ["aria-hidden", "true", 1, "fa-solid", "fa-layer-group"], ["aria-hidden", "true", 1, "fa-solid", "fa-angles-right"], [1, "check"], ["aria-hidden", "true", 1, "fa-solid", "fa-check"], ["type", "button", "role", "menuitem", 1, "context-menu-item"], ["aria-hidden", "true", 1, "fa-solid", "fa-arrow-up-right-from-square"], ["aria-hidden", "true", 1, "fa-solid", "fa-file-lines"]], template: function TabContainerComponent_Template(rf, ctx) { if (rf & 1) {
|
|
2566
|
+
i0.ɵɵelementStart(0, "div", 3)(1, "div", 4);
|
|
2567
|
+
i0.ɵɵconditionalCreate(2, TabContainerComponent_Conditional_2_Template, 2, 0, "div", 5)(3, TabContainerComponent_Conditional_3_Template, 2, 0, "div", 6);
|
|
2568
|
+
i0.ɵɵelementEnd();
|
|
2569
|
+
i0.ɵɵconditionalCreate(4, TabContainerComponent_Conditional_4_Template, 5, 5);
|
|
2570
|
+
i0.ɵɵconditionalCreate(5, TabContainerComponent_Conditional_5_Template, 24, 12, "div", 7);
|
|
1618
2571
|
i0.ɵɵelementEnd();
|
|
1619
2572
|
} if (rf & 2) {
|
|
1620
2573
|
i0.ɵɵadvance();
|
|
1621
|
-
i0.ɵɵ
|
|
2574
|
+
i0.ɵɵclassProp("region-hidden", ctx.ShowRecordsRegion);
|
|
2575
|
+
i0.ɵɵadvance();
|
|
2576
|
+
i0.ɵɵconditional(ctx.useSingleResourceMode ? 2 : 3);
|
|
1622
2577
|
i0.ɵɵadvance(2);
|
|
1623
|
-
i0.ɵɵconditional(ctx.
|
|
1624
|
-
|
|
2578
|
+
i0.ɵɵconditional(ctx.RecordsStyleActive ? 4 : -1);
|
|
2579
|
+
i0.ɵɵadvance();
|
|
2580
|
+
i0.ɵɵconditional(ctx.contextMenuVisible ? 5 : -1);
|
|
2581
|
+
} }, dependencies: [i2.RecordBarComponent, i3.RecordSwitcherSheetComponent], 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: var(--mj-bg-page);\n}\n\n/* Main region (nav pages) and the separate RECORDS region \u2014 exactly one is\n visible at a time. CRITICAL: the hidden region keeps its FULL LAYOUT BOX\n (visibility, not display:none). Golden Layout instances live in both;\n display:none would feed GL 0-size resizes and let it initialize/restore\n against a zero baseline \u2014 the root cause of clipped tab headers, stale\n strips, and the collapsed-sliver layout. */\n.tab-container {\n position: relative;\n}\n\n.main-region,\n.records-region {\n position: absolute;\n inset: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--mj-bg-page);\n}\n\n/* The records GL host fills the region below the origin bar. Must keep a\n real layout box at all times (see the region comment above) \u2014 it's a flex\n child of an always-sized absolute region, so it does. */\n.records-gl-container {\n flex: 1;\n min-height: 0;\n position: relative;\n background: var(--mj-bg-page);\n}\n\n/* Pane layout with an origin crumb as the FIRST element: the crumb keeps\n its fixed 32px (component host); the content sibling flex-fills. The\n height override neutralizes inline height:100% that cached wrapper\n elements carry from earlier renders (CSS !important beats inline). */\nmj-tab-container .lm_items > div > mj-record-origin-crumb,\n.direct-content-container > mj-record-origin-crumb {\n flex-shrink: 0;\n}\n\nmj-tab-container .lm_items > div > *:not(mj-record-origin-crumb),\n.direct-content-container > *:not(mj-record-origin-crumb) {\n flex: 1 1 auto;\n min-height: 0;\n height: auto !important;\n}\n\n/* Defense against visibility punch-through: `visibility: visible` on a\n DESCENDANT defeats an ancestor's visibility:hidden (unlike display:none).\n Real case: explorer-app's global `.visible { visibility: visible\n !important }` utility collides with components whose show-state class is\n named `visible` (several bespoke detail panels did this), leaving their\n overlays painted on top of the records region. The two-class specificity\n here outranks the single-class utility even with both !important. */\n.region-hidden,\n.region-hidden * {\n visibility: hidden !important;\n pointer-events: none !important;\n /* Region swaps must be INSTANT: `transition: all` rules on GL internals\n (e.g. .lm_stack) would otherwise transition `visibility` \u2014 which\n interpolates discretely, keeping the outgoing region painted on top for\n the full transition duration (a ~200ms flash of the record form over\n the destination page). */\n transition: none !important;\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: var(--mj-bg-page);\n overflow: hidden;\n}\n\n.tab-content-container {\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n padding: 20px;\n}\n\n/* Context Menu */\n.context-menu {\n position: fixed;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\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: var(--mj-text-primary);\n transition: background 0.15s;\n /* Real <button role=\"menuitem\"> now \u2014 neutralize UA chrome */\n width: 100%;\n text-align: left;\n background: transparent;\n border: none;\n font-family: inherit;\n}\n\n/* Keyboard roving highlights like hover; ring omitted on purpose \u2014 the\n moving highlight IS the focus indicator inside a menu. */\n.context-menu .context-menu-item:focus-visible {\n outline: none;\n background: var(--mj-bg-surface-hover);\n}\n\n.context-menu .context-menu-item i {\n width: 16px;\n text-align: center;\n color: var(--mj-text-secondary);\n}\n\n.context-menu .context-menu-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.context-menu .context-menu-divider {\n height: 1px;\n background: var(--mj-border-default);\n margin: 4px 0;\n}\n\n/* Context menu item variants */\n.context-menu-item.new-item i { color: var(--mj-brand-primary); }\n.context-menu-item.new-item { font-weight: 500; }\n.context-menu-item.disabled { color: var(--mj-text-disabled); cursor: default; }\n.context-menu-item.disabled:hover { background: transparent; }\n.context-menu-item.disabled i { color: var(--mj-text-disabled); }\n.context-menu-item .check { margin-left: auto; color: var(--mj-status-success); font-size: 12px; }\n\n/* Override Golden Layout styles */\n/* Global overrides */\n\n/* Ensure GL root container fills available space */\nmj-tab-container .lm_goldenlayout {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure the root layout item fills space */\nmj-tab-container .lm_root {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure proper box-sizing for all GL layout elements */\nmj-tab-container .lm_item,\nmj-tab-container .lm_content,\nmj-tab-container .lm_stack,\nmj-tab-container .lm_row,\nmj-tab-container .lm_column {\n box-sizing: border-box !important;\n}\n\n/* Ensure layout items don't overflow */\nmj-tab-container .lm_item {\n overflow: hidden !important;\n}\n\n/* SPLIT panes get a subtle frame so side-by-side content doesn't bleed\n together (single-pane layouts stay frameless). \"Adjacent to a splitter\"\n is exactly and only the split case \u2014 any orientation, any nesting. */\nmj-tab-container .lm_stack:has(+ .lm_splitter),\nmj-tab-container .lm_splitter + .lm_stack {\n border: 1px solid var(--mj-border-default) !important;\n}\n\n/* Fix for .lm_items - the content container inside stacks */\n/* This is separate from .lm_item - it holds the actual tab content */\nmj-tab-container .lm_items {\n width: 100% !important;\n height: calc(100% - 38px) !important; /* Account for header height (38px) */\n box-sizing: border-box !important;\n position: relative !important;\n}\n\n/* When tabs are maximized, no header visible */\nmj-tab-container .lm_stack.lm_maximised > .lm_items {\n height: 100% !important;\n}\n\n/* Target the anonymous ComponentItem div inside lm_items (has no class) */\n/* Created in golden-layout/src/ts/items/component-item.ts:51 without a class */\n/* NOTE: Do NOT set display here - GL uses display:none to hide inactive tabs */\nmj-tab-container .lm_items > div {\n width: 100% !important;\n height: 100% !important;\n box-sizing: border-box !important;\n flex-direction: column !important;\n}\n\n/* Only apply flex display to the active/visible tab content div */\nmj-tab-container .lm_items > div:not([style*=\"display: none\"]) {\n display: flex !important;\n}\n\n/* Clearfix for float-based row layout - GL uses float:left for horizontal panes */\nmj-tab-container .lm_row::after {\n content: \"\" !important;\n display: table !important;\n clear: both !important;\n}\n\n/* Force content children to respect parent bounds (GL sets pixel widths inline) */\nmj-tab-container .lm_content > * {\n max-width: 100% !important;\n width: 100% !important;\n}\n\nmj-tab-container .lm_content {\n background: var(--mj-bg-page) !important;\n display: flex !important;\n flex-direction: column !important;\n height: 100% !important;\n width: 100% !important;\n}\n\nmj-tab-container .lm_item_container {\n background: var(--mj-bg-page) !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: var(--mj-bg-surface-sunken) !important;\n border-bottom: 1px solid var(--mj-border-default) !important;\n overflow: visible !important;\n box-sizing: border-box !important;\n}\n\n/* SCROLLING TAB STRIP (browser-tab pattern, Matt's call over GL's overflow\n dropdown): tabs keep natural width and the strip scrolls horizontally.\n Works with settingsConfig()'s effectively-infinite tabOverlapAllowance \u2014\n GL then never re-homes tabs to its dropdown, only writes inline zIndex +\n negative marginLeft, both neutralized here. Scrollbar hidden per the\n browser-tab convention; wheel input is translated to horizontal scroll by\n GoldenLayoutManager.onStripWheel, and activation scrolls the tab into view. */\nmj-tab-container .lm_tabs {\n height: 36px !important;\n left: 0;\n right: 0;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n}\n\nmj-tab-container .lm_tabs::-webkit-scrollbar {\n display: none;\n}\n\n/* Strip nudge arrows \u2014 the VISIBLE overflow affordance (created by\n GoldenLayoutManager.updateStripNudges, one pair per header, [hidden]\n unless that direction has more tabs). The gradient doubles as the\n \"content continues\" edge fade. */\nmj-tab-container .mj-strip-nudge {\n position: absolute;\n top: 0;\n height: 36px;\n width: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n padding: 0;\n margin: 0;\n z-index: 3; /* above tabs (z 1/2) within the header context */\n font-size: 11px;\n color: var(--mj-text-secondary);\n cursor: pointer;\n}\n\nmj-tab-container .mj-strip-nudge[hidden] {\n display: none;\n}\n\nmj-tab-container .mj-strip-nudge--left {\n left: 0;\n background: linear-gradient(to right, var(--mj-bg-surface-sunken) 55%, transparent);\n justify-content: flex-start;\n padding-left: 6px;\n}\n\nmj-tab-container .mj-strip-nudge--right {\n right: 0;\n background: linear-gradient(to left, var(--mj-bg-surface-sunken) 55%, transparent);\n justify-content: flex-end;\n padding-right: 6px;\n}\n\nmj-tab-container .mj-strip-nudge:hover {\n color: var(--mj-text-primary);\n}\n\nmj-tab-container .mj-strip-nudge:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: -2px;\n}\n\n/* Hide Golden Layout WINDOW controls (popout/maximise/stack-close). The\n scrolling strip replaces GL's lm_tabdropdown overflow menu entirely (it\n never activates with the infinite overlap allowance). */\nmj-tab-container .lm_controls .lm_popout,\nmj-tab-container .lm_controls .lm_maximise,\nmj-tab-container .lm_controls .lm_close {\n display: none !important;\n}\n\nmj-tab-container .lm_header .lm_tab {\n /* Flex centering: the type-icon slot (and any future adornment) must sit\n on the tab's vertical center \u2014 inline baseline alignment inside the\n 35px line-height text line pushed it ~15px low. */\n /* FULL FLEX FLOW (Matt's refactor): [type icon] [title] [pin] [close] are\n ordinary flex siblings \u2014 no absolute anchoring, no padding reservations,\n no vertical transforms. gap owns horizontal rhythm; align-items owns\n vertical centering for every child. */\n display: flex !important;\n align-items: center !important;\n gap: 5px !important;\n padding: 0 9px !important;\n font-size: 13px !important;\n height: 35px !important;\n /* Scrolling strip: tabs are flex items of .lm_tabs \u2014 never let them\n shrink to \"fit\" (that's what the strip's scroll is for), and zero out\n the negative overlap margins GL writes inline when tabs overflow. */\n flex: 0 0 auto !important;\n margin-left: 0 !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 var(--mj-border-default) !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/* Pipe separators between INACTIVE tabs (browser-tab pattern): a short\n centered divider on each tab's right edge establishes separation without\n boxing every tab. Suppressed around the ACTIVE tab (its raised card has\n real edges), around the hovered tab, and after the last tab. */\nmj-tab-container .lm_header .lm_tab::after {\n content: '' !important;\n position: absolute !important;\n right: -1px !important;\n top: 50% !important;\n transform: translateY(-50%) !important;\n width: 1px !important;\n height: 16px !important;\n background: var(--mj-border-strong) !important;\n}\n\nmj-tab-container .lm_header .lm_tab.lm_active::after,\nmj-tab-container .lm_header .lm_tab:has(+ .lm_tab.lm_active)::after,\nmj-tab-container .lm_header .lm_tab:hover::after,\nmj-tab-container .lm_header .lm_tab:has(+ .lm_tab:hover)::after,\nmj-tab-container .lm_header .lm_tab:last-child::after {\n content: none !important;\n}\n\n/* Type-icon slot (Matt's tab design, 2026-07-29): every tab leads with its\n TYPE icon tinted in the app color \u2014 entity icon for records, nav-item/app\n icon for pages. The old 3px app-color left-edge bar is GONE; the icon\n tint carries app identity now. Hovering the TAB swaps the icon to an\n ellipsis: the slot is the visible \"this tab has actions\" affordance \u2014\n clicking it opens the tab-actions menu (same menu as right-click). */\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot {\n display: inline-flex !important;\n align-items: center !important;\n justify-content: center !important;\n /* 24px = WCAG 2.2 AA minimum pointer target (SC 2.5.8); glyph stays 12px */\n width: 24px !important;\n height: 24px !important;\n border-radius: 5px !important;\n flex-shrink: 0 !important;\n cursor: pointer !important;\n /* The slot is a real <button> now \u2014 neutralize UA button chrome */\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n font: inherit !important;\n color: inherit !important;\n}\n\n/* Keyboard parity with hover: focused slot shows the dots + house focus\n ring (outline idiom \u2014 GL headers clip box-shadows). */\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:focus-visible {\n outline: 2px solid var(--mj-border-focus) !important;\n outline-offset: -2px !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:focus-visible .mj-tab-type-ico {\n display: none !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:focus-visible .mj-tab-dots {\n display: inline-block !important;\n}\n/* GL's base stylesheet styles EVERY `i` inside a tab as its legacy 2x19px\n absolutely-positioned corner decoration (.lm_header .lm_tab i) \u2014 which\n turns our icon elements into slivers the glyph overflows. Restore normal\n flow for the slot's icons. */\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot i {\n position: static !important;\n width: auto !important;\n height: auto !important;\n}\n\nmj-tab-container .lm_header .lm_tab .mj-tab-type-ico {\n color: var(--app-color, var(--mj-text-muted)) !important;\n font-size: 12px !important;\n line-height: 1 !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-dots {\n display: none !important;\n color: var(--mj-text-secondary) !important;\n font-size: 12px !important;\n line-height: 1 !important;\n}\nmj-tab-container .lm_header .lm_tab:hover .mj-tab-type-ico {\n display: none !important;\n}\nmj-tab-container .lm_header .lm_tab:hover .mj-tab-dots {\n display: inline-block !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:hover {\n background: var(--mj-bg-surface-hover) !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:hover .mj-tab-dots {\n color: var(--mj-text-primary) !important;\n}\n\nmj-tab-container .lm_header .lm_tab:hover {\n background: var(--mj-bg-surface-hover) !important;\n}\n\n\nmj-tab-container .lm_header .lm_tab.lm_active {\n background: var(--mj-bg-surface) !important;\n height: 36px !important;\n margin-bottom: -1px !important;\n margin-right: 0 !important;\n border: 1px solid var(--mj-border-default) !important;\n border-bottom-color: var(--mj-bg-surface) !important;\n border-radius: 4px 4px 0 0 !important;\n z-index: 2 !important;\n}\n\n\nmj-tab-container .lm_title {\n cursor: pointer !important;\n user-select: none !important;\n /* In-flow flex child: normalize the inherited 35px line-height so the\n text box is glyph-height and centers identically to its siblings;\n truncate instead of colliding when GL constrains tab width. */\n line-height: normal !important;\n flex: 0 1 auto !important;\n min-width: 0 !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n white-space: nowrap !important;\n /* Glyph-overhang room: italics (unpinned tabs) paint past the last\n glyph's advance width, and overflow:hidden clips at the padding edge \u2014\n without this the final letter loses its slant. */\n padding-right: 3px !important;\n}\n\nmj-tab-container .lm_close_tab {\n position: static !important;\n order: 10 !important;\n transform: none !important;\n margin: 0 !important;\n /* 24px WCAG target; glyph stays 10px */\n width: 24px !important;\n height: 24px !important;\n border-radius: 5px !important;\n cursor: pointer !important;\n /* ALWAYS visible (Matt's call): quiet at rest, full on its own hover/focus.\n Discoverability shouldn't depend on hovering. */\n opacity: 0.55 !important;\n transition: opacity 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: var(--mj-text-secondary) !important;\n}\n\nmj-tab-container .lm_close_tab:focus-visible {\n outline: 2px solid var(--mj-border-focus) !important;\n outline-offset: -2px !important;\n opacity: 1 !important;\n}\n\nmj-tab-container .lm_close_tab:hover {\n opacity: 1 !important;\n color: var(--mj-status-error) !important;\n /* background-COLOR, never the shorthand: GL's stock close was a\n background-image PNG and the shorthand silently wiped it (now it's an\n FA ::before glyph, but the discipline stands). */\n background-color: var(--mj-bg-surface-hover) !important;\n}\n\n/* Pinned tabs never show close button (the pin control replaces it) */\nmj-tab-container .lm_header .lm_tab.pinned .lm_close_tab {\n display: none !important;\n}\n\n\n"], encapsulation: 2 });
|
|
1625
2582
|
}
|
|
1626
2583
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(TabContainerComponent, [{
|
|
1627
2584
|
type: Component,
|
|
1628
|
-
args: [{ standalone: false, 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 @if (contextMenuVisible) {\n <div\n class=\"context-menu\"\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\" [class.new-item]=\"!isContextTabPinnedToHome\" [class.disabled]=\"isContextTabPinnedToHome\" (click)=\"onContextPinToHome()\">\n <i class=\"fa-solid fa-house-chimney\"></i>\n @if (isContextTabPinnedToHome) {\n <span>Pinned to Home</span>\n <span class=\"check\"><i class=\"fa-solid fa-check\"></i></span>\n } @else {\n <span>Pin to Home</span>\n }\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 }\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: var(--mj-bg-page);\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: var(--mj-bg-page);\n overflow: hidden;\n}\n\n.tab-content-container {\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n padding: 20px;\n}\n\n/* Context Menu */\n.context-menu {\n position: fixed;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\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: var(--mj-text-primary);\n transition: background 0.15s;\n}\n\n.context-menu .context-menu-item i {\n width: 16px;\n text-align: center;\n color: var(--mj-text-secondary);\n}\n\n.context-menu .context-menu-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.context-menu .context-menu-divider {\n height: 1px;\n background: var(--mj-border-default);\n margin: 4px 0;\n}\n\n/* Context menu item variants */\n.context-menu-item.new-item i { color: var(--mj-brand-primary); }\n.context-menu-item.new-item { font-weight: 500; }\n.context-menu-item.disabled { color: var(--mj-text-disabled); cursor: default; }\n.context-menu-item.disabled:hover { background: transparent; }\n.context-menu-item.disabled i { color: var(--mj-text-disabled); }\n.context-menu-item .check { margin-left: auto; color: var(--mj-status-success); font-size: 12px; }\n\n/* Override Golden Layout styles */\n/* Global overrides */\n\n/* Ensure GL root container fills available space */\nmj-tab-container .lm_goldenlayout {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure the root layout item fills space */\nmj-tab-container .lm_root {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure proper box-sizing for all GL layout elements */\nmj-tab-container .lm_item,\nmj-tab-container .lm_content,\nmj-tab-container .lm_stack,\nmj-tab-container .lm_row,\nmj-tab-container .lm_column {\n box-sizing: border-box !important;\n}\n\n/* Ensure layout items don't overflow */\nmj-tab-container .lm_item {\n overflow: hidden !important;\n}\n\n/* Fix for .lm_items - the content container inside stacks */\n/* This is separate from .lm_item - it holds the actual tab content */\nmj-tab-container .lm_items {\n width: 100% !important;\n height: calc(100% - 38px) !important; /* Account for header height (38px) */\n box-sizing: border-box !important;\n position: relative !important;\n}\n\n/* When tabs are maximized, no header visible */\nmj-tab-container .lm_stack.lm_maximised > .lm_items {\n height: 100% !important;\n}\n\n/* Target the anonymous ComponentItem div inside lm_items (has no class) */\n/* Created in golden-layout/src/ts/items/component-item.ts:51 without a class */\n/* NOTE: Do NOT set display here - GL uses display:none to hide inactive tabs */\nmj-tab-container .lm_items > div {\n width: 100% !important;\n height: 100% !important;\n box-sizing: border-box !important;\n flex-direction: column !important;\n}\n\n/* Only apply flex display to the active/visible tab content div */\nmj-tab-container .lm_items > div:not([style*=\"display: none\"]) {\n display: flex !important;\n}\n\n/* Clearfix for float-based row layout - GL uses float:left for horizontal panes */\nmj-tab-container .lm_row::after {\n content: \"\" !important;\n display: table !important;\n clear: both !important;\n}\n\n/* Force content children to respect parent bounds (GL sets pixel widths inline) */\nmj-tab-container .lm_content > * {\n max-width: 100% !important;\n width: 100% !important;\n}\n\nmj-tab-container .lm_content {\n background: var(--mj-bg-page) !important;\n display: flex !important;\n flex-direction: column !important;\n height: 100% !important;\n width: 100% !important;\n}\n\nmj-tab-container .lm_item_container {\n background: var(--mj-bg-page) !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: var(--mj-bg-surface-sunken) !important;\n border-bottom: 1px solid var(--mj-border-default) !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 var(--mj-border-default) !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: var(--mj-bg-surface-hover) !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: var(--mj-bg-surface) !important;\n height: 36px !important;\n margin-bottom: -1px !important;\n margin-right: 0 !important;\n border: 1px solid var(--mj-border-default) !important;\n border-bottom-color: var(--mj-bg-surface) !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: var(--mj-text-secondary) !important;\n}\n\nmj-tab-container .lm_close_tab:hover {\n opacity: 1 !important;\n color: var(--mj-status-error) !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"] }]
|
|
2585
|
+
args: [{ standalone: false, selector: 'mj-tab-container', encapsulation: ViewEncapsulation.None, template: "<div class=\"tab-container\">\n\n <!-- Main region: nav pages (single-resource or main Golden Layout).\n Hidden (not destroyed) while the RECORDS region is showing, so nav\n content keeps its state and reappears instantly. -->\n <div class=\"main-region\" [class.region-hidden]=\"ShowRecordsRegion\">\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 </div>\n\n <!-- RECORDS region: a SEPARATE Golden Layout hosting only record tabs\n (records-style record opens). Always in the DOM once created so the\n layout survives visibility flips; shown only while the active tab is\n a record. Native GL tab bar = record tabs, drag-to-split included. -->\n @if (RecordsStyleActive) {\n <!-- Origin crumbs are PANE-LEVEL now (first element inside each record\n pane \u2014 see RecordOriginCrumbComponent); the region needs no bar. -->\n <div class=\"records-region\" [class.region-hidden]=\"!ShowRecordsRegion\">\n <!-- MOBILE: the GL tab strip is unusable at phone widths (one tab\n fits, no overflow UI) \u2014 the record bar replaces it and opens the\n record switcher sheet. The GL itself runs headerless (HideHeaders). -->\n @if (IsMobileRecords) {\n <mj-record-bar\n [Title]=\"RecordBarTitle\"\n [Icon]=\"RecordBarIcon\"\n [Color]=\"RecordBarColor\"\n [Count]=\"OpenRecordCount\"\n (SwitcherRequested)=\"OpenRecordSwitcher()\">\n </mj-record-bar>\n }\n <div #recordsGlContainer class=\"records-gl-container\"></div>\n </div>\n\n <!-- MOBILE record switcher: bottom sheet listing ALL open records\n (region + docked). Fixed-position \u2014 DOM location here is\n visual-neutral; living in tab-container keeps close routing on the\n same path as the tab context menu. -->\n <mj-record-switcher-sheet\n [Entries]=\"SwitcherEntries\"\n [Visible]=\"RecordSwitcherVisible\"\n (VisibleChange)=\"OnSwitcherVisibleChange($event)\"\n (ActivateRequested)=\"OnSwitcherActivate($event)\"\n (CloseRequested)=\"OnSwitcherClose($event)\">\n </mj-record-switcher-sheet>\n }\n\n <!-- Context Menu (only relevant in multi-tab mode). Real buttons with\n menu semantics: arrow keys rove, Escape closes with focus return to\n the invoking control (see onMenuKeydown / hideContextMenu). -->\n @if (contextMenuVisible) {\n <div\n class=\"context-menu\"\n role=\"menu\"\n aria-label=\"Tab actions\"\n (keydown)=\"onMenuKeydown($event)\"\n [style.left.px]=\"contextMenuX\"\n [style.top.px]=\"contextMenuY\">\n <button type=\"button\" role=\"menuitem\" class=\"context-menu-item\" (click)=\"onContextPin()\">\n <i class=\"fa-solid fa-thumbtack\" aria-hidden=\"true\"></i>\n <span>{{ isContextTabPinned ? 'Unpin Tab' : 'Pin Tab' }}</span>\n </button>\n <div class=\"context-menu-divider\"></div>\n <button type=\"button\" role=\"menuitem\" class=\"context-menu-item\" [class.new-item]=\"!isContextTabPinnedToHome\" [class.disabled]=\"isContextTabPinnedToHome\" [disabled]=\"isContextTabPinnedToHome\" (click)=\"onContextPinToHome()\">\n <i class=\"fa-solid fa-house-chimney\" aria-hidden=\"true\"></i>\n @if (isContextTabPinnedToHome) {\n <span>Pinned to Home</span>\n <span class=\"check\"><i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i></span>\n } @else {\n <span>Pin to Home</span>\n }\n </button>\n @if (canContextMoveToWorkspace || canContextMoveToRecords) {\n <div class=\"context-menu-divider\"></div>\n @if (canContextMoveToWorkspace) {\n <button type=\"button\" role=\"menuitem\" class=\"context-menu-item\" (click)=\"onContextMoveToWorkspace()\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\" aria-hidden=\"true\"></i>\n <span>Move to Workspace</span>\n </button>\n }\n @if (canContextMoveToRecords) {\n <button type=\"button\" role=\"menuitem\" class=\"context-menu-item\" (click)=\"onContextMoveToRecords()\">\n <i class=\"fa-solid fa-file-lines\" aria-hidden=\"true\"></i>\n <span>Move to Records</span>\n </button>\n }\n }\n <div class=\"context-menu-divider\"></div>\n <button type=\"button\" role=\"menuitem\" class=\"context-menu-item\" (click)=\"onContextClose()\">\n <i class=\"fa-solid fa-xmark\" aria-hidden=\"true\"></i>\n <span>Close Tab</span>\n </button>\n <button type=\"button\" role=\"menuitem\" class=\"context-menu-item\" (click)=\"onContextCloseOthers()\">\n <i class=\"fa-solid fa-layer-group\" aria-hidden=\"true\"></i>\n <span>Close Others</span>\n </button>\n <button type=\"button\" role=\"menuitem\" class=\"context-menu-item\" (click)=\"onContextCloseToRight()\">\n <i class=\"fa-solid fa-angles-right\" aria-hidden=\"true\"></i>\n <span>Close to Right</span>\n </button>\n </div>\n }\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: var(--mj-bg-page);\n}\n\n/* Main region (nav pages) and the separate RECORDS region \u2014 exactly one is\n visible at a time. CRITICAL: the hidden region keeps its FULL LAYOUT BOX\n (visibility, not display:none). Golden Layout instances live in both;\n display:none would feed GL 0-size resizes and let it initialize/restore\n against a zero baseline \u2014 the root cause of clipped tab headers, stale\n strips, and the collapsed-sliver layout. */\n.tab-container {\n position: relative;\n}\n\n.main-region,\n.records-region {\n position: absolute;\n inset: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--mj-bg-page);\n}\n\n/* The records GL host fills the region below the origin bar. Must keep a\n real layout box at all times (see the region comment above) \u2014 it's a flex\n child of an always-sized absolute region, so it does. */\n.records-gl-container {\n flex: 1;\n min-height: 0;\n position: relative;\n background: var(--mj-bg-page);\n}\n\n/* Pane layout with an origin crumb as the FIRST element: the crumb keeps\n its fixed 32px (component host); the content sibling flex-fills. The\n height override neutralizes inline height:100% that cached wrapper\n elements carry from earlier renders (CSS !important beats inline). */\nmj-tab-container .lm_items > div > mj-record-origin-crumb,\n.direct-content-container > mj-record-origin-crumb {\n flex-shrink: 0;\n}\n\nmj-tab-container .lm_items > div > *:not(mj-record-origin-crumb),\n.direct-content-container > *:not(mj-record-origin-crumb) {\n flex: 1 1 auto;\n min-height: 0;\n height: auto !important;\n}\n\n/* Defense against visibility punch-through: `visibility: visible` on a\n DESCENDANT defeats an ancestor's visibility:hidden (unlike display:none).\n Real case: explorer-app's global `.visible { visibility: visible\n !important }` utility collides with components whose show-state class is\n named `visible` (several bespoke detail panels did this), leaving their\n overlays painted on top of the records region. The two-class specificity\n here outranks the single-class utility even with both !important. */\n.region-hidden,\n.region-hidden * {\n visibility: hidden !important;\n pointer-events: none !important;\n /* Region swaps must be INSTANT: `transition: all` rules on GL internals\n (e.g. .lm_stack) would otherwise transition `visibility` \u2014 which\n interpolates discretely, keeping the outgoing region painted on top for\n the full transition duration (a ~200ms flash of the record form over\n the destination page). */\n transition: none !important;\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: var(--mj-bg-page);\n overflow: hidden;\n}\n\n.tab-content-container {\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n padding: 20px;\n}\n\n/* Context Menu */\n.context-menu {\n position: fixed;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\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: var(--mj-text-primary);\n transition: background 0.15s;\n /* Real <button role=\"menuitem\"> now \u2014 neutralize UA chrome */\n width: 100%;\n text-align: left;\n background: transparent;\n border: none;\n font-family: inherit;\n}\n\n/* Keyboard roving highlights like hover; ring omitted on purpose \u2014 the\n moving highlight IS the focus indicator inside a menu. */\n.context-menu .context-menu-item:focus-visible {\n outline: none;\n background: var(--mj-bg-surface-hover);\n}\n\n.context-menu .context-menu-item i {\n width: 16px;\n text-align: center;\n color: var(--mj-text-secondary);\n}\n\n.context-menu .context-menu-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.context-menu .context-menu-divider {\n height: 1px;\n background: var(--mj-border-default);\n margin: 4px 0;\n}\n\n/* Context menu item variants */\n.context-menu-item.new-item i { color: var(--mj-brand-primary); }\n.context-menu-item.new-item { font-weight: 500; }\n.context-menu-item.disabled { color: var(--mj-text-disabled); cursor: default; }\n.context-menu-item.disabled:hover { background: transparent; }\n.context-menu-item.disabled i { color: var(--mj-text-disabled); }\n.context-menu-item .check { margin-left: auto; color: var(--mj-status-success); font-size: 12px; }\n\n/* Override Golden Layout styles */\n/* Global overrides */\n\n/* Ensure GL root container fills available space */\nmj-tab-container .lm_goldenlayout {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure the root layout item fills space */\nmj-tab-container .lm_root {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Ensure proper box-sizing for all GL layout elements */\nmj-tab-container .lm_item,\nmj-tab-container .lm_content,\nmj-tab-container .lm_stack,\nmj-tab-container .lm_row,\nmj-tab-container .lm_column {\n box-sizing: border-box !important;\n}\n\n/* Ensure layout items don't overflow */\nmj-tab-container .lm_item {\n overflow: hidden !important;\n}\n\n/* SPLIT panes get a subtle frame so side-by-side content doesn't bleed\n together (single-pane layouts stay frameless). \"Adjacent to a splitter\"\n is exactly and only the split case \u2014 any orientation, any nesting. */\nmj-tab-container .lm_stack:has(+ .lm_splitter),\nmj-tab-container .lm_splitter + .lm_stack {\n border: 1px solid var(--mj-border-default) !important;\n}\n\n/* Fix for .lm_items - the content container inside stacks */\n/* This is separate from .lm_item - it holds the actual tab content */\nmj-tab-container .lm_items {\n width: 100% !important;\n height: calc(100% - 38px) !important; /* Account for header height (38px) */\n box-sizing: border-box !important;\n position: relative !important;\n}\n\n/* When tabs are maximized, no header visible */\nmj-tab-container .lm_stack.lm_maximised > .lm_items {\n height: 100% !important;\n}\n\n/* Target the anonymous ComponentItem div inside lm_items (has no class) */\n/* Created in golden-layout/src/ts/items/component-item.ts:51 without a class */\n/* NOTE: Do NOT set display here - GL uses display:none to hide inactive tabs */\nmj-tab-container .lm_items > div {\n width: 100% !important;\n height: 100% !important;\n box-sizing: border-box !important;\n flex-direction: column !important;\n}\n\n/* Only apply flex display to the active/visible tab content div */\nmj-tab-container .lm_items > div:not([style*=\"display: none\"]) {\n display: flex !important;\n}\n\n/* Clearfix for float-based row layout - GL uses float:left for horizontal panes */\nmj-tab-container .lm_row::after {\n content: \"\" !important;\n display: table !important;\n clear: both !important;\n}\n\n/* Force content children to respect parent bounds (GL sets pixel widths inline) */\nmj-tab-container .lm_content > * {\n max-width: 100% !important;\n width: 100% !important;\n}\n\nmj-tab-container .lm_content {\n background: var(--mj-bg-page) !important;\n display: flex !important;\n flex-direction: column !important;\n height: 100% !important;\n width: 100% !important;\n}\n\nmj-tab-container .lm_item_container {\n background: var(--mj-bg-page) !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: var(--mj-bg-surface-sunken) !important;\n border-bottom: 1px solid var(--mj-border-default) !important;\n overflow: visible !important;\n box-sizing: border-box !important;\n}\n\n/* SCROLLING TAB STRIP (browser-tab pattern, Matt's call over GL's overflow\n dropdown): tabs keep natural width and the strip scrolls horizontally.\n Works with settingsConfig()'s effectively-infinite tabOverlapAllowance \u2014\n GL then never re-homes tabs to its dropdown, only writes inline zIndex +\n negative marginLeft, both neutralized here. Scrollbar hidden per the\n browser-tab convention; wheel input is translated to horizontal scroll by\n GoldenLayoutManager.onStripWheel, and activation scrolls the tab into view. */\nmj-tab-container .lm_tabs {\n height: 36px !important;\n left: 0;\n right: 0;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n}\n\nmj-tab-container .lm_tabs::-webkit-scrollbar {\n display: none;\n}\n\n/* Strip nudge arrows \u2014 the VISIBLE overflow affordance (created by\n GoldenLayoutManager.updateStripNudges, one pair per header, [hidden]\n unless that direction has more tabs). The gradient doubles as the\n \"content continues\" edge fade. */\nmj-tab-container .mj-strip-nudge {\n position: absolute;\n top: 0;\n height: 36px;\n width: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n padding: 0;\n margin: 0;\n z-index: 3; /* above tabs (z 1/2) within the header context */\n font-size: 11px;\n color: var(--mj-text-secondary);\n cursor: pointer;\n}\n\nmj-tab-container .mj-strip-nudge[hidden] {\n display: none;\n}\n\nmj-tab-container .mj-strip-nudge--left {\n left: 0;\n background: linear-gradient(to right, var(--mj-bg-surface-sunken) 55%, transparent);\n justify-content: flex-start;\n padding-left: 6px;\n}\n\nmj-tab-container .mj-strip-nudge--right {\n right: 0;\n background: linear-gradient(to left, var(--mj-bg-surface-sunken) 55%, transparent);\n justify-content: flex-end;\n padding-right: 6px;\n}\n\nmj-tab-container .mj-strip-nudge:hover {\n color: var(--mj-text-primary);\n}\n\nmj-tab-container .mj-strip-nudge:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: -2px;\n}\n\n/* Hide Golden Layout WINDOW controls (popout/maximise/stack-close). The\n scrolling strip replaces GL's lm_tabdropdown overflow menu entirely (it\n never activates with the infinite overlap allowance). */\nmj-tab-container .lm_controls .lm_popout,\nmj-tab-container .lm_controls .lm_maximise,\nmj-tab-container .lm_controls .lm_close {\n display: none !important;\n}\n\nmj-tab-container .lm_header .lm_tab {\n /* Flex centering: the type-icon slot (and any future adornment) must sit\n on the tab's vertical center \u2014 inline baseline alignment inside the\n 35px line-height text line pushed it ~15px low. */\n /* FULL FLEX FLOW (Matt's refactor): [type icon] [title] [pin] [close] are\n ordinary flex siblings \u2014 no absolute anchoring, no padding reservations,\n no vertical transforms. gap owns horizontal rhythm; align-items owns\n vertical centering for every child. */\n display: flex !important;\n align-items: center !important;\n gap: 5px !important;\n padding: 0 9px !important;\n font-size: 13px !important;\n height: 35px !important;\n /* Scrolling strip: tabs are flex items of .lm_tabs \u2014 never let them\n shrink to \"fit\" (that's what the strip's scroll is for), and zero out\n the negative overlap margins GL writes inline when tabs overflow. */\n flex: 0 0 auto !important;\n margin-left: 0 !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 var(--mj-border-default) !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/* Pipe separators between INACTIVE tabs (browser-tab pattern): a short\n centered divider on each tab's right edge establishes separation without\n boxing every tab. Suppressed around the ACTIVE tab (its raised card has\n real edges), around the hovered tab, and after the last tab. */\nmj-tab-container .lm_header .lm_tab::after {\n content: '' !important;\n position: absolute !important;\n right: -1px !important;\n top: 50% !important;\n transform: translateY(-50%) !important;\n width: 1px !important;\n height: 16px !important;\n background: var(--mj-border-strong) !important;\n}\n\nmj-tab-container .lm_header .lm_tab.lm_active::after,\nmj-tab-container .lm_header .lm_tab:has(+ .lm_tab.lm_active)::after,\nmj-tab-container .lm_header .lm_tab:hover::after,\nmj-tab-container .lm_header .lm_tab:has(+ .lm_tab:hover)::after,\nmj-tab-container .lm_header .lm_tab:last-child::after {\n content: none !important;\n}\n\n/* Type-icon slot (Matt's tab design, 2026-07-29): every tab leads with its\n TYPE icon tinted in the app color \u2014 entity icon for records, nav-item/app\n icon for pages. The old 3px app-color left-edge bar is GONE; the icon\n tint carries app identity now. Hovering the TAB swaps the icon to an\n ellipsis: the slot is the visible \"this tab has actions\" affordance \u2014\n clicking it opens the tab-actions menu (same menu as right-click). */\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot {\n display: inline-flex !important;\n align-items: center !important;\n justify-content: center !important;\n /* 24px = WCAG 2.2 AA minimum pointer target (SC 2.5.8); glyph stays 12px */\n width: 24px !important;\n height: 24px !important;\n border-radius: 5px !important;\n flex-shrink: 0 !important;\n cursor: pointer !important;\n /* The slot is a real <button> now \u2014 neutralize UA button chrome */\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n font: inherit !important;\n color: inherit !important;\n}\n\n/* Keyboard parity with hover: focused slot shows the dots + house focus\n ring (outline idiom \u2014 GL headers clip box-shadows). */\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:focus-visible {\n outline: 2px solid var(--mj-border-focus) !important;\n outline-offset: -2px !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:focus-visible .mj-tab-type-ico {\n display: none !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:focus-visible .mj-tab-dots {\n display: inline-block !important;\n}\n/* GL's base stylesheet styles EVERY `i` inside a tab as its legacy 2x19px\n absolutely-positioned corner decoration (.lm_header .lm_tab i) \u2014 which\n turns our icon elements into slivers the glyph overflows. Restore normal\n flow for the slot's icons. */\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot i {\n position: static !important;\n width: auto !important;\n height: auto !important;\n}\n\nmj-tab-container .lm_header .lm_tab .mj-tab-type-ico {\n color: var(--app-color, var(--mj-text-muted)) !important;\n font-size: 12px !important;\n line-height: 1 !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-dots {\n display: none !important;\n color: var(--mj-text-secondary) !important;\n font-size: 12px !important;\n line-height: 1 !important;\n}\nmj-tab-container .lm_header .lm_tab:hover .mj-tab-type-ico {\n display: none !important;\n}\nmj-tab-container .lm_header .lm_tab:hover .mj-tab-dots {\n display: inline-block !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:hover {\n background: var(--mj-bg-surface-hover) !important;\n}\nmj-tab-container .lm_header .lm_tab .mj-tab-type-slot:hover .mj-tab-dots {\n color: var(--mj-text-primary) !important;\n}\n\nmj-tab-container .lm_header .lm_tab:hover {\n background: var(--mj-bg-surface-hover) !important;\n}\n\n\nmj-tab-container .lm_header .lm_tab.lm_active {\n background: var(--mj-bg-surface) !important;\n height: 36px !important;\n margin-bottom: -1px !important;\n margin-right: 0 !important;\n border: 1px solid var(--mj-border-default) !important;\n border-bottom-color: var(--mj-bg-surface) !important;\n border-radius: 4px 4px 0 0 !important;\n z-index: 2 !important;\n}\n\n\nmj-tab-container .lm_title {\n cursor: pointer !important;\n user-select: none !important;\n /* In-flow flex child: normalize the inherited 35px line-height so the\n text box is glyph-height and centers identically to its siblings;\n truncate instead of colliding when GL constrains tab width. */\n line-height: normal !important;\n flex: 0 1 auto !important;\n min-width: 0 !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n white-space: nowrap !important;\n /* Glyph-overhang room: italics (unpinned tabs) paint past the last\n glyph's advance width, and overflow:hidden clips at the padding edge \u2014\n without this the final letter loses its slant. */\n padding-right: 3px !important;\n}\n\nmj-tab-container .lm_close_tab {\n position: static !important;\n order: 10 !important;\n transform: none !important;\n margin: 0 !important;\n /* 24px WCAG target; glyph stays 10px */\n width: 24px !important;\n height: 24px !important;\n border-radius: 5px !important;\n cursor: pointer !important;\n /* ALWAYS visible (Matt's call): quiet at rest, full on its own hover/focus.\n Discoverability shouldn't depend on hovering. */\n opacity: 0.55 !important;\n transition: opacity 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: var(--mj-text-secondary) !important;\n}\n\nmj-tab-container .lm_close_tab:focus-visible {\n outline: 2px solid var(--mj-border-focus) !important;\n outline-offset: -2px !important;\n opacity: 1 !important;\n}\n\nmj-tab-container .lm_close_tab:hover {\n opacity: 1 !important;\n color: var(--mj-status-error) !important;\n /* background-COLOR, never the shorthand: GL's stock close was a\n background-image PNG and the shorthand silently wiped it (now it's an\n FA ::before glyph, but the discipline stands). */\n background-color: var(--mj-bg-surface-hover) !important;\n}\n\n/* Pinned tabs never show close button (the pin control replaces it) */\nmj-tab-container .lm_header .lm_tab.pinned .lm_close_tab {\n display: none !important;\n}\n\n\n"] }]
|
|
1629
2586
|
}], () => [{ type: i1.GoldenLayoutManager }, { type: i1.WorkspaceStateManager }, { type: i1.ApplicationManager }, { type: i0.ApplicationRef }, { type: i0.EnvironmentInjector }, { type: i0.ChangeDetectorRef }], { glContainer: [{
|
|
1630
2587
|
type: ViewChild,
|
|
1631
2588
|
args: ['glContainer', { static: false }]
|
|
1632
2589
|
}], directContentContainer: [{
|
|
1633
2590
|
type: ViewChild,
|
|
1634
2591
|
args: ['directContentContainer', { static: false }]
|
|
2592
|
+
}], recordsGlContainer: [{
|
|
2593
|
+
type: ViewChild,
|
|
2594
|
+
args: ['recordsGlContainer', { static: false }]
|
|
1635
2595
|
}], firstResourceLoadComplete: [{
|
|
1636
2596
|
type: Output
|
|
1637
2597
|
}], layoutInitError: [{
|
|
@@ -1640,5 +2600,5 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
1640
2600
|
type: HostListener,
|
|
1641
2601
|
args: ['window:resize']
|
|
1642
2602
|
}] }); })();
|
|
1643
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(TabContainerComponent, { className: "TabContainerComponent", filePath: "src/lib/shell/components/tabs/tab-container.component.ts", lineNumber:
|
|
2603
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(TabContainerComponent, { className: "TabContainerComponent", filePath: "src/lib/shell/components/tabs/tab-container.component.ts", lineNumber: 62 }); })();
|
|
1644
2604
|
//# sourceMappingURL=tab-container.component.js.map
|