@zat-design/sisyphus-react 4.6.0-beta.2 → 4.6.0-beta.3
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/es/ProLayout/components/TabsManager/hooks/useTabsState.js +14 -7
- package/es/ProLayout/components/TabsManager/hooks/useTabsUrlSync.js +29 -16
- package/es/ProLayout/components/TabsManager/utils/index.d.ts +5 -0
- package/es/ProLayout/components/TabsManager/utils/index.js +47 -2
- package/es/ProLayout/propTypes.d.ts +5 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getMenuRoute,
|
|
16
16
|
hasRelationKey,
|
|
17
17
|
findExistingTab,
|
|
18
|
+
getComparableTabUrl,
|
|
18
19
|
normalizeTabUrl,
|
|
19
20
|
isSafeExternalUrl,
|
|
20
21
|
isSafeHttpUrl,
|
|
@@ -30,6 +31,7 @@ const useTabsState = (options) => {
|
|
|
30
31
|
const finalConfig = { ...DEFAULT_TABS_CONFIG, ...config };
|
|
31
32
|
const urlSyncEnabled = finalConfig.urlSync === true || typeof finalConfig.urlSync === "object" && finalConfig.urlSync !== null;
|
|
32
33
|
const urlSyncIdentity = typeof finalConfig.urlSync === "object" && finalConfig.urlSync !== null ? finalConfig.urlSync.identity || "url" : "url";
|
|
34
|
+
const urlSyncBasename = typeof finalConfig.urlSync === "object" && finalConfig.urlSync !== null ? finalConfig.urlSync.basename : void 0;
|
|
33
35
|
const [state, setState] = useState(() => ({
|
|
34
36
|
tabsList: [],
|
|
35
37
|
activeKey: "",
|
|
@@ -71,8 +73,11 @@ const useTabsState = (options) => {
|
|
|
71
73
|
const normalizedUrl = normalizeTabUrl(tab.url);
|
|
72
74
|
if (!normalizedUrl)
|
|
73
75
|
return;
|
|
76
|
+
const comparableUrl = getComparableTabUrl(normalizedUrl, urlSyncBasename);
|
|
77
|
+
if (!comparableUrl)
|
|
78
|
+
return;
|
|
74
79
|
const relationKey = tab.relationKey ?? ((_a2 = tab.menuItem) == null ? void 0 : _a2.relationKey);
|
|
75
|
-
const identity = urlSyncIdentity === "relationKey" && hasRelationKey(relationKey) ? `relationKey:${typeof relationKey}:${String(relationKey)}` : `url:${
|
|
80
|
+
const identity = urlSyncIdentity === "relationKey" && hasRelationKey(relationKey) ? `relationKey:${typeof relationKey}:${String(relationKey)}` : `url:${comparableUrl}`;
|
|
76
81
|
const retainedTab = retainedTabByIdentity.get(identity);
|
|
77
82
|
if (retainedTab) {
|
|
78
83
|
retainedTabByIdentity.set(identity, { ...retainedTab, url: normalizedUrl });
|
|
@@ -93,7 +98,7 @@ const useTabsState = (options) => {
|
|
|
93
98
|
setState(restoredState);
|
|
94
99
|
}
|
|
95
100
|
setIsInitialized(true);
|
|
96
|
-
}, [restoreFromCache, urlSyncEnabled, urlSyncIdentity]);
|
|
101
|
+
}, [restoreFromCache, urlSyncEnabled, urlSyncIdentity, urlSyncBasename]);
|
|
97
102
|
const lastActiveKeyRef = useRef("");
|
|
98
103
|
const shouldSkipSaveRef = useRef(false);
|
|
99
104
|
const initialTabsAppliedRef = useRef(false);
|
|
@@ -132,7 +137,8 @@ const useTabsState = (options) => {
|
|
|
132
137
|
if (urlSyncEnabled || !forceNew) {
|
|
133
138
|
const existingTab = findExistingTab(state.tabsList, menuItem, {
|
|
134
139
|
urlSync: urlSyncEnabled,
|
|
135
|
-
identity: urlSyncIdentity
|
|
140
|
+
identity: urlSyncIdentity,
|
|
141
|
+
basename: urlSyncBasename
|
|
136
142
|
});
|
|
137
143
|
if (existingTab) {
|
|
138
144
|
return true;
|
|
@@ -143,7 +149,7 @@ const useTabsState = (options) => {
|
|
|
143
149
|
}
|
|
144
150
|
return true;
|
|
145
151
|
},
|
|
146
|
-
[state.tabsList, finalConfig, urlSyncEnabled, urlSyncIdentity]
|
|
152
|
+
[state.tabsList, finalConfig, urlSyncEnabled, urlSyncIdentity, urlSyncBasename]
|
|
147
153
|
);
|
|
148
154
|
const addTab = useCallback(
|
|
149
155
|
(menuItem, options2) => {
|
|
@@ -170,11 +176,12 @@ const useTabsState = (options) => {
|
|
|
170
176
|
const resolvedMenuItem = normalizedUrl ? { ...menuItem, url: normalizedUrl, redirectUrl: void 0 } : menuItem;
|
|
171
177
|
const existingTab = urlSyncEnabled || !forceNew ? findExistingTab(prevState.tabsList, resolvedMenuItem, {
|
|
172
178
|
urlSync: urlSyncEnabled,
|
|
173
|
-
identity: urlSyncIdentity
|
|
179
|
+
identity: urlSyncIdentity,
|
|
180
|
+
basename: urlSyncBasename
|
|
174
181
|
}) : void 0;
|
|
175
182
|
if (existingTab) {
|
|
176
183
|
const shouldFixClosable = resolvedMenuItem.closable === false && existingTab.closable !== false;
|
|
177
|
-
const shouldUpdateUrl = urlSyncIdentity === "relationKey" && normalizedUrl !== void 0 && existingTab.url !== normalizedUrl;
|
|
184
|
+
const shouldUpdateUrl = urlSyncIdentity === "relationKey" && normalizedUrl !== void 0 && getComparableTabUrl(existingTab.url, urlSyncBasename) !== getComparableTabUrl(normalizedUrl, urlSyncBasename);
|
|
178
185
|
const correctedTab = shouldFixClosable || shouldUpdateUrl ? {
|
|
179
186
|
...existingTab,
|
|
180
187
|
...shouldFixClosable ? { closable: false } : {},
|
|
@@ -216,7 +223,7 @@ const useTabsState = (options) => {
|
|
|
216
223
|
};
|
|
217
224
|
});
|
|
218
225
|
},
|
|
219
|
-
[finalConfig, urlSyncEnabled, urlSyncIdentity]
|
|
226
|
+
[finalConfig, urlSyncEnabled, urlSyncIdentity, urlSyncBasename]
|
|
220
227
|
);
|
|
221
228
|
useEffect(() => {
|
|
222
229
|
const initialTabs = finalConfig.initialTabs;
|
|
@@ -3,6 +3,8 @@ import * as ReactRouterDom from "react-router-dom";
|
|
|
3
3
|
import {
|
|
4
4
|
canOpenAsTab,
|
|
5
5
|
flattenMenuData,
|
|
6
|
+
getBrowserTabUrl,
|
|
7
|
+
getComparableTabUrl,
|
|
6
8
|
getCurrentTabUrl,
|
|
7
9
|
getMenuRoute,
|
|
8
10
|
getTabUrlPathname,
|
|
@@ -92,6 +94,7 @@ const useTabsUrlSync = ({
|
|
|
92
94
|
const navigator = navigationContext == null ? void 0 : navigationContext.navigator;
|
|
93
95
|
const enabled = config.urlSync === true || typeof config.urlSync === "object" && config.urlSync !== null;
|
|
94
96
|
const historyMode = typeof config.urlSync === "object" ? config.urlSync.history || "replace" : "replace";
|
|
97
|
+
const basename = typeof config.urlSync === "object" ? config.urlSync.basename : void 0;
|
|
95
98
|
const bootstrappedRef = useRef(false);
|
|
96
99
|
const writingUrlRef = useRef(false);
|
|
97
100
|
const preserveUrlInteractionRef = useRef(null);
|
|
@@ -156,11 +159,14 @@ const useTabsUrlSync = ({
|
|
|
156
159
|
if (!enabled || !isInitialized || typeof window === "undefined")
|
|
157
160
|
return;
|
|
158
161
|
const currentUrl = normalizeTabUrl(locationChange.url);
|
|
159
|
-
|
|
162
|
+
const comparableCurrentUrl = getComparableTabUrl(currentUrl, basename);
|
|
163
|
+
if (!currentUrl || !comparableCurrentUrl)
|
|
160
164
|
return;
|
|
161
165
|
if (handledLocationSequenceRef.current === locationChange.sequence)
|
|
162
166
|
return;
|
|
163
|
-
const exactTab = state.tabsList.find(
|
|
167
|
+
const exactTab = state.tabsList.find(
|
|
168
|
+
(tab) => getComparableTabUrl(tab.url, basename) === comparableCurrentUrl
|
|
169
|
+
);
|
|
164
170
|
if (exactTab) {
|
|
165
171
|
handledLocationSequenceRef.current = locationChange.sequence;
|
|
166
172
|
bootstrappedRef.current = true;
|
|
@@ -170,16 +176,16 @@ const useTabsUrlSync = ({
|
|
|
170
176
|
}
|
|
171
177
|
return;
|
|
172
178
|
}
|
|
173
|
-
const currentPathname = getTabUrlPathname(
|
|
179
|
+
const currentPathname = getTabUrlPathname(comparableCurrentUrl);
|
|
174
180
|
const sourceMenus = getSourceMenus(dataSource);
|
|
175
181
|
const activeTab = state.tabsList.find((tab) => tab.id === state.activeKey);
|
|
176
182
|
const activeCode = ((_a = activeTab == null ? void 0 : activeTab.menuItem) == null ? void 0 : _a.code) || (activeTab == null ? void 0 : activeTab.code);
|
|
177
183
|
const activeSourceMenu = activeCode ? sourceMenus.find((item) => item.code === activeCode) : void 0;
|
|
178
184
|
const activeMenuItem = activeSourceMenu || (activeTab == null ? void 0 : activeTab.menuItem);
|
|
179
|
-
const activeBasePathname = activeMenuItem ? getTabUrlPathname(getMenuRoute(activeMenuItem)) : getTabUrlPathname(activeTab == null ? void 0 : activeTab.url);
|
|
185
|
+
const activeBasePathname = activeMenuItem ? getTabUrlPathname(getComparableTabUrl(getMenuRoute(activeMenuItem), basename)) : getTabUrlPathname(getComparableTabUrl(activeTab == null ? void 0 : activeTab.url, basename));
|
|
180
186
|
const targetMenu = (_b = sourceMenus.map((item) => ({
|
|
181
187
|
item,
|
|
182
|
-
pathname: getTabUrlPathname(getMenuRoute(item))
|
|
188
|
+
pathname: getTabUrlPathname(getComparableTabUrl(getMenuRoute(item), basename))
|
|
183
189
|
})).filter(
|
|
184
190
|
(entry) => !!entry.pathname && isSameOrNestedPath(entry.pathname, currentPathname)
|
|
185
191
|
).sort((left, right) => right.pathname.length - left.pathname.length)[0]) == null ? void 0 : _b.item;
|
|
@@ -223,7 +229,8 @@ const useTabsUrlSync = ({
|
|
|
223
229
|
addTab,
|
|
224
230
|
switchTab,
|
|
225
231
|
updateTabUrl,
|
|
226
|
-
interactionSequence
|
|
232
|
+
interactionSequence,
|
|
233
|
+
basename
|
|
227
234
|
]);
|
|
228
235
|
useEffect(() => {
|
|
229
236
|
if (!enabled || !isInitialized || !bootstrappedRef.current || typeof window === "undefined") {
|
|
@@ -231,13 +238,14 @@ const useTabsUrlSync = ({
|
|
|
231
238
|
}
|
|
232
239
|
const activeTab = state.tabsList.find((tab) => tab.id === state.activeKey);
|
|
233
240
|
const targetUrl = normalizeTabUrl(activeTab == null ? void 0 : activeTab.url);
|
|
241
|
+
const browserTargetUrl = getBrowserTabUrl(targetUrl, basename);
|
|
234
242
|
const commitActiveTab = () => {
|
|
235
243
|
committedActiveKeyRef.current = state.activeKey;
|
|
236
244
|
committedTabIdsRef.current = new Set(state.tabsList.map((tab) => tab.id));
|
|
237
245
|
};
|
|
238
246
|
if (preserveUrlInteractionRef.current !== null) {
|
|
239
247
|
if (preserveUrlInteractionRef.current === interactionSequence) {
|
|
240
|
-
if (
|
|
248
|
+
if (browserTargetUrl === getCurrentTabUrl()) {
|
|
241
249
|
preserveUrlInteractionRef.current = null;
|
|
242
250
|
}
|
|
243
251
|
commitActiveTab();
|
|
@@ -245,7 +253,7 @@ const useTabsUrlSync = ({
|
|
|
245
253
|
}
|
|
246
254
|
preserveUrlInteractionRef.current = null;
|
|
247
255
|
}
|
|
248
|
-
if (!targetUrl ||
|
|
256
|
+
if (!targetUrl || !browserTargetUrl || browserTargetUrl === getCurrentTabUrl()) {
|
|
249
257
|
const pendingNavigation = pendingDataNavigationRef.current;
|
|
250
258
|
navigationAttemptRef.current += 1;
|
|
251
259
|
pendingDataNavigationRef.current = false;
|
|
@@ -286,30 +294,34 @@ const useTabsUrlSync = ({
|
|
|
286
294
|
pendingDataNavigationRef.current = false;
|
|
287
295
|
pendingDataNavigationUrlRef.current = null;
|
|
288
296
|
const currentUrl = getCurrentTabUrl();
|
|
289
|
-
if (
|
|
297
|
+
if (browserTargetUrl === currentUrl || committedDataNavigationUrl === currentUrl) {
|
|
290
298
|
commitActiveTab();
|
|
291
299
|
return;
|
|
292
300
|
}
|
|
293
301
|
rollbackTabActivation(previousActiveKey, attemptedActiveKey, removeAttemptedTab);
|
|
294
302
|
};
|
|
295
303
|
pendingDataNavigationRef.current = true;
|
|
296
|
-
pendingDataNavigationUrlRef.current =
|
|
297
|
-
void dataRouterContext.router.navigate(getDataRouterUrl(
|
|
304
|
+
pendingDataNavigationUrlRef.current = browserTargetUrl;
|
|
305
|
+
void dataRouterContext.router.navigate(getDataRouterUrl(browserTargetUrl, dataRouterContext.basename), {
|
|
298
306
|
replace: historyMode === "replace",
|
|
299
307
|
state: routerState
|
|
300
308
|
}).then(settleNavigation);
|
|
301
309
|
} else if (navigator) {
|
|
302
310
|
if (historyMode === "push") {
|
|
303
|
-
navigator.push(
|
|
311
|
+
navigator.push(browserTargetUrl, routerState);
|
|
304
312
|
} else {
|
|
305
|
-
navigator.replace(
|
|
313
|
+
navigator.replace(browserTargetUrl, routerState);
|
|
306
314
|
}
|
|
307
315
|
commitActiveTab();
|
|
308
316
|
} else if (historyMode === "push") {
|
|
309
|
-
window.history.pushState(
|
|
317
|
+
window.history.pushState(
|
|
318
|
+
createPushHistoryState(window.history.state),
|
|
319
|
+
"",
|
|
320
|
+
browserTargetUrl
|
|
321
|
+
);
|
|
310
322
|
commitActiveTab();
|
|
311
323
|
} else {
|
|
312
|
-
window.history.replaceState(window.history.state, "",
|
|
324
|
+
window.history.replaceState(window.history.state, "", browserTargetUrl);
|
|
313
325
|
commitActiveTab();
|
|
314
326
|
}
|
|
315
327
|
} finally {
|
|
@@ -324,7 +336,8 @@ const useTabsUrlSync = ({
|
|
|
324
336
|
interactionSequence,
|
|
325
337
|
navigator,
|
|
326
338
|
dataRouterContext,
|
|
327
|
-
rollbackTabActivation
|
|
339
|
+
rollbackTabActivation,
|
|
340
|
+
basename
|
|
328
341
|
]);
|
|
329
342
|
};
|
|
330
343
|
export {
|
|
@@ -14,10 +14,15 @@ export declare const normalizeTabUrl: (value?: string) => string | undefined;
|
|
|
14
14
|
export declare const isSafeExternalUrl: (value?: string) => boolean;
|
|
15
15
|
export declare const isSafeHttpUrl: (value?: string) => boolean;
|
|
16
16
|
export declare const getCurrentTabUrl: () => string;
|
|
17
|
+
/** 将浏览器地址和 Tab 内部地址转为同一身份,仅按完整路径段移除 basename。 */
|
|
18
|
+
export declare const getComparableTabUrl: (value?: string, basename?: string) => string | undefined;
|
|
19
|
+
/** 将 Tab 内部地址转为浏览器地址,已带 basename 时不重复添加。 */
|
|
20
|
+
export declare const getBrowserTabUrl: (value?: string, basename?: string) => string | undefined;
|
|
17
21
|
export declare const getTabUrlPathname: (value?: string) => string | undefined;
|
|
18
22
|
export interface FindExistingTabOptions {
|
|
19
23
|
urlSync?: boolean;
|
|
20
24
|
identity?: TabsUrlSyncConfig['identity'];
|
|
25
|
+
basename?: TabsUrlSyncConfig['basename'];
|
|
21
26
|
}
|
|
22
27
|
export declare const findExistingTab: (tabsList: TabItem[], menuItem: MenusType, options?: FindExistingTabOptions) => TabItem | undefined;
|
|
23
28
|
/**
|
|
@@ -29,6 +29,49 @@ const isSafeExternalUrl = (value) => {
|
|
|
29
29
|
};
|
|
30
30
|
const isSafeHttpUrl = (value) => !!resolveHttpUrl(value);
|
|
31
31
|
const getCurrentTabUrl = () => `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
32
|
+
const normalizeTabsBasename = (basename) => {
|
|
33
|
+
const normalized = String(basename ?? "").trim();
|
|
34
|
+
if (!normalized || normalized === "/")
|
|
35
|
+
return "";
|
|
36
|
+
return `/${normalized.replace(/^\/+|\/+$/g, "")}`;
|
|
37
|
+
};
|
|
38
|
+
const splitNormalizedTabUrl = (url) => {
|
|
39
|
+
const pathnameEnd = [url.indexOf("?"), url.indexOf("#")].filter((index) => index >= 0).reduce((current, index) => Math.min(current, index), url.length);
|
|
40
|
+
return {
|
|
41
|
+
pathname: url.slice(0, pathnameEnd),
|
|
42
|
+
suffix: url.slice(pathnameEnd)
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const hasPathBasename = (pathname, basename) => {
|
|
46
|
+
const lowerPathname = pathname.toLowerCase();
|
|
47
|
+
const lowerBasename = basename.toLowerCase();
|
|
48
|
+
return lowerPathname === lowerBasename || lowerPathname.startsWith(`${lowerBasename}/`);
|
|
49
|
+
};
|
|
50
|
+
const getComparableTabUrl = (value, basename) => {
|
|
51
|
+
const normalizedUrl = normalizeTabUrl(value);
|
|
52
|
+
if (!normalizedUrl)
|
|
53
|
+
return void 0;
|
|
54
|
+
const normalizedBasename = normalizeTabsBasename(basename);
|
|
55
|
+
if (!normalizedBasename)
|
|
56
|
+
return normalizedUrl;
|
|
57
|
+
const { pathname, suffix } = splitNormalizedTabUrl(normalizedUrl);
|
|
58
|
+
if (!hasPathBasename(pathname, normalizedBasename))
|
|
59
|
+
return normalizedUrl;
|
|
60
|
+
const barePathname = pathname.slice(normalizedBasename.length) || "/";
|
|
61
|
+
return `${barePathname}${suffix}`;
|
|
62
|
+
};
|
|
63
|
+
const getBrowserTabUrl = (value, basename) => {
|
|
64
|
+
const normalizedUrl = normalizeTabUrl(value);
|
|
65
|
+
if (!normalizedUrl)
|
|
66
|
+
return void 0;
|
|
67
|
+
const normalizedBasename = normalizeTabsBasename(basename);
|
|
68
|
+
if (!normalizedBasename)
|
|
69
|
+
return normalizedUrl;
|
|
70
|
+
const { pathname, suffix } = splitNormalizedTabUrl(normalizedUrl);
|
|
71
|
+
if (hasPathBasename(pathname, normalizedBasename))
|
|
72
|
+
return normalizedUrl;
|
|
73
|
+
return `${normalizedBasename}${pathname}${suffix}`;
|
|
74
|
+
};
|
|
32
75
|
const getTabUrlPathname = (value) => {
|
|
33
76
|
const normalized = normalizeTabUrl(value);
|
|
34
77
|
if (!normalized || typeof window === "undefined")
|
|
@@ -45,10 +88,10 @@ const findExistingTab = (tabsList, menuItem, options = {}) => {
|
|
|
45
88
|
}
|
|
46
89
|
);
|
|
47
90
|
}
|
|
48
|
-
const targetUrl =
|
|
91
|
+
const targetUrl = getComparableTabUrl(getMenuRoute(menuItem), options.basename);
|
|
49
92
|
if (!targetUrl)
|
|
50
93
|
return void 0;
|
|
51
|
-
return tabsList.find((tab) =>
|
|
94
|
+
return tabsList.find((tab) => getComparableTabUrl(tab.url, options.basename) === targetUrl);
|
|
52
95
|
}
|
|
53
96
|
if (hasRelationKey(menuItem.relationKey)) {
|
|
54
97
|
return tabsList.find(
|
|
@@ -232,6 +275,8 @@ export {
|
|
|
232
275
|
findExistingTab,
|
|
233
276
|
flattenMenuData,
|
|
234
277
|
generateTabId,
|
|
278
|
+
getBrowserTabUrl,
|
|
279
|
+
getComparableTabUrl,
|
|
235
280
|
getCurrentTabUrl,
|
|
236
281
|
getMenuRoute,
|
|
237
282
|
getPinnedBoundary,
|
|
@@ -373,6 +373,11 @@ export interface TabsUrlSyncConfig {
|
|
|
373
373
|
* @default 'url'
|
|
374
374
|
*/
|
|
375
375
|
identity?: 'url' | 'relationKey';
|
|
376
|
+
/**
|
|
377
|
+
* @description 浏览器地址中的显式基础路径;Tab 内部 URL 可以不带该前缀
|
|
378
|
+
* @example '/wjs'
|
|
379
|
+
*/
|
|
380
|
+
basename?: string;
|
|
376
381
|
}
|
|
377
382
|
/**
|
|
378
383
|
* @description 添加标签页的业务参数
|