@nocobase/client-v2 2.2.0-alpha.8 → 2.2.0-alpha.9
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/APIClient.d.ts +1 -3
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +5 -1
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.mjs +103 -102
- package/lib/index.js +117 -116
- package/lib/locale/languageCodes.js +2 -1
- package/package.json +7 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/__tests__/app.test.tsx +18 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/form/ScanInput/CodeScanner.tsx +23 -6
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +35 -1
- package/src/components/form/ScanInput/useCodeScanner.ts +16 -3
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +86 -11
- package/src/flow/components/filter/VariableFilterItem.tsx +20 -5
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +49 -1
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +184 -3
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +572 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -1
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/locale/languageCodes.ts +2 -1
- package/src/nocobase-buildin-plugin/index.tsx +12 -0
|
@@ -131,6 +131,10 @@ export interface VariableFilterItemProps {
|
|
|
131
131
|
* 默认使用整棵 ctx 的 metaTree:model.context.getPropertyMetaTree()
|
|
132
132
|
*/
|
|
133
133
|
rightMetaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
134
|
+
/**
|
|
135
|
+
* 右侧变量的领域转换器。常量和空值仍由 VariableFilterItem 处理,未匹配的值回退到 FlowEngine 默认转换器。
|
|
136
|
+
*/
|
|
137
|
+
rightVariableConverters?: Pick<Converters, 'resolvePathFromValue' | 'resolveValueFromPath'>;
|
|
134
138
|
ignoreFieldNames?: string[];
|
|
135
139
|
maxAssociationFieldDepth?: number;
|
|
136
140
|
}
|
|
@@ -352,7 +356,16 @@ function findMetaTreeNodeByPath(metaTree: MetaTreeNode[], targetPath: string[]):
|
|
|
352
356
|
* 上下文筛选项组件
|
|
353
357
|
*/
|
|
354
358
|
export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
355
|
-
({
|
|
359
|
+
({
|
|
360
|
+
value,
|
|
361
|
+
model,
|
|
362
|
+
disabled = false,
|
|
363
|
+
rightAsVariable,
|
|
364
|
+
rightMetaTree,
|
|
365
|
+
rightVariableConverters,
|
|
366
|
+
ignoreFieldNames,
|
|
367
|
+
maxAssociationFieldDepth,
|
|
368
|
+
}) => {
|
|
356
369
|
// 使用 View 上下文,确保可访问 ctx.view 的异步子树
|
|
357
370
|
const ctx = useFlowViewContext();
|
|
358
371
|
const t = model.translate;
|
|
@@ -684,17 +697,19 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
684
697
|
const first = meta?.paths?.[0];
|
|
685
698
|
if (first === 'constant') return '';
|
|
686
699
|
if (first === 'null') return null;
|
|
687
|
-
return
|
|
700
|
+
return rightVariableConverters?.resolveValueFromPath?.(meta);
|
|
688
701
|
},
|
|
689
702
|
resolvePathFromValue: (val) => {
|
|
690
703
|
if (val === null) return ['null'];
|
|
691
|
-
//
|
|
692
|
-
const parsed =
|
|
704
|
+
// 变量表达式:优先使用领域转换器,再回退内置解析;其他静态值走 constant
|
|
705
|
+
const parsed =
|
|
706
|
+
rightVariableConverters?.resolvePathFromValue?.(val) ??
|
|
707
|
+
(typeof val === 'string' ? parseValueToPath(val) : undefined);
|
|
693
708
|
if (parsed) return parsed;
|
|
694
709
|
return ['constant'];
|
|
695
710
|
},
|
|
696
711
|
};
|
|
697
|
-
}, [NullComponent, staticInputRenderer]);
|
|
712
|
+
}, [NullComponent, rightVariableConverters, staticInputRenderer]);
|
|
698
713
|
|
|
699
714
|
// 为 2.0 左侧字段选择器追加“接口 filterable.children”定义(如 chinaRegion 的“省市区名称”子项),
|
|
700
715
|
// 以恢复 1.0 左侧子菜单的能力。
|
|
@@ -11,7 +11,7 @@ import React from 'react';
|
|
|
11
11
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
12
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
13
13
|
import { VariableFilterItem, VariableFilterItemValue } from '../VariableFilterItem';
|
|
14
|
-
import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
14
|
+
import { FlowEngine, FlowModel, type Converters, type MetaTreeNode } from '@nocobase/flow-engine';
|
|
15
15
|
import { observable } from '@formily/reactive';
|
|
16
16
|
import { createMockFlowApp, TestCollectionFieldInterface } from '../../../__tests__/helpers/mockFlowApp';
|
|
17
17
|
|
|
@@ -179,6 +179,54 @@ describe('VariableFilterItem', () => {
|
|
|
179
179
|
expect(operatorSelect).toHaveClass('ant-select-disabled');
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
it('composes custom right variable converters with constant, null, and core fallbacks', () => {
|
|
183
|
+
const value: VariableFilterItemValue = {
|
|
184
|
+
path: 'name',
|
|
185
|
+
operator: '$eq',
|
|
186
|
+
value: '{{$context.data.id}}',
|
|
187
|
+
};
|
|
188
|
+
const model = CreateModel();
|
|
189
|
+
const rightVariableConverters: Pick<Converters, 'resolvePathFromValue' | 'resolveValueFromPath'> = {
|
|
190
|
+
resolvePathFromValue: (input) => (input === '{{$context.data.id}}' ? ['$context', 'data', 'id'] : undefined),
|
|
191
|
+
resolveValueFromPath: (metaTreeNode) =>
|
|
192
|
+
metaTreeNode.paths?.[0] === '$context' ? `{{${metaTreeNode.paths.join('.')}}}` : undefined,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
render(
|
|
196
|
+
<VariableFilterItem
|
|
197
|
+
value={value}
|
|
198
|
+
model={model}
|
|
199
|
+
rightAsVariable
|
|
200
|
+
rightVariableConverters={rightVariableConverters}
|
|
201
|
+
/>,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const variableInputProps = (
|
|
205
|
+
globalThis as {
|
|
206
|
+
__LAST_VARIABLE_INPUT_PROPS__?: {
|
|
207
|
+
converters?: Converters;
|
|
208
|
+
value?: unknown;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
).__LAST_VARIABLE_INPUT_PROPS__;
|
|
212
|
+
const converters = variableInputProps?.converters;
|
|
213
|
+
const constantNode: MetaTreeNode = { name: 'constant', title: 'Constant', type: 'string', paths: ['constant'] };
|
|
214
|
+
const nullNode: MetaTreeNode = { name: 'null', title: 'Null', type: 'object', paths: ['null'] };
|
|
215
|
+
const workflowNode: MetaTreeNode = {
|
|
216
|
+
name: 'id',
|
|
217
|
+
title: 'ID',
|
|
218
|
+
type: 'number',
|
|
219
|
+
paths: ['$context', 'data', 'id'],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
expect(variableInputProps?.value).toBe('{{$context.data.id}}');
|
|
223
|
+
expect(converters?.resolvePathFromValue?.('{{$context.data.id}}')).toEqual(['$context', 'data', 'id']);
|
|
224
|
+
expect(converters?.resolvePathFromValue?.('{{ ctx.$context.data.id }}')).toEqual(['$context', 'data', 'id']);
|
|
225
|
+
expect(converters?.resolveValueFromPath?.(constantNode)).toBe('');
|
|
226
|
+
expect(converters?.resolveValueFromPath?.(nullNode)).toBeNull();
|
|
227
|
+
expect(converters?.resolveValueFromPath?.(workflowNode)).toBe('{{$context.data.id}}');
|
|
228
|
+
});
|
|
229
|
+
|
|
182
230
|
it('uses scoped context dataSourceManager when app dataSourceManager has no field interface manager', async () => {
|
|
183
231
|
const value = observable({ path: '', operator: '', value: '' }) as any;
|
|
184
232
|
const model = CreateModel();
|
|
@@ -33,6 +33,7 @@ import { commonConditionHandler, ConditionBuilder } from '../../../components/Co
|
|
|
33
33
|
import { TextAreaWithContextSelector } from '../../../components/TextAreaWithContextSelector';
|
|
34
34
|
import { confirmUnsavedChangesHandler } from './closeGuard';
|
|
35
35
|
import { BasePageTabModel } from './PageTabModel';
|
|
36
|
+
import { FilteredPageTabBar, NO_ACTIVE_PAGE_TAB_KEY } from './PageModelTabBar';
|
|
36
37
|
|
|
37
38
|
type PageModelStructure = {
|
|
38
39
|
subModels: {
|
|
@@ -49,6 +50,54 @@ type PageModelContextWithRoute = {
|
|
|
49
50
|
currentRoute?: CurrentRouteWithTabs | null;
|
|
50
51
|
};
|
|
51
52
|
|
|
53
|
+
type RequestedTabKey = {
|
|
54
|
+
key?: string;
|
|
55
|
+
source: 'url' | 'implicit' | 'props' | 'none';
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type TabActiveResolution = RequestedTabKey & {
|
|
59
|
+
status: 'unspecified' | 'visible' | 'hidden' | 'unknown';
|
|
60
|
+
effectiveKey?: string;
|
|
61
|
+
allTabs: BasePageTabModel[];
|
|
62
|
+
unhiddenTabs: BasePageTabModel[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type TabActiveKeySyncState = {
|
|
66
|
+
previousEffectiveActiveKey?: string;
|
|
67
|
+
effectiveActiveKey?: string;
|
|
68
|
+
previousAllHidden: boolean;
|
|
69
|
+
lastCorrectionSignature?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type TabActiveKeySyncProps = {
|
|
73
|
+
dependencyKey: string;
|
|
74
|
+
onSync: (state: TabActiveKeySyncState) => TabActiveKeySyncState;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type LocallyCommittedTabTransition = {
|
|
78
|
+
activeKey: string;
|
|
79
|
+
previousActiveKey?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function TabActiveKeySync({ dependencyKey, onSync }: TabActiveKeySyncProps) {
|
|
83
|
+
const previousEffectiveActiveKey = React.useRef<string>();
|
|
84
|
+
const previousAllHidden = React.useRef(false);
|
|
85
|
+
const lastCorrectionSignature = React.useRef<string>();
|
|
86
|
+
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
const nextState = onSync({
|
|
89
|
+
previousEffectiveActiveKey: previousEffectiveActiveKey.current,
|
|
90
|
+
previousAllHidden: previousAllHidden.current,
|
|
91
|
+
lastCorrectionSignature: lastCorrectionSignature.current,
|
|
92
|
+
});
|
|
93
|
+
previousEffectiveActiveKey.current = nextState.effectiveActiveKey;
|
|
94
|
+
previousAllHidden.current = nextState.previousAllHidden;
|
|
95
|
+
lastCorrectionSignature.current = nextState.lastCorrectionSignature;
|
|
96
|
+
}, [dependencyKey, onSync]);
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
52
101
|
export class PageModel extends FlowModel<PageModelStructure> {
|
|
53
102
|
tabBarExtraContent: { left?: ReactNode; right?: ReactNode } = {};
|
|
54
103
|
private viewActivatedListener?: (_payload?: unknown) => void;
|
|
@@ -57,6 +106,8 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
57
106
|
private dirtyRefreshScheduled = false;
|
|
58
107
|
private unmounted = false;
|
|
59
108
|
private documentTitleUpdateVersion = 0;
|
|
109
|
+
private implicitActiveKey?: string;
|
|
110
|
+
private locallyCommittedTabTransition?: LocallyCommittedTabTransition;
|
|
60
111
|
|
|
61
112
|
/**
|
|
62
113
|
* 根页面标签页开关以路由表为准,避免 flow model 里的旧配置覆盖路由管理设置。
|
|
@@ -75,14 +126,277 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
75
126
|
return !!this.props.enableTabs;
|
|
76
127
|
}
|
|
77
128
|
|
|
78
|
-
private
|
|
129
|
+
private getAllTabs(): BasePageTabModel[] {
|
|
130
|
+
return this.subModels?.tabs || [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private getUnhiddenTabs(): BasePageTabModel[] {
|
|
134
|
+
return this.getAllTabs().filter((tab) => !tab.hidden);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private getRequestedTabKey(): RequestedTabKey {
|
|
79
138
|
const viewParams = this.context.view?.navigation?.viewParams;
|
|
80
139
|
if (viewParams) {
|
|
81
|
-
|
|
140
|
+
const urlKey = typeof viewParams.tabUid === 'string' && viewParams.tabUid ? viewParams.tabUid : undefined;
|
|
141
|
+
if (urlKey) {
|
|
142
|
+
this.implicitActiveKey = undefined;
|
|
143
|
+
return { key: urlKey, source: 'url' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const activeKey =
|
|
147
|
+
typeof this.props.tabActiveKey === 'string' && this.props.tabActiveKey ? this.props.tabActiveKey : undefined;
|
|
148
|
+
if (activeKey && activeKey === this.implicitActiveKey) {
|
|
149
|
+
return { key: activeKey, source: 'implicit' };
|
|
150
|
+
}
|
|
151
|
+
return { source: 'none' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const activeKey =
|
|
155
|
+
typeof this.props.tabActiveKey === 'string' && this.props.tabActiveKey ? this.props.tabActiveKey : undefined;
|
|
156
|
+
if (!activeKey) {
|
|
157
|
+
return { source: 'none' };
|
|
158
|
+
}
|
|
159
|
+
return { key: activeKey, source: activeKey === this.implicitActiveKey ? 'implicit' : 'props' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private resolveTabActiveState(): TabActiveResolution {
|
|
163
|
+
const allTabs = this.getAllTabs();
|
|
164
|
+
const unhiddenTabs = this.getUnhiddenTabs();
|
|
165
|
+
const requested = this.getRequestedTabKey();
|
|
166
|
+
|
|
167
|
+
if (!requested.key) {
|
|
168
|
+
return {
|
|
169
|
+
...requested,
|
|
170
|
+
status: 'unspecified',
|
|
171
|
+
effectiveKey: unhiddenTabs[0]?.uid,
|
|
172
|
+
allTabs,
|
|
173
|
+
unhiddenTabs,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const requestedTab = allTabs.find((tab) => tab.uid === requested.key);
|
|
178
|
+
if (requested.source === 'implicit' && (!requestedTab || requestedTab.hidden)) {
|
|
179
|
+
return {
|
|
180
|
+
...requested,
|
|
181
|
+
status: 'unspecified',
|
|
182
|
+
effectiveKey: unhiddenTabs[0]?.uid,
|
|
183
|
+
allTabs,
|
|
184
|
+
unhiddenTabs,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!requestedTab) {
|
|
189
|
+
return {
|
|
190
|
+
...requested,
|
|
191
|
+
status: 'unknown',
|
|
192
|
+
effectiveKey: requested.key,
|
|
193
|
+
allTabs,
|
|
194
|
+
unhiddenTabs,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (unhiddenTabs.some((tab) => tab.uid === requested.key)) {
|
|
199
|
+
return {
|
|
200
|
+
...requested,
|
|
201
|
+
status: 'visible',
|
|
202
|
+
effectiveKey: requested.key,
|
|
203
|
+
allTabs,
|
|
204
|
+
unhiddenTabs,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
...requested,
|
|
210
|
+
status: 'hidden',
|
|
211
|
+
effectiveKey: requested.key,
|
|
212
|
+
allTabs,
|
|
213
|
+
unhiddenTabs,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private getActiveTabKey(): string | undefined {
|
|
218
|
+
if (!this.getEnableTabs()) {
|
|
219
|
+
const firstTabKey = this.getFirstTab()?.uid;
|
|
220
|
+
if (firstTabKey) {
|
|
221
|
+
return firstTabKey;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return this.resolveTabActiveState().effectiveKey;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private getTabActiveKeySyncDependency() {
|
|
228
|
+
const allTabs = this.getAllTabs();
|
|
229
|
+
const unhiddenTabs = this.getUnhiddenTabs();
|
|
230
|
+
const viewParams = this.context.view?.navigation?.viewParams;
|
|
231
|
+
return JSON.stringify([
|
|
232
|
+
!!this.context.flowSettingsEnabled,
|
|
233
|
+
viewParams ? viewParams.tabUid || null : null,
|
|
234
|
+
this.props.tabActiveKey || null,
|
|
235
|
+
allTabs.map((tab) => tab.uid),
|
|
236
|
+
unhiddenTabs.map((tab) => tab.uid),
|
|
237
|
+
this.implicitActiveKey || null,
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private requestDocumentTitleUpdate(preferredActiveTabKey?: string, retryCount = 0) {
|
|
242
|
+
this.updateDocumentTitle(preferredActiveTabKey, retryCount).catch((error) => {
|
|
243
|
+
console.warn('[PageModel] Failed to update document title', error);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private rememberImplicitActiveKey(activeKey: string) {
|
|
248
|
+
this.implicitActiveKey = activeKey;
|
|
249
|
+
if (this.props.tabActiveKey !== activeKey) {
|
|
250
|
+
this.setProps('tabActiveKey', activeKey);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private commitTabActiveKey(
|
|
255
|
+
activeKey: string | undefined,
|
|
256
|
+
options: {
|
|
257
|
+
previousActiveKey?: string;
|
|
258
|
+
navigate: boolean;
|
|
259
|
+
preserveImplicitKey?: boolean;
|
|
260
|
+
rememberLocalTransition?: boolean;
|
|
261
|
+
},
|
|
262
|
+
) {
|
|
263
|
+
const { previousActiveKey, navigate, preserveImplicitKey = false, rememberLocalTransition = false } = options;
|
|
264
|
+
if (rememberLocalTransition && activeKey) {
|
|
265
|
+
this.locallyCommittedTabTransition = { activeKey, previousActiveKey };
|
|
266
|
+
}
|
|
267
|
+
this.implicitActiveKey = activeKey && preserveImplicitKey ? activeKey : undefined;
|
|
268
|
+
|
|
269
|
+
if (navigate) {
|
|
270
|
+
this.context.view?.navigation?.changeTo?.({ tabUid: activeKey });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (activeKey && activeKey !== previousActiveKey) {
|
|
274
|
+
this.invokeTabModelLifecycleMethod(activeKey, 'onActive');
|
|
275
|
+
}
|
|
276
|
+
if (previousActiveKey && previousActiveKey !== activeKey) {
|
|
277
|
+
this.invokeTabModelLifecycleMethod(previousActiveKey, 'onInactive');
|
|
278
|
+
}
|
|
279
|
+
this.setProps('tabActiveKey', activeKey);
|
|
280
|
+
|
|
281
|
+
if (!activeKey) {
|
|
282
|
+
this.requestDocumentTitleUpdate();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private consumeLocallyCommittedTabTransition(previousActiveKey: string | undefined, activeKey: string | undefined) {
|
|
287
|
+
const transition = this.locallyCommittedTabTransition;
|
|
288
|
+
if (!transition || !activeKey) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
if (transition.activeKey === activeKey && transition.previousActiveKey === previousActiveKey) {
|
|
292
|
+
this.locallyCommittedTabTransition = undefined;
|
|
293
|
+
return true;
|
|
82
294
|
}
|
|
83
|
-
|
|
295
|
+
if (activeKey !== transition.previousActiveKey) {
|
|
296
|
+
this.locallyCommittedTabTransition = undefined;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
84
299
|
}
|
|
85
300
|
|
|
301
|
+
private synchronizeTabActiveKey = (state: TabActiveKeySyncState): TabActiveKeySyncState => {
|
|
302
|
+
const resolution = this.resolveTabActiveState();
|
|
303
|
+
const allHidden = resolution.allTabs.length > 0 && resolution.unhiddenTabs.length === 0;
|
|
304
|
+
const createNextState = (effectiveActiveKey: string | undefined, lastCorrectionSignature?: string) => ({
|
|
305
|
+
previousEffectiveActiveKey: effectiveActiveKey,
|
|
306
|
+
effectiveActiveKey,
|
|
307
|
+
previousAllHidden: allHidden,
|
|
308
|
+
lastCorrectionSignature,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (resolution.status === 'unknown') {
|
|
312
|
+
return createNextState(resolution.effectiveKey);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (resolution.status === 'visible') {
|
|
316
|
+
const activeKey = resolution.effectiveKey;
|
|
317
|
+
if (this.consumeLocallyCommittedTabTransition(state.previousEffectiveActiveKey, activeKey)) {
|
|
318
|
+
return createNextState(activeKey);
|
|
319
|
+
}
|
|
320
|
+
const shouldActivateAfterRestore = state.previousAllHidden && resolution.source !== 'url';
|
|
321
|
+
const shouldSyncChangedVisibleKey =
|
|
322
|
+
!!state.previousEffectiveActiveKey && state.previousEffectiveActiveKey !== activeKey;
|
|
323
|
+
if ((shouldActivateAfterRestore || shouldSyncChangedVisibleKey) && activeKey) {
|
|
324
|
+
const signature = `visible:${resolution.source}:${state.previousEffectiveActiveKey || ''}->${activeKey}`;
|
|
325
|
+
if (signature !== state.lastCorrectionSignature) {
|
|
326
|
+
this.commitTabActiveKey(activeKey, {
|
|
327
|
+
previousActiveKey: state.previousEffectiveActiveKey,
|
|
328
|
+
navigate: false,
|
|
329
|
+
preserveImplicitKey: resolution.source === 'implicit',
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return createNextState(activeKey, signature);
|
|
333
|
+
}
|
|
334
|
+
return createNextState(activeKey);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (resolution.status === 'hidden') {
|
|
338
|
+
const activeKey = resolution.effectiveKey;
|
|
339
|
+
if (this.consumeLocallyCommittedTabTransition(state.previousEffectiveActiveKey, activeKey)) {
|
|
340
|
+
return createNextState(activeKey);
|
|
341
|
+
}
|
|
342
|
+
if (activeKey && state.previousEffectiveActiveKey !== activeKey) {
|
|
343
|
+
const signature = `hidden:${resolution.source}:${state.previousEffectiveActiveKey || ''}->${activeKey}`;
|
|
344
|
+
if (signature !== state.lastCorrectionSignature) {
|
|
345
|
+
this.commitTabActiveKey(activeKey, {
|
|
346
|
+
previousActiveKey: state.previousEffectiveActiveKey,
|
|
347
|
+
navigate: false,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return createNextState(activeKey, signature);
|
|
351
|
+
}
|
|
352
|
+
return createNextState(activeKey);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const activeKey = resolution.effectiveKey;
|
|
356
|
+
if (!activeKey) {
|
|
357
|
+
if (!allHidden) {
|
|
358
|
+
if (resolution.source !== 'implicit' || !resolution.key) {
|
|
359
|
+
return createNextState(undefined);
|
|
360
|
+
}
|
|
361
|
+
const signature = `implicit-missing:${state.previousEffectiveActiveKey || ''}`;
|
|
362
|
+
if (signature !== state.lastCorrectionSignature) {
|
|
363
|
+
this.commitTabActiveKey(undefined, {
|
|
364
|
+
previousActiveKey: state.previousEffectiveActiveKey,
|
|
365
|
+
navigate: false,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return createNextState(undefined, signature);
|
|
369
|
+
}
|
|
370
|
+
const signature = `all-hidden:${state.previousEffectiveActiveKey || ''}`;
|
|
371
|
+
if (!state.previousAllHidden && signature !== state.lastCorrectionSignature) {
|
|
372
|
+
this.implicitActiveKey = undefined;
|
|
373
|
+
if (this.props.tabActiveKey || state.previousEffectiveActiveKey) {
|
|
374
|
+
this.commitTabActiveKey(undefined, {
|
|
375
|
+
previousActiveKey: state.previousEffectiveActiveKey,
|
|
376
|
+
navigate: false,
|
|
377
|
+
});
|
|
378
|
+
} else {
|
|
379
|
+
this.requestDocumentTitleUpdate();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return createNextState(undefined, signature);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const shouldSwitchImplicitActiveKey =
|
|
386
|
+
state.previousAllHidden || (!!state.previousEffectiveActiveKey && state.previousEffectiveActiveKey !== activeKey);
|
|
387
|
+
const signature = `implicit:${state.previousEffectiveActiveKey || ''}->${activeKey}`;
|
|
388
|
+
if (shouldSwitchImplicitActiveKey && signature !== state.lastCorrectionSignature) {
|
|
389
|
+
this.commitTabActiveKey(activeKey, {
|
|
390
|
+
previousActiveKey: state.previousEffectiveActiveKey,
|
|
391
|
+
navigate: false,
|
|
392
|
+
preserveImplicitKey: true,
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
this.rememberImplicitActiveKey(activeKey);
|
|
396
|
+
}
|
|
397
|
+
return createNextState(activeKey, shouldSwitchImplicitActiveKey ? signature : undefined);
|
|
398
|
+
};
|
|
399
|
+
|
|
86
400
|
private scheduleActiveLifecycleRefresh(forceRefresh = false): void {
|
|
87
401
|
if (this.dirtyRefreshScheduled) return;
|
|
88
402
|
this.dirtyRefreshScheduled = true;
|
|
@@ -107,7 +421,7 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
107
421
|
}
|
|
108
422
|
|
|
109
423
|
deactivateCurrentTab() {
|
|
110
|
-
const activeKey = this.
|
|
424
|
+
const activeKey = this.getActiveTabKey();
|
|
111
425
|
if (activeKey) {
|
|
112
426
|
this.invokeTabModelLifecycleMethod(activeKey, 'onInactive');
|
|
113
427
|
}
|
|
@@ -116,9 +430,10 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
116
430
|
onMount(): void {
|
|
117
431
|
super.onMount();
|
|
118
432
|
this.unmounted = false;
|
|
433
|
+
this.implicitActiveKey = undefined;
|
|
119
434
|
this.setProps('tabActiveKey', this.context.view.inputArgs?.tabUid);
|
|
120
435
|
if (this.context?.pageInfo) this.context.pageInfo.version = 'v2';
|
|
121
|
-
|
|
436
|
+
this.requestDocumentTitleUpdate();
|
|
122
437
|
|
|
123
438
|
// When a nested view (popup/page) is closed, the opener view becomes active again.
|
|
124
439
|
// We align this with the existing tab lifecycle by invoking `onActive` for the current tab blocks.
|
|
@@ -189,7 +504,7 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
189
504
|
}
|
|
190
505
|
|
|
191
506
|
if (method === 'onActive') {
|
|
192
|
-
|
|
507
|
+
this.requestDocumentTitleUpdate(tabActiveKey);
|
|
193
508
|
}
|
|
194
509
|
}
|
|
195
510
|
|
|
@@ -236,11 +551,15 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
236
551
|
}
|
|
237
552
|
};
|
|
238
553
|
|
|
554
|
+
const unhiddenTabs = this.getUnhiddenTabs();
|
|
555
|
+
const activeTabKey = preferredActiveTabKey || this.getActiveTabKey();
|
|
556
|
+
const shouldUsePageTitle = !this.getEnableTabs() || (!activeTabKey && unhiddenTabs.length === 0);
|
|
557
|
+
|
|
239
558
|
let nextTitle = '';
|
|
240
|
-
if (
|
|
241
|
-
const activeTabKey = preferredActiveTabKey || this.getActiveTabKey();
|
|
559
|
+
if (!shouldUsePageTitle) {
|
|
242
560
|
const activeTabModel = activeTabKey
|
|
243
|
-
?
|
|
561
|
+
? this.findSubModel('tabs', (model) => model.uid === activeTabKey) ||
|
|
562
|
+
(this.flowEngine.getModel(activeTabKey) as BasePageTabModel | undefined)
|
|
244
563
|
: this.getFirstTab();
|
|
245
564
|
if (!activeTabModel && retryCount < 5) {
|
|
246
565
|
window.setTimeout(() => {
|
|
@@ -251,7 +570,7 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
251
570
|
if (updateVersion !== this.documentTitleUpdateVersion) {
|
|
252
571
|
return;
|
|
253
572
|
}
|
|
254
|
-
|
|
573
|
+
this.requestDocumentTitleUpdate(activeTabKey, retryCount + 1);
|
|
255
574
|
}, 0);
|
|
256
575
|
return;
|
|
257
576
|
}
|
|
@@ -293,33 +612,31 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
293
612
|
|
|
294
613
|
mapTabs() {
|
|
295
614
|
return this.mapSubModels('tabs', (model) => {
|
|
296
|
-
return
|
|
297
|
-
|
|
298
|
-
:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
};
|
|
322
|
-
}).filter(Boolean);
|
|
615
|
+
return {
|
|
616
|
+
key: model.uid,
|
|
617
|
+
label: (
|
|
618
|
+
<Droppable model={model}>
|
|
619
|
+
<FlowModelRenderer
|
|
620
|
+
model={model}
|
|
621
|
+
showFlowSettings={{
|
|
622
|
+
showBackground: true,
|
|
623
|
+
showBorder: false,
|
|
624
|
+
toolbarPosition: 'above',
|
|
625
|
+
style: { transform: 'translateY(8px)' },
|
|
626
|
+
}}
|
|
627
|
+
extraToolbarItems={[
|
|
628
|
+
{
|
|
629
|
+
key: 'drag-handler',
|
|
630
|
+
component: DragHandler,
|
|
631
|
+
sort: 1,
|
|
632
|
+
},
|
|
633
|
+
]}
|
|
634
|
+
/>
|
|
635
|
+
</Droppable>
|
|
636
|
+
),
|
|
637
|
+
children: model.renderChildren(),
|
|
638
|
+
};
|
|
639
|
+
});
|
|
323
640
|
}
|
|
324
641
|
|
|
325
642
|
getFirstTab() {
|
|
@@ -336,6 +653,17 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
336
653
|
}
|
|
337
654
|
|
|
338
655
|
renderTabs() {
|
|
656
|
+
const activeState = this.resolveTabActiveState();
|
|
657
|
+
const tabItems = this.mapTabs();
|
|
658
|
+
const hiddenTabKeys = new Set(
|
|
659
|
+
this.context.flowSettingsEnabled ? [] : activeState.allTabs.filter((tab) => tab.hidden).map((tab) => tab.uid),
|
|
660
|
+
);
|
|
661
|
+
const hiddenActiveTabLabel =
|
|
662
|
+
activeState.status === 'hidden'
|
|
663
|
+
? activeState.allTabs.find((tab) => tab.uid === activeState.effectiveKey)?.getTabTitle?.()
|
|
664
|
+
: undefined;
|
|
665
|
+
const tabsActiveKey =
|
|
666
|
+
activeState.effectiveKey || (activeState.allTabs.length > 0 ? NO_ACTIVE_PAGE_TAB_KEY : undefined);
|
|
339
667
|
const tabNavPaddingInlineStart = this.context.themeToken?.paddingLG ?? 16;
|
|
340
668
|
const leftExtraContent =
|
|
341
669
|
this.tabBarExtraContent.left !== undefined ? (
|
|
@@ -366,25 +694,31 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
366
694
|
|
|
367
695
|
return (
|
|
368
696
|
<DndProvider onDragEnd={this.handleDragEnd.bind(this)}>
|
|
697
|
+
<TabActiveKeySync dependencyKey={this.getTabActiveKeySyncDependency()} onSync={this.synchronizeTabActiveKey} />
|
|
369
698
|
<Tabs
|
|
370
|
-
activeKey={
|
|
371
|
-
this.context.view?.navigation?.viewParams
|
|
372
|
-
? this.context.view.navigation.viewParams.tabUid || this.getFirstTab()?.uid
|
|
373
|
-
: this.props.tabActiveKey
|
|
374
|
-
}
|
|
699
|
+
activeKey={tabsActiveKey}
|
|
375
700
|
tabBarStyle={this.props.tabBarStyle}
|
|
376
|
-
items={
|
|
701
|
+
items={tabItems}
|
|
702
|
+
renderTabBar={
|
|
703
|
+
hiddenTabKeys.size > 0
|
|
704
|
+
? (tabBarProps) => (
|
|
705
|
+
<FilteredPageTabBar
|
|
706
|
+
hiddenTabKeys={hiddenTabKeys}
|
|
707
|
+
hiddenActiveTabLabel={hiddenActiveTabLabel}
|
|
708
|
+
items={tabItems}
|
|
709
|
+
tabBarProps={tabBarProps}
|
|
710
|
+
/>
|
|
711
|
+
)
|
|
712
|
+
: undefined
|
|
713
|
+
}
|
|
377
714
|
onChange={(activeKey) => {
|
|
378
|
-
const previousActiveKey = this.
|
|
379
|
-
this.
|
|
380
|
-
|
|
715
|
+
const previousActiveKey = this.getActiveTabKey();
|
|
716
|
+
this.implicitActiveKey = undefined;
|
|
717
|
+
this.commitTabActiveKey(activeKey, {
|
|
718
|
+
previousActiveKey,
|
|
719
|
+
navigate: !!this.context.view?.navigation,
|
|
720
|
+
rememberLocalTransition: true,
|
|
381
721
|
});
|
|
382
|
-
|
|
383
|
-
this.invokeTabModelLifecycleMethod(activeKey, 'onActive');
|
|
384
|
-
if (previousActiveKey && previousActiveKey !== activeKey) {
|
|
385
|
-
this.invokeTabModelLifecycleMethod(previousActiveKey, 'onInactive');
|
|
386
|
-
}
|
|
387
|
-
this.setProps('tabActiveKey', activeKey);
|
|
388
722
|
}}
|
|
389
723
|
// destroyInactiveTabPane
|
|
390
724
|
tabBarExtraContent={{
|
|
@@ -527,7 +861,7 @@ PageModel.registerFlow({
|
|
|
527
861
|
marginBottom: 0,
|
|
528
862
|
});
|
|
529
863
|
}
|
|
530
|
-
|
|
864
|
+
await (ctx.model as PageModel).updateDocumentTitle();
|
|
531
865
|
},
|
|
532
866
|
},
|
|
533
867
|
},
|