@nocobase/client-v2 2.3.0-alpha.1 → 3.0.0-alpha.2

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 (123) hide show
  1. package/es/acl/ACLProvider.d.ts +10 -0
  2. package/es/authRedirect.d.ts +5 -0
  3. package/es/components/AttachmentUpload.d.ts +51 -0
  4. package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
  5. package/es/components/index.d.ts +1 -0
  6. package/es/flow/actions/linkageRules.d.ts +1 -0
  7. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  8. package/es/flow/components/FlowRoute.d.ts +2 -2
  9. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  10. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  11. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  12. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  13. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  14. package/es/index.d.ts +4 -1
  15. package/es/index.mjs +139 -125
  16. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  17. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  18. package/es/settings-app/SettingsApplication.d.ts +18 -0
  19. package/es/settings-app/SettingsBrand.d.ts +16 -0
  20. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  21. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  22. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  23. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  24. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  25. package/es/settings-app/SettingsSearch.d.ts +17 -0
  26. package/es/settings-app/SettingsShell.d.ts +10 -0
  27. package/es/settings-app/isSettingsApp.d.ts +18 -0
  28. package/es/settings-app/runtimePaths.d.ts +18 -0
  29. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  30. package/es/settings-app/settingsTheme.d.ts +87 -0
  31. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  32. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  33. package/es/settings-center/groups.d.ts +66 -0
  34. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  35. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  36. package/es/settings-center/utils.d.ts +24 -0
  37. package/es/theme/index.d.ts +1 -0
  38. package/es/theme/itemActive.d.ts +24 -0
  39. package/es/theme/type.d.ts +9 -0
  40. package/lib/index.js +141 -127
  41. package/lib/locale/languageCodes.js +1 -0
  42. package/package.json +8 -7
  43. package/src/__tests__/authRedirect.test.ts +137 -0
  44. package/src/__tests__/browserChecker.test.ts +124 -6
  45. package/src/__tests__/mockSettingsApplication.ts +29 -0
  46. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +51 -10
  47. package/src/__tests__/plugin-manager.test.tsx +7 -6
  48. package/src/__tests__/settings-application.test.ts +164 -0
  49. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  50. package/src/__tests__/settings-center.test.tsx +38 -37
  51. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  52. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  53. package/src/__tests__/settings-shell.test.tsx +200 -0
  54. package/src/acl/ACLProvider.tsx +21 -0
  55. package/src/authRedirect.ts +72 -3
  56. package/src/components/AttachmentUpload.tsx +275 -0
  57. package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
  58. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  59. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
  60. package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
  61. package/src/components/index.ts +1 -0
  62. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  63. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  64. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  65. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  66. package/src/flow/actions/linkageRules.tsx +26 -6
  67. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
  68. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  69. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  70. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  71. package/src/flow/common/Markdown/style.ts +4 -0
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/FlowRoute.tsx +71 -29
  74. package/src/flow/index.ts +2 -0
  75. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  76. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  77. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  78. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  79. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  80. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  81. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  82. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  83. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  84. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  85. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  86. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  87. package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
  88. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  89. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  90. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  91. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  92. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  93. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  94. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  95. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  96. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  97. package/src/index.ts +10 -1
  98. package/src/locale/languageCodes.ts +1 -0
  99. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  100. package/src/nocobase-buildin-plugin/index.tsx +99 -71
  101. package/src/settings-app/SettingsApplication.ts +31 -0
  102. package/src/settings-app/SettingsBrand.tsx +108 -0
  103. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  104. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  105. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  106. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  107. package/src/settings-app/SettingsRouterManager.ts +92 -0
  108. package/src/settings-app/SettingsSearch.tsx +238 -0
  109. package/src/settings-app/SettingsShell.tsx +167 -0
  110. package/src/settings-app/isSettingsApp.ts +21 -0
  111. package/src/settings-app/runtimePaths.ts +104 -0
  112. package/src/settings-app/settingsDocumentPath.ts +66 -0
  113. package/src/settings-app/settingsTheme.ts +301 -0
  114. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  115. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  116. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  117. package/src/settings-center/groups.ts +108 -0
  118. package/src/settings-center/useSettingsGroups.ts +105 -0
  119. package/src/settings-center/useSettingsSearch.ts +173 -0
  120. package/src/settings-center/utils.tsx +50 -1
  121. package/src/theme/index.tsx +1 -0
  122. package/src/theme/itemActive.ts +31 -0
  123. package/src/theme/type.ts +10 -0
@@ -0,0 +1,108 @@
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 { PluginSettingsPageType } from '../PluginSettingsManager';
11
+
12
+ /**
13
+ * Settings 顶层分组。
14
+ *
15
+ * 顶栏铺一级(分组),左侧栏铺该分组下的二级 / 三级菜单。
16
+ * 分组归属优先取配置项自身的 `group` 字段,其次落到内置映射表,最后兜底到 `system`,
17
+ * 因此第三方插件不改代码也能出现在设置中心里。
18
+ */
19
+ export type SettingsGroupKey = 'applications' | 'plugins' | 'data' | 'access' | 'automation' | 'ai' | 'system';
20
+
21
+ export type SettingsGroupDefinition = {
22
+ key: SettingsGroupKey;
23
+ /** 未翻译的分组标题,交由调用方过 i18n */
24
+ title: string;
25
+ /**
26
+ * 分组的门面配置项。
27
+ *
28
+ * 它会被排到该分组的第一位,并作为点击顶栏分组时的落点;
29
+ * 不存在(插件没装 / 无权限)时自然退回到分组内的第一项。
30
+ */
31
+ primary?: string;
32
+ };
33
+
34
+ /**
35
+ * 顶栏铺六个独立入口 + 一个「其他设置」兜底分组。
36
+ *
37
+ * 前六个各自只有一个配置项、且都是日常高频,直接当一级入口(标题取配置项自己的名字,
38
+ * 顺带继承插件的翻译);其余零散配置全部收进其他设置,靠左栏和悬浮下拉展开。
39
+ *
40
+ * 兜底分组不叫「系统设置」:它下面本来就有一个同名的配置项(站点标题、logo 那个),
41
+ * 分组和子项重名会让人以为点进去是同一个东西。
42
+ */
43
+ export const SETTINGS_GROUPS: SettingsGroupDefinition[] = [
44
+ { key: 'applications', title: 'Applications', primary: 'multi-portal' },
45
+ { key: 'plugins', title: 'Plugin manager', primary: 'plugin-manager' },
46
+ { key: 'data', title: 'Data sources', primary: 'data-source-manager' },
47
+ { key: 'access', title: 'Users & Permissions', primary: 'users-permissions' },
48
+ { key: 'automation', title: 'Workflow', primary: 'workflow' },
49
+ { key: 'ai', title: 'AI employees', primary: 'ai' },
50
+ { key: 'system', title: 'Other settings' },
51
+ ];
52
+
53
+ export const DEFAULT_SETTINGS_GROUP: SettingsGroupKey = 'system';
54
+
55
+ /**
56
+ * 内置配置项到分组的映射。
57
+ *
58
+ * key 为 `pluginSettingsManager.addMenuItem` 时传入的 `key`;
59
+ * 没列出来的一律落到其他设置,第三方插件不改代码也能出现。
60
+ */
61
+ const BUILTIN_SETTINGS_GROUP_MAP: Record<string, SettingsGroupKey> = {
62
+ 'multi-portal': 'applications',
63
+ 'plugin-manager': 'plugins',
64
+ 'data-source-manager': 'data',
65
+ 'users-permissions': 'access',
66
+ workflow: 'automation',
67
+ ai: 'ai',
68
+ };
69
+
70
+ /**
71
+ * 解析某个顶级配置项所属的分组。
72
+ *
73
+ * @param {PluginSettingsPageType | null | undefined} setting 顶级配置项
74
+ * @returns {SettingsGroupKey} 分组 key
75
+ */
76
+ export function getSettingsGroupKey(setting?: PluginSettingsPageType | null): SettingsGroupKey {
77
+ if (!setting) {
78
+ return DEFAULT_SETTINGS_GROUP;
79
+ }
80
+
81
+ const declared = (setting as PluginSettingsPageType & { group?: string }).group;
82
+ if (declared && SETTINGS_GROUPS.some((group) => group.key === declared)) {
83
+ return declared as SettingsGroupKey;
84
+ }
85
+
86
+ return BUILTIN_SETTINGS_GROUP_MAP[setting.name] || DEFAULT_SETTINGS_GROUP;
87
+ }
88
+
89
+ /**
90
+ * 按分组切分顶级配置项,并丢掉没有任何配置项的空分组。
91
+ *
92
+ * @param {PluginSettingsPageType[]} settings 已排序的顶级配置项
93
+ * @returns 非空分组
94
+ */
95
+ export function groupTopLevelSettings(settings: readonly PluginSettingsPageType[] = []) {
96
+ return SETTINGS_GROUPS.map((group) => {
97
+ const members = settings.filter((setting) => getSettingsGroupKey(setting) === group.key);
98
+ const primaryIndex = group.primary ? members.findIndex((item) => item.name === group.primary) : -1;
99
+
100
+ return {
101
+ ...group,
102
+ settings:
103
+ primaryIndex > 0
104
+ ? [members[primaryIndex], ...members.slice(0, primaryIndex), ...members.slice(primaryIndex + 1)]
105
+ : members,
106
+ };
107
+ }).filter((group) => group.settings.length > 0);
108
+ }
@@ -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;