@nocobase/client-v2 2.3.0-beta.6 → 2.4.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 (90) hide show
  1. package/es/acl/ACLProvider.d.ts +10 -0
  2. package/es/acl/aclCheckReadiness.d.ts +11 -0
  3. package/es/authRedirect.d.ts +11 -2
  4. package/es/components/AttachmentUpload.d.ts +51 -0
  5. package/es/components/index.d.ts +1 -0
  6. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  7. package/es/index.d.ts +4 -1
  8. package/es/index.mjs +129 -118
  9. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  10. package/es/nocobase-buildin-plugin/index.d.ts +10 -0
  11. package/es/settings-app/SettingsApplication.d.ts +18 -0
  12. package/es/settings-app/SettingsBrand.d.ts +16 -0
  13. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  14. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  15. package/es/settings-app/SettingsGroupNav.d.ts +18 -0
  16. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  17. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  18. package/es/settings-app/SettingsSearch.d.ts +17 -0
  19. package/es/settings-app/SettingsShell.d.ts +10 -0
  20. package/es/settings-app/isSettingsApp.d.ts +18 -0
  21. package/es/settings-app/runtimePaths.d.ts +18 -0
  22. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  23. package/es/settings-app/settingsTheme.d.ts +87 -0
  24. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  25. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  26. package/es/settings-center/groups.d.ts +66 -0
  27. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  28. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  29. package/es/settings-center/utils.d.ts +24 -0
  30. package/es/theme/index.d.ts +1 -0
  31. package/es/theme/itemActive.d.ts +24 -0
  32. package/es/theme/type.d.ts +9 -0
  33. package/lib/index.js +157 -146
  34. package/package.json +7 -7
  35. package/src/__tests__/app.test.tsx +122 -3
  36. package/src/__tests__/authRedirect.test.ts +154 -0
  37. package/src/__tests__/browserChecker.test.ts +172 -6
  38. package/src/__tests__/mockSettingsApplication.ts +29 -0
  39. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +81 -8
  40. package/src/__tests__/plugin-manager.test.tsx +7 -6
  41. package/src/__tests__/settings-application.test.ts +164 -0
  42. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  43. package/src/__tests__/settings-center.test.tsx +168 -130
  44. package/src/__tests__/settings-layout-root.test.tsx +600 -0
  45. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  46. package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
  47. package/src/__tests__/settings-shell.test.tsx +261 -0
  48. package/src/acl/ACLProvider.tsx +53 -3
  49. package/src/acl/aclCheckReadiness.ts +44 -0
  50. package/src/authRedirect.ts +85 -8
  51. package/src/components/AppComponents.tsx +16 -7
  52. package/src/components/AttachmentUpload.tsx +275 -0
  53. package/src/components/index.ts +1 -0
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
  55. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  56. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  57. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  58. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  59. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  60. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  61. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  62. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  63. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  64. package/src/index.ts +11 -1
  65. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  66. package/src/nocobase-buildin-plugin/index.tsx +112 -71
  67. package/src/settings-app/SettingsApplication.ts +31 -0
  68. package/src/settings-app/SettingsBrand.tsx +108 -0
  69. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  70. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  71. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  72. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  73. package/src/settings-app/SettingsRouterManager.ts +92 -0
  74. package/src/settings-app/SettingsSearch.tsx +343 -0
  75. package/src/settings-app/SettingsShell.tsx +184 -0
  76. package/src/settings-app/isSettingsApp.ts +21 -0
  77. package/src/settings-app/runtimePaths.ts +103 -0
  78. package/src/settings-app/settingsDocumentPath.ts +66 -0
  79. package/src/settings-app/settingsTheme.ts +369 -0
  80. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  81. package/src/settings-center/AdminSettingsLayout.tsx +154 -169
  82. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  83. package/src/settings-center/__tests__/groups.test.ts +25 -0
  84. package/src/settings-center/groups.ts +108 -0
  85. package/src/settings-center/useSettingsGroups.ts +105 -0
  86. package/src/settings-center/useSettingsSearch.ts +173 -0
  87. package/src/settings-center/utils.tsx +50 -1
  88. package/src/theme/index.tsx +1 -0
  89. package/src/theme/itemActive.ts +31 -0
  90. 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 { css } from '@emotion/css';
11
+ import { observer } from '@nocobase/flow-engine';
12
+ import { theme as antdTheme } from 'antd';
13
+ import React from 'react';
14
+ import { useTranslation } from 'react-i18next';
15
+ import { useSystemSettings } from '../flow/system-settings';
16
+ import { getSettingsHeaderColors } from './settingsTheme';
17
+
18
+ const brandClassName = css`
19
+ display: inline-flex;
20
+ align-items: center;
21
+ gap: 8px;
22
+ height: 100%;
23
+ flex-shrink: 0;
24
+ min-width: 0;
25
+ `;
26
+
27
+ const logoImageClassName = css`
28
+ height: 24px;
29
+ width: auto;
30
+ max-width: 140px;
31
+ object-fit: contain;
32
+ display: block;
33
+ `;
34
+
35
+ const titleClassName = css`
36
+ font-weight: 600;
37
+ line-height: 1;
38
+ white-space: nowrap;
39
+ overflow: hidden;
40
+ text-overflow: ellipsis;
41
+ `;
42
+
43
+ const suffixClassName = css`
44
+ line-height: 1;
45
+ white-space: nowrap;
46
+ font-weight: 400;
47
+ `;
48
+
49
+ /**
50
+ * 设置中心顶栏品牌区。
51
+ *
52
+ * 沿用系统设置里的站点 logo / 标题,并在其后补一个弱化的「设置中心」副标题,
53
+ * 用于区分设置中心和业务端。
54
+ */
55
+ export const SettingsBrand = observer(() => {
56
+ const { token } = antdTheme.useToken();
57
+ const headerColors = getSettingsHeaderColors(token);
58
+ const result = useSystemSettings();
59
+ const { t } = useTranslation();
60
+ const { t: tCollections } = useTranslation('lm-collections');
61
+ const title = tCollections(result?.data?.data?.title || 'NocoBase');
62
+ const logoUrl = result?.data?.data?.logo?.url;
63
+
64
+ if (result?.loading) {
65
+ return <div className={brandClassName} />;
66
+ }
67
+
68
+ return (
69
+ <div className={brandClassName}>
70
+ {logoUrl ? (
71
+ // 站点 logo 通常是为深色顶栏画的浅色图;设置中心顶栏是灰白的,
72
+ // 统一压成单色,避免浅色 logo 直接消失在白底上。
73
+ <img
74
+ alt={title}
75
+ className={logoImageClassName}
76
+ src={logoUrl}
77
+ style={{ filter: headerColors.dark ? 'brightness(0) invert(1)' : 'brightness(0)' }}
78
+ />
79
+ ) : (
80
+ <span className={titleClassName} style={{ color: headerColors.text, fontSize: token.fontSizeHeading4 }}>
81
+ {title}
82
+ </span>
83
+ )}
84
+ <span
85
+ aria-hidden
86
+ style={{
87
+ alignSelf: 'center',
88
+ background: headerColors.text,
89
+ height: 16,
90
+ opacity: 0.3,
91
+ width: 1,
92
+ }}
93
+ />
94
+ <span
95
+ className={suffixClassName}
96
+ style={{
97
+ color: headerColors.text,
98
+ fontSize: token.fontSize,
99
+ opacity: 0.75,
100
+ }}
101
+ >
102
+ {t('Settings center')}
103
+ </span>
104
+ </div>
105
+ );
106
+ });
107
+
108
+ SettingsBrand.displayName = 'SettingsBrand';
@@ -0,0 +1,10 @@
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 { SettingsBuildInPlugin } from '../nocobase-buildin-plugin';
@@ -0,0 +1,26 @@
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 React, { useEffect } from 'react';
11
+ import { useLocation } from 'react-router-dom';
12
+ import { useApp } from '../hooks/useApp';
13
+ import { resolveStandaloneSettingsPath } from './runtimePaths';
14
+
15
+ export function SettingsDocumentRedirect() {
16
+ const app = useApp();
17
+ const location = useLocation();
18
+ const sourcePath = `${location.pathname}${location.search}${location.hash}`;
19
+ const targetPath = resolveStandaloneSettingsPath(app, sourcePath, location.pathname);
20
+
21
+ useEffect(() => {
22
+ window.location.replace(targetPath);
23
+ }, [targetPath]);
24
+
25
+ return app.renderComponent('AppSpin');
26
+ }
@@ -0,0 +1,124 @@
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 { observer } from '@nocobase/flow-engine';
11
+ import { ConfigProvider, Menu, theme as antdTheme } from 'antd';
12
+ import React, { useCallback, useMemo } from 'react';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { useLocation, useNavigate } from 'react-router-dom';
15
+ import { useACLCheckReady } from '../acl/aclCheckReadiness';
16
+ import { useApp } from '../hooks/useApp';
17
+ import { useSettingsGroups } from '../settings-center/useSettingsGroups';
18
+ import { buildSettingsHeaderMenuTheme } from './settingsTheme';
19
+
20
+ const navStyle: React.CSSProperties = {
21
+ background: 'transparent',
22
+ borderBottom: 'none',
23
+ flex: 'auto',
24
+ minWidth: 0,
25
+ lineHeight: '46px',
26
+ };
27
+
28
+ /**
29
+ * 设置中心顶栏的一级导航。
30
+ *
31
+ * Applications and the plugin manager have a single page each and act as top-level
32
+ * entries; everything else sits under "other settings" and expands on hover — that
33
+ * group has no sidebar, so the dropdown is the only way into its pages.
34
+ */
35
+ export const SettingsGroupNav: React.FC = observer(() => {
36
+ const { t } = useTranslation();
37
+ const app = useApp();
38
+ const navigate = useNavigate();
39
+ const location = useLocation();
40
+ const { token } = antdTheme.useToken();
41
+ const { groups, activeGroupKey, currentTopLevelSetting, getGroupEntryPath } = useSettingsGroups();
42
+ const isACLReady = useACLCheckReady(app);
43
+ const headerMenuTheme = useMemo(() => buildSettingsHeaderMenuTheme(token), [token]);
44
+ // 登录 / 找回密码等免鉴权页面共用同一个 shell,这些页面上不该出现设置导航。
45
+ const isAuthRoute = app.router.isSkippedAuthCheckRoute(location.pathname);
46
+
47
+ const handleClick = useCallback(
48
+ ({ key }: { key: string }) => {
49
+ const [groupKey, settingName] = key.split('::');
50
+ const targetPath = settingName
51
+ ? groups.find((group) => group.key === groupKey)?.settings.find((item) => item.name === settingName)?.path
52
+ : getGroupEntryPath(groupKey);
53
+
54
+ if (targetPath && targetPath !== location.pathname) {
55
+ navigate(targetPath);
56
+ }
57
+ },
58
+ [getGroupEntryPath, groups, location.pathname, navigate],
59
+ );
60
+
61
+ // 单入口分组直接当一级项,标题取配置项自己的名字(顺带继承插件的翻译);
62
+ // 系统设置这种大分组挂上子项,鼠标移上去展开。
63
+ const items = useMemo(
64
+ () =>
65
+ groups.map((group) => {
66
+ if (group.settings.length <= 1) {
67
+ return {
68
+ key: group.key,
69
+ label: group.settings[0]?.label ?? t(group.title),
70
+ icon: group.settings[0]?.icon,
71
+ };
72
+ }
73
+
74
+ // The dropdown still opens while standing inside the group: with the sidebar
75
+ // gone, this is the only way to reach the other pages in it.
76
+ return {
77
+ key: group.key,
78
+ label: t(group.title),
79
+ children: group.settings
80
+ .filter((setting) => !setting.hidden)
81
+ .map((setting) => ({
82
+ key: `${group.key}::${setting.name}`,
83
+ label: setting.label ?? setting.title,
84
+ icon: setting.icon,
85
+ })),
86
+ // The group title only expands the dropdown and is not clickable: it is not a
87
+ // page, so clicking it has no well-defined destination. Pick a page instead.
88
+ };
89
+ }),
90
+ [groups, handleClick, t],
91
+ );
92
+
93
+ // 子菜单标题只有在它的某个子项被选中时才会高亮,所以两个 key 都要给。
94
+ const selectedKeys = useMemo(() => {
95
+ if (!activeGroupKey) {
96
+ return [];
97
+ }
98
+ return currentTopLevelSetting?.name
99
+ ? [activeGroupKey, `${activeGroupKey}::${currentTopLevelSetting.name}`]
100
+ : [activeGroupKey];
101
+ }, [activeGroupKey, currentTopLevelSetting?.name]);
102
+
103
+ if (isAuthRoute || !isACLReady || items.length <= 1) {
104
+ return <div style={{ flex: 'auto' }} />;
105
+ }
106
+
107
+ return (
108
+ <ConfigProvider theme={headerMenuTheme}>
109
+ <Menu
110
+ mode="horizontal"
111
+ disabledOverflow={false}
112
+ selectedKeys={selectedKeys}
113
+ items={items}
114
+ style={navStyle}
115
+ triggerSubMenuAction="hover"
116
+ onClick={handleClick}
117
+ />
118
+ </ConfigProvider>
119
+ );
120
+ });
121
+
122
+ SettingsGroupNav.displayName = 'SettingsGroupNav';
123
+
124
+ export default SettingsGroupNav;
@@ -0,0 +1,38 @@
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 { BaseApplication } from '../BaseApplication';
11
+ import { PluginSettingsManager } from '../PluginSettingsManager';
12
+ import { resolveSettingsAppScopeWithinPublicPath } from './settingsDocumentPath';
13
+
14
+ const SETTINGS_ROUTE_PREFIX = 'settings.';
15
+ const MAIN_SETTINGS_PATH_PREFIX = '/settings/';
16
+
17
+ export class SettingsPluginSettingsManager<
18
+ TApp extends BaseApplication<any> = BaseApplication<any>,
19
+ > extends PluginSettingsManager<TApp> {
20
+ getRouteName(name: string) {
21
+ return `${SETTINGS_ROUTE_PREFIX}${name}`;
22
+ }
23
+
24
+ getRoutePath(name: string) {
25
+ const appScope = resolveSettingsAppScopeWithinPublicPath(this.app.getPublicPath(), this.app.router.getBasename?.());
26
+ const pathPrefix = appScope ? '/' : MAIN_SETTINGS_PATH_PREFIX;
27
+ const separatorIndex = name.indexOf('.');
28
+ const menuName = separatorIndex < 0 ? name : name.slice(0, separatorIndex);
29
+ const pageName = separatorIndex < 0 ? undefined : name.slice(separatorIndex + 1);
30
+ const menuPath = `${pathPrefix}${menuName}`;
31
+
32
+ if (!pageName || pageName === 'index') {
33
+ return menuPath;
34
+ }
35
+
36
+ return `${menuPath}/${pageName}`;
37
+ }
38
+ }
@@ -0,0 +1,92 @@
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 { BaseApplication } from '../BaseApplication';
11
+ import { RouterManager, type RouteType } from '../RouterManager';
12
+ import { resolveSettingsAppScopeWithinPublicPath } from './settingsDocumentPath';
13
+
14
+ function isSettingsOwnedRoute(name: string) {
15
+ return (
16
+ name === 'not-found' ||
17
+ name === 'settings' ||
18
+ name.startsWith('settings.') ||
19
+ name === 'settingsDetails' ||
20
+ name.startsWith('settingsDetails.') ||
21
+ name === 'auth' ||
22
+ name.startsWith('auth.') ||
23
+ name === '2fa' ||
24
+ name.startsWith('2fa.')
25
+ );
26
+ }
27
+
28
+ function isSettingsAuthenticationRoute(name: string) {
29
+ return name === 'auth' || name.startsWith('auth.') || name === '2fa' || name.startsWith('2fa.');
30
+ }
31
+
32
+ export class SettingsRouterManager<
33
+ TApp extends BaseApplication<any> = BaseApplication<any>,
34
+ > extends RouterManager<TApp> {
35
+ private rebaseScopedSettingsRoute(route: RouteType) {
36
+ const appScope = resolveSettingsAppScopeWithinPublicPath(this.app.getPublicPath(), this.getBasename());
37
+ if (!route.path?.startsWith('/') || !appScope) {
38
+ return route;
39
+ }
40
+ if (route.path === '/settings') {
41
+ return {
42
+ ...route,
43
+ path: '/',
44
+ };
45
+ }
46
+ if (!route.path.startsWith('/settings/')) {
47
+ return route;
48
+ }
49
+ return {
50
+ ...route,
51
+ path: route.path.slice('/settings'.length),
52
+ };
53
+ }
54
+
55
+ private rebaseAuthenticationRoute(route: RouteType) {
56
+ if (!route.path?.startsWith('/')) {
57
+ return route;
58
+ }
59
+
60
+ const settingsRoot = this.app.pluginSettingsManager.getRoutePath('').replace(/\/+$/, '');
61
+ if (!settingsRoot) {
62
+ return route;
63
+ }
64
+ if (route.path === settingsRoot || route.path.startsWith(`${settingsRoot}/`)) {
65
+ return route;
66
+ }
67
+
68
+ return {
69
+ ...route,
70
+ path: `${settingsRoot}/${route.path.replace(/^\/+/, '')}`,
71
+ };
72
+ }
73
+
74
+ add(name: string, route: RouteType) {
75
+ if (!isSettingsOwnedRoute(name)) {
76
+ return;
77
+ }
78
+
79
+ const scopedRoute = this.rebaseScopedSettingsRoute(route);
80
+ const ownedRoute = isSettingsAuthenticationRoute(name) ? this.rebaseAuthenticationRoute(scopedRoute) : scopedRoute;
81
+
82
+ super.add(
83
+ name,
84
+ name === 'settingsDetails' || name.startsWith('settingsDetails.')
85
+ ? {
86
+ ...ownedRoute,
87
+ authCheck: true,
88
+ }
89
+ : ownedRoute,
90
+ );
91
+ }
92
+ }