@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,77 @@
|
|
|
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, { useMemo } from 'react';
|
|
14
|
+
import { useTranslation } from 'react-i18next';
|
|
15
|
+
import { useSystemSettings } from '../../system-settings';
|
|
16
|
+
import type { CustomToken } from '../../../theme';
|
|
17
|
+
|
|
18
|
+
const logoContainerClassName = css`
|
|
19
|
+
height: var(--nb-header-height);
|
|
20
|
+
margin-right: 4px;
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
flex-shrink: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
align-items: center;
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const fixedWidthClassName = css`
|
|
28
|
+
${logoContainerClassName}
|
|
29
|
+
width: 168px;
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const autoWidthClassName = css`
|
|
33
|
+
${logoContainerClassName}
|
|
34
|
+
width: auto;
|
|
35
|
+
min-width: 168px;
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const logoImageClassName = css`
|
|
39
|
+
object-fit: contain;
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const titleClassName = css`
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: 100%;
|
|
47
|
+
font-weight: 500;
|
|
48
|
+
text-align: center;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
text-overflow: ellipsis;
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
export const NocoBaseLogo = observer(() => {
|
|
55
|
+
const { token } = antdTheme.useToken();
|
|
56
|
+
const customToken = token as CustomToken;
|
|
57
|
+
const result = useSystemSettings();
|
|
58
|
+
const { t } = useTranslation('lm-collections');
|
|
59
|
+
const title = t(result?.data?.data?.title || 'NocoBase');
|
|
60
|
+
const logoUrl = result?.data?.data?.logo?.url;
|
|
61
|
+
const titleStyle = useMemo<React.CSSProperties>(
|
|
62
|
+
() => ({ color: customToken.colorTextHeaderMenu, fontSize: customToken.fontSizeHeading3 }),
|
|
63
|
+
[customToken.colorTextHeaderMenu, customToken.fontSizeHeading3],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const logo = logoUrl ? (
|
|
67
|
+
<img alt={title} className={logoImageClassName} src={logoUrl} />
|
|
68
|
+
) : (
|
|
69
|
+
<span style={titleStyle} className={titleClassName}>
|
|
70
|
+
{title}
|
|
71
|
+
</span>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return <div className={logoUrl ? fixedWidthClassName : autoWidthClassName}>{result?.loading ? null : logo}</div>;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
NocoBaseLogo.displayName = 'NocoBaseLogo';
|
|
@@ -24,7 +24,9 @@ const topbarActionsBarClassName = css`
|
|
|
24
24
|
align-items: center;
|
|
25
25
|
color: var(--nb-topbar-action-color);
|
|
26
26
|
|
|
27
|
-
.nb-topbar-actions-list
|
|
27
|
+
.nb-topbar-actions-list,
|
|
28
|
+
.nb-topbar-utility-actions-list,
|
|
29
|
+
.nb-topbar-plugin-settings-action {
|
|
28
30
|
display: inline-flex;
|
|
29
31
|
align-items: center;
|
|
30
32
|
height: 100%;
|
|
@@ -127,12 +129,18 @@ const getTopbarActionId = (action: TopbarActionModel) => {
|
|
|
127
129
|
return action?.actionId || action?.uid || '';
|
|
128
130
|
};
|
|
129
131
|
|
|
132
|
+
const PLUGIN_SETTINGS_ACTION_ID = 'plugin-settings';
|
|
133
|
+
|
|
130
134
|
const TopbarActionsContent = React.memo((props: { actions: TopbarActionModel[]; onActionClick?: () => void }) => {
|
|
131
135
|
const { allow } = useAclSnippets();
|
|
132
136
|
const { token } = theme.useToken();
|
|
133
137
|
const customToken = token as CustomToken;
|
|
134
138
|
const actions = useMemo(() => getVisibleTopbarActions(props.actions, allow), [allow, props.actions]);
|
|
135
|
-
const mainActions = actions.filter((action) =>
|
|
139
|
+
const mainActions = actions.filter((action) => {
|
|
140
|
+
const actionId = getTopbarActionId(action);
|
|
141
|
+
return actionId !== PLUGIN_SETTINGS_ACTION_ID && actionId !== USER_CENTER_ACTION_ID;
|
|
142
|
+
});
|
|
143
|
+
const pluginSettingsAction = actions.find((action) => getTopbarActionId(action) === PLUGIN_SETTINGS_ACTION_ID);
|
|
136
144
|
const userCenterAction = actions.find((action) => getTopbarActionId(action) === USER_CENTER_ACTION_ID);
|
|
137
145
|
|
|
138
146
|
return (
|
|
@@ -153,18 +161,33 @@ const TopbarActionsContent = React.memo((props: { actions: TopbarActionModel[];
|
|
|
153
161
|
<ConfigProvider theme={dividerTheme}>
|
|
154
162
|
<Divider type="vertical" />
|
|
155
163
|
</ConfigProvider>
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
<div className="nb-topbar-utility-actions-list">
|
|
165
|
+
{pluginSettingsAction ? (
|
|
166
|
+
<span className="nb-topbar-plugin-settings-action" onClick={props.onActionClick}>
|
|
167
|
+
<ErrorBoundary
|
|
168
|
+
key={pluginSettingsAction.uid}
|
|
169
|
+
FallbackComponent={TopbarActionErrorFallback}
|
|
170
|
+
onError={(error) => {
|
|
171
|
+
console.error('[NocoBase] Topbar action render failed.', error);
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
<FlowModelRenderer model={pluginSettingsAction} />
|
|
175
|
+
</ErrorBoundary>
|
|
176
|
+
</span>
|
|
177
|
+
) : null}
|
|
178
|
+
<HelpLite />
|
|
179
|
+
{userCenterAction ? (
|
|
180
|
+
<ErrorBoundary
|
|
181
|
+
key={userCenterAction.uid}
|
|
182
|
+
FallbackComponent={TopbarActionErrorFallback}
|
|
183
|
+
onError={(error) => {
|
|
184
|
+
console.error('[NocoBase] Topbar action render failed.', error);
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
<FlowModelRenderer model={userCenterAction} />
|
|
188
|
+
</ErrorBoundary>
|
|
189
|
+
) : null}
|
|
190
|
+
</div>
|
|
168
191
|
</div>
|
|
169
192
|
);
|
|
170
193
|
});
|
|
@@ -136,6 +136,16 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
136
136
|
title: 'Plugin manager',
|
|
137
137
|
path: '/admin/settings/plugin-manager',
|
|
138
138
|
icon: null,
|
|
139
|
+
sort: -200,
|
|
140
|
+
componentLoader: async () => null,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: 'multi-portal',
|
|
144
|
+
name: 'multi-portal',
|
|
145
|
+
title: 'Portal manager',
|
|
146
|
+
path: '/admin/settings/multi-portal',
|
|
147
|
+
icon: null,
|
|
148
|
+
sort: -300,
|
|
139
149
|
componentLoader: async () => null,
|
|
140
150
|
},
|
|
141
151
|
{
|
|
@@ -159,13 +169,14 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
159
169
|
});
|
|
160
170
|
|
|
161
171
|
expect((items as any[]).map((item) => item.type || item.key)).toEqual([
|
|
172
|
+
'multi-portal',
|
|
162
173
|
'plugin-manager',
|
|
163
174
|
'divider',
|
|
164
175
|
'system-settings',
|
|
165
176
|
'divider',
|
|
166
177
|
'security',
|
|
167
178
|
]);
|
|
168
|
-
expect((items as any[])
|
|
179
|
+
expect((items as any[]).find((item) => item.key === 'system-settings')).toMatchObject({
|
|
169
180
|
key: 'system-settings',
|
|
170
181
|
name: 'system-settings',
|
|
171
182
|
path: '/admin/settings/system-settings',
|
|
@@ -191,7 +202,7 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
191
202
|
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
192
203
|
|
|
193
204
|
const link = screen.getByRole('link', { name: 'System settings' });
|
|
194
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
205
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/system-settings');
|
|
195
206
|
expect(link).toHaveAttribute('target', '_blank');
|
|
196
207
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
197
208
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
|
@@ -216,7 +227,7 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
216
227
|
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
217
228
|
|
|
218
229
|
const link = screen.getByRole('link', { name: 'Plugin manager' });
|
|
219
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
230
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/plugin-manager');
|
|
220
231
|
expect(link).toHaveAttribute('target', '_blank');
|
|
221
232
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
222
233
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
|
@@ -241,13 +252,13 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
241
252
|
renderSettingsLabel((items as any[])[0].label, '/apps/a_9xlild35jir/crm-amd/ekeisumx1zu');
|
|
242
253
|
|
|
243
254
|
const link = screen.getByRole('link', { name: 'System settings' });
|
|
244
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
255
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/apps/a_9xlild35jir/system-settings');
|
|
245
256
|
expect(link).toHaveAttribute('target', '_blank');
|
|
246
257
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
247
258
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
|
248
259
|
});
|
|
249
260
|
|
|
250
|
-
it('should
|
|
261
|
+
it('should open regular admin settings in the standalone SPA from admin runtime', () => {
|
|
251
262
|
const items = getTopbarPluginSettingsItems({
|
|
252
263
|
canManagePlugins: false,
|
|
253
264
|
t: (key) => key,
|
|
@@ -266,11 +277,12 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
266
277
|
renderSettingsLabel((items as any[])[0].label, '/admin/settings/routes');
|
|
267
278
|
|
|
268
279
|
const link = screen.getByRole('link', { name: 'Routes' });
|
|
269
|
-
expect(link).toHaveAttribute('href', '/
|
|
270
|
-
expect(link).
|
|
280
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/routes');
|
|
281
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
282
|
+
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
271
283
|
});
|
|
272
284
|
|
|
273
|
-
it('should
|
|
285
|
+
it('should open sub-app settings in the standalone SPA from sub-app admin runtime', () => {
|
|
274
286
|
const items = getTopbarPluginSettingsItems({
|
|
275
287
|
canManagePlugins: false,
|
|
276
288
|
t: (key) => key,
|
|
@@ -289,8 +301,9 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
289
301
|
renderSettingsLabel((items as any[])[0].label, '/apps/a_9xlild35jir/admin/settings/routes');
|
|
290
302
|
|
|
291
303
|
const link = screen.getByRole('link', { name: 'Routes' });
|
|
292
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
293
|
-
expect(link).
|
|
304
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/apps/a_9xlild35jir/routes');
|
|
305
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
306
|
+
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
294
307
|
});
|
|
295
308
|
|
|
296
309
|
it('should not treat admin-like paths as admin runtime', () => {
|
|
@@ -312,11 +325,11 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
312
325
|
renderSettingsLabel((items as any[])[0].label, '/admin2/foo');
|
|
313
326
|
|
|
314
327
|
const link = screen.getByRole('link', { name: 'System settings' });
|
|
315
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
328
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/system-settings');
|
|
316
329
|
expect(link).toHaveAttribute('target', '_blank');
|
|
317
330
|
});
|
|
318
331
|
|
|
319
|
-
it('should
|
|
332
|
+
it('should remove the modern basename when building standalone settings hrefs', () => {
|
|
320
333
|
const items = getTopbarPluginSettingsItems({
|
|
321
334
|
canManagePlugins: false,
|
|
322
335
|
t: (key) => key,
|
|
@@ -335,7 +348,30 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
335
348
|
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
336
349
|
|
|
337
350
|
const link = screen.getByRole('link', { name: 'System settings' });
|
|
338
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
351
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/system-settings');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should preserve the new sub-app scope in standalone settings hrefs', () => {
|
|
355
|
+
const items = getTopbarPluginSettingsItems({
|
|
356
|
+
canManagePlugins: false,
|
|
357
|
+
t: (key) => key,
|
|
358
|
+
settings: [
|
|
359
|
+
{
|
|
360
|
+
key: 'system-settings',
|
|
361
|
+
name: 'system-settings',
|
|
362
|
+
title: 'System settings',
|
|
363
|
+
path: '/admin/settings/system-settings',
|
|
364
|
+
icon: null,
|
|
365
|
+
componentLoader: async () => null,
|
|
366
|
+
},
|
|
367
|
+
] as any,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
renderSettingsLabel((items as any[])[0].label, '/_app/a_new/admin/settings/system-settings');
|
|
371
|
+
|
|
372
|
+
const link = screen.getByRole('link', { name: 'System settings' });
|
|
373
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/_app/a_new/system-settings');
|
|
374
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
339
375
|
});
|
|
340
376
|
|
|
341
377
|
it('should keep external settings opening in a new tab', () => {
|
|
@@ -402,6 +438,34 @@ describe('TopbarActionsBar', () => {
|
|
|
402
438
|
vi.restoreAllMocks();
|
|
403
439
|
});
|
|
404
440
|
|
|
441
|
+
it('should group plugin settings with Help and user center', () => {
|
|
442
|
+
render(
|
|
443
|
+
<TopbarActionsBar
|
|
444
|
+
actions={[
|
|
445
|
+
createAction({ uid: 'notification', actionId: 'notification' }),
|
|
446
|
+
createAction({ uid: 'plugin-settings', actionId: 'plugin-settings' }),
|
|
447
|
+
createAction({ uid: 'user-center', actionId: 'user-center' }),
|
|
448
|
+
]}
|
|
449
|
+
/>,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const notification = screen.getByTestId('flow-model-notification');
|
|
453
|
+
const pluginSettings = screen.getByTestId('flow-model-plugin-settings');
|
|
454
|
+
const help = screen.getByTestId('help-lite');
|
|
455
|
+
const userCenter = screen.getByTestId('flow-model-user-center');
|
|
456
|
+
const mainGroup = notification.closest('.nb-topbar-actions-list');
|
|
457
|
+
const utilityGroup = pluginSettings.closest('.nb-topbar-utility-actions-list');
|
|
458
|
+
|
|
459
|
+
expect(mainGroup).toContainElement(notification);
|
|
460
|
+
expect(mainGroup).not.toContainElement(pluginSettings);
|
|
461
|
+
expect(utilityGroup).not.toBeNull();
|
|
462
|
+
expect(utilityGroup).toContainElement(pluginSettings);
|
|
463
|
+
expect(utilityGroup).toContainElement(help);
|
|
464
|
+
expect(utilityGroup).toContainElement(userCenter);
|
|
465
|
+
expect(pluginSettings.compareDocumentPosition(help) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
466
|
+
expect(help.compareDocumentPosition(userCenter) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
467
|
+
});
|
|
468
|
+
|
|
405
469
|
it('should keep HelpLite rendered when one action fails', () => {
|
|
406
470
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
407
471
|
|
|
@@ -26,6 +26,10 @@ export default function useStyle() {
|
|
|
26
26
|
() => ({
|
|
27
27
|
[`.${COMPONENT_CLS}`]: {
|
|
28
28
|
'.vditor-reset': { fontSize: `${token.fontSize}px !important`, color: 'unset' },
|
|
29
|
+
'.vditor-reset h2': {
|
|
30
|
+
borderBottom: 'none !important',
|
|
31
|
+
boxShadow: 'none !important',
|
|
32
|
+
},
|
|
29
33
|
'.vditor': {
|
|
30
34
|
borderRadius: 8,
|
|
31
35
|
},
|
|
@@ -150,7 +150,7 @@ export const BlockItemCard = React.forwardRef(
|
|
|
150
150
|
) => {
|
|
151
151
|
const { t } = useTranslation();
|
|
152
152
|
const { token } = theme.useToken();
|
|
153
|
-
const { title: blockTitle, description, children, className, heightMode, ...rest } = props;
|
|
153
|
+
const { title: blockTitle, description, children, className, heightMode, style, ...rest } = props;
|
|
154
154
|
const cardRef = useRef<HTMLDivElement | null>(null);
|
|
155
155
|
const setCardRef = useCallback(
|
|
156
156
|
(node: HTMLDivElement | null) => {
|
|
@@ -185,7 +185,7 @@ export const BlockItemCard = React.forwardRef(
|
|
|
185
185
|
<Card
|
|
186
186
|
ref={setCardRef as any}
|
|
187
187
|
title={title}
|
|
188
|
-
style={{ display: 'flex', flexDirection: 'column', height: height }}
|
|
188
|
+
style={{ display: 'flex', flexDirection: 'column', ...style, height: height ?? style?.height }}
|
|
189
189
|
styles={{
|
|
190
190
|
body: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'auto' },
|
|
191
191
|
header: {
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { Button, Result } from 'antd';
|
|
11
12
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
|
-
import {
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
13
14
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
15
|
+
import { getModernClientPrefix, stripModernClientPrefix } from '../../authRedirect';
|
|
14
16
|
import { useApp } from '../../hooks/useApp';
|
|
15
17
|
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
|
|
16
18
|
import {
|
|
@@ -22,12 +24,14 @@ import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutM
|
|
|
22
24
|
import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
|
|
23
25
|
import { AppNotFound } from '../../components';
|
|
24
26
|
import { useKeepAlive } from '../../components/KeepAlive';
|
|
27
|
+
import { registerDeviceTypeContext } from '../internal/registerDeviceTypeContext';
|
|
25
28
|
|
|
26
29
|
type FlowRouteGuardState = {
|
|
27
30
|
pageUid?: string;
|
|
28
31
|
pending: boolean;
|
|
29
32
|
allowBridge: boolean;
|
|
30
33
|
notFound: boolean;
|
|
34
|
+
legacyPageUnsupported?: boolean;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
|
|
@@ -156,31 +160,9 @@ const BridgeFlowRoute = ({
|
|
|
156
160
|
const layoutContentRef = useRef<HTMLDivElement>(null);
|
|
157
161
|
|
|
158
162
|
useEffect(() => {
|
|
159
|
-
flowEngine.context.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
meta: {
|
|
163
|
-
type: 'string',
|
|
164
|
-
title: flowEngine.translate('Current device type'),
|
|
165
|
-
interface: 'select',
|
|
166
|
-
uiSchema: {
|
|
167
|
-
enum: [
|
|
168
|
-
{ label: flowEngine.translate('Computer'), value: 'computer' },
|
|
169
|
-
{ label: flowEngine.translate('Mobile'), value: 'mobile' },
|
|
170
|
-
{ label: flowEngine.translate('Tablet'), value: 'tablet' },
|
|
171
|
-
{ label: flowEngine.translate('SmartTv'), value: 'smarttv' },
|
|
172
|
-
{ label: flowEngine.translate('Console'), value: 'console' },
|
|
173
|
-
{ label: flowEngine.translate('Wearable'), value: 'wearable' },
|
|
174
|
-
{ label: flowEngine.translate('Embedded'), value: 'embedded' },
|
|
175
|
-
],
|
|
176
|
-
'x-component': 'Select',
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
info: {
|
|
180
|
-
description: 'Current device type (computer/mobile/tablet/...).',
|
|
181
|
-
detail: 'string',
|
|
182
|
-
},
|
|
183
|
-
});
|
|
163
|
+
if (!flowEngine.context.getPropertyOptions('deviceType')) {
|
|
164
|
+
registerDeviceTypeContext(flowEngine);
|
|
165
|
+
}
|
|
184
166
|
}, [flowEngine]);
|
|
185
167
|
|
|
186
168
|
useLayoutRoutePage({
|
|
@@ -195,11 +177,58 @@ const BridgeFlowRoute = ({
|
|
|
195
177
|
return <div ref={layoutContentRef} />;
|
|
196
178
|
};
|
|
197
179
|
|
|
180
|
+
type RouteLocation = {
|
|
181
|
+
pathname: string;
|
|
182
|
+
search: string;
|
|
183
|
+
hash: string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const getLegacyPageHref = (app: { getPublicPath: () => string }, location: RouteLocation) => {
|
|
187
|
+
const modernPublicPath = app.getPublicPath();
|
|
188
|
+
const browserLocationMatchesPublicPath = window.location.pathname.startsWith(modernPublicPath);
|
|
189
|
+
const currentLocation = browserLocationMatchesPublicPath ? window.location : location;
|
|
190
|
+
const pathWithinModernClient = currentLocation.pathname.startsWith(modernPublicPath)
|
|
191
|
+
? currentLocation.pathname.slice(modernPublicPath.length)
|
|
192
|
+
: currentLocation.pathname.replace(/^\/+/, '');
|
|
193
|
+
return `${stripModernClientPrefix(modernPublicPath)}${pathWithinModernClient}${currentLocation.search}${
|
|
194
|
+
currentLocation.hash
|
|
195
|
+
}`;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const LegacyPageUnsupported = ({
|
|
199
|
+
app,
|
|
200
|
+
location,
|
|
201
|
+
}: {
|
|
202
|
+
app: { getPublicPath: () => string };
|
|
203
|
+
location: RouteLocation;
|
|
204
|
+
}) => {
|
|
205
|
+
const { t } = useTranslation();
|
|
206
|
+
const modernClientPath = `/${getModernClientPrefix()}/`;
|
|
207
|
+
const withModernClientPath = (message: string) => message.replaceAll('{{modernClientPath}}', modernClientPath);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Result
|
|
211
|
+
status="warning"
|
|
212
|
+
title={withModernClientPath(t('This page is not supported in the {{modernClientPath}} branch'))}
|
|
213
|
+
subTitle={withModernClientPath(
|
|
214
|
+
t(
|
|
215
|
+
'The {{modernClientPath}} branch only supports new pages. This page is a legacy page. Please open it from the original entry.',
|
|
216
|
+
),
|
|
217
|
+
)}
|
|
218
|
+
extra={
|
|
219
|
+
<Button href={getLegacyPageHref(app, location)} type="primary">
|
|
220
|
+
{t('Open from the original entry')}
|
|
221
|
+
</Button>
|
|
222
|
+
}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
198
227
|
/**
|
|
199
228
|
* 管理后台动态页面路由组件。
|
|
200
229
|
*
|
|
201
|
-
* 负责读取当前路由页面 UID
|
|
202
|
-
*
|
|
230
|
+
* 负责读取当前路由页面 UID,并把页面生命周期桥接到 AdminLayout host model。
|
|
231
|
+
* 设备变量通常由 PluginFlowEngine 共享初始化提供;独立渲染时会在挂载后补充注册。
|
|
203
232
|
*
|
|
204
233
|
* @example
|
|
205
234
|
* ```tsx
|
|
@@ -377,7 +406,13 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
377
406
|
|
|
378
407
|
if (target.reason === 'unsupportedV2Runtime') {
|
|
379
408
|
if (active && requestId === requestIdRef.current) {
|
|
380
|
-
setGuardState({
|
|
409
|
+
setGuardState({
|
|
410
|
+
pageUid,
|
|
411
|
+
pending: false,
|
|
412
|
+
allowBridge: false,
|
|
413
|
+
notFound: false,
|
|
414
|
+
legacyPageUnsupported: true,
|
|
415
|
+
});
|
|
381
416
|
}
|
|
382
417
|
return;
|
|
383
418
|
}
|
|
@@ -414,6 +449,10 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
414
449
|
return <AppNotFound />;
|
|
415
450
|
}
|
|
416
451
|
|
|
452
|
+
if (guardState.legacyPageUnsupported) {
|
|
453
|
+
return <LegacyPageUnsupported app={app} location={location} />;
|
|
454
|
+
}
|
|
455
|
+
|
|
417
456
|
if (!guardState.allowBridge) {
|
|
418
457
|
return null;
|
|
419
458
|
}
|
|
@@ -421,11 +460,14 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
421
460
|
return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
|
|
422
461
|
}, [
|
|
423
462
|
active,
|
|
463
|
+
app,
|
|
424
464
|
getLayoutModel,
|
|
425
465
|
guardState.allowBridge,
|
|
466
|
+
guardState.legacyPageUnsupported,
|
|
426
467
|
guardState.notFound,
|
|
427
468
|
guardState.pageUid,
|
|
428
469
|
guardState.pending,
|
|
470
|
+
location,
|
|
429
471
|
pageUid,
|
|
430
472
|
]);
|
|
431
473
|
|
package/src/flow/index.ts
CHANGED
|
@@ -22,12 +22,14 @@ import { Markdown } from './common/Markdown/Markdown';
|
|
|
22
22
|
import { LiquidEngine } from './common/Liquid';
|
|
23
23
|
import type { PreviewRunJSResult } from './components/code-editor/runjsDiagnostics';
|
|
24
24
|
import { TextAreaWithContextSelector } from './components/TextAreaWithContextSelector';
|
|
25
|
+
import { registerDeviceTypeContext } from './internal/registerDeviceTypeContext';
|
|
25
26
|
|
|
26
27
|
export class PluginFlowEngine<TApp extends BaseApplication<any> = BaseApplication<any>> extends Plugin<
|
|
27
28
|
PluginOptions<any>,
|
|
28
29
|
TApp
|
|
29
30
|
> {
|
|
30
31
|
async load() {
|
|
32
|
+
registerDeviceTypeContext(this.flowEngine);
|
|
31
33
|
this.app.addComponents({ FlowRoute });
|
|
32
34
|
this.app.flowEngine.setModelRepository(new FlowModelRepository(this.app));
|
|
33
35
|
const filteredModels = Object.fromEntries(
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { deviceType } from 'react-device-detect';
|
|
12
|
+
|
|
13
|
+
export const registerDeviceTypeContext = (flowEngine: FlowEngine) => {
|
|
14
|
+
flowEngine.context.defineProperty('deviceType', {
|
|
15
|
+
get: () => (deviceType === 'browser' ? 'computer' : deviceType),
|
|
16
|
+
cache: false,
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
title: flowEngine.translate('Current device type'),
|
|
20
|
+
interface: 'select',
|
|
21
|
+
uiSchema: {
|
|
22
|
+
enum: [
|
|
23
|
+
{ label: flowEngine.translate('Computer'), value: 'computer' },
|
|
24
|
+
{ label: flowEngine.translate('Mobile'), value: 'mobile' },
|
|
25
|
+
{ label: flowEngine.translate('Tablet'), value: 'tablet' },
|
|
26
|
+
{ label: flowEngine.translate('SmartTv'), value: 'smarttv' },
|
|
27
|
+
{ label: flowEngine.translate('Console'), value: 'console' },
|
|
28
|
+
{ label: flowEngine.translate('Wearable'), value: 'wearable' },
|
|
29
|
+
{ label: flowEngine.translate('Embedded'), value: 'embedded' },
|
|
30
|
+
],
|
|
31
|
+
'x-component': 'Select',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
info: {
|
|
35
|
+
description: 'Current device type (computer/mobile/tablet/...).',
|
|
36
|
+
detail: 'string',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
};
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { FlowModel, tExpr, useFlowModel } from '@nocobase/flow-engine';
|
|
11
|
+
import type { DefaultStructure, FlowModelOptions } from '@nocobase/flow-engine';
|
|
11
12
|
import { Icon } from '../../../components';
|
|
12
13
|
import { Button, Tooltip } from 'antd';
|
|
13
14
|
import type { ButtonProps } from 'antd/es/button';
|
|
@@ -87,8 +88,10 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
onInit(options:
|
|
91
|
+
onInit(options: FlowModelOptions<T>): void {
|
|
91
92
|
super.onInit(options);
|
|
93
|
+
// Action disabled state is computed at runtime and must never be restored from persisted props.
|
|
94
|
+
delete this.props.disabled;
|
|
92
95
|
this.context.defineProperty('actionName', {
|
|
93
96
|
get: () => {
|
|
94
97
|
return this.getAclActionName();
|
|
@@ -97,6 +100,12 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
97
100
|
});
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
serialize() {
|
|
104
|
+
const data = super.serialize();
|
|
105
|
+
delete data.props.disabled;
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
getInputArgs() {
|
|
101
110
|
const inputArgs: Record<string, any> = {};
|
|
102
111
|
const defaultKeys: string[] = [];
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
FlowSettingsButton,
|
|
15
15
|
buildSubModelGroups,
|
|
16
16
|
buildSubModelItems,
|
|
17
|
+
type FlowModel,
|
|
17
18
|
type SubModelItem,
|
|
18
19
|
type SubModelItemsType,
|
|
20
|
+
VIEW_ACTIVATED_EVENT,
|
|
19
21
|
} from '@nocobase/flow-engine';
|
|
20
22
|
import React from 'react';
|
|
21
23
|
import { FilterManager } from '../blocks/filter-manager/FilterManager';
|
|
@@ -24,6 +26,8 @@ import { GridModel } from './GridModel';
|
|
|
24
26
|
const SELECT_SCENE_ALLOWED_OTHER_BLOCK_MODELS = ['JSBlockModel', 'IframeBlockModel', 'MarkdownBlockModel'] as const;
|
|
25
27
|
|
|
26
28
|
export class BlockGridModel extends GridModel {
|
|
29
|
+
private viewActivatedListener?: () => void;
|
|
30
|
+
|
|
27
31
|
dragOverlayConfig: DragOverlayConfig = {
|
|
28
32
|
// 列内插入
|
|
29
33
|
columnInsert: {
|
|
@@ -50,6 +54,28 @@ export class BlockGridModel extends GridModel {
|
|
|
50
54
|
});
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
onMount() {
|
|
58
|
+
super.onMount();
|
|
59
|
+
if (this.context.view?.inputArgs?.scene !== 'select' || this.viewActivatedListener) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.viewActivatedListener = () => {
|
|
64
|
+
this.mapSubModels('items', (item) => {
|
|
65
|
+
(item as FlowModel & { onActive?: () => void }).onActive?.();
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
this.flowEngine.emitter.on(VIEW_ACTIVATED_EVENT, this.viewActivatedListener);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected onUnmount() {
|
|
72
|
+
if (this.viewActivatedListener) {
|
|
73
|
+
this.flowEngine.emitter.off(VIEW_ACTIVATED_EVENT, this.viewActivatedListener);
|
|
74
|
+
this.viewActivatedListener = undefined;
|
|
75
|
+
}
|
|
76
|
+
super.onUnmount();
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
get subModelBaseClasses() {
|
|
54
80
|
const inputArgs = this.context.view?.inputArgs ?? {};
|
|
55
81
|
if (inputArgs.collectionName && !inputArgs.filterByTk) {
|