@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -0
@@ -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 getActiveTabKey(): string | undefined {
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
- return viewParams.tabUid || this.getFirstTab()?.uid;
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
- return this.props.tabActiveKey || this.getFirstTab()?.uid;
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.props.tabActiveKey || this.getFirstTab()?.uid;
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
- void this.updateDocumentTitle();
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
- void this.updateDocumentTitle(tabActiveKey);
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 (this.getEnableTabs()) {
241
- const activeTabKey = preferredActiveTabKey || this.getActiveTabKey();
559
+ if (!shouldUsePageTitle) {
242
560
  const activeTabModel = activeTabKey
243
- ? (this.flowEngine.getModel(activeTabKey) as BasePageTabModel | undefined)
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
- void this.updateDocumentTitle(activeTabKey, retryCount + 1);
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 !this.context.flowSettingsEnabled && model.hidden
297
- ? null
298
- : {
299
- key: model.uid,
300
- label: (
301
- <Droppable model={model}>
302
- <FlowModelRenderer
303
- model={model}
304
- showFlowSettings={{
305
- showBackground: true,
306
- showBorder: false,
307
- toolbarPosition: 'above',
308
- style: { transform: 'translateY(8px)' },
309
- }}
310
- extraToolbarItems={[
311
- {
312
- key: 'drag-handler',
313
- component: DragHandler,
314
- sort: 1,
315
- },
316
- ]}
317
- />
318
- </Droppable>
319
- ),
320
- children: model.renderChildren(),
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={this.mapTabs()}
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.props.tabActiveKey || this.getActiveTabKey();
379
- this.context.view.navigation?.changeTo?.({
380
- tabUid: activeKey,
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
- void (ctx.model as PageModel).updateDocumentTitle();
864
+ await (ctx.model as PageModel).updateDocumentTitle();
531
865
  },
532
866
  },
533
867
  },
@@ -0,0 +1,114 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { Tabs } from 'antd';
11
+ import type { TabsProps } from 'antd';
12
+ import React from 'react';
13
+
14
+ export const NO_ACTIVE_PAGE_TAB_KEY = '__no_active_page_tab__';
15
+
16
+ type RenderTabBar = NonNullable<React.ComponentProps<typeof Tabs>['renderTabBar']>;
17
+ type PageTabItem = NonNullable<TabsProps['items']>[number];
18
+
19
+ type FilteredPageTabBarProps = {
20
+ hiddenTabKeys: Set<string>;
21
+ hiddenActiveTabLabel?: React.ReactNode;
22
+ items: PageTabItem[];
23
+ tabBarProps: Parameters<RenderTabBar>[0];
24
+ };
25
+
26
+ export function FilteredPageTabBar({
27
+ hiddenTabKeys,
28
+ hiddenActiveTabLabel,
29
+ items,
30
+ tabBarProps,
31
+ }: FilteredPageTabBarProps) {
32
+ // Keep the outer Tabs instance responsible for every pane, while a navigation-only Tabs instance
33
+ // receives only visible items. This avoids relying on the rc-tabs Context instance bundled by antd.
34
+ const visibleItems = items
35
+ .filter((item) => !hiddenTabKeys.has(String(item.key)))
36
+ .map(
37
+ ({
38
+ children: _children,
39
+ className: _className,
40
+ destroyInactiveTabPane: _destroyInactiveTabPane,
41
+ forceRender: _forceRender,
42
+ style: _style,
43
+ ...item
44
+ }) => ({
45
+ ...item,
46
+ children: null,
47
+ }),
48
+ );
49
+ const activeKey = visibleItems.some((item) => item.key === tabBarProps.activeKey)
50
+ ? tabBarProps.activeKey
51
+ : NO_ACTIVE_PAGE_TAB_KEY;
52
+ const firstFocusableTabKey = visibleItems.find((item) => !item.disabled)?.key;
53
+ const hiddenActiveTabKey = hiddenTabKeys.has(String(tabBarProps.activeKey))
54
+ ? String(tabBarProps.activeKey)
55
+ : undefined;
56
+ const hiddenActiveTabLabelId =
57
+ hiddenActiveTabKey && tabBarProps.id ? `${tabBarProps.id}-tab-${hiddenActiveTabKey}` : undefined;
58
+ const renderTabNode = (node: React.ReactElement) => {
59
+ const tabKey = String(node.key);
60
+ if (activeKey !== NO_ACTIVE_PAGE_TAB_KEY || tabKey !== String(firstFocusableTabKey)) {
61
+ return node;
62
+ }
63
+ let tabButtonFound = false;
64
+ const children = React.Children.map(
65
+ (node.props as { children?: React.ReactNode }).children,
66
+ (child: React.ReactNode) => {
67
+ if (tabButtonFound || !React.isValidElement(child)) {
68
+ return child;
69
+ }
70
+ const childProps = child.props as {
71
+ role?: string;
72
+ tabIndex?: number | null;
73
+ };
74
+ if (childProps.role !== 'tab') {
75
+ return child;
76
+ }
77
+ tabButtonFound = true;
78
+ return React.cloneElement(child as React.ReactElement<typeof childProps>, {
79
+ tabIndex: 0,
80
+ });
81
+ },
82
+ );
83
+ return React.cloneElement(node, undefined, children);
84
+ };
85
+
86
+ return (
87
+ <>
88
+ {hiddenActiveTabLabelId ? (
89
+ <span id={hiddenActiveTabLabelId} hidden>
90
+ {hiddenActiveTabLabel || hiddenActiveTabKey}
91
+ </span>
92
+ ) : null}
93
+ <Tabs
94
+ activeKey={NO_ACTIVE_PAGE_TAB_KEY}
95
+ animated={{ inkBar: tabBarProps.animated.inkBar, tabPane: false }}
96
+ destroyInactiveTabPane
97
+ id={tabBarProps.id}
98
+ items={visibleItems}
99
+ more={tabBarProps.more}
100
+ onTabClick={(key, event) => tabBarProps.onTabClick(key, event)}
101
+ onTabScroll={tabBarProps.onTabScroll}
102
+ renderTabBar={(navigationTabBarProps, NavigationDefaultTabBar) => (
103
+ <NavigationDefaultTabBar {...navigationTabBarProps} activeKey={activeKey}>
104
+ {renderTabNode}
105
+ </NavigationDefaultTabBar>
106
+ )}
107
+ tabBarExtraContent={tabBarProps.extra}
108
+ tabBarGutter={tabBarProps.tabBarGutter}
109
+ tabBarStyle={tabBarProps.style}
110
+ tabPosition={tabBarProps.tabPosition}
111
+ />
112
+ </>
113
+ );
114
+ }