@xh/hoist 79.0.0-SNAPSHOT.1766020485210 → 79.0.0-SNAPSHOT.1766097863558
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/CHANGELOG.md +16 -0
- package/admin/AppComponent.ts +9 -1
- package/admin/AppModel.ts +0 -4
- package/admin/tabs/cluster/instances/InstancesTab.ts +1 -1
- package/admin/tabs/cluster/instances/InstancesTabModel.ts +0 -1
- package/admin/tabs/userData/roles/details/RoleDetailsModel.ts +0 -1
- package/build/types/cmp/tab/TabContainer.d.ts +19 -4
- package/build/types/cmp/tab/TabContainerModel.d.ts +18 -19
- package/build/types/cmp/tab/Types.d.ts +61 -0
- package/build/types/cmp/tab/index.d.ts +1 -1
- package/build/types/core/elem.d.ts +3 -3
- package/build/types/data/RecordAction.d.ts +4 -1
- package/build/types/desktop/cmp/dash/canvas/DashCanvas.d.ts +3 -2
- package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +45 -3
- package/build/types/desktop/cmp/panel/Panel.d.ts +2 -2
- package/build/types/desktop/cmp/rest/RestGrid.d.ts +3 -3
- package/build/types/desktop/cmp/tab/TabSwitcher.d.ts +1 -1
- package/build/types/desktop/cmp/tab/dynamic/DynamicTabSwitcher.d.ts +7 -0
- package/build/types/desktop/cmp/tab/dynamic/DynamicTabSwitcherModel.d.ts +30 -0
- package/build/types/desktop/cmp/tab/dynamic/scroller/Scroller.d.ts +19 -0
- package/build/types/desktop/cmp/tab/dynamic/scroller/ScrollerModel.d.ts +23 -0
- package/build/types/desktop/cmp/tab/impl/Tab.d.ts +7 -2
- package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/build/types/desktop/cmp/tab/impl/TabContextMenuItems.d.ts +4 -0
- package/build/types/desktop/cmp/tab/index.d.ts +1 -0
- package/build/types/dynamics/desktop.d.ts +1 -0
- package/build/types/mobile/cmp/panel/Panel.d.ts +2 -2
- package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/cmp/tab/TabContainer.ts +19 -4
- package/cmp/tab/TabContainerModel.ts +113 -54
- package/cmp/tab/TabModel.ts +1 -2
- package/cmp/tab/Types.ts +80 -0
- package/cmp/tab/index.ts +1 -1
- package/core/elem.ts +5 -5
- package/data/RecordAction.ts +4 -1
- package/desktop/appcontainer/AppContainer.ts +3 -2
- package/desktop/cmp/dash/canvas/DashCanvas.ts +57 -35
- package/desktop/cmp/dash/canvas/DashCanvasModel.ts +135 -21
- package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilter.ts +1 -1
- package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilterModel.ts +0 -1
- package/desktop/cmp/panel/Panel.ts +2 -2
- package/desktop/cmp/rest/RestGrid.ts +4 -5
- package/desktop/cmp/tab/TabSwitcher.ts +18 -3
- package/desktop/cmp/tab/Tabs.scss +1 -0
- package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.scss +53 -0
- package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.ts +237 -0
- package/desktop/cmp/tab/dynamic/DynamicTabSwitcherModel.ts +167 -0
- package/desktop/cmp/tab/dynamic/scroller/Scroller.ts +69 -0
- package/desktop/cmp/tab/dynamic/scroller/ScrollerModel.ts +92 -0
- package/desktop/cmp/tab/impl/Tab.ts +30 -6
- package/desktop/cmp/tab/impl/TabContainer.ts +34 -9
- package/desktop/cmp/tab/impl/TabContextMenuItems.ts +21 -0
- package/desktop/cmp/tab/index.ts +1 -0
- package/dynamics/desktop.ts +2 -0
- package/mobile/cmp/panel/Panel.ts +2 -2
- package/mobile/cmp/tab/impl/TabContainer.ts +16 -9
- package/package.json +2 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/build/types/cmp/tab/TabSwitcherProps.d.ts +0 -16
- package/cmp/tab/TabSwitcherProps.ts +0 -28
package/cmp/tab/TabContainer.ts
CHANGED
|
@@ -4,19 +4,34 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {TabSwitcherProps} from '@xh/hoist/cmp/tab/Types';
|
|
7
8
|
import {BoxProps, hoistCmp, HoistProps, refreshContextView, uses, XH} from '@xh/hoist/core';
|
|
8
9
|
import {tabContainerImpl as desktopTabContainerImpl} from '@xh/hoist/dynamics/desktop';
|
|
9
10
|
import {tabContainerImpl as mobileTabContainerImpl} from '@xh/hoist/dynamics/mobile';
|
|
10
11
|
import {TabContainerModel} from './TabContainerModel';
|
|
11
12
|
|
|
12
|
-
export
|
|
13
|
+
export interface TabContainerProps extends HoistProps<TabContainerModel>, BoxProps {
|
|
14
|
+
/**
|
|
15
|
+
* Indicates whether to include a default switcher docked within this component. Specify as a
|
|
16
|
+
* boolean or props for a TabSwitcher component. False to not include a switcher. Default true.
|
|
17
|
+
*/
|
|
18
|
+
switcher?: boolean | TabSwitcherProps;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Props to apply to child TabContainers, if any. Specify as an object or a function
|
|
22
|
+
* that returns props based on the tabId and depth of the child container.
|
|
23
|
+
*/
|
|
24
|
+
childContainerProps?:
|
|
25
|
+
| TabContainerProps
|
|
26
|
+
| ((ctx: {tabId: string; depth: number}) => TabContainerProps);
|
|
27
|
+
}
|
|
13
28
|
|
|
14
29
|
/**
|
|
15
30
|
* Display a set of child Tabs and (optionally) a switcher control.
|
|
16
31
|
*
|
|
17
|
-
* By default this TabContainer will install a TabSwitcher above the Tabs to control the currently
|
|
18
|
-
* displayed Tab. The '
|
|
19
|
-
*
|
|
32
|
+
* By default, this TabContainer will install a TabSwitcher above the Tabs to control the currently
|
|
33
|
+
* displayed Tab. The 'switcher' prop can be adjusted to place the switcher control on alternative
|
|
34
|
+
* edges of the container.
|
|
20
35
|
*
|
|
21
36
|
* If `switcher` is set to false then no TabSwitcher will be installed. This setting
|
|
22
37
|
* is useful for applications that wish to place an associated TabSwitcher elsewhere in the
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {
|
|
8
|
+
TabSwitcherConfig,
|
|
9
|
+
IDynamicTabSwitcherModel,
|
|
10
|
+
TabContainerModelPersistOptions
|
|
11
|
+
} from '@xh/hoist/cmp/tab/Types';
|
|
7
12
|
import {
|
|
8
13
|
HoistModel,
|
|
9
14
|
managed,
|
|
10
|
-
Persistable,
|
|
11
15
|
PersistableState,
|
|
12
16
|
PersistenceProvider,
|
|
13
17
|
PersistOptions,
|
|
@@ -16,14 +20,14 @@ import {
|
|
|
16
20
|
RenderMode,
|
|
17
21
|
XH
|
|
18
22
|
} from '@xh/hoist/core';
|
|
23
|
+
import {DynamicTabSwitcherModel} from '@xh/hoist/desktop/cmp/tab/dynamic/DynamicTabSwitcherModel';
|
|
19
24
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
20
25
|
import {wait} from '@xh/hoist/promise';
|
|
21
26
|
import {isOmitted} from '@xh/hoist/utils/impl';
|
|
22
27
|
import {ensureUniqueBy, throwIf} from '@xh/hoist/utils/js';
|
|
23
|
-
import {difference, find, findLast, isString, without} from 'lodash';
|
|
28
|
+
import {difference, find, findLast, isObject, isString, without} from 'lodash';
|
|
24
29
|
import {ReactNode} from 'react';
|
|
25
30
|
import {TabConfig, TabModel} from './TabModel';
|
|
26
|
-
import {TabSwitcherProps} from './TabSwitcherProps';
|
|
27
31
|
|
|
28
32
|
export interface TabContainerConfig {
|
|
29
33
|
/** Tabs to be displayed. */
|
|
@@ -42,11 +46,10 @@ export interface TabContainerConfig {
|
|
|
42
46
|
route?: string;
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* include a switcher. Defaults to true.
|
|
49
|
+
* Specification for type of switcher. Specify `dynamic` or config for user-configurable tabs.
|
|
50
|
+
* Default `{mode: 'static'}` for simple, static switcher.
|
|
48
51
|
*/
|
|
49
|
-
switcher?:
|
|
52
|
+
switcher?: TabSwitcherConfig;
|
|
50
53
|
|
|
51
54
|
/**
|
|
52
55
|
* True to enable activity tracking of tab views (default false). Viewing of each tab will
|
|
@@ -87,7 +90,7 @@ export interface TabContainerConfig {
|
|
|
87
90
|
*
|
|
88
91
|
* Note: Routing is currently enabled for desktop applications only.
|
|
89
92
|
*/
|
|
90
|
-
export class TabContainerModel extends HoistModel
|
|
93
|
+
export class TabContainerModel extends HoistModel {
|
|
91
94
|
declare config: TabContainerConfig;
|
|
92
95
|
|
|
93
96
|
@managed
|
|
@@ -97,42 +100,47 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
97
100
|
@observable
|
|
98
101
|
activeTabId: string;
|
|
99
102
|
|
|
103
|
+
depth: number; // Depth in hierarchy of nested TabContainerModels
|
|
100
104
|
route: string;
|
|
101
105
|
defaultTabId: string;
|
|
102
|
-
switcher: TabSwitcherProps;
|
|
103
106
|
track: boolean;
|
|
104
107
|
renderMode: RenderMode;
|
|
105
108
|
refreshMode: RefreshMode;
|
|
106
109
|
emptyText: ReactNode;
|
|
110
|
+
switcherConfig: TabSwitcherConfig;
|
|
107
111
|
|
|
108
112
|
@managed
|
|
109
113
|
refreshContextModel: RefreshContextModel;
|
|
110
114
|
|
|
115
|
+
@managed
|
|
116
|
+
dynamicTabSwitcherModel: IDynamicTabSwitcherModel;
|
|
117
|
+
|
|
111
118
|
protected lastActiveTabId: string;
|
|
112
119
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
/**
|
|
121
|
+
* @param config - TabContainer configuration.
|
|
122
|
+
* @param [depth] - Depth in hierarchy of nested TabContainerModels. Not for application use.
|
|
123
|
+
*/
|
|
124
|
+
constructor(
|
|
125
|
+
{
|
|
126
|
+
tabs = [],
|
|
127
|
+
defaultTabId = null,
|
|
128
|
+
route = null,
|
|
129
|
+
track = false,
|
|
130
|
+
renderMode = 'lazy',
|
|
131
|
+
refreshMode = 'onShowLazy',
|
|
132
|
+
persistWith,
|
|
133
|
+
emptyText = 'No tabs to display.',
|
|
134
|
+
xhImpl = false,
|
|
135
|
+
switcher = {mode: 'static'}
|
|
136
|
+
}: TabContainerConfig,
|
|
137
|
+
depth = 0
|
|
138
|
+
) {
|
|
125
139
|
super();
|
|
126
140
|
makeObservable(this);
|
|
127
141
|
this.xhImpl = xhImpl;
|
|
128
142
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Create default switcher props
|
|
132
|
-
if (switcher === true) switcher = {orientation: XH.isMobileApp ? 'bottom' : 'top'};
|
|
133
|
-
if (switcher === false) switcher = null;
|
|
134
|
-
|
|
135
|
-
this.switcher = switcher as TabSwitcherProps;
|
|
143
|
+
this.depth = depth;
|
|
136
144
|
this.renderMode = renderMode;
|
|
137
145
|
this.refreshMode = refreshMode;
|
|
138
146
|
this.defaultTabId = defaultTabId;
|
|
@@ -142,6 +150,8 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
142
150
|
this.setTabs(tabs);
|
|
143
151
|
this.refreshContextModel = new RefreshContextModel();
|
|
144
152
|
this.refreshContextModel.xhImpl = xhImpl;
|
|
153
|
+
this.switcherConfig = switcher;
|
|
154
|
+
this.dynamicTabSwitcherModel = this.parseSwitcher(switcher);
|
|
145
155
|
|
|
146
156
|
if (route) {
|
|
147
157
|
if (XH.isMobileApp) {
|
|
@@ -156,17 +166,10 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
156
166
|
wait().then(() => this.syncWithRouter());
|
|
157
167
|
|
|
158
168
|
this.forwardRouterToTab(this.activeTabId);
|
|
159
|
-
} else if (persistWith) {
|
|
160
|
-
((this.persistWith = {
|
|
161
|
-
path: 'tabContainer',
|
|
162
|
-
...persistWith
|
|
163
|
-
}),
|
|
164
|
-
PersistenceProvider.create({
|
|
165
|
-
persistOptions: this.persistWith,
|
|
166
|
-
target: this
|
|
167
|
-
}));
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
if (persistWith) this.initPersist(persistWith);
|
|
172
|
+
|
|
170
173
|
if (track) {
|
|
171
174
|
this.addReaction({
|
|
172
175
|
track: () => this.activeTab,
|
|
@@ -269,16 +272,18 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
269
272
|
return this.findTab(this.activeTabId);
|
|
270
273
|
}
|
|
271
274
|
|
|
272
|
-
/** The tab immediately before the active tab in the model's tab list. */
|
|
275
|
+
/** The visitable tab immediately before the active tab in the model's tab list. */
|
|
273
276
|
get prevTab(): TabModel {
|
|
274
|
-
const
|
|
275
|
-
|
|
277
|
+
const tabs = this.tabs.filter(t => !t.disabled || t === this.activeTab),
|
|
278
|
+
activeTabIdx = tabs.indexOf(this.activeTab);
|
|
279
|
+
return activeTabIdx > 0 ? tabs[activeTabIdx - 1] : null;
|
|
276
280
|
}
|
|
277
281
|
|
|
278
|
-
/** The tab immediately after the active tab in the model's tab list. */
|
|
282
|
+
/** The visitable tab immediately after the active tab in the model's tab list. */
|
|
279
283
|
get nextTab(): TabModel {
|
|
280
|
-
const
|
|
281
|
-
|
|
284
|
+
const tabs = this.tabs.filter(t => !t.disabled || t === this.activeTab),
|
|
285
|
+
activeTabIdx = tabs.indexOf(this.activeTab);
|
|
286
|
+
return activeTabIdx < tabs.length - 1 ? tabs[activeTabIdx + 1] : null;
|
|
282
287
|
}
|
|
283
288
|
|
|
284
289
|
/**
|
|
@@ -330,17 +335,6 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
330
335
|
if (target) this.activateTab(target);
|
|
331
336
|
}
|
|
332
337
|
|
|
333
|
-
//-------------------------
|
|
334
|
-
// Persistable Interface
|
|
335
|
-
//-------------------------
|
|
336
|
-
getPersistableState(): PersistableState<{activeTabId: string}> {
|
|
337
|
-
return new PersistableState({activeTabId: this.activeTabId});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
setPersistableState(state: PersistableState<{activeTabId: string}>): void {
|
|
341
|
-
this.activateTab(state.value.activeTabId);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
338
|
//-------------------------
|
|
345
339
|
// Implementation
|
|
346
340
|
//-------------------------
|
|
@@ -395,6 +389,71 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
395
389
|
|
|
396
390
|
return null;
|
|
397
391
|
}
|
|
392
|
+
|
|
393
|
+
private parseSwitcher(switcher: TabContainerConfig['switcher']): IDynamicTabSwitcherModel {
|
|
394
|
+
if (!switcher || switcher.mode === 'static') return null;
|
|
395
|
+
throwIf(XH.isMobileApp, 'DynamicTabSwitcherModel not supported for mobile TabContainer.');
|
|
396
|
+
|
|
397
|
+
return this.markManaged(new DynamicTabSwitcherModel(switcher, this));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private initPersist({
|
|
401
|
+
persistActiveTabId = !this.route,
|
|
402
|
+
persistFavoriteTabIds = !!this.dynamicTabSwitcherModel,
|
|
403
|
+
path = 'tabContainer',
|
|
404
|
+
...rootPersistWith
|
|
405
|
+
}: TabContainerModelPersistOptions) {
|
|
406
|
+
if (persistActiveTabId) {
|
|
407
|
+
if (this.route) {
|
|
408
|
+
this.logWarn('persistActiveTabId and route cannot both be specified.');
|
|
409
|
+
} else {
|
|
410
|
+
const persistWith = isObject(persistActiveTabId)
|
|
411
|
+
? PersistenceProvider.mergePersistOptions(rootPersistWith, persistActiveTabId)
|
|
412
|
+
: rootPersistWith;
|
|
413
|
+
PersistenceProvider.create({
|
|
414
|
+
persistOptions: {
|
|
415
|
+
path: `${path}.activeTabId`,
|
|
416
|
+
...persistWith
|
|
417
|
+
},
|
|
418
|
+
target: {
|
|
419
|
+
getPersistableState: () => new PersistableState(this.activeTabId),
|
|
420
|
+
setPersistableState: ({value}) => this.activateTab(value)
|
|
421
|
+
},
|
|
422
|
+
owner: this
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (persistFavoriteTabIds) {
|
|
428
|
+
const {dynamicTabSwitcherModel} = this;
|
|
429
|
+
if (!dynamicTabSwitcherModel) {
|
|
430
|
+
this.logWarn(
|
|
431
|
+
'persistFavoriteTabIds is set but no DynamicTabSwitcherModel is present.'
|
|
432
|
+
);
|
|
433
|
+
} else {
|
|
434
|
+
const persistWith = isObject(persistFavoriteTabIds)
|
|
435
|
+
? PersistenceProvider.mergePersistOptions(
|
|
436
|
+
rootPersistWith,
|
|
437
|
+
persistFavoriteTabIds
|
|
438
|
+
)
|
|
439
|
+
: rootPersistWith;
|
|
440
|
+
PersistenceProvider.create({
|
|
441
|
+
persistOptions: {
|
|
442
|
+
path: `${path}.favoriteTabIds`,
|
|
443
|
+
...persistWith
|
|
444
|
+
},
|
|
445
|
+
target: {
|
|
446
|
+
getPersistableState: () =>
|
|
447
|
+
new PersistableState(dynamicTabSwitcherModel.favoriteTabIds),
|
|
448
|
+
setPersistableState: ({value}) => {
|
|
449
|
+
dynamicTabSwitcherModel.setFavoriteTabIds(value);
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
owner: this
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
398
457
|
}
|
|
399
458
|
|
|
400
459
|
export interface AddTabOptions {
|
package/cmp/tab/TabModel.ts
CHANGED
|
@@ -196,7 +196,6 @@ export class TabModel extends HoistModel {
|
|
|
196
196
|
renderMode: parent.renderMode,
|
|
197
197
|
refreshMode: parent.refreshMode,
|
|
198
198
|
emptyText: parent.emptyText,
|
|
199
|
-
switcher: parent.switcher,
|
|
200
199
|
track: parent.track,
|
|
201
200
|
...childConfig
|
|
202
201
|
};
|
|
@@ -211,7 +210,7 @@ export class TabModel extends HoistModel {
|
|
|
211
210
|
};
|
|
212
211
|
}
|
|
213
212
|
|
|
214
|
-
this.childContainerModel = new TabContainerModel(childConfig);
|
|
213
|
+
this.childContainerModel = new TabContainerModel(childConfig, parent.depth + 1);
|
|
215
214
|
return tabContainer({model: this.childContainerModel});
|
|
216
215
|
}
|
|
217
216
|
}
|
package/cmp/tab/Types.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {TabContainerModel} from '@xh/hoist/cmp/tab/TabContainerModel';
|
|
2
|
+
import {TabModel} from '@xh/hoist/cmp/tab/TabModel';
|
|
3
|
+
import {
|
|
4
|
+
BoxProps,
|
|
5
|
+
HoistModel,
|
|
6
|
+
HoistProps,
|
|
7
|
+
MenuContext,
|
|
8
|
+
MenuItemLike,
|
|
9
|
+
MenuToken,
|
|
10
|
+
PersistOptions,
|
|
11
|
+
Side
|
|
12
|
+
} from '@xh/hoist/core';
|
|
13
|
+
|
|
14
|
+
export interface TabSwitcherProps extends HoistProps<TabContainerModel>, BoxProps {
|
|
15
|
+
/** Relative position within the parent TabContainer. Defaults to 'top'. */
|
|
16
|
+
orientation?: Side;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* True to animate the indicator when switching tabs. False (default) to change instantly.
|
|
20
|
+
* Not supported by DynamicTabSwitcher.
|
|
21
|
+
*/
|
|
22
|
+
animate?: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Enable scrolling and place tabs that overflow into a menu. Default to false.
|
|
26
|
+
* Not supported by DynamicTabSwitcher.
|
|
27
|
+
*/
|
|
28
|
+
enableOverflow?: boolean;
|
|
29
|
+
|
|
30
|
+
/** Width (in px) to render tabs. Only applies to horizontal orientations */
|
|
31
|
+
tabWidth?: number;
|
|
32
|
+
|
|
33
|
+
/** Minimum width (in px) to render tabs. Only applies to horizontal orientations */
|
|
34
|
+
tabMinWidth?: number;
|
|
35
|
+
|
|
36
|
+
/** Maximum width (in px) to render tabs. Only applies to horizontal orientations */
|
|
37
|
+
tabMaxWidth?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TabSwitcherConfig {
|
|
41
|
+
/** Specification for type of switcher. Specify `dynamic`for user-configurable tabs */
|
|
42
|
+
mode: 'static' | 'dynamic';
|
|
43
|
+
/** Additional menu items to include in tab context menus. Only supported on desktop */
|
|
44
|
+
extraMenuItems?: Array<MenuItemLike<MenuToken, TabSwitcherMenuContext>>;
|
|
45
|
+
/** IDs of favorite tabs to display by default (in order). Only for `dynamic` switchers */
|
|
46
|
+
initialFavorites?: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TabContainerModelPersistOptions extends PersistOptions {
|
|
50
|
+
/** True (default) to persist the active tab ID or provide custom PersistOptions. */
|
|
51
|
+
persistActiveTabId?: boolean;
|
|
52
|
+
/** True (default) to persist favorite tab IDs or provide custom PersistOptions. */
|
|
53
|
+
persistFavoriteTabIds?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TabSwitcherMenuContext extends MenuContext {
|
|
57
|
+
tab: TabModel;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Cross-platform interface for desktop and mobile (TBA) DynamicTabSwitcherModels. */
|
|
61
|
+
export interface IDynamicTabSwitcherModel extends HoistModel {
|
|
62
|
+
/** IDs of favorite tabs, in order. */
|
|
63
|
+
get favoriteTabIds(): string[];
|
|
64
|
+
/** Tabs displayed in switcher, in order. */
|
|
65
|
+
get visibleTabs(): TabModel[];
|
|
66
|
+
/** Enabled tabs displayed in switcher, in order. */
|
|
67
|
+
get enabledVisibleTabs(): TabModel[];
|
|
68
|
+
/** Whether the specified tab is currently active in the TabContainer. */
|
|
69
|
+
isTabActive(tabId: string): boolean;
|
|
70
|
+
/** Whether the specified tab is currently marked as a favorite. */
|
|
71
|
+
isTabFavorite(tabId: string): boolean;
|
|
72
|
+
/** Toggle the favorite status of the specified tab. */
|
|
73
|
+
toggleTabFavorite(tabId: string): void;
|
|
74
|
+
/** Activate the specified tab in the TabContainer. */
|
|
75
|
+
activate(tabId: string): void;
|
|
76
|
+
/** Remove the specified tab from the switcher. */
|
|
77
|
+
hide(tabId: string): void;
|
|
78
|
+
/** Set the IDs of all favorite tabs, in order. */
|
|
79
|
+
setFavoriteTabIds(tabIds: string[]): void;
|
|
80
|
+
}
|
package/cmp/tab/index.ts
CHANGED
package/core/elem.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
ReactElement,
|
|
16
16
|
ReactNode
|
|
17
17
|
} from 'react';
|
|
18
|
-
import {PlainObject,
|
|
18
|
+
import {PlainObject, Thunkable} from './types/Types';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Alternative format for specifying React Elements in render functions. This type is designed to
|
|
@@ -45,10 +45,10 @@ export type ElementSpec<P> = Omit<P, 'items' | 'item' | 'omit'> & {
|
|
|
45
45
|
// Enhanced attributes to support element factory
|
|
46
46
|
//---------------------------------------------
|
|
47
47
|
/** Child Element(s). Equivalent provided as Rest Arguments to React.createElement.*/
|
|
48
|
-
items?:
|
|
48
|
+
items?: ReactNode;
|
|
49
49
|
|
|
50
50
|
/** Equivalent to `items`, offered for code clarity when only one child is needed. */
|
|
51
|
-
item?:
|
|
51
|
+
item?: ReactNode;
|
|
52
52
|
|
|
53
53
|
/** True to exclude the Element. */
|
|
54
54
|
omit?: Thunkable<boolean>;
|
|
@@ -126,7 +126,7 @@ export function elementFactory<C extends ReactComponent>(component: C): ElementF
|
|
|
126
126
|
export function elementFactory<P extends PlainObject>(component: ReactComponent): ElementFactory<P>;
|
|
127
127
|
export function elementFactory(component: ReactComponent): ElementFactory {
|
|
128
128
|
const ret = function (...args) {
|
|
129
|
-
return createElement(component, normalizeArgs(args
|
|
129
|
+
return createElement(component, normalizeArgs(args));
|
|
130
130
|
};
|
|
131
131
|
ret.isElementFactory = true;
|
|
132
132
|
return ret;
|
|
@@ -135,7 +135,7 @@ export function elementFactory(component: ReactComponent): ElementFactory {
|
|
|
135
135
|
//------------------------
|
|
136
136
|
// Implementation
|
|
137
137
|
//------------------------
|
|
138
|
-
function normalizeArgs(args: any[]
|
|
138
|
+
function normalizeArgs(args: any[]) {
|
|
139
139
|
const len = args.length;
|
|
140
140
|
if (len === 0) return {};
|
|
141
141
|
if (len === 1) {
|
package/data/RecordAction.ts
CHANGED
|
@@ -33,7 +33,10 @@ export interface RecordActionSpec extends TestSupportProps {
|
|
|
33
33
|
/** Function called on action execution. */
|
|
34
34
|
actionFn?: (data: ActionFnData) => void;
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Function called to append / override display properties prior to each render. This function
|
|
38
|
+
* allows dynamic control over display properties.
|
|
39
|
+
* */
|
|
37
40
|
displayFn?: (data: ActionFnData) => RecordActionSpec;
|
|
38
41
|
|
|
39
42
|
/** Sub-actions for this action. */
|
|
@@ -25,7 +25,7 @@ import {storeFilterFieldImpl} from '@xh/hoist/desktop/cmp/store/impl/StoreFilter
|
|
|
25
25
|
import {tabContainerImpl} from '@xh/hoist/desktop/cmp/tab/impl/TabContainer';
|
|
26
26
|
import {zoneMapperDialog as zoneMapper} from '@xh/hoist/desktop/cmp/zoneGrid/impl/ZoneMapperDialog';
|
|
27
27
|
import {useContextMenu, useHotkeys} from '@xh/hoist/desktop/hooks';
|
|
28
|
-
import {installDesktopImpls} from '@xh/hoist/dynamics/desktop';
|
|
28
|
+
import {DynamicTabSwitcherModel, installDesktopImpls} from '@xh/hoist/dynamics/desktop';
|
|
29
29
|
import {inspectorPanel} from '@xh/hoist/inspector/InspectorPanel';
|
|
30
30
|
import {blueprintProvider} from '@xh/hoist/kit/blueprint';
|
|
31
31
|
import {consumeEvent} from '@xh/hoist/utils/js';
|
|
@@ -59,7 +59,8 @@ installDesktopImpls({
|
|
|
59
59
|
useContextMenu,
|
|
60
60
|
ModalSupportModel,
|
|
61
61
|
errorMessageImpl,
|
|
62
|
-
maskImpl
|
|
62
|
+
maskImpl,
|
|
63
|
+
DynamicTabSwitcherModel
|
|
63
64
|
});
|
|
64
65
|
/**
|
|
65
66
|
* Top-level wrapper for Desktop applications.
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {DragEvent} from 'react';
|
|
8
|
+
import ReactGridLayout, {
|
|
9
|
+
type LayoutItem,
|
|
10
|
+
type GridLayoutProps,
|
|
11
|
+
useContainerWidth,
|
|
12
|
+
noCompactor,
|
|
13
|
+
verticalCompactor
|
|
14
|
+
} from 'react-grid-layout';
|
|
15
|
+
|
|
7
16
|
import {showContextMenu} from '@xh/hoist/kit/blueprint';
|
|
8
17
|
import composeRefs from '@seznam/compose-react-refs';
|
|
9
18
|
import {div, vbox, vspacer} from '@xh/hoist/cmp/layout';
|
|
@@ -20,13 +29,12 @@ import '@xh/hoist/desktop/register';
|
|
|
20
29
|
import {Classes, overlay} from '@xh/hoist/kit/blueprint';
|
|
21
30
|
import {consumeEvent, TEST_ID} from '@xh/hoist/utils/js';
|
|
22
31
|
import classNames from 'classnames';
|
|
23
|
-
import ReactGridLayout, {WidthProvider} from 'react-grid-layout';
|
|
24
|
-
import type {ReactGridLayoutProps} from 'react-grid-layout';
|
|
25
32
|
import {DashCanvasModel} from './DashCanvasModel';
|
|
26
33
|
import {dashCanvasContextMenu} from './impl/DashCanvasContextMenu';
|
|
27
34
|
import {dashCanvasView} from './impl/DashCanvasView';
|
|
28
35
|
|
|
29
36
|
import 'react-grid-layout/css/styles.css';
|
|
37
|
+
import 'react-resizable/css/styles.css';
|
|
30
38
|
import './DashCanvas.scss';
|
|
31
39
|
|
|
32
40
|
export interface DashCanvasProps extends HoistProps<DashCanvasModel>, TestSupportProps {
|
|
@@ -36,7 +44,7 @@ export interface DashCanvasProps extends HoistProps<DashCanvasModel>, TestSuppor
|
|
|
36
44
|
* {@link https://www.npmjs.com/package/react-grid-layout#grid-layout-props}
|
|
37
45
|
* Note that some ReactGridLayout props are managed directly by DashCanvas and will be overridden if provided here.
|
|
38
46
|
*/
|
|
39
|
-
rglOptions?:
|
|
47
|
+
rglOptions?: GridLayoutProps;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
@@ -59,7 +67,7 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
|
|
|
59
67
|
const isDraggable = !model.layoutLocked,
|
|
60
68
|
isResizable = !model.layoutLocked,
|
|
61
69
|
[padX, padY] = model.containerPadding;
|
|
62
|
-
|
|
70
|
+
const {width, containerRef, mounted} = useContainerWidth();
|
|
63
71
|
return refreshContextView({
|
|
64
72
|
model: model.refreshContextModel,
|
|
65
73
|
item: div({
|
|
@@ -69,37 +77,51 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
|
|
|
69
77
|
isResizable ? `${className}--resizable` : null
|
|
70
78
|
),
|
|
71
79
|
style: {padding: `${padY}px ${padX}px`},
|
|
72
|
-
ref: composeRefs(ref, model.ref),
|
|
80
|
+
ref: composeRefs(ref, model.ref, containerRef),
|
|
73
81
|
onContextMenu: e => onContextMenu(e, model),
|
|
74
|
-
items:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
82
|
+
items: mounted
|
|
83
|
+
? [
|
|
84
|
+
reactGridLayout({
|
|
85
|
+
layout: model.rglLayout,
|
|
86
|
+
width,
|
|
87
|
+
gridConfig: {
|
|
88
|
+
cols: model.columns,
|
|
89
|
+
rowHeight: model.rowHeight,
|
|
90
|
+
margin: model.margin,
|
|
91
|
+
maxRows: model.maxRows
|
|
92
|
+
},
|
|
93
|
+
dragConfig: {
|
|
94
|
+
enabled: isDraggable,
|
|
95
|
+
handle: '.xh-dash-tab.xh-panel > .xh-panel__content > .xh-panel-header',
|
|
96
|
+
cancel: '.xh-button',
|
|
97
|
+
bounded: true
|
|
98
|
+
},
|
|
99
|
+
resizeConfig: {
|
|
100
|
+
enabled: isResizable
|
|
101
|
+
},
|
|
102
|
+
dropConfig: {
|
|
103
|
+
enabled: model.contentLocked ? false : model.droppable,
|
|
104
|
+
defaultItem: {w: 6, h: 6}
|
|
105
|
+
},
|
|
106
|
+
compactor: model.compact ? verticalCompactor : noCompactor,
|
|
107
|
+
onLayoutChange: (layout: LayoutItem[]) =>
|
|
108
|
+
model.onRglLayoutChange(layout),
|
|
109
|
+
onResizeStart: () => (model.isResizing = true),
|
|
110
|
+
onResizeStop: () => (model.isResizing = false),
|
|
111
|
+
items: model.viewModels.map(vm =>
|
|
112
|
+
div({
|
|
113
|
+
key: vm.id,
|
|
114
|
+
item: dashCanvasView({model: vm})
|
|
115
|
+
})
|
|
116
|
+
),
|
|
117
|
+
onDropDragOver: (evt: DragEvent) => model.onDropDragOver(evt),
|
|
118
|
+
onDrop: (layout: LayoutItem[], layoutItem: LayoutItem, evt: Event) =>
|
|
119
|
+
model.onDrop(layout, layoutItem, evt),
|
|
120
|
+
...rglOptions
|
|
121
|
+
}),
|
|
122
|
+
emptyContainerOverlay({omit: !model.showAddViewButtonWhenEmpty})
|
|
123
|
+
]
|
|
124
|
+
: [],
|
|
103
125
|
[TEST_ID]: testId
|
|
104
126
|
})
|
|
105
127
|
});
|
|
@@ -147,4 +169,4 @@ const onContextMenu = (e, model) => {
|
|
|
147
169
|
}
|
|
148
170
|
};
|
|
149
171
|
|
|
150
|
-
const reactGridLayout = elementFactory(
|
|
172
|
+
const reactGridLayout = elementFactory<GridLayoutProps>(ReactGridLayout);
|