@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
@@ -0,0 +1,105 @@
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 { useCallback, useMemo } from 'react';
11
+ import { useLocation } from 'react-router-dom';
12
+ import { useACLSnippets } from '../acl';
13
+ import { useApp } from '../hooks/useApp';
14
+ import type { PluginSettingsPageType } from '../PluginSettingsManager';
15
+ import { getSettingsGroupKey, groupTopLevelSettings, type SettingsGroupKey } from './groups';
16
+ import {
17
+ createSettingsPathMap,
18
+ filterRenderableSettings,
19
+ filterVisibleSettings,
20
+ findFirstInternalSettingsPage,
21
+ matchSettingsRoute,
22
+ PLUGIN_MANAGER_SETTING_NAME,
23
+ sortTopLevelSettings,
24
+ } from './utils';
25
+
26
+ /**
27
+ * 设置中心分组导航的共享状态。
28
+ *
29
+ * 顶栏一级导航和左侧栏二三级菜单都从这里取数据,保证两边永远一致:
30
+ * 同一份可见配置树、同一套分组规则、同一个「当前分组」判定。
31
+ */
32
+ export function useSettingsGroups() {
33
+ const app = useApp();
34
+ const location = useLocation();
35
+ const snippets = useACLSnippets();
36
+
37
+ const allSettings = useMemo(
38
+ () => filterRenderableSettings(app.pluginSettingsManager.getList(false) as PluginSettingsPageType[]),
39
+ [app.pluginSettingsManager],
40
+ );
41
+
42
+ const visibleSettings = useMemo(() => {
43
+ const list = filterVisibleSettings(
44
+ filterRenderableSettings(app.pluginSettingsManager.getList(true) as PluginSettingsPageType[]),
45
+ );
46
+ return sortTopLevelSettings(
47
+ list.filter((item) => item.name !== PLUGIN_MANAGER_SETTING_NAME || snippets.includes('pm')),
48
+ );
49
+ }, [app.pluginSettingsManager, snippets]);
50
+
51
+ const groups = useMemo(() => groupTopLevelSettings(visibleSettings), [visibleSettings]);
52
+
53
+ const registeredSettingsMapByPath = useMemo(() => createSettingsPathMap(allSettings), [allSettings]);
54
+
55
+ const currentSetting = useMemo(
56
+ () => matchSettingsRoute(registeredSettingsMapByPath, location.pathname),
57
+ [location.pathname, registeredSettingsMapByPath],
58
+ );
59
+
60
+ const currentTopLevelSetting = useMemo(() => {
61
+ if (!currentSetting) {
62
+ return null;
63
+ }
64
+ return allSettings.find((item) => item.name === currentSetting.topLevelName) || currentSetting;
65
+ }, [allSettings, currentSetting]);
66
+
67
+ const activeGroupKey = useMemo<SettingsGroupKey | null>(() => {
68
+ if (!currentTopLevelSetting) {
69
+ return groups[0]?.key ?? null;
70
+ }
71
+ const key = getSettingsGroupKey(currentTopLevelSetting);
72
+ return groups.some((group) => group.key === key) ? key : groups[0]?.key ?? null;
73
+ }, [currentTopLevelSetting, groups]);
74
+
75
+ const activeGroup = useMemo(() => groups.find((group) => group.key === activeGroupKey), [activeGroupKey, groups]);
76
+ const activeGroupSettings = activeGroup?.settings ?? [];
77
+
78
+ /**
79
+ * 计算某个分组被点击后的落点。
80
+ *
81
+ * @param {string} groupKey 分组 key
82
+ * @returns {string | undefined} 该分组内第一个可内部打开的页面路径
83
+ */
84
+ const getGroupEntryPath = useCallback(
85
+ (groupKey: string) => {
86
+ const group = groups.find((item) => item.key === groupKey);
87
+ if (!group) {
88
+ return undefined;
89
+ }
90
+ return findFirstInternalSettingsPage(group.settings)?.path;
91
+ },
92
+ [groups],
93
+ );
94
+
95
+ return {
96
+ activeGroupKey,
97
+ activeGroupSettings,
98
+ allSettings,
99
+ currentSetting,
100
+ currentTopLevelSetting,
101
+ getGroupEntryPath,
102
+ groups,
103
+ visibleSettings,
104
+ };
105
+ }
@@ -0,0 +1,173 @@
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 { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
11
+ import { useTranslation } from 'react-i18next';
12
+ import type { PluginSettingsPageType } from '../PluginSettingsManager';
13
+ import { useSettingsGroups } from './useSettingsGroups';
14
+ import { hasOwnSettingsPage } from './utils';
15
+
16
+ const RECENT_STORAGE_KEY = 'nb-settings-recent';
17
+ const RECENT_MAX = 5;
18
+
19
+ export type SettingsSearchItem = {
20
+ /** 配置项名称,同时用作最近访问的存储标识 */
21
+ name: string;
22
+ title: string;
23
+ /** 分组名 + 上级菜单名,用于在结果里给出上下文 */
24
+ breadcrumb: string;
25
+ path: string;
26
+ /** 外链配置项跳转到站外 */
27
+ link?: string;
28
+ icon?: ReactNode;
29
+ /** 预先拼好并转小写的匹配文本 */
30
+ searchText: string;
31
+ };
32
+
33
+ /**
34
+ * 读取最近访问过的配置项名称。
35
+ *
36
+ * 存的是名称而不是整个配置项:插件卸载、改名、权限收回之后,解析不到的记录会自然消失。
37
+ *
38
+ * @returns {string[]} 由近到远的配置项名称
39
+ */
40
+ function readRecentNames(): string[] {
41
+ try {
42
+ const raw = localStorage.getItem(RECENT_STORAGE_KEY);
43
+ const parsed = raw ? JSON.parse(raw) : null;
44
+ if (!Array.isArray(parsed)) {
45
+ return [];
46
+ }
47
+ // 存量数据可能是别的版本写的,统一在读取处去重并截断,别让脏数据一直传下去。
48
+ return [...new Set(parsed.filter((item): item is string => typeof item === 'string' && !!item))].slice(
49
+ 0,
50
+ RECENT_MAX,
51
+ );
52
+ } catch (error) {
53
+ return [];
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 把一次访问记到最近列表最前面。
59
+ *
60
+ * @param {string} name 配置项名称
61
+ * @returns {string[]} 写入后的名称列表
62
+ */
63
+ function writeRecentName(name: string): string[] {
64
+ const next = [name, ...readRecentNames().filter((item) => item !== name)].slice(0, RECENT_MAX);
65
+
66
+ try {
67
+ localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(next));
68
+ } catch (error) {
69
+ // 隐私模式下 localStorage 会抛异常,最近访问是锦上添花,静默降级即可。
70
+ }
71
+
72
+ return next;
73
+ }
74
+
75
+ /**
76
+ * 取配置项标题的纯文本形式。
77
+ *
78
+ * `title` 类型上是 ReactNode,实际都是 i18n 出来的字符串;拿不到字符串时退回名称。
79
+ *
80
+ * @param {PluginSettingsPageType} setting 配置项
81
+ * @returns {string} 可用于展示和匹配的标题
82
+ */
83
+ function getSettingTitleText(setting: PluginSettingsPageType): string {
84
+ return typeof setting.title === 'string' ? setting.title : setting.name;
85
+ }
86
+
87
+ /**
88
+ * 设置中心的搜索与最近访问。
89
+ *
90
+ * 一级分组只剩五个、二三级收进左侧栏之后,深层页面变得不好够;这里把整棵可见配置树拍平成
91
+ * 一份可搜索的清单,并记住最近去过的几个,作为搜索框的空态。
92
+ *
93
+ * @returns 搜索清单、匹配函数、最近访问项,以及记录访问的回调
94
+ */
95
+ export function useSettingsSearch() {
96
+ const { t } = useTranslation();
97
+ const { groups, currentSetting } = useSettingsGroups();
98
+ const [recentNames, setRecentNames] = useState<string[]>(() => readRecentNames());
99
+
100
+ const items = useMemo<SettingsSearchItem[]>(() => {
101
+ const result: SettingsSearchItem[] = [];
102
+
103
+ const walk = (setting: PluginSettingsPageType, groupTitle: string, ancestorTitles: string[]) => {
104
+ const title = getSettingTitleText(setting);
105
+ const selfHasPage = hasOwnSettingsPage(setting);
106
+
107
+ if (selfHasPage) {
108
+ // 上级里跟自己同名的那一层(典型是承载默认内容的 index 子页)没有信息量,去掉。
109
+ const breadcrumb = [groupTitle, ...ancestorTitles.filter((item) => item !== title)].join(' / ');
110
+
111
+ result.push({
112
+ name: setting.name,
113
+ title,
114
+ breadcrumb,
115
+ path: setting.path,
116
+ link: setting.link,
117
+ icon: setting.icon,
118
+ searchText: [title, breadcrumb, setting.name].join(' ').toLowerCase(),
119
+ });
120
+ }
121
+
122
+ const childAncestors = [...ancestorTitles, title];
123
+ setting.children?.forEach((child) => {
124
+ const childSetting = child as PluginSettingsPageType;
125
+
126
+ // 插件常用一个与菜单同名的 index 子页承载默认内容(`addPageTabItem({ key: 'index' })`),
127
+ // 它和菜单本身指向同一件事,只在菜单没有自己的页面时才需要作为入口出现。
128
+ if (selfHasPage && getSettingTitleText(childSetting) === title) {
129
+ return;
130
+ }
131
+
132
+ walk(childSetting, groupTitle, childAncestors);
133
+ });
134
+ };
135
+
136
+ groups.forEach((group) => {
137
+ const groupTitle = t(group.title);
138
+ group.settings.forEach((setting) => walk(setting, groupTitle, []));
139
+ });
140
+
141
+ return result;
142
+ }, [groups, t]);
143
+
144
+ const search = useCallback(
145
+ (keyword: string) => {
146
+ const normalized = keyword.trim().toLowerCase();
147
+
148
+ if (!normalized) {
149
+ return [];
150
+ }
151
+
152
+ // 空格分词后全部命中才算匹配,方便用「用户 角色」这种写法逐步收窄。
153
+ const terms = normalized.split(/\s+/);
154
+ return items.filter((item) => terms.every((term) => item.searchText.includes(term)));
155
+ },
156
+ [items],
157
+ );
158
+
159
+ const recentItems = useMemo(
160
+ () => recentNames.map((name) => items.find((item) => item.name === name)).filter(Boolean) as SettingsSearchItem[],
161
+ [items, recentNames],
162
+ );
163
+
164
+ // 当前停留的页面即为一次访问;由所在页面自身触发,避免每个入口都要记一次。
165
+ useEffect(() => {
166
+ if (!currentSetting?.name) {
167
+ return;
168
+ }
169
+ setRecentNames(writeRecentName(currentSetting.name));
170
+ }, [currentSetting?.name]);
171
+
172
+ return { items, recentItems, search };
173
+ }
@@ -7,12 +7,16 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ import type { MenuProps } from 'antd';
10
11
  import { Outlet } from 'react-router-dom';
11
12
  import type { PluginSettingsPageType } from '../PluginSettingsManager';
12
13
 
14
+ export type SidebarMenuItem = NonNullable<MenuProps['items']>[number];
15
+
13
16
  export const ADMIN_SETTINGS_LAYOUT_MODEL_UID = 'admin-settings-layout-model';
14
17
  export const PLUGIN_MANAGER_SETTING_NAME = 'plugin-manager';
15
18
  export const SYSTEM_SETTINGS_SETTING_NAME = 'system-settings';
19
+ export const MULTI_PORTAL_SETTING_NAME = 'multi-portal';
16
20
 
17
21
  /**
18
22
  * 判断配置项是否拥有可直接渲染的页面。
@@ -218,7 +222,8 @@ export function findSettingsByName(
218
222
  * @returns {string | undefined} 默认跳转路径
219
223
  */
220
224
  export function getDefaultSettingsPath(settings: readonly PluginSettingsPageType[] = []) {
221
- const preferredNames = [SYSTEM_SETTINGS_SETTING_NAME, PLUGIN_MANAGER_SETTING_NAME];
225
+ // Portal 中心是设置中心的门面:登录后先落在应用入口,再往下走系统配置。
226
+ const preferredNames = [MULTI_PORTAL_SETTING_NAME, SYSTEM_SETTINGS_SETTING_NAME, PLUGIN_MANAGER_SETTING_NAME];
222
227
 
223
228
  for (const name of preferredNames) {
224
229
  const preferred = findSettingsByName(settings, name);
@@ -239,6 +244,50 @@ export function getDefaultSettingsPath(settings: readonly PluginSettingsPageType
239
244
  return findFirstInternalSettingsPage(settings)?.path;
240
245
  }
241
246
 
247
+ /**
248
+ * 构造左侧栏菜单。
249
+ *
250
+ * 左栏只铺**一级**配置项,不下钻子菜单:备份管理器、通知管理这类插件的子页面统一走页头下的
251
+ * Tab(和 v1 设置中心一致),左栏折叠展开再叠一层反而多一次点击、也和 Tab 表达重复。
252
+ *
253
+ * @param {PluginSettingsPageType[]} settings 某个分组下的配置项
254
+ * @returns {SidebarMenuItem[]} antd Menu items
255
+ */
256
+ export function getSidebarMenuItems(settings: readonly PluginSettingsPageType[] = []): SidebarMenuItem[] {
257
+ return settings
258
+ .filter((item) => !item.hidden)
259
+ .map((item) => ({
260
+ key: item.name,
261
+ label: item.label ?? item.title,
262
+ title: typeof item.title === 'string' ? item.title : undefined,
263
+ icon: item.icon,
264
+ }));
265
+ }
266
+
267
+ /**
268
+ * 把当前命中的配置项换算成左侧栏里真实存在的菜单 key。
269
+ *
270
+ * 左栏只有一级,命中的可能是某个子页面(它在左栏没有自己的条目),
271
+ * 这里回退到它所属的那个一级项。
272
+ *
273
+ * @param {PluginSettingsPageType[]} settings 某个分组下的配置项
274
+ * @param {string | undefined} activeName 当前命中的配置项名称
275
+ * @returns {string | undefined} 左栏应选中的 key
276
+ */
277
+ export function getSidebarSelectedKey(
278
+ settings: readonly PluginSettingsPageType[] = [],
279
+ activeName?: string,
280
+ ): string | undefined {
281
+ if (!activeName) {
282
+ return undefined;
283
+ }
284
+
285
+ const contains = (items: readonly PluginSettingsPageType[]): boolean =>
286
+ items.some((item) => item.name === activeName || contains(item.children || []));
287
+
288
+ return settings.find((item) => item.name === activeName || contains(item.children || []))?.name;
289
+ }
290
+
242
291
  /**
243
292
  * 把 settings 结构转换成 antd Menu items。
244
293
  *
@@ -101,5 +101,6 @@ export const GlobalThemeProvider: FC<GlobalThemeProviderProps> = ({ children, th
101
101
 
102
102
  export { default as compatOldTheme } from './compatOldTheme';
103
103
  export { addCustomAlgorithmToTheme, customAlgorithm } from './customAlgorithm';
104
+ export { getItemActiveColors } from './itemActive';
104
105
  export * from './type';
105
106
  export { defaultTheme };
@@ -0,0 +1,31 @@
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 type { GlobalToken } from 'antd';
11
+ import type { CustomToken } from './type';
12
+
13
+ /**
14
+ * 取「选中项」的底色与文字色。
15
+ *
16
+ * antd 只有 `controlItemBgActive` 一个背景 token,没有配对的文字色,页面各自写背景、
17
+ * 文字色照旧,底色一被主题改深就成了深底深字。这里统一读 `colorBgItemActive` /
18
+ * `colorTextItemActive` 这一对自定义 token,没设置时回落到 antd 的默认表现,
19
+ * 于是同一段代码在默认主题和设置中心的黑白主题下都对。
20
+ *
21
+ * @param {GlobalToken} token antd token
22
+ * @returns {{ bg: string; color: string | undefined }} 选中项底色,以及需要显式指定的文字色(回落时为 undefined)
23
+ */
24
+ export function getItemActiveColors(token: GlobalToken) {
25
+ const customToken = token as Partial<CustomToken>;
26
+
27
+ return {
28
+ bg: customToken.colorBgItemActive || token.controlItemBgActive,
29
+ color: customToken.colorTextItemActive || undefined,
30
+ };
31
+ }
package/src/theme/type.ts CHANGED
@@ -26,6 +26,16 @@ export interface CustomToken extends AliasToken {
26
26
  colorBgSiderMenuHover: string;
27
27
  colorBgSiderMenuActive: string;
28
28
 
29
+ /**
30
+ * 「选中项」的底色与压在其上的文字色。
31
+ *
32
+ * antd 只给了 `controlItemBgActive` 一个背景 token,没有配对的文字色,于是各页面
33
+ * 自己写背景、文字色照旧,底色一变深就成了深底深字。这一对补齐了那半边:
34
+ * 列表行、树节点、卡片这类自绘选中态统一读它,未设置时回落到 antd 的默认表现。
35
+ */
36
+ colorBgItemActive: string;
37
+ colorTextItemActive: string;
38
+
29
39
  colorSettings: string;
30
40
  colorBgSettingsHover: string;
31
41
  colorTemplateBgSettingsHover: string;
@@ -0,0 +1,185 @@
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
+ export type RouteRuntimeVersion = 'legacy' | 'modern';
11
+
12
+ declare global {
13
+ interface Window {
14
+ __nocobase_modern_client_prefix__?: string;
15
+ __nocobase_public_path__?: string;
16
+ }
17
+ }
18
+
19
+ function hasModernClientPrefix() {
20
+ return (
21
+ typeof window !== 'undefined' &&
22
+ typeof window.__nocobase_modern_client_prefix__ === 'string' &&
23
+ window.__nocobase_modern_client_prefix__.trim().length > 0
24
+ );
25
+ }
26
+
27
+ function hasConfiguredPublicPath() {
28
+ return typeof window !== 'undefined' && typeof window.__nocobase_public_path__ === 'string';
29
+ }
30
+
31
+ /** Normalize any app/public-path-like input into a root-relative pathname:
32
+ *
33
+ * - `nocobase//v/` -> `/nocobase/v`
34
+ * - `/` -> `/`
35
+ */
36
+ function normalizeRootRelativePath(value = '/') {
37
+ const normalized = `/${String(value || '/').trim() || '/'}`.replace(/\/{2,}/g, '/');
38
+ if (normalized !== '/' && normalized.endsWith('/')) {
39
+ return normalized.replace(/\/+$/g, '');
40
+ }
41
+ return normalized;
42
+ }
43
+
44
+ /** Public-path form always ends with `/`, so it can be compared as a base path:
45
+ *
46
+ * - `/nocobase/v` -> `/nocobase/v/`
47
+ * - `/` -> `/`
48
+ */
49
+ function normalizePublicPath(value = '/') {
50
+ const normalized = normalizeRootRelativePath(value);
51
+ return normalized.endsWith('/') ? normalized : `${normalized}/`;
52
+ }
53
+
54
+ /** Check whether a concrete pathname is rendered under a runtime base path:
55
+ *
56
+ * - pathname `/nocobase/v/admin/workflow/workflows/1`
57
+ * basePath `/nocobase/v/`
58
+ * => true
59
+ *
60
+ * - pathname `/nocobase/admin/settings/workflow/workflows/1`
61
+ * basePath `/nocobase/v/`
62
+ * => false
63
+ */
64
+ function isPathnameInsideBasePath(pathname: string, basePath: string) {
65
+ const normalizedPathname = normalizeRootRelativePath(pathname);
66
+ const normalizedBasePath = normalizePublicPath(basePath);
67
+ const trimmedBasePath = normalizedBasePath.replace(/\/+$/g, '') || '/';
68
+
69
+ if (trimmedBasePath === '/') {
70
+ return true;
71
+ }
72
+
73
+ return normalizedPathname === trimmedBasePath || normalizedPathname.startsWith(`${trimmedBasePath}/`);
74
+ }
75
+
76
+ function getNormalizedModernClientPrefix() {
77
+ if (typeof window === 'undefined') {
78
+ return '';
79
+ }
80
+
81
+ const prefix = window.__nocobase_modern_client_prefix__?.trim();
82
+ return prefix ? prefix.replace(/^\/+|\/+$/g, '') : '';
83
+ }
84
+
85
+ /** Derive the actual modern-client base path from injected globals.
86
+ *
87
+ * Only returns a value when `__nocobase_public_path__` itself already points to
88
+ * the modern client shell:
89
+ *
90
+ * - legacy shell:
91
+ * `__nocobase_public_path__ = "/nocobase/"`
92
+ * `__nocobase_modern_client_prefix__ = "v"`
93
+ * => `""`
94
+ *
95
+ * - modern shell:
96
+ * `__nocobase_public_path__ = "/nocobase/v/"`
97
+ * `__nocobase_modern_client_prefix__ = "v"`
98
+ * => `"/nocobase/v/"`
99
+ */
100
+ function getModernClientBasePath() {
101
+ const normalizedPrefix = getNormalizedModernClientPrefix();
102
+ if (!normalizedPrefix) {
103
+ return '';
104
+ }
105
+
106
+ if (typeof window === 'undefined') {
107
+ return '';
108
+ }
109
+
110
+ const publicPath = window.__nocobase_public_path__?.trim();
111
+ if (publicPath && normalizePublicPath(publicPath).endsWith(`/${normalizedPrefix}/`)) {
112
+ return normalizePublicPath(publicPath);
113
+ }
114
+
115
+ return '';
116
+ }
117
+
118
+ /** True only when the current page is actually running under the resolved
119
+ * modern-client base path.
120
+ *
121
+ * Example:
122
+ * - pathname `/v/admin/workflow/workflows/1`, modern base `/v/` => true
123
+ * - pathname `/admin/settings/workflow/workflows/1`, modern base `/v/` => false
124
+ */
125
+ function isPathnameInModernClientRuntime() {
126
+ if (typeof window === 'undefined') {
127
+ return false;
128
+ }
129
+
130
+ const pathname = window.location?.pathname ?? '';
131
+ const modernClientBasePath = getModernClientBasePath();
132
+ if (!pathname || !modernClientBasePath) {
133
+ return false;
134
+ }
135
+
136
+ return isPathnameInsideBasePath(pathname, modernClientBasePath);
137
+ }
138
+
139
+ /** Fallback for incomplete/mocked environments where public path was not
140
+ * injected but the modern prefix still exists.
141
+ *
142
+ * Example:
143
+ * - pathname `/v/admin/workflow/workflows/1`
144
+ * `__nocobase_modern_client_prefix__ = "v"`
145
+ * `__nocobase_public_path__` missing
146
+ * => true
147
+ */
148
+ function isPathnameInModernClientRuntimeByPrefix() {
149
+ if (typeof window === 'undefined') {
150
+ return false;
151
+ }
152
+
153
+ const normalizedPrefix = getNormalizedModernClientPrefix();
154
+ const pathname = window.location?.pathname ?? '';
155
+ if (!normalizedPrefix || !pathname) {
156
+ return false;
157
+ }
158
+
159
+ return isPathnameInsideBasePath(pathname, normalizePublicPath(normalizedPrefix));
160
+ }
161
+
162
+ /** Resolve which client shell currently owns the route.
163
+ *
164
+ * - returns `modern` when the current page is actually mounted under the modern
165
+ * client base path (`/v/`, `/nocobase/v/`, ...)
166
+ * - returns `legacy` when the route is still under the legacy shell, even if the
167
+ * modern prefix global has been injected into the page
168
+ */
169
+ export function getRouteRuntimeVersion(): RouteRuntimeVersion {
170
+ if (typeof window === 'undefined') {
171
+ return hasModernClientPrefix() ? 'modern' : 'legacy';
172
+ }
173
+
174
+ if (hasConfiguredPublicPath()) {
175
+ return isPathnameInModernClientRuntime() ? 'modern' : 'legacy';
176
+ }
177
+
178
+ // Defensive fallback for incomplete test/mocked environments where public
179
+ // path was not injected but the modern prefix still exists.
180
+ if (isPathnameInModernClientRuntimeByPrefix()) {
181
+ return 'modern';
182
+ }
183
+
184
+ return 'legacy';
185
+ }
@@ -11,6 +11,7 @@ import React, { ComponentType, FC } from 'react';
11
11
  import { BlankComponent } from '../components';
12
12
 
13
13
  export * from './appVersionHTML';
14
+ export * from './getRouteRuntimeVersion';
14
15
 
15
16
  export function normalizeContainer(container: Element | ShadowRoot | string): Element | null {
16
17
  if (!container) {