@nocobase/client-v2 2.1.0-beta.36 → 2.1.0-beta.38

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 (154) hide show
  1. package/es/Application.d.ts +1 -0
  2. package/es/BaseApplication.d.ts +4 -0
  3. package/es/RouterManager.d.ts +1 -0
  4. package/es/components/KeepAlive.d.ts +22 -0
  5. package/es/components/RouterBridge.d.ts +9 -0
  6. package/es/components/form/DialogFormLayout.d.ts +5 -29
  7. package/es/components/form/filter/CollectionFilter.d.ts +41 -0
  8. package/es/components/form/filter/CollectionFilterItem.d.ts +41 -0
  9. package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
  10. package/es/components/form/filter/FilterValueInput.d.ts +29 -0
  11. package/es/components/form/filter/index.d.ts +11 -0
  12. package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
  13. package/es/components/form/index.d.ts +1 -0
  14. package/es/data-source/ExtendCollectionsProvider.d.ts +50 -0
  15. package/es/data-source/index.d.ts +9 -0
  16. package/es/flow/FlowPage.d.ts +2 -1
  17. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  18. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  19. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  21. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  22. package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
  23. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  24. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  25. package/es/flow/components/FlowRoute.d.ts +10 -1
  26. package/es/flow/components/filter/index.d.ts +2 -0
  27. package/es/flow/components/filter/useFilterOptions.d.ts +54 -0
  28. package/es/flow/index.d.ts +4 -0
  29. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  30. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  32. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  33. package/es/flow-compat/passwordUtils.d.ts +1 -1
  34. package/es/index.d.ts +2 -0
  35. package/es/index.mjs +491 -439
  36. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  37. package/es/layout-manager/LayoutManager.d.ts +22 -0
  38. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  39. package/es/layout-manager/index.d.ts +13 -0
  40. package/es/layout-manager/types.d.ts +20 -0
  41. package/es/layout-manager/utils.d.ts +14 -0
  42. package/es/nocobase-buildin-plugin/index.d.ts +3 -10
  43. package/es/settings-center/index.d.ts +1 -1
  44. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  45. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  46. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  47. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  48. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  49. package/lib/index.js +491 -439
  50. package/package.json +8 -7
  51. package/src/Application.tsx +27 -12
  52. package/src/BaseApplication.tsx +19 -0
  53. package/src/PluginSettingsManager.ts +1 -1
  54. package/src/RouterManager.tsx +17 -1
  55. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  56. package/src/__tests__/app.test.tsx +17 -1
  57. package/src/__tests__/globalDeps.test.ts +1 -0
  58. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
  59. package/src/__tests__/plugin-manager.test.tsx +177 -0
  60. package/src/__tests__/settings-center.test.tsx +24 -2
  61. package/src/components/KeepAlive.tsx +131 -0
  62. package/src/components/README.md +89 -6
  63. package/src/components/README.zh-CN.md +89 -7
  64. package/src/components/RouterBridge.tsx +28 -4
  65. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  66. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  67. package/src/components/form/DialogFormLayout.tsx +5 -29
  68. package/src/components/form/filter/CollectionFilter.tsx +101 -0
  69. package/src/components/form/filter/CollectionFilterItem.tsx +176 -0
  70. package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
  71. package/src/components/form/filter/FilterValueInput.tsx +198 -0
  72. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +205 -0
  73. package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
  74. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
  75. package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
  76. package/src/components/form/filter/index.ts +13 -0
  77. package/src/components/form/filter/useFilterActionProps.ts +200 -0
  78. package/src/components/form/index.tsx +1 -0
  79. package/src/data-source/ExtendCollectionsProvider.tsx +144 -0
  80. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  81. package/src/data-source/index.ts +10 -0
  82. package/src/flow/FlowPage.tsx +35 -7
  83. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  84. package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
  85. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  86. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  87. package/src/flow/actions/aclCheck.tsx +4 -0
  88. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  89. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  90. package/src/flow/actions/linkageRules.tsx +122 -0
  91. package/src/flow/actions/openView.tsx +28 -4
  92. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  93. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  94. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  95. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  96. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
  97. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  98. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
  99. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  100. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  101. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
  102. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  103. package/src/flow/admin-shell/admin-layout/index.ts +2 -0
  104. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  105. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  106. package/src/flow/components/AdminLayout.tsx +4 -154
  107. package/src/flow/components/FlowRoute.tsx +105 -15
  108. package/src/flow/components/filter/index.ts +3 -0
  109. package/src/flow/components/filter/useFilterOptions.ts +80 -0
  110. package/src/flow/index.ts +4 -0
  111. package/src/flow/models/base/ActionModel.tsx +8 -1
  112. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  113. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  114. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  115. package/src/flow/models/base/RouteModel.tsx +1 -1
  116. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  117. package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
  118. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  119. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  120. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  121. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  122. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  123. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  124. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  125. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  126. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  127. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  128. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  129. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  130. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  131. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  132. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  133. package/src/index.ts +2 -0
  134. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  135. package/src/layout-manager/LayoutManager.tsx +185 -0
  136. package/src/layout-manager/LayoutRoute.tsx +138 -0
  137. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  138. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  139. package/src/layout-manager/index.ts +14 -0
  140. package/src/layout-manager/types.ts +22 -0
  141. package/src/layout-manager/utils.ts +37 -0
  142. package/src/nocobase-buildin-plugin/index.tsx +69 -67
  143. package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
  144. package/src/settings-center/index.ts +1 -1
  145. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  146. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  147. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  148. package/src/settings-center/plugin-manager/index.tsx +254 -0
  149. package/src/settings-center/plugin-manager/types.ts +35 -0
  150. package/src/settings-center/utils.tsx +8 -1
  151. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  152. package/src/theme/globalStyles.ts +10 -0
  153. package/src/utils/globalDeps.ts +2 -0
  154. package/src/settings-center/PluginManagerPage.tsx +0 -162
@@ -0,0 +1,502 @@
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 {
11
+ FlowEngine,
12
+ type FlowContext,
13
+ FlowModel,
14
+ observable,
15
+ parsePathnameToViewParams,
16
+ ViewNavigation,
17
+ type ViewParam,
18
+ } from '@nocobase/flow-engine';
19
+ import React from 'react';
20
+ import { getViewDiffAndUpdateHidden, getKey } from '../getViewDiffAndUpdateHidden';
21
+ import { getOpenViewStepParams } from '../flows/openViewFlow';
22
+ import { RouteModel } from '../models/base/RouteModel';
23
+ import { resolveViewParamsToViewList, updateViewListHidden, type ViewItem } from '../resolveViewParamsToViewList';
24
+ import type { LayoutDefinition } from '../../layout-manager/types';
25
+
26
+ export interface RoutePageMeta {
27
+ active: boolean;
28
+ refreshDesktopRoutes?: () => Promise<unknown>;
29
+ layoutContentElement?: HTMLElement | null;
30
+ }
31
+
32
+ export interface BaseLayoutRouteCoordinatorOptions {
33
+ layout?: LayoutDefinition;
34
+ layoutContext?: FlowContext;
35
+ basePathname?: string;
36
+ }
37
+
38
+ interface ViewRuntimeState {
39
+ destroy: (force?: boolean) => void;
40
+ update: (value: any) => void;
41
+ activate?: (forceRefresh?: boolean) => void;
42
+ deactivate?: () => void;
43
+ navigation: ViewNavigation;
44
+ }
45
+
46
+ interface RoutePageRuntime {
47
+ pageUid: string;
48
+ routeModel: RouteModel;
49
+ meta: RoutePageMeta;
50
+ viewState: Record<string, ViewRuntimeState>;
51
+ prevViewList: ViewItem[];
52
+ hasStepNavigated: boolean;
53
+ forceStop: boolean;
54
+ }
55
+
56
+ interface RouteLike {
57
+ layoutRouteName?: string;
58
+ params?: { name?: string };
59
+ pathname?: string;
60
+ pageUid?: string;
61
+ layoutBasePathname?: string;
62
+ layoutRoute?: {
63
+ type: string;
64
+ pageUid?: string;
65
+ basePathname?: string;
66
+ } | null;
67
+ }
68
+
69
+ const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
70
+
71
+ const normalizeBasePathname = (basePathname?: string) => {
72
+ return `/${(basePathname || '/admin').replace(/^\/+/, '').replace(/\/+$/, '')}`;
73
+ };
74
+
75
+ const getDefaultBasePathnameFromRoutePath = (routePath?: string) => {
76
+ if (routePath?.startsWith('/')) {
77
+ return normalizeBasePathname(routePath);
78
+ }
79
+ return '';
80
+ };
81
+
82
+ /**
83
+ * 通用 Layout 路由协调器。
84
+ *
85
+ * 负责把当前路由路径解析为 v2 view stack,并驱动 RouteModel 上的弹窗、抽屉和 page tab 状态。
86
+ * Admin、Embed 等布局只需要提供当前 Layout 的基准路径,即可复用同一套 view 编排逻辑。
87
+ */
88
+ export class BaseLayoutRouteCoordinator {
89
+ protected readonly flowEngine: FlowEngine;
90
+ protected readonly layout: LayoutDefinition | undefined;
91
+ private readonly layoutContext?: FlowContext;
92
+ private basePathname: string;
93
+ private readonly runtimes = new Map<string, RoutePageRuntime>();
94
+ private layoutContentElement: HTMLElement | null = null;
95
+
96
+ constructor(flowEngine: FlowEngine, options: BaseLayoutRouteCoordinatorOptions = {}) {
97
+ this.flowEngine = flowEngine;
98
+ this.layout = options.layout;
99
+ this.layoutContext = options.layoutContext;
100
+ this.basePathname =
101
+ options.basePathname ||
102
+ getDefaultBasePathnameFromRoutePath(options.layout?.routePath) ||
103
+ (options.layout ? '' : normalizeBasePathname());
104
+ }
105
+
106
+ setLayoutContentElement(element: HTMLElement | null) {
107
+ this.layoutContentElement = element;
108
+ }
109
+
110
+ registerPage(pageUid: string, meta: RoutePageMeta) {
111
+ const routeModel = this.getOrCreateRouteModel(pageUid);
112
+ const runtime: RoutePageRuntime = {
113
+ pageUid,
114
+ routeModel,
115
+ meta: {
116
+ active: !!meta.active,
117
+ refreshDesktopRoutes: meta.refreshDesktopRoutes,
118
+ layoutContentElement: meta.layoutContentElement || null,
119
+ },
120
+ viewState: {},
121
+ prevViewList: [],
122
+ hasStepNavigated: false,
123
+ forceStop: false,
124
+ };
125
+
126
+ this.ensureRouteModelContext(runtime);
127
+ this.runtimes.set(pageUid, runtime);
128
+ this.syncPageMeta(pageUid, meta);
129
+
130
+ return runtime.routeModel;
131
+ }
132
+
133
+ syncPageMeta(pageUid: string, meta: Partial<RoutePageMeta>) {
134
+ const runtime = this.runtimes.get(pageUid);
135
+ if (!runtime) {
136
+ return;
137
+ }
138
+
139
+ const nextActive = typeof meta.active === 'boolean' ? meta.active : undefined;
140
+ runtime.meta = {
141
+ ...runtime.meta,
142
+ ...meta,
143
+ active: runtime.meta.active,
144
+ layoutContentElement: meta.layoutContentElement ?? runtime.meta.layoutContentElement,
145
+ };
146
+
147
+ if (typeof nextActive === 'boolean') {
148
+ this.setRuntimeActive(runtime, nextActive);
149
+ }
150
+ }
151
+
152
+ unregisterPage(pageUid: string) {
153
+ this.cleanupPage(pageUid);
154
+ this.runtimes.delete(pageUid);
155
+ }
156
+
157
+ syncRoute(routeLike: RouteLike) {
158
+ if (routeLike?.layoutRouteName && this.layout?.routeName && routeLike.layoutRouteName !== this.layout.routeName) {
159
+ this.runtimes.forEach((runtime) => {
160
+ this.setRuntimeActive(runtime, false);
161
+ });
162
+ return;
163
+ }
164
+
165
+ const activePageUid = routeLike?.pageUid || routeLike?.layoutRoute?.pageUid || routeLike?.params?.name;
166
+ const pathname = routeLike?.pathname;
167
+ const nextBasePathname = routeLike?.layoutBasePathname || routeLike?.layoutRoute?.basePathname;
168
+
169
+ if (nextBasePathname) {
170
+ this.basePathname = normalizeBasePathname(nextBasePathname);
171
+ }
172
+
173
+ this.runtimes.forEach((runtime) => {
174
+ this.setRuntimeActive(runtime, runtime.pageUid === activePageUid);
175
+ });
176
+
177
+ if (!activePageUid || !pathname) {
178
+ return;
179
+ }
180
+
181
+ this.runtimes.forEach((runtime) => {
182
+ if (runtime.pageUid !== activePageUid) {
183
+ runtime.forceStop = true;
184
+ }
185
+ });
186
+
187
+ const runtime = this.runtimes.get(activePageUid);
188
+ if (!runtime) {
189
+ return;
190
+ }
191
+
192
+ runtime.forceStop = false;
193
+ this.syncRuntimeWithPathname(runtime, pathname);
194
+ }
195
+
196
+ cleanupPage(pageUid: string) {
197
+ const runtime = this.runtimes.get(pageUid);
198
+ if (!runtime) {
199
+ return;
200
+ }
201
+
202
+ runtime.forceStop = true;
203
+ runtime.prevViewList.forEach((viewItem) => {
204
+ this.flowEngine.removeModelWithSubModels(viewItem.params.viewUid);
205
+ runtime.viewState[getKey(viewItem)]?.destroy?.();
206
+ delete runtime.viewState[getKey(viewItem)];
207
+ });
208
+ runtime.prevViewList = [];
209
+ runtime.hasStepNavigated = false;
210
+ }
211
+
212
+ destroy() {
213
+ Array.from(this.runtimes.keys()).forEach((pageUid) => {
214
+ this.cleanupPage(pageUid);
215
+ this.runtimes.delete(pageUid);
216
+ });
217
+ }
218
+
219
+ protected getCurrentRouteByPageUid(pageUid: string) {
220
+ return this.flowEngine.context.routeRepository?.getRouteBySchemaUid?.(pageUid) || {};
221
+ }
222
+
223
+ private setRuntimeActive(runtime: RoutePageRuntime, active: boolean) {
224
+ const wasActive = !!runtime.meta.active;
225
+ runtime.meta.active = active;
226
+ if (runtime.routeModel.context.pageActive?.value !== active) {
227
+ runtime.routeModel.context.pageActive.value = active;
228
+ }
229
+ this.syncViewListVisibility(runtime);
230
+ if (wasActive === active) {
231
+ return;
232
+ }
233
+
234
+ this.notifyRuntimeActiveChange(runtime, active);
235
+ }
236
+
237
+ private notifyRuntimeActiveChange(runtime: RoutePageRuntime, active: boolean) {
238
+ const rootViewItem = runtime.prevViewList[0];
239
+ if (!rootViewItem) {
240
+ return;
241
+ }
242
+
243
+ const viewState = runtime.viewState[getKey(rootViewItem)];
244
+ if (active) {
245
+ viewState?.activate?.(true);
246
+ return;
247
+ }
248
+
249
+ viewState?.deactivate?.();
250
+ }
251
+
252
+ private syncRuntimeWithPathname(runtime: RoutePageRuntime, pathname: string) {
253
+ try {
254
+ if (!this.basePathname) {
255
+ return;
256
+ }
257
+
258
+ const viewStack = parsePathnameToViewParams(pathname, { basePath: this.basePathname });
259
+ const viewList = resolveViewParamsToViewList(this.flowEngine, viewStack, runtime.routeModel);
260
+
261
+ if (this.shouldStepNavigate(runtime, viewList)) {
262
+ this.stepNavigate(viewList, 0);
263
+ runtime.hasStepNavigated = true;
264
+ this.scheduleInitialDeepLinkReplay(runtime, pathname);
265
+ return;
266
+ }
267
+
268
+ const { viewsToClose, viewsToOpen } = getViewDiffAndUpdateHidden(runtime.prevViewList, viewList);
269
+
270
+ if (viewsToClose.length) {
271
+ viewsToClose.forEach((viewItem) => {
272
+ runtime.viewState[getKey(viewItem)]?.destroy?.(true);
273
+ delete runtime.viewState[getKey(viewItem)];
274
+ });
275
+ updateViewListHidden(viewList);
276
+ }
277
+
278
+ if (viewsToOpen.length) {
279
+ void this.handleOpenViews(runtime, viewList, viewsToOpen);
280
+ }
281
+
282
+ if (viewsToClose.length === 0 && viewsToOpen.length === 0) {
283
+ const currentViewItem = viewList.at(-1);
284
+ if (currentViewItem) {
285
+ runtime.viewState[getKey(currentViewItem)]?.navigation.setViewStack(viewList.map((item) => item.params));
286
+ }
287
+ }
288
+
289
+ runtime.prevViewList = [...viewList];
290
+ this.syncViewListVisibility(runtime);
291
+ } catch (error) {
292
+ console.error(`[NocoBase] Failed to resolve view params to view list:`, error);
293
+ }
294
+ }
295
+
296
+ private syncViewListVisibility(runtime: RoutePageRuntime, viewList = runtime.prevViewList) {
297
+ if (!viewList.length) {
298
+ return;
299
+ }
300
+
301
+ if (runtime.meta.active) {
302
+ updateViewListHidden(viewList);
303
+ return;
304
+ }
305
+
306
+ viewList.forEach((viewItem) => {
307
+ viewItem.hidden.value = true;
308
+ });
309
+ }
310
+
311
+ private shouldStepNavigate(runtime: RoutePageRuntime, viewList: ViewItem[]) {
312
+ return runtime.prevViewList.length === 0 && viewList.length > 1 && !runtime.hasStepNavigated;
313
+ }
314
+
315
+ private scheduleInitialDeepLinkReplay(runtime: RoutePageRuntime, pathname: string) {
316
+ Promise.resolve()
317
+ .then(() => {
318
+ if (runtime.forceStop || runtime.prevViewList.length > 0) {
319
+ return;
320
+ }
321
+
322
+ this.syncRuntimeWithPathname(runtime, pathname);
323
+ })
324
+ .catch(() => {
325
+ // ignore
326
+ });
327
+ }
328
+
329
+ private stepNavigate(viewList: ViewItem[], index: number) {
330
+ if (!viewList[index]) {
331
+ return;
332
+ }
333
+
334
+ if (index === 0) {
335
+ new ViewNavigation(this.flowEngine.context, [], { basePath: this.basePathname }).navigateTo(
336
+ viewList[index].params,
337
+ {
338
+ replace: true,
339
+ },
340
+ );
341
+ } else {
342
+ new ViewNavigation(
343
+ this.flowEngine.context,
344
+ viewList.slice(0, index).map((item) => item.params),
345
+ { basePath: this.basePathname },
346
+ ).navigateTo(viewList[index].params);
347
+ }
348
+
349
+ this.stepNavigate(viewList, index + 1);
350
+ }
351
+
352
+ private async handleOpenViews(runtime: RoutePageRuntime, viewList: ViewItem[], viewsToOpen: ViewItem[]) {
353
+ const missingModels = viewsToOpen.filter((v) => !v.model);
354
+ if (missingModels.length > 0) {
355
+ await Promise.all(
356
+ missingModels.map(async (viewItem) => {
357
+ try {
358
+ viewItem.model = await this.flowEngine.loadModel({ uid: viewItem.modelUid });
359
+ } catch (error) {
360
+ console.error(`[NocoBase] Failed to load model ${viewItem.modelUid}:`, error);
361
+ }
362
+ }),
363
+ );
364
+ }
365
+
366
+ if (runtime.forceStop) {
367
+ return;
368
+ }
369
+
370
+ this.syncViewListVisibility(runtime, viewList);
371
+ this.openViews(runtime, viewList, viewsToOpen, 0);
372
+ }
373
+
374
+ private openViews(runtime: RoutePageRuntime, viewList: ViewItem[], viewsToOpen: ViewItem[], index: number) {
375
+ if (!viewsToOpen[index]) {
376
+ return;
377
+ }
378
+
379
+ const viewItem = viewsToOpen[index];
380
+ if (!viewItem.model) {
381
+ return;
382
+ }
383
+
384
+ const destroyRef = React.createRef<(result?: any, force?: boolean) => void>();
385
+ const updateRef = React.createRef<(value: any) => void>();
386
+ const activateRef = React.createRef<(forceRefresh?: boolean) => void>();
387
+ const deactivateRef = React.createRef<() => void>();
388
+ const openViewParams = getOpenViewStepParams(viewItem.model);
389
+ const associationName =
390
+ openViewParams?.associationName && !hasUsableSourceId(viewItem.params.sourceId)
391
+ ? null
392
+ : openViewParams?.associationName;
393
+ const openerUids = viewList.slice(0, viewItem.index).map((item) => item.params.viewUid);
394
+ const navigation = new ViewNavigation(
395
+ this.flowEngine.context,
396
+ viewList.slice(0, viewItem.index + 1).map((item) => item.params),
397
+ { basePath: this.basePathname },
398
+ );
399
+
400
+ viewItem.model.dispatchEvent('click', {
401
+ target: this.layoutContentElement || runtime.meta.layoutContentElement,
402
+ collectionName: openViewParams?.collectionName,
403
+ associationName,
404
+ dataSourceKey: openViewParams?.dataSourceKey,
405
+ destroyRef,
406
+ updateRef,
407
+ activateRef,
408
+ deactivateRef,
409
+ openerUids,
410
+ ...viewItem.params,
411
+ pageActive: runtime.meta.active,
412
+ activationControlledByLayout: true,
413
+ navigation,
414
+ onOpen: () => {
415
+ this.openViews(runtime, viewList, viewsToOpen, index + 1);
416
+ },
417
+ hidden: viewItem.hidden,
418
+ isMobileLayout: !!this.layoutContext?.isMobileLayout,
419
+ triggerByRouter: true,
420
+ });
421
+
422
+ runtime.viewState[getKey(viewItem)] = {
423
+ destroy: (_force?: boolean) => destroyRef.current?.(),
424
+ update: (value: any) => updateRef.current?.(value),
425
+ activate: (forceRefresh?: boolean) => activateRef.current?.(forceRefresh),
426
+ deactivate: () => deactivateRef.current?.(),
427
+ navigation,
428
+ };
429
+ }
430
+
431
+ private ensureRouteModelContext(runtime: RoutePageRuntime) {
432
+ if (this.layoutContext) {
433
+ runtime.routeModel.context.addDelegate(this.layoutContext);
434
+ }
435
+
436
+ if (!runtime.routeModel.context.pageActive) {
437
+ runtime.routeModel.context.defineProperty('pageActive', {
438
+ value: observable.ref(false),
439
+ info: {
440
+ description:
441
+ 'Whether current page route is active (keep-alive). This is an observable.ref<boolean> (use ctx.pageActive.value to read/write).',
442
+ detail: 'observable.ref<boolean>',
443
+ },
444
+ });
445
+ }
446
+
447
+ runtime.routeModel.context.defineProperty('currentRoute', {
448
+ get: () => this.getCurrentRouteByPageUid(runtime.pageUid),
449
+ // 当前路由来自 routeRepository,菜单更新后必须实时读取最新对象。
450
+ cache: false,
451
+ });
452
+
453
+ runtime.routeModel.context.defineProperty('refreshDesktopRoutes', {
454
+ get: () => runtime.meta.refreshDesktopRoutes,
455
+ // refresh 回调来自页面桥接层,切页后也需要读取最新引用。
456
+ cache: false,
457
+ });
458
+ runtime.routeModel.context.defineProperty('layout', {
459
+ get: () => this.layout || this.layoutContext?.layout,
460
+ // RouteModel 是打开根页面的入口,根页面及其内部 schema 需要读取当前 Layout 定义。
461
+ cache: false,
462
+ });
463
+ runtime.routeModel.context.defineProperty('layoutRoute', {
464
+ get: () => this.layoutContext?.layoutRoute,
465
+ cache: false,
466
+ });
467
+ runtime.routeModel.context.defineProperty('layoutContext', {
468
+ get: () => this.layoutContext,
469
+ cache: false,
470
+ });
471
+ }
472
+
473
+ private getOrCreateRouteModel(pageUid: string): RouteModel {
474
+ const existing = this.flowEngine.getModel(pageUid);
475
+ if (existing instanceof RouteModel) {
476
+ return existing as RouteModel;
477
+ }
478
+
479
+ if (existing) {
480
+ this.flowEngine.removeModelWithSubModels(pageUid);
481
+ }
482
+
483
+ return this.flowEngine.createModel({
484
+ uid: pageUid,
485
+ use: 'RouteModel',
486
+ }) as RouteModel;
487
+ }
488
+ }
489
+
490
+ /**
491
+ * 将 pathname 解析结果和 pageUid 对齐,便于测试里复用。
492
+ */
493
+ export function toViewStack(pathname: string, options: BaseLayoutRouteCoordinatorOptions = {}): ViewParam[] {
494
+ const basePath = options.basePathname || getDefaultBasePathnameFromRoutePath(options.layout?.routePath);
495
+ if (!basePath) {
496
+ return [];
497
+ }
498
+
499
+ return parsePathnameToViewParams(pathname, {
500
+ basePath,
501
+ });
502
+ }