@nocobase/client-v2 2.3.0-alpha.1 → 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 (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,99 @@
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 { afterEach, describe, expect, it } from 'vitest';
11
+ import { resolveStandaloneSettingsPath } from '../settings-app/runtimePaths';
12
+
13
+ const originalModernPrefix = window.__nocobase_modern_client_prefix__;
14
+
15
+ const createApp = (publicPath: string, basename: string, name = 'main') =>
16
+ ({
17
+ name,
18
+ getPublicPath: () => publicPath,
19
+ router: {
20
+ getBasename: () => basename,
21
+ },
22
+ }) as any;
23
+
24
+ describe('standalone settings runtime paths', () => {
25
+ afterEach(() => {
26
+ window.__nocobase_modern_client_prefix__ = originalModernPrefix;
27
+ });
28
+
29
+ it('maps legacy v2 settings paths to the root-public standalone SPA', () => {
30
+ const app = createApp('/nocobase/v/', '/nocobase/v');
31
+
32
+ expect(resolveStandaloneSettingsPath(app, '/nocobase/v/admin/settings/system-settings?tab=mail#smtp')).toBe(
33
+ '/nocobase/settings/system-settings?tab=mail#smtp',
34
+ );
35
+ expect(resolveStandaloneSettingsPath(app, '/nocobase/v/admin/workflow/workflows/42?tab=nodes#canvas')).toBe(
36
+ '/nocobase/settings/workflow/workflows/42?tab=nodes#canvas',
37
+ );
38
+ expect(resolveStandaloneSettingsPath(app, '/nocobase/v/admin/workflow/executions/88')).toBe(
39
+ '/nocobase/settings/workflow/executions/88',
40
+ );
41
+ expect(resolveStandaloneSettingsPath(app, '/nocobase/v/admin/ai/knowledge-base/detail/k1/documents')).toBe(
42
+ '/nocobase/settings/ai/knowledge-base/detail/k1/documents',
43
+ );
44
+ });
45
+
46
+ it('preserves both supported sub-application scopes', () => {
47
+ const appsApp = createApp('/nocobase/v/', '/nocobase/v/apps/demo');
48
+ const newApp = createApp('/nocobase/v/', '/nocobase/v/_app/demo');
49
+
50
+ expect(resolveStandaloneSettingsPath(appsApp, '/nocobase/v/apps/demo/admin/settings/routes')).toBe(
51
+ '/nocobase/settings/apps/demo/routes',
52
+ );
53
+ expect(resolveStandaloneSettingsPath(newApp, '/nocobase/v/_app/demo/admin/settings/routes')).toBe(
54
+ '/nocobase/settings/_app/demo/routes',
55
+ );
56
+ });
57
+
58
+ it('supports a custom modern prefix without leaking it into settings URLs', () => {
59
+ window.__nocobase_modern_client_prefix__ = 'modern';
60
+ const app = createApp('/base/modern/', '/base/modern');
61
+
62
+ expect(resolveStandaloneSettingsPath(app, '/base/modern/admin/settings/security')).toBe('/base/settings/security');
63
+ });
64
+
65
+ it('does not treat an apps segment inside the main application public path as a sub-application scope', () => {
66
+ window.__nocobase_modern_client_prefix__ = 'modern';
67
+ const app = createApp('/tenant/apps/root/modern/', '/tenant/apps/root/modern');
68
+
69
+ expect(resolveStandaloneSettingsPath(app, '/tenant/apps/root/modern/admin/settings/security')).toBe(
70
+ '/tenant/apps/root/settings/security',
71
+ );
72
+ });
73
+
74
+ it.each(['apps', '_app'])(
75
+ 'does not remove a matching %s segment from the root public path for a real sub-application',
76
+ (scope) => {
77
+ window.__nocobase_modern_client_prefix__ = 'modern';
78
+ const app = createApp(`/tenant/${scope}/demo/modern/`, `/tenant/${scope}/demo/modern/${scope}/demo`, 'demo');
79
+
80
+ expect(
81
+ resolveStandaloneSettingsPath(app, `/tenant/${scope}/demo/modern/${scope}/demo/admin/settings/security`),
82
+ ).toBe(`/tenant/${scope}/demo/settings/${scope}/demo/security`);
83
+ expect(
84
+ resolveStandaloneSettingsPath(
85
+ app,
86
+ `/tenant/${scope}/demo/modern/${scope}/demo/admin/settings/mail/oauth2?code=abc#done`,
87
+ ),
88
+ ).toBe(`/tenant/${scope}/demo/${scope}/demo/admin/settings/mail/oauth2?code=abc#done`);
89
+ },
90
+ );
91
+
92
+ it('returns the legacy V1 callback for Email OAuth', () => {
93
+ const app = createApp('/nocobase/v/', '/nocobase/v/apps/demo');
94
+
95
+ expect(resolveStandaloneSettingsPath(app, '/nocobase/v/apps/demo/admin/settings/mail/oauth2?code=abc#done')).toBe(
96
+ '/nocobase/apps/demo/admin/settings/mail/oauth2?code=abc#done',
97
+ );
98
+ });
99
+ });
@@ -0,0 +1,200 @@
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 { render, screen } from '@testing-library/react';
11
+ import { ConfigProvider } from 'antd';
12
+ import React from 'react';
13
+ import { MemoryRouter } from 'react-router-dom';
14
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
15
+ import { setCurrentUserAuthStatus } from '../nocobase-buildin-plugin/currentUserAuthStatus';
16
+ import { SettingsShell } from '../settings-app/SettingsShell';
17
+ import type { ThemeConfig } from '../theme';
18
+
19
+ const userCenterModel = { uid: 'settings-user-center' };
20
+ const createModel = vi.fn(() => userCenterModel);
21
+ const matchRoutes = vi.fn(() => [{ route: { id: 'settings' } }]);
22
+ const mockApp = {
23
+ flowEngine: {
24
+ createModel,
25
+ getModel: vi.fn(() => null),
26
+ getModelClass: vi.fn(() => true),
27
+ },
28
+ router: {
29
+ matchRoutes,
30
+ },
31
+ };
32
+
33
+ vi.mock('../hooks/useApp', () => ({
34
+ useApp: () => mockApp,
35
+ }));
36
+
37
+ vi.mock('../settings-app/SettingsBrand', () => ({
38
+ SettingsBrand: () => <div data-testid="settings-logo">logo</div>,
39
+ }));
40
+
41
+ vi.mock('../settings-app/SettingsGroupNav', () => ({
42
+ SettingsGroupNav: () => <div data-testid="settings-group-nav">groups</div>,
43
+ }));
44
+
45
+ vi.mock('../settings-app/SettingsSearch', () => ({
46
+ SettingsSearch: () => <div data-testid="settings-search">search</div>,
47
+ }));
48
+
49
+ vi.mock('../flow/admin-shell/admin-layout/HelpLite', () => ({
50
+ HelpLite: () => <div data-testid="settings-help">help</div>,
51
+ }));
52
+
53
+ vi.mock('@nocobase/flow-engine', async (importOriginal) => {
54
+ const actual = await importOriginal<typeof import('@nocobase/flow-engine')>();
55
+ return {
56
+ ...actual,
57
+ FlowModelRenderer: ({ model }: { model: { uid: string } }) => (
58
+ <div data-testid="settings-user-center">{model.uid}</div>
59
+ ),
60
+ };
61
+ });
62
+
63
+ describe('SettingsShell', () => {
64
+ beforeEach(() => {
65
+ createModel.mockClear();
66
+ matchRoutes.mockReset();
67
+ matchRoutes.mockReturnValue([{ route: { id: 'settings' } }]);
68
+ setCurrentUserAuthStatus(mockApp, 'authenticated');
69
+ });
70
+
71
+ it('renders only the settings brand, group nav, help and user center around its content', () => {
72
+ render(
73
+ <MemoryRouter initialEntries={['/settings/system-settings']}>
74
+ <SettingsShell>
75
+ <div>settings content</div>
76
+ </SettingsShell>
77
+ </MemoryRouter>,
78
+ );
79
+
80
+ expect(screen.getByTestId('settings-logo')).toBeInTheDocument();
81
+ expect(screen.getByTestId('settings-group-nav')).toBeInTheDocument();
82
+ expect(screen.getByTestId('settings-search')).toBeInTheDocument();
83
+ expect(screen.getByTestId('settings-help')).toBeInTheDocument();
84
+ expect(screen.getByTestId('settings-user-center')).toHaveTextContent('settings-user-center');
85
+ expect(screen.getByText('settings content')).toBeInTheDocument();
86
+ expect(screen.queryByTestId('plugin-settings-button')).not.toBeInTheDocument();
87
+ expect(screen.queryByTestId('notifications-button')).not.toBeInTheDocument();
88
+ });
89
+
90
+ it('keeps its neutral header instead of following the business-side header color', () => {
91
+ render(
92
+ <ConfigProvider
93
+ theme={
94
+ {
95
+ token: {
96
+ colorBgHeader: '#001529',
97
+ colorPrimary: '#1777FF',
98
+ },
99
+ } as ThemeConfig
100
+ }
101
+ >
102
+ <MemoryRouter initialEntries={['/settings/system-settings']}>
103
+ <SettingsShell>
104
+ <div>settings content</div>
105
+ </SettingsShell>
106
+ </MemoryRouter>
107
+ </ConfigProvider>,
108
+ );
109
+
110
+ // 主题编辑器里的深色顶栏只作用于业务端;设置中心固定用容器底色。
111
+ expect(screen.getByRole('banner')).toHaveStyle({ background: '#ffffff' });
112
+ });
113
+
114
+ it('places the settings content and embed container side by side below the header', () => {
115
+ const { container } = render(
116
+ <MemoryRouter initialEntries={['/settings/theme-editor']}>
117
+ <SettingsShell>
118
+ <div>settings content</div>
119
+ </SettingsShell>
120
+ </MemoryRouter>,
121
+ );
122
+
123
+ const header = screen.getByRole('banner');
124
+ const content = screen.getByRole('main');
125
+ const embedContainer = container.querySelector<HTMLElement>('#nocobase-embed-container');
126
+ const workspace = content.parentElement;
127
+
128
+ expect(embedContainer).not.toBeNull();
129
+ if (!embedContainer) {
130
+ throw new Error('Expected the Settings shell to render the global embed container');
131
+ }
132
+ expect(header.nextElementSibling).toBe(workspace);
133
+ expect(workspace).toHaveStyle({
134
+ display: 'flex',
135
+ flex: '1',
136
+ minWidth: '0',
137
+ minHeight: '0',
138
+ overflow: 'hidden',
139
+ });
140
+ expect(workspace?.children).toHaveLength(2);
141
+ expect(workspace?.firstElementChild).toBe(content);
142
+ expect(workspace?.lastElementChild).toBe(embedContainer);
143
+ expect(content).toHaveStyle({
144
+ flex: '1',
145
+ minWidth: '0',
146
+ minHeight: '0',
147
+ overflow: 'hidden',
148
+ });
149
+ expect(embedContainer).toHaveStyle({
150
+ flexShrink: '0',
151
+ height: '100%',
152
+ position: 'relative',
153
+ });
154
+
155
+ embedContainer.style.width = '33.3%';
156
+ embedContainer.style.maxWidth = '800px';
157
+ expect(workspace?.lastElementChild).toBe(embedContainer);
158
+ expect(embedContainer).toHaveStyle({ width: '33.3%', maxWidth: '800px' });
159
+
160
+ embedContainer.style.width = 'auto';
161
+ embedContainer.style.maxWidth = 'none';
162
+ expect(content).toHaveStyle({ flex: '1' });
163
+ expect(embedContainer).toHaveStyle({ width: 'auto', maxWidth: 'none' });
164
+ });
165
+
166
+ it.each(['auth.signin', '2fa.verify'])('does not render the settings shell for %s', (routeId) => {
167
+ matchRoutes.mockReturnValue([{ route: { id: routeId } }]);
168
+
169
+ render(
170
+ <MemoryRouter initialEntries={[routeId === 'auth.signin' ? '/settings/signin' : '/settings/2fa']}>
171
+ <SettingsShell>
172
+ <div>authentication content</div>
173
+ </SettingsShell>
174
+ </MemoryRouter>,
175
+ );
176
+
177
+ expect(screen.getByText('authentication content')).toBeInTheDocument();
178
+ expect(screen.queryByRole('banner')).not.toBeInTheDocument();
179
+ expect(screen.queryByTestId('settings-logo')).not.toBeInTheDocument();
180
+ expect(screen.queryByTestId('settings-help')).not.toBeInTheDocument();
181
+ expect(screen.queryByTestId('settings-user-center')).not.toBeInTheDocument();
182
+ expect(document.querySelector('#nocobase-embed-container')).not.toBeInTheDocument();
183
+ });
184
+
185
+ it('does not render the Settings header before authentication completes', () => {
186
+ setCurrentUserAuthStatus(mockApp, 'unknown');
187
+
188
+ render(
189
+ <MemoryRouter initialEntries={['/settings/system-settings']}>
190
+ <SettingsShell>
191
+ <div>settings content</div>
192
+ </SettingsShell>
193
+ </MemoryRouter>,
194
+ );
195
+
196
+ expect(screen.getByText('settings content')).toBeInTheDocument();
197
+ expect(document.querySelector('header')).toHaveStyle({ display: 'none' });
198
+ expect(screen.queryByTestId('settings-logo')).not.toBeVisible();
199
+ });
200
+ });
@@ -167,6 +167,27 @@ export const useACLContext = () => {
167
167
  return useContext(ACLContext);
168
168
  };
169
169
 
170
+ /**
171
+ * 读取当前角色的 snippets,且不要求调用方位于 `ACLRolesCheckProvider` 之内。
172
+ *
173
+ * 设置中心顶栏导航挂在 app 级 provider 上,位置比 `ACLRolesCheckProvider` 更外层,
174
+ * 这种情况下退回到 app context 上的 ACL observable store。
175
+ * 调用方需要用 `observer` 包裹,store 更新后才会重新渲染。
176
+ *
177
+ * @returns {string[]} 当前角色的 snippets
178
+ */
179
+ export const useACLSnippets = (): string[] => {
180
+ const app = useApp();
181
+ const ctx = useACLContext();
182
+ const contextSnippets = ctx?.data?.data?.snippets;
183
+ // 必须走 ensureACLStore:调用方可能位于 `ACLRolesCheckProvider` 之外,此时 store 还没建,
184
+ // 直接读 `app.context.acl` 拿到的是 undefined,observer 就订阅不到后续的权限写入,
185
+ // 会一直停在首帧的空 snippets 上。
186
+ const storeSnippets = ensureACLStore(app)?.data?.snippets;
187
+
188
+ return contextSnippets?.length ? contextSnippets : storeSnippets || [];
189
+ };
190
+
170
191
  /**
171
192
  * 触发当前角色权限重新检查。
172
193
  */
@@ -8,8 +8,19 @@
8
8
  */
9
9
 
10
10
  import type { BaseApplication } from './BaseApplication';
11
+ import {
12
+ resolveSettingsAppScope,
13
+ resolveSettingsAppScopeWithinPublicPath,
14
+ resolveSettingsDocumentPath,
15
+ type SettingsAppScope,
16
+ } from './settings-app/settingsDocumentPath';
11
17
 
12
18
  type AppLike = Pick<BaseApplication<any>, 'getPublicPath'> & {
19
+ name?: string;
20
+ pluginSettingsManager?: {
21
+ getRouteName?: (name: string) => string;
22
+ getRoutePath?: (name: string) => string;
23
+ };
13
24
  router?: {
14
25
  getBasename?: () => string | undefined;
15
26
  };
@@ -52,7 +63,8 @@ function normalizePublicPath(value?: string) {
52
63
  }
53
64
 
54
65
  function normalizePathname(value?: string) {
55
- const normalized = ensureLeadingSlash(String(value || '/').trim() || '/').replace(/\/{2,}/g, '/');
66
+ const pathname = ensureLeadingSlash(String(value || '/').trim() || '/');
67
+ const normalized = new URL(pathname, 'http://nocobase.local').pathname.replace(/\/{2,}/g, '/');
56
68
  if (normalized !== '/' && normalized.endsWith('/')) {
57
69
  return trimTrailingSlashes(normalized);
58
70
  }
@@ -179,7 +191,53 @@ function joinRootRelativePath(basePath: string, pathname: string) {
179
191
  return normalizePathname(`/${trimLeadingSlashes(normalizedBasePath)}/${trimLeadingSlashes(normalizedPathname)}`);
180
192
  }
181
193
 
194
+ function isStandaloneSettingsApp(app: AppLike) {
195
+ return app.pluginSettingsManager?.getRouteName?.('') === 'settings.';
196
+ }
197
+
198
+ function getSettingsRootPublicPath(app: AppLike) {
199
+ const publicPath = normalizePublicPath(app.getPublicPath());
200
+ if (isStandaloneSettingsApp(app)) {
201
+ return publicPath;
202
+ }
203
+
204
+ const segments = trimTrailingSlashes(publicPath).split('/');
205
+ segments.pop();
206
+ return normalizePublicPath(segments.join('/') || '/');
207
+ }
208
+
209
+ function getSettingsAppScope(app: AppLike): SettingsAppScope {
210
+ const publicPath = app.getPublicPath();
211
+ return (
212
+ resolveSettingsAppScopeWithinPublicPath(publicPath, app.router?.getBasename?.()) ||
213
+ (app.name && app.name !== 'main' ? resolveSettingsAppScope(`/apps/${app.name}`) : '')
214
+ );
215
+ }
216
+
217
+ function getStandaloneSettingsBasePath(app: AppLike) {
218
+ return resolveSettingsDocumentPath(getSettingsRootPublicPath(app), getSettingsAppScope(app), '/settings');
219
+ }
220
+
221
+ function isStandaloneSettingsTarget(app: AppLike, pathname: string) {
222
+ const normalizedPathname = normalizePathname(pathname);
223
+ const settingsBasePath = normalizePathname(getStandaloneSettingsBasePath(app));
224
+ return normalizedPathname === settingsBasePath || normalizedPathname.startsWith(`${settingsBasePath}/`);
225
+ }
226
+
227
+ function isSettingsTargetForAnotherApp(app: AppLike, pathname: string) {
228
+ const normalizedPathname = normalizePathname(pathname);
229
+ const rootPublicPath = trimTrailingSlashes(getSettingsRootPublicPath(app)) || '';
230
+ const pathWithinRoot =
231
+ rootPublicPath && normalizedPathname.startsWith(`${rootPublicPath}/`)
232
+ ? normalizedPathname.slice(rootPublicPath.length)
233
+ : normalizedPathname;
234
+ return /^\/settings(?:\/|$)/.test(pathWithinRoot);
235
+ }
236
+
182
237
  function getV2SigninPath(app: AppLike) {
238
+ if (isStandaloneSettingsApp(app)) {
239
+ return joinRootRelativePath(getStandaloneSettingsBasePath(app), '/signin');
240
+ }
183
241
  return joinRootRelativePath(getV2EffectiveBasePath(app), '/signin');
184
242
  }
185
243
 
@@ -204,7 +262,7 @@ function getDefaultV2AdminRedirectPath(app: AppLike) {
204
262
  return joinRootRelativePath(getV2EffectiveBasePath(app), '/admin');
205
263
  }
206
264
 
207
- function isSafeRootRelativePath(value?: string | null) {
265
+ function isSafeRootRelativePath(value?: string | null): value is string {
208
266
  return !!value && value.startsWith('/') && !value.startsWith('//') && !value.startsWith('/\\');
209
267
  }
210
268
 
@@ -224,6 +282,10 @@ export function normalizeV2RedirectPath(app: AppLike, target?: string | null, fa
224
282
  let { pathname, search, hash } = splitPathLike(rawTarget);
225
283
  let normalizedPathname = normalizePathname(pathname);
226
284
 
285
+ if (isStandaloneSettingsTarget(app, normalizedPathname)) {
286
+ return `${preserveTrailingSlash(pathname, normalizedPathname)}${normalizeSearch(search)}${normalizeHash(hash)}`;
287
+ }
288
+
227
289
  // Already under the current v2 runtime, e.g. `/v/apps/a/admin/`.
228
290
  if (basePath === '/' || normalizedPathname === basePath || normalizedPathname.startsWith(`${basePath}/`)) {
229
291
  return `${preserveTrailingSlash(pathname, normalizedPathname)}${normalizeSearch(search)}${normalizeHash(hash)}`;
@@ -237,6 +299,11 @@ export function normalizeV2RedirectPath(app: AppLike, target?: string | null, fa
237
299
  normalizedPathname = normalizePathname(pathname);
238
300
  }
239
301
 
302
+ if (isSettingsTargetForAnotherApp(app, normalizedPathname)) {
303
+ ({ pathname, search, hash } = splitPathLike(fallbackTarget));
304
+ normalizedPathname = normalizePathname(pathname);
305
+ }
306
+
240
307
  // Basename-relative target, e.g. `/admin/`, becomes `/v/apps/a/admin/`.
241
308
  const joinedPathname = preserveTrailingSlash(pathname, joinRootRelativePath(basePath, normalizedPathname));
242
309
  return `${joinedPathname}${normalizeSearch(search)}${normalizeHash(hash)}`;
@@ -306,7 +373,9 @@ export function resolveV2SigninRedirect(value: string | undefined | null, app: A
306
373
  return null;
307
374
  }
308
375
 
309
- const validPathnames = new Set(['/signin', getV2SigninPath(app)]);
376
+ const validPathnames = new Set(
377
+ isStandaloneSettingsApp(app) ? [getV2SigninPath(app)] : ['/signin', getV2SigninPath(app)],
378
+ );
310
379
  if (!validPathnames.has(url.pathname)) {
311
380
  return null;
312
381
  }