@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.
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/authRedirect.d.ts +5 -0
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/linkageRules.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/index.d.ts +4 -1
- package/es/index.mjs +139 -125
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +9 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +17 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/lib/index.js +141 -127
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +8 -7
- package/src/__tests__/authRedirect.test.ts +137 -0
- package/src/__tests__/browserChecker.test.ts +124 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +51 -10
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +38 -37
- package/src/__tests__/settings-layout-root.test.tsx +260 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-shell.test.tsx +200 -0
- package/src/acl/ACLProvider.tsx +21 -0
- package/src/authRedirect.ts +72 -3
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
- package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/linkageRules.tsx +26 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
- package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/index.ts +10 -1
- package/src/locale/languageCodes.ts +1 -0
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +99 -71
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +238 -0
- package/src/settings-app/SettingsShell.tsx +167 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +104 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +301 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +122 -141
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- 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
|
+
});
|
package/src/acl/ACLProvider.tsx
CHANGED
|
@@ -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
|
*/
|
package/src/authRedirect.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
}
|