@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  4. package/es/components/category-tabs/index.d.ts +9 -0
  5. package/es/components/form/JsonTextArea.d.ts +2 -1
  6. package/es/components/form/TypedVariableInput.d.ts +22 -4
  7. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  8. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  9. package/es/components/form/filter/index.d.ts +2 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/actions/afterSuccess.d.ts +8 -1
  13. package/es/flow/actions/index.d.ts +1 -1
  14. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  15. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  16. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  19. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  20. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  21. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  22. package/es/flow/index.d.ts +1 -0
  23. package/es/flow/models/base/GridModel.d.ts +1 -1
  24. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  25. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  26. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  27. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  28. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  29. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  30. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  32. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  33. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  34. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  35. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  36. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  37. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  38. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  39. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  40. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  41. package/es/flow/utils/formValueDeps.d.ts +32 -0
  42. package/es/flow-compat/FieldValidation.d.ts +2 -1
  43. package/es/index.d.ts +2 -1
  44. package/es/index.mjs +233 -137
  45. package/lib/index.js +234 -138
  46. package/package.json +7 -7
  47. package/src/PluginSettingsManager.ts +53 -0
  48. package/src/RouteRepository.ts +126 -24
  49. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  50. package/src/__tests__/RouteRepository.test.ts +216 -0
  51. package/src/__tests__/browserChecker.test.ts +61 -0
  52. package/src/__tests__/exports.test.ts +16 -0
  53. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  54. package/src/__tests__/plugin-manager.test.tsx +44 -2
  55. package/src/__tests__/settings-center.test.tsx +40 -1
  56. package/src/collection-manager/field-configure.ts +1 -1
  57. package/src/collection-manager/interfaces/id.ts +1 -1
  58. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  59. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  60. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  61. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  62. package/src/collection-manager/interfaces/obo.tsx +2 -2
  63. package/src/collection-manager/interfaces/oho.tsx +2 -2
  64. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  65. package/src/collection-manager/interfaces/uuid.ts +1 -1
  66. package/src/components/AppComponents.tsx +19 -3
  67. package/src/components/README.md +7 -1
  68. package/src/components/README.zh-CN.md +6 -1
  69. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  70. package/src/components/category-tabs/index.ts +10 -0
  71. package/src/components/form/JsonTextArea.tsx +21 -11
  72. package/src/components/form/TypedVariableInput.tsx +416 -93
  73. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  74. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  75. package/src/components/form/__tests__/TypedVariableInput.test.tsx +268 -9
  76. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  77. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  78. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  79. package/src/components/form/filter/index.ts +2 -0
  80. package/src/components/form/index.tsx +1 -0
  81. package/src/components/index.ts +2 -0
  82. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  83. package/src/flow/__tests__/getKey.test.ts +7 -0
  84. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  85. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  86. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  87. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  88. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  89. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  90. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  91. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  92. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  93. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  94. package/src/flow/actions/afterSuccess.tsx +142 -3
  95. package/src/flow/actions/customVariable.tsx +1 -2
  96. package/src/flow/actions/dataScopeFilter.ts +10 -0
  97. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  98. package/src/flow/actions/index.ts +1 -1
  99. package/src/flow/actions/linkageRules.tsx +123 -35
  100. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  101. package/src/flow/actions/openView.tsx +38 -4
  102. package/src/flow/actions/runjs.tsx +2 -14
  103. package/src/flow/actions/validation.tsx +62 -30
  104. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  105. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  106. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  107. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
  108. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  109. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  110. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  111. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  112. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  113. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  115. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  118. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  119. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  120. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  121. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  122. package/src/flow/components/DefaultValue.tsx +1 -2
  123. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  124. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  125. package/src/flow/components/FieldAssignValueInput.tsx +10 -5
  126. package/src/flow/components/FlowRoute.tsx +56 -11
  127. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  128. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  129. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  130. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  131. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  132. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  133. package/src/flow/components/fieldAssignOptions.ts +155 -27
  134. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  135. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  136. package/src/flow/index.ts +2 -1
  137. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  138. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  139. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  140. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  141. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  142. package/src/flow/models/base/ActionModel.tsx +12 -1
  143. package/src/flow/models/base/ActionModelCore.tsx +6 -4
  144. package/src/flow/models/base/GridModel.tsx +38 -7
  145. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  146. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  147. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  148. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  149. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  150. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  151. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  152. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  153. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  154. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  155. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  156. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  157. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  158. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  159. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  160. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  161. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  162. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  163. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  164. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  165. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  166. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  167. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  168. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  169. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  170. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  171. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  172. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  173. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  174. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  175. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  176. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  177. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  178. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  179. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  180. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  181. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  182. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  183. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  184. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  185. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  186. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  187. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  188. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  189. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  190. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  191. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  192. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  193. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  194. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  195. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  196. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  197. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  198. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  199. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  200. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  201. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  202. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  203. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  204. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  205. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  206. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  207. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  208. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  209. package/src/flow/utils/formValueDeps.ts +170 -0
  210. package/src/flow-compat/FieldValidation.tsx +122 -60
  211. package/src/flow-compat/Popover.tsx +43 -4
  212. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  213. package/src/index.ts +8 -1
  214. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  215. package/src/nocobase-buildin-plugin/index.tsx +44 -14
  216. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  217. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  218. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  219. package/src/settings-center/plugin-manager/index.tsx +3 -0
  220. package/src/settings-center/utils.tsx +0 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.2.0-alpha.1",
3
+ "version": "2.2.0-alpha.3",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.2.0-alpha.1",
31
- "@nocobase/flow-engine": "2.2.0-alpha.1",
32
- "@nocobase/sdk": "2.2.0-alpha.1",
33
- "@nocobase/shared": "2.2.0-alpha.1",
34
- "@nocobase/utils": "2.2.0-alpha.1",
30
+ "@nocobase/evaluators": "2.2.0-alpha.3",
31
+ "@nocobase/flow-engine": "2.2.0-alpha.3",
32
+ "@nocobase/sdk": "2.2.0-alpha.3",
33
+ "@nocobase/shared": "2.2.0-alpha.3",
34
+ "@nocobase/utils": "2.2.0-alpha.3",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -46,5 +46,5 @@
46
46
  "react-i18next": "^11.15.1",
47
47
  "react-router-dom": "^6.30.1"
48
48
  },
49
- "gitHead": "303663aba6c6eefa27e6a6435b4c0352074ec40f"
49
+ "gitHead": "e3ec19361fd7981af9fe2a7f24ad335025cbefcd"
50
50
  }
@@ -97,6 +97,7 @@ interface InternalPageItemRecord extends PluginSettingsPageItemOptions {
97
97
  export class PluginSettingsManager<TApp extends BaseApplication<any> = BaseApplication<any>> {
98
98
  protected menus: Record<string, InternalMenuItemRecord> = {};
99
99
  protected pages: Record<string, InternalPageItemRecord> = {};
100
+ protected pluginSettingsLinks: Record<string, string> = {};
100
101
  protected aclSnippets: string[] = [];
101
102
  public app: TApp;
102
103
 
@@ -294,6 +295,58 @@ export class PluginSettingsManager<TApp extends BaseApplication<any> = BaseAppli
294
295
  return !!(this.menus[name] || this.pages[name]);
295
296
  }
296
297
 
298
+ /**
299
+ * 绑定插件管理列表中的 plugin name 到实际 settings 入口。
300
+ *
301
+ * 插件管理列表使用 applicationPlugins.name 判断是否显示 Settings 入口,
302
+ * 但部分插件会把设置页挂到共享菜单或历史路径下。
303
+ *
304
+ * @param pluginName 插件管理列表中的插件名
305
+ * @param settingsName menu 或 page 名称
306
+ * @returns {void}
307
+ */
308
+ setPluginSettingsLink(pluginName: string, settingsName: string) {
309
+ if (!pluginName) {
310
+ throw new Error('Plugin settings link pluginName is required');
311
+ }
312
+
313
+ if (!settingsName) {
314
+ throw new Error('Plugin settings link settingsName is required');
315
+ }
316
+
317
+ this.pluginSettingsLinks[pluginName] = settingsName;
318
+ }
319
+
320
+ /**
321
+ * 获取插件管理列表中的 plugin name 对应的 settings 名称。
322
+ *
323
+ * @param pluginName 插件管理列表中的插件名
324
+ * @returns {string} settings 名称
325
+ */
326
+ getPluginSettingsName(pluginName: string) {
327
+ return this.pluginSettingsLinks[pluginName] || pluginName;
328
+ }
329
+
330
+ /**
331
+ * 判断插件管理列表中的插件是否有可跳转的 settings 入口。
332
+ *
333
+ * @param pluginName 插件管理列表中的插件名
334
+ * @returns {boolean} 是否已注册 settings 入口
335
+ */
336
+ hasPluginSettings(pluginName: string) {
337
+ return this.has(this.getPluginSettingsName(pluginName));
338
+ }
339
+
340
+ /**
341
+ * 获取插件管理列表中的插件对应的 settings 路径。
342
+ *
343
+ * @param pluginName 插件管理列表中的插件名
344
+ * @returns {string} settings 绝对路径
345
+ */
346
+ getPluginSettingsRoutePath(pluginName: string) {
347
+ return this.getRoutePath(this.getPluginSettingsName(pluginName));
348
+ }
349
+
297
350
  /**
298
351
  * 获取单个 menu 或 page 的只读快照。
299
352
  *
@@ -9,15 +9,24 @@
9
9
 
10
10
  import { APIClient } from '@nocobase/sdk';
11
11
  import type { NocoBaseDesktopRoute } from './flow-compat';
12
+ import { ADMIN_LAYOUT_MODEL_UID } from './flow/admin-shell/admin-layout/constants';
12
13
 
13
14
  type RouteSubscriber = () => void;
14
15
  type RouteCreateValues = Partial<NocoBaseDesktopRoute>;
15
16
  type RouteUpdateValues = Partial<NocoBaseDesktopRoute>;
17
+ type RouteLayoutLike = {
18
+ uid?: unknown;
19
+ };
16
20
 
17
21
  type RouteMutationOptions = {
18
22
  refreshAfterMutation?: boolean;
19
23
  };
20
24
 
25
+ type AccessibleRouteCache = {
26
+ accessibleLoaded: boolean;
27
+ routes: NocoBaseDesktopRoute[];
28
+ };
29
+
21
30
  type MoveRouteOptions = {
22
31
  sourceId: string | number;
23
32
  targetId?: string | number;
@@ -30,21 +39,47 @@ type MoveRouteOptions = {
30
39
 
31
40
  export class RouteRepository {
32
41
  routes: NocoBaseDesktopRoute[] = [];
33
- protected subscribers = new Set<RouteSubscriber>();
34
- protected accessibleLoaded = false;
35
- protected accessibleLoadingPromise: Promise<NocoBaseDesktopRoute[]> | null = null;
42
+ protected subscribers = new Map<RouteSubscriber, string>();
43
+ protected accessibleLoadingPromises = new Map<string, Promise<NocoBaseDesktopRoute[]>>();
44
+ private layoutActivations: Array<{ token: symbol; uid: string }> = [];
45
+ private routeCaches = new Map<string, AccessibleRouteCache>();
46
+ private refreshRequestIds = new Map<string, number>();
47
+ private notifiedLayoutUid: string | undefined;
36
48
 
37
49
  constructor(protected ctx: { api?: APIClient }) {}
38
50
 
51
+ activateLayout(layout?: RouteLayoutLike) {
52
+ const uid = this.normalizeLayoutUid(layout?.uid);
53
+ const token = Symbol('route-layout');
54
+
55
+ this.layoutActivations.push({
56
+ token,
57
+ uid,
58
+ });
59
+ this.syncRoutesProperty();
60
+
61
+ return () => {
62
+ const index = this.layoutActivations.findIndex((item) => item.token === token);
63
+ if (index >= 0) {
64
+ this.layoutActivations.splice(index, 1);
65
+ }
66
+ this.syncRoutesProperty();
67
+ };
68
+ }
69
+
39
70
  /**
40
71
  * 同步当前可访问桌面路由,并通知订阅方刷新。
41
72
  *
42
73
  * @param routes 最新的桌面路由树
43
74
  */
44
- setRoutes(routes: NocoBaseDesktopRoute[]) {
45
- this.routes = routes;
46
- this.accessibleLoaded = true;
47
- this.emitChange();
75
+ setRoutes(routes: NocoBaseDesktopRoute[], layoutUid = this.getCurrentLayoutUid()) {
76
+ const cache = this.getRouteCache(layoutUid);
77
+ cache.routes = routes;
78
+ cache.accessibleLoaded = true;
79
+ if (layoutUid === this.getCurrentLayoutUid()) {
80
+ this.routes = routes;
81
+ }
82
+ this.emitChange(layoutUid);
48
83
  }
49
84
 
50
85
  /**
@@ -53,7 +88,9 @@ export class RouteRepository {
53
88
  * @returns 当前缓存的路由数组
54
89
  */
55
90
  listAccessible() {
56
- return this.routes;
91
+ const routes = this.getRouteCache(this.getReadableLayoutUid()).routes;
92
+ this.routes = routes;
93
+ return routes;
57
94
  }
58
95
 
59
96
  /**
@@ -62,7 +99,7 @@ export class RouteRepository {
62
99
  * @returns {boolean} 是否已初始化完成
63
100
  */
64
101
  isAccessibleLoaded() {
65
- return this.accessibleLoaded;
102
+ return this.getRouteCache().accessibleLoaded;
66
103
  }
67
104
 
68
105
  /**
@@ -71,12 +108,20 @@ export class RouteRepository {
71
108
  * @returns 最新的路由数组
72
109
  */
73
110
  async refreshAccessible() {
111
+ const layoutUid = this.getCurrentLayoutUid();
112
+ return this.refreshAccessibleByLayout(layoutUid);
113
+ }
114
+
115
+ private async refreshAccessibleByLayout(layoutUid: string) {
116
+ const requestId = this.nextRefreshRequestId(layoutUid);
74
117
  const response = await this.getAPIClient().request({
75
118
  url: '/desktopRoutes:listAccessible',
76
- params: { tree: true, sort: 'sort' },
119
+ params: { tree: true, sort: 'sort', layout: layoutUid },
77
120
  });
78
121
  const routes = response?.data?.data || [];
79
- this.setRoutes(routes);
122
+ if (requestId === this.getRefreshRequestId(layoutUid)) {
123
+ this.setRoutes(routes, layoutUid);
124
+ }
80
125
  return routes;
81
126
  }
82
127
 
@@ -86,19 +131,26 @@ export class RouteRepository {
86
131
  * @returns 当前可访问的路由数组
87
132
  */
88
133
  async ensureAccessibleLoaded() {
89
- if (this.accessibleLoaded) {
90
- return this.routes;
134
+ const layoutUid = this.getCurrentLayoutUid();
135
+ const cache = this.getRouteCache(layoutUid);
136
+ if (cache.accessibleLoaded) {
137
+ this.routes = cache.routes;
138
+ return cache.routes;
91
139
  }
92
140
 
93
- if (this.accessibleLoadingPromise) {
94
- return this.accessibleLoadingPromise;
141
+ const existingLoadingPromise = this.accessibleLoadingPromises.get(layoutUid);
142
+ if (existingLoadingPromise) {
143
+ return existingLoadingPromise;
95
144
  }
96
145
 
97
- this.accessibleLoadingPromise = this.refreshAccessible().finally(() => {
98
- this.accessibleLoadingPromise = null;
146
+ const loadingPromise = this.refreshAccessibleByLayout(layoutUid).finally(() => {
147
+ if (this.accessibleLoadingPromises.get(layoutUid) === loadingPromise) {
148
+ this.accessibleLoadingPromises.delete(layoutUid);
149
+ }
99
150
  });
151
+ this.accessibleLoadingPromises.set(layoutUid, loadingPromise);
100
152
 
101
- return this.accessibleLoadingPromise;
153
+ return loadingPromise;
102
154
  }
103
155
 
104
156
  /**
@@ -108,7 +160,7 @@ export class RouteRepository {
108
160
  * @returns 取消订阅函数
109
161
  */
110
162
  subscribe(subscriber: RouteSubscriber) {
111
- this.subscribers.add(subscriber);
163
+ this.subscribers.set(subscriber, this.getCurrentLayoutUid());
112
164
  return () => {
113
165
  this.subscribers.delete(subscriber);
114
166
  };
@@ -132,7 +184,10 @@ export class RouteRepository {
132
184
  */
133
185
  async createRoute(values: RouteCreateValues, options: RouteMutationOptions = {}) {
134
186
  const { refreshAfterMutation = true } = options;
135
- const res = await this.getResource('desktopRoutes').create({ values });
187
+ const res = await this.getResource('desktopRoutes').create({
188
+ values,
189
+ layout: this.getCurrentLayoutUid(),
190
+ });
136
191
  if (refreshAfterMutation) {
137
192
  await this.refreshAccessible();
138
193
  }
@@ -210,7 +265,7 @@ export class RouteRepository {
210
265
  * @returns 匹配到的路由节点
211
266
  */
212
267
  getRouteBySchemaUid(schemaUid: string): NocoBaseDesktopRoute | undefined {
213
- return this.findRoute(this.routes, schemaUid);
268
+ return this.findRoute(this.listAccessible(), schemaUid);
214
269
  }
215
270
 
216
271
  protected getAPIClient(): APIClient {
@@ -224,12 +279,59 @@ export class RouteRepository {
224
279
  return this.getAPIClient().resource(collectionName);
225
280
  }
226
281
 
227
- protected emitChange() {
228
- this.subscribers.forEach((subscriber) => {
229
- subscriber();
282
+ protected emitChange(layoutUid = this.getCurrentLayoutUid()) {
283
+ this.subscribers.forEach((subscriberLayoutUid, subscriber) => {
284
+ if (subscriberLayoutUid !== layoutUid) {
285
+ return;
286
+ }
287
+ const previousNotifiedLayoutUid = this.notifiedLayoutUid;
288
+ this.notifiedLayoutUid = layoutUid;
289
+ try {
290
+ subscriber();
291
+ } finally {
292
+ this.notifiedLayoutUid = previousNotifiedLayoutUid;
293
+ }
230
294
  });
231
295
  }
232
296
 
297
+ private getCurrentLayoutUid() {
298
+ return this.layoutActivations[this.layoutActivations.length - 1]?.uid || ADMIN_LAYOUT_MODEL_UID;
299
+ }
300
+
301
+ private normalizeLayoutUid(uid: unknown) {
302
+ return typeof uid === 'string' && uid.trim() ? uid : ADMIN_LAYOUT_MODEL_UID;
303
+ }
304
+
305
+ private getReadableLayoutUid() {
306
+ return this.notifiedLayoutUid || this.getCurrentLayoutUid();
307
+ }
308
+
309
+ private getRouteCache(layoutUid = this.getCurrentLayoutUid()) {
310
+ let cache = this.routeCaches.get(layoutUid);
311
+ if (!cache) {
312
+ cache = {
313
+ accessibleLoaded: false,
314
+ routes: [],
315
+ };
316
+ this.routeCaches.set(layoutUid, cache);
317
+ }
318
+ return cache;
319
+ }
320
+
321
+ private syncRoutesProperty() {
322
+ this.routes = this.getRouteCache().routes;
323
+ }
324
+
325
+ private nextRefreshRequestId(layoutUid: string) {
326
+ const requestId = this.getRefreshRequestId(layoutUid) + 1;
327
+ this.refreshRequestIds.set(layoutUid, requestId);
328
+ return requestId;
329
+ }
330
+
331
+ private getRefreshRequestId(layoutUid: string) {
332
+ return this.refreshRequestIds.get(layoutUid) || 0;
333
+ }
334
+
233
335
  private findRoute(routes: NocoBaseDesktopRoute[], schemaUid: string): NocoBaseDesktopRoute | undefined {
234
336
  for (const route of routes) {
235
337
  if (route.schemaUid === schemaUid) {
@@ -78,6 +78,19 @@ describe('PluginSettingsManager v2', () => {
78
78
  expect(app.router.get('admin.settings.demo.advanced')).toMatchObject({ path: 'advanced' });
79
79
  });
80
80
 
81
+ it('should resolve plugin settings links for plugin manager entries', () => {
82
+ const app = createMockClient();
83
+
84
+ app.pluginSettingsManager.addMenuItem({ key: 'shared', title: 'Shared' });
85
+ app.pluginSettingsManager.addPageTabItem({ menuKey: 'shared', key: 'target', title: 'Target' });
86
+ app.pluginSettingsManager.setPluginSettingsLink('demo-plugin', 'shared.target');
87
+
88
+ expect(app.pluginSettingsManager.has('demo-plugin')).toBe(false);
89
+ expect(app.pluginSettingsManager.hasPluginSettings('demo-plugin')).toBe(true);
90
+ expect(app.pluginSettingsManager.getPluginSettingsName('demo-plugin')).toBe('shared.target');
91
+ expect(app.pluginSettingsManager.getPluginSettingsRoutePath('demo-plugin')).toBe('/admin/settings/shared/target');
92
+ });
93
+
81
94
  it('should render string icon on both menu and page tab via renderIcon', () => {
82
95
  // Previously `renderPage` spread the raw `icon` string straight to the antd
83
96
  // Menu item, which displayed "LockOutlinedTitle" as text. Both `renderMenuItem`
@@ -0,0 +1,216 @@
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 { describe, expect, it, vi } from 'vitest';
11
+ import type { NocoBaseDesktopRoute } from '../flow-compat';
12
+ import { RouteRepository } from '../RouteRepository';
13
+
14
+ function createRouteRepository() {
15
+ const request = vi.fn().mockResolvedValue({
16
+ data: {
17
+ data: [],
18
+ },
19
+ });
20
+ const create = vi.fn().mockResolvedValue({
21
+ data: {
22
+ data: {},
23
+ },
24
+ });
25
+ const resource = vi.fn(() => ({
26
+ create,
27
+ }));
28
+ const repository = new RouteRepository({
29
+ api: {
30
+ request,
31
+ resource,
32
+ },
33
+ } as never);
34
+
35
+ return {
36
+ create,
37
+ repository,
38
+ request,
39
+ resource,
40
+ };
41
+ }
42
+
43
+ function route(schemaUid: string): NocoBaseDesktopRoute {
44
+ return { schemaUid } as NocoBaseDesktopRoute;
45
+ }
46
+
47
+ describe('RouteRepository', () => {
48
+ it('should pass the default admin layout when loading accessible routes', async () => {
49
+ const { repository, request } = createRouteRepository();
50
+
51
+ await repository.refreshAccessible();
52
+
53
+ expect(request).toHaveBeenCalledWith({
54
+ url: '/desktopRoutes:listAccessible',
55
+ params: {
56
+ tree: true,
57
+ sort: 'sort',
58
+ layout: 'admin-layout-model',
59
+ },
60
+ });
61
+ });
62
+
63
+ it('should pass the active client-v2 layout when loading accessible routes', async () => {
64
+ const { repository, request } = createRouteRepository();
65
+ const deactivateLayout = repository.activateLayout({
66
+ uid: 'custom-desktop-layout-model',
67
+ });
68
+
69
+ await repository.refreshAccessible();
70
+ deactivateLayout();
71
+
72
+ expect(request).toHaveBeenCalledWith({
73
+ url: '/desktopRoutes:listAccessible',
74
+ params: {
75
+ tree: true,
76
+ sort: 'sort',
77
+ layout: 'custom-desktop-layout-model',
78
+ },
79
+ });
80
+ });
81
+
82
+ it('should reload accessible routes when the active client-v2 layout changes', async () => {
83
+ const { repository, request } = createRouteRepository();
84
+ const deactivateFirstLayout = repository.activateLayout({
85
+ uid: 'first-layout-model',
86
+ });
87
+ await repository.ensureAccessibleLoaded();
88
+ deactivateFirstLayout();
89
+
90
+ const deactivateSecondLayout = repository.activateLayout({
91
+ uid: 'second-layout-model',
92
+ });
93
+ await repository.ensureAccessibleLoaded();
94
+ deactivateSecondLayout();
95
+
96
+ expect(request).toHaveBeenNthCalledWith(1, {
97
+ url: '/desktopRoutes:listAccessible',
98
+ params: {
99
+ tree: true,
100
+ sort: 'sort',
101
+ layout: 'first-layout-model',
102
+ },
103
+ });
104
+ expect(request).toHaveBeenNthCalledWith(2, {
105
+ url: '/desktopRoutes:listAccessible',
106
+ params: {
107
+ tree: true,
108
+ sort: 'sort',
109
+ layout: 'second-layout-model',
110
+ },
111
+ });
112
+ });
113
+
114
+ it('should report accessible routes as unloaded when the active layout changes', () => {
115
+ const { repository } = createRouteRepository();
116
+
117
+ repository.setRoutes([], 'first-layout-model');
118
+ const deactivateLayout = repository.activateLayout({
119
+ uid: 'second-layout-model',
120
+ });
121
+
122
+ expect(repository.isAccessibleLoaded()).toBe(false);
123
+ deactivateLayout();
124
+ });
125
+
126
+ it('should keep accessible route caches isolated by active layout', () => {
127
+ const { repository } = createRouteRepository();
128
+
129
+ repository.setRoutes([route('admin-page')], 'admin-layout-model');
130
+ const deactivateLayout = repository.activateLayout({
131
+ uid: 'custom-desktop-layout-model',
132
+ });
133
+ repository.setRoutes([route('custom-page')], 'custom-desktop-layout-model');
134
+
135
+ expect(repository.listAccessible().map((route) => route.schemaUid)).toEqual(['custom-page']);
136
+ expect(repository.getRouteBySchemaUid('admin-page')).toBeUndefined();
137
+ deactivateLayout();
138
+
139
+ expect(repository.listAccessible().map((route) => route.schemaUid)).toEqual(['admin-page']);
140
+ expect(repository.getRouteBySchemaUid('custom-page')).toBeUndefined();
141
+ });
142
+
143
+ it('should notify only subscribers for the changed layout cache', () => {
144
+ const { repository } = createRouteRepository();
145
+ const adminEvents: string[][] = [];
146
+ const customEvents: string[][] = [];
147
+
148
+ const unsubscribeAdmin = repository.subscribe(() => {
149
+ adminEvents.push(repository.listAccessible().map((route) => route.schemaUid || ''));
150
+ });
151
+ const deactivateLayout = repository.activateLayout({
152
+ uid: 'custom-desktop-layout-model',
153
+ });
154
+ const unsubscribeCustom = repository.subscribe(() => {
155
+ customEvents.push(repository.listAccessible().map((route) => route.schemaUid || ''));
156
+ });
157
+
158
+ repository.setRoutes([route('custom-page')], 'custom-desktop-layout-model');
159
+
160
+ expect(adminEvents).toEqual([]);
161
+ expect(customEvents).toEqual([['custom-page']]);
162
+
163
+ deactivateLayout();
164
+ repository.setRoutes([route('admin-page')], 'admin-layout-model');
165
+
166
+ expect(adminEvents).toEqual([['admin-page']]);
167
+ expect(customEvents).toEqual([['custom-page']]);
168
+
169
+ unsubscribeAdmin();
170
+ unsubscribeCustom();
171
+ });
172
+
173
+ it('should pass the default admin layout when creating a route', async () => {
174
+ const { repository, create } = createRouteRepository();
175
+
176
+ await repository.createRoute(
177
+ {
178
+ title: 'Admin page',
179
+ },
180
+ {
181
+ refreshAfterMutation: false,
182
+ },
183
+ );
184
+
185
+ expect(create).toHaveBeenCalledWith({
186
+ values: {
187
+ title: 'Admin page',
188
+ },
189
+ layout: 'admin-layout-model',
190
+ });
191
+ });
192
+
193
+ it('should pass the active client-v2 layout when creating a route', async () => {
194
+ const { repository, create } = createRouteRepository();
195
+ const deactivateLayout = repository.activateLayout({
196
+ uid: 'custom-desktop-layout-model',
197
+ });
198
+
199
+ await repository.createRoute(
200
+ {
201
+ title: 'Custom page',
202
+ },
203
+ {
204
+ refreshAfterMutation: false,
205
+ },
206
+ );
207
+ deactivateLayout();
208
+
209
+ expect(create).toHaveBeenCalledWith({
210
+ values: {
211
+ title: 'Custom page',
212
+ },
213
+ layout: 'custom-desktop-layout-model',
214
+ });
215
+ });
216
+ });
@@ -15,6 +15,8 @@ import { fileURLToPath } from 'node:url';
15
15
  type BrowserCheckerCase = {
16
16
  pathname: string;
17
17
  publicPath: string;
18
+ modernClientPrefix?: string;
19
+ appClientEntryMode?: string;
18
20
  expectedRedirect?: string;
19
21
  };
20
22
 
@@ -40,6 +42,8 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
40
42
  showLog: false,
41
43
  window: {
42
44
  __nocobase_public_path__: input.publicPath,
45
+ __nocobase_modern_client_prefix__: input.modernClientPrefix,
46
+ __nocobase_app_client_entry_mode__: input.appClientEntryMode,
43
47
  location: {
44
48
  origin: 'http://c.local.nocobase.com',
45
49
  pathname: input.pathname,
@@ -109,4 +113,61 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
109
113
 
110
114
  expect(replace).not.toHaveBeenCalled();
111
115
  });
116
+
117
+ if (scriptPath.includes('/app/client/public/')) {
118
+ it('redirects app root to modern entry for modern-default', () => {
119
+ const replace = executeBrowserChecker(scriptPath, {
120
+ pathname: '/',
121
+ publicPath: '/',
122
+ modernClientPrefix: 'v',
123
+ appClientEntryMode: 'modern-default',
124
+ });
125
+
126
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/v/');
127
+ });
128
+
129
+ it('does not redirect legacy deep links for modern-default', () => {
130
+ const replace = executeBrowserChecker(scriptPath, {
131
+ pathname: '/admin',
132
+ publicPath: '/',
133
+ modernClientPrefix: 'v',
134
+ appClientEntryMode: 'modern-default',
135
+ });
136
+
137
+ expect(replace).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it('rewrites legacy document paths for modern-only', () => {
141
+ const replace = executeBrowserChecker(scriptPath, {
142
+ pathname: '/admin/settings/workflow',
143
+ publicPath: '/',
144
+ modernClientPrefix: 'v',
145
+ appClientEntryMode: 'modern-only',
146
+ });
147
+
148
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/v/admin/settings/workflow');
149
+ });
150
+
151
+ it('redirects sub-path site root directly to final modern target', () => {
152
+ const replace = executeBrowserChecker(scriptPath, {
153
+ pathname: '/nocobase/',
154
+ publicPath: '/nocobase/',
155
+ modernClientPrefix: 'v',
156
+ appClientEntryMode: 'modern-default',
157
+ });
158
+
159
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/');
160
+ });
161
+
162
+ it('rewrites sub-app legacy deep links for modern-only without collapsing the sub-app segment', () => {
163
+ const replace = executeBrowserChecker(scriptPath, {
164
+ pathname: '/nocobase/apps/a_31itq60q4kg/admin/',
165
+ publicPath: '/nocobase/',
166
+ modernClientPrefix: 'v',
167
+ appClientEntryMode: 'modern-only',
168
+ });
169
+
170
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/apps/a_31itq60q4kg/admin');
171
+ });
172
+ }
112
173
  });
@@ -0,0 +1,16 @@
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 { IconPicker } from '..';
11
+
12
+ describe('@nocobase/client-v2 exports', () => {
13
+ it('should export IconPicker from the root entry', () => {
14
+ expect(IconPicker).toBeDefined();
15
+ });
16
+ });