@nocobase/client-v2 2.3.0-beta.9 → 2.4.0-alpha.10
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/RouteRepository.d.ts +6 -0
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +11 -2
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +10 -1
- package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
- package/es/flow/internal/utils/quickEditViewContainer.d.ts +27 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
- package/es/index.d.ts +4 -1
- package/es/index.mjs +129 -118
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +10 -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 +18 -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 +133 -122
- package/package.json +7 -7
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +14 -0
- package/src/__tests__/RouteRepository.test.ts +51 -0
- package/src/__tests__/app.test.tsx +135 -3
- package/src/__tests__/authRedirect.test.ts +154 -0
- package/src/__tests__/browserChecker.test.ts +172 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +81 -8
- 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 +168 -130
- package/src/__tests__/settings-layout-root.test.tsx +600 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +261 -0
- package/src/acl/ACLProvider.tsx +53 -3
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +85 -8
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/index.ts +1 -0
- package/src/flow/actions/__tests__/validation.test.ts +48 -0
- 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 +104 -20
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -4
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
- package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
- package/src/flow/internal/utils/quickEditDataScope.tsx +177 -0
- package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
- package/src/flow/models/actions/JSItemActionModel.tsx +19 -3
- package/src/flow/models/actions/__tests__/JSItemActionModel.test.tsx +47 -1
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +45 -11
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +5 -1
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +23 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +31 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +64 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +17 -10
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +6 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
- package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/index.ts +11 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +112 -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 +343 -0
- package/src/settings-app/SettingsShell.tsx +184 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +103 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +369 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +154 -169
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- 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,48 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { validation } from '../validation';
|
|
12
|
+
|
|
13
|
+
vi.mock('../../../flow-compat', () => ({ FieldValidation: () => null }));
|
|
14
|
+
|
|
15
|
+
describe('validation action', () => {
|
|
16
|
+
it('rejects excess decimal places with a translated error and retains collection rules', async () => {
|
|
17
|
+
const setProps = vi.fn();
|
|
18
|
+
const collectionRule = { validator: vi.fn().mockResolvedValue(undefined) };
|
|
19
|
+
const t = vi.fn((key: string, options?: Record<string, unknown>) => {
|
|
20
|
+
return `${options?.label} 精度不能超过 ${options?.limit} 位小数`;
|
|
21
|
+
});
|
|
22
|
+
const ctx = {
|
|
23
|
+
model: {
|
|
24
|
+
props: { label: '工龄' },
|
|
25
|
+
collectionField: { getComponentProps: () => ({ rules: [collectionRule] }) },
|
|
26
|
+
setProps,
|
|
27
|
+
},
|
|
28
|
+
t,
|
|
29
|
+
} as unknown as Parameters<typeof validation.handler>[0];
|
|
30
|
+
|
|
31
|
+
await validation.handler(ctx, {
|
|
32
|
+
validation: { type: 'number', rules: [{ name: 'precision', args: { limit: 2 } }] },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { rules } = setProps.mock.calls[0][0] as {
|
|
36
|
+
rules: { validator: (rule: unknown, value: unknown) => Promise<void> }[];
|
|
37
|
+
};
|
|
38
|
+
expect(rules).toHaveLength(2);
|
|
39
|
+
expect(rules[0]).toBe(collectionRule);
|
|
40
|
+
await expect(rules[1].validator({}, 39.2234)).rejects.toBe('工龄 精度不能超过 2 位小数');
|
|
41
|
+
expect(t).toHaveBeenCalledWith(
|
|
42
|
+
'number.precision',
|
|
43
|
+
expect.objectContaining({ ns: 'data-source-main', label: '工龄', limit: 2 }),
|
|
44
|
+
);
|
|
45
|
+
await expect(rules[1].validator({}, 39.22)).resolves.toBeUndefined();
|
|
46
|
+
await expect(rules[1].validator({}, null)).resolves.toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -18,7 +18,6 @@ import { theme as antdTheme, ConfigProvider, Grid, Popover } from 'antd';
|
|
|
18
18
|
import { createStyles, createGlobalStyle } from 'antd-style';
|
|
19
19
|
import React, { FC, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
20
20
|
import ReactDOM from 'react-dom';
|
|
21
|
-
import { useTranslation } from 'react-i18next';
|
|
22
21
|
import { useLocation } from 'react-router-dom';
|
|
23
22
|
import {
|
|
24
23
|
AdminLayoutMenuModelRenderer,
|
|
@@ -35,7 +34,6 @@ import { ResetThemeTokenAndKeepAlgorithm } from './ResetThemeTokenAndKeepAlgorit
|
|
|
35
34
|
import { PinnedPluginListLite } from './PinnedPluginListLite';
|
|
36
35
|
import { useApplications } from './useApplications';
|
|
37
36
|
import { useGlobalTheme, type CustomToken } from '../../../theme';
|
|
38
|
-
import { useSystemSettings } from '../../system-settings';
|
|
39
37
|
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
|
|
40
38
|
import {
|
|
41
39
|
FLOW_SETTINGS_PREFERENCE_CHANGE_EVENT,
|
|
@@ -44,60 +42,7 @@ import {
|
|
|
44
42
|
} from './flowSettingsPreference';
|
|
45
43
|
import { joinAdminLayoutRoutePath, type AdminLayoutRoutePathLike } from './resolveAdminRouteRuntimeTarget';
|
|
46
44
|
import { useAppListRender } from './AppListRender';
|
|
47
|
-
|
|
48
|
-
const className1 = css`
|
|
49
|
-
height: var(--nb-header-height);
|
|
50
|
-
margin-right: 4px;
|
|
51
|
-
display: inline-flex;
|
|
52
|
-
flex-shrink: 0;
|
|
53
|
-
color: #fff;
|
|
54
|
-
padding: 0;
|
|
55
|
-
align-items: center;
|
|
56
|
-
`;
|
|
57
|
-
const className1WithFixedWidth = css`
|
|
58
|
-
${className1}
|
|
59
|
-
width: 168px;
|
|
60
|
-
`;
|
|
61
|
-
const className1WithAutoWidth = css`
|
|
62
|
-
${className1}
|
|
63
|
-
width: auto;
|
|
64
|
-
min-width: 168px;
|
|
65
|
-
`;
|
|
66
|
-
const className2 = css`
|
|
67
|
-
object-fit: contain;
|
|
68
|
-
width: 100%;
|
|
69
|
-
height: 100%;
|
|
70
|
-
`;
|
|
71
|
-
const className3 = css`
|
|
72
|
-
width: 100%;
|
|
73
|
-
height: 100%;
|
|
74
|
-
font-weight: 500;
|
|
75
|
-
text-align: center;
|
|
76
|
-
overflow: hidden;
|
|
77
|
-
text-overflow: ellipsis;
|
|
78
|
-
white-space: nowrap;
|
|
79
|
-
`;
|
|
80
|
-
|
|
81
|
-
const NocoBaseLogo = observer(() => {
|
|
82
|
-
const { token } = antdTheme.useToken();
|
|
83
|
-
const customToken = token as CustomToken;
|
|
84
|
-
const result = useSystemSettings();
|
|
85
|
-
const { t } = useTranslation('lm-collections');
|
|
86
|
-
const fontSizeStyle = useMemo(() => ({ fontSize: customToken.fontSizeHeading3 }), [customToken.fontSizeHeading3]);
|
|
87
|
-
|
|
88
|
-
const hasLogo = result?.data?.data?.logo?.url;
|
|
89
|
-
const logo = hasLogo ? (
|
|
90
|
-
<img className={className2} src={result?.data?.data?.logo?.url} />
|
|
91
|
-
) : (
|
|
92
|
-
<span style={fontSizeStyle} className={className3}>
|
|
93
|
-
{t(result?.data?.data?.title)}
|
|
94
|
-
</span>
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<div className={hasLogo ? className1WithFixedWidth : className1WithAutoWidth}>{result?.loading ? null : logo}</div>
|
|
99
|
-
);
|
|
100
|
-
});
|
|
45
|
+
import { NocoBaseLogo } from './NocoBaseLogo';
|
|
101
46
|
|
|
102
47
|
const resetStyle = css`
|
|
103
48
|
.ant-layout-sider-children {
|
|
@@ -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
|
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
12
|
-
import { MemoryRouter
|
|
12
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
13
13
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
14
|
|
|
15
15
|
const { allowMock, appMock, flowModelRendererSpy } = vi.hoisted(() => {
|
|
@@ -17,6 +17,7 @@ const { allowMock, appMock, flowModelRendererSpy } = vi.hoisted(() => {
|
|
|
17
17
|
allowMock: vi.fn(),
|
|
18
18
|
appMock: {
|
|
19
19
|
current: {
|
|
20
|
+
name: 'main',
|
|
20
21
|
router: {
|
|
21
22
|
getBasename: () => '/nocobase/v',
|
|
22
23
|
},
|
|
@@ -86,6 +87,7 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
86
87
|
allowMock.mockReset();
|
|
87
88
|
flowModelRendererSpy.mockClear();
|
|
88
89
|
appMock.current = {
|
|
90
|
+
name: 'main',
|
|
89
91
|
router: {
|
|
90
92
|
getBasename: () => '/nocobase/v',
|
|
91
93
|
},
|
|
@@ -136,6 +138,16 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
136
138
|
title: 'Plugin manager',
|
|
137
139
|
path: '/admin/settings/plugin-manager',
|
|
138
140
|
icon: null,
|
|
141
|
+
sort: -200,
|
|
142
|
+
componentLoader: async () => null,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'multi-portal',
|
|
146
|
+
name: 'multi-portal',
|
|
147
|
+
title: 'Portal manager',
|
|
148
|
+
path: '/admin/settings/multi-portal',
|
|
149
|
+
icon: null,
|
|
150
|
+
sort: -300,
|
|
139
151
|
componentLoader: async () => null,
|
|
140
152
|
},
|
|
141
153
|
{
|
|
@@ -159,13 +171,14 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
159
171
|
});
|
|
160
172
|
|
|
161
173
|
expect((items as any[]).map((item) => item.type || item.key)).toEqual([
|
|
174
|
+
'multi-portal',
|
|
162
175
|
'plugin-manager',
|
|
163
176
|
'divider',
|
|
164
177
|
'system-settings',
|
|
165
178
|
'divider',
|
|
166
179
|
'security',
|
|
167
180
|
]);
|
|
168
|
-
expect((items as any[])
|
|
181
|
+
expect((items as any[]).find((item) => item.key === 'system-settings')).toMatchObject({
|
|
169
182
|
key: 'system-settings',
|
|
170
183
|
name: 'system-settings',
|
|
171
184
|
path: '/admin/settings/system-settings',
|
|
@@ -191,7 +204,7 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
191
204
|
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
192
205
|
|
|
193
206
|
const link = screen.getByRole('link', { name: 'System settings' });
|
|
194
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
207
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/system-settings');
|
|
195
208
|
expect(link).toHaveAttribute('target', '_blank');
|
|
196
209
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
197
210
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
|
@@ -216,7 +229,7 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
216
229
|
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
217
230
|
|
|
218
231
|
const link = screen.getByRole('link', { name: 'Plugin manager' });
|
|
219
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
232
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/plugin-manager');
|
|
220
233
|
expect(link).toHaveAttribute('target', '_blank');
|
|
221
234
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
222
235
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
|
@@ -241,13 +254,13 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
241
254
|
renderSettingsLabel((items as any[])[0].label, '/apps/a_9xlild35jir/crm-amd/ekeisumx1zu');
|
|
242
255
|
|
|
243
256
|
const link = screen.getByRole('link', { name: 'System settings' });
|
|
244
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
257
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/apps/a_9xlild35jir/system-settings');
|
|
245
258
|
expect(link).toHaveAttribute('target', '_blank');
|
|
246
259
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
247
260
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
|
248
261
|
});
|
|
249
262
|
|
|
250
|
-
it('should
|
|
263
|
+
it('should open regular admin settings in the standalone SPA from admin runtime', () => {
|
|
251
264
|
const items = getTopbarPluginSettingsItems({
|
|
252
265
|
canManagePlugins: false,
|
|
253
266
|
t: (key) => key,
|
|
@@ -266,13 +279,38 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
266
279
|
renderSettingsLabel((items as any[])[0].label, '/admin/settings/routes');
|
|
267
280
|
|
|
268
281
|
const link = screen.getByRole('link', { name: 'Routes' });
|
|
269
|
-
expect(link).toHaveAttribute('href', '/
|
|
270
|
-
expect(link).
|
|
282
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/routes');
|
|
283
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
284
|
+
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
271
285
|
});
|
|
272
286
|
|
|
287
|
+
it('should open sub-app settings in the standalone SPA from sub-app admin runtime', () => {
|
|
288
|
+
const items = getTopbarPluginSettingsItems({
|
|
289
|
+
canManagePlugins: false,
|
|
290
|
+
t: (key) => key,
|
|
291
|
+
settings: [
|
|
292
|
+
{
|
|
293
|
+
key: 'routes',
|
|
294
|
+
name: 'routes',
|
|
295
|
+
title: 'Routes',
|
|
296
|
+
path: '/admin/settings/routes',
|
|
297
|
+
icon: null,
|
|
298
|
+
componentLoader: async () => null,
|
|
299
|
+
},
|
|
300
|
+
] as any,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
renderSettingsLabel((items as any[])[0].label, '/apps/a_9xlild35jir/admin/settings/routes');
|
|
304
|
+
|
|
305
|
+
const link = screen.getByRole('link', { name: 'Routes' });
|
|
306
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/apps/a_9xlild35jir/routes');
|
|
307
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
308
|
+
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
309
|
+
});
|
|
273
310
|
it.each(['/nocobase/v', '/v', '/nocobase/v/apps/jhb20'])(
|
|
274
|
-
'should
|
|
311
|
+
'should open standalone sub-app settings with basename %s',
|
|
275
312
|
(basename) => {
|
|
313
|
+
appMock.current.name = 'jhb20';
|
|
276
314
|
appMock.current.router.getBasename = () => basename;
|
|
277
315
|
const items = getTopbarPluginSettingsItems({
|
|
278
316
|
canManagePlugins: false,
|
|
@@ -293,22 +331,17 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
293
331
|
throw new Error('Expected settings menu item');
|
|
294
332
|
}
|
|
295
333
|
const appBase = basename.endsWith('/apps/jhb20') ? basename : `${basename}/apps/jhb20`;
|
|
296
|
-
const targetHref = `${
|
|
297
|
-
const LocationDisplay = () => <output aria-label="Current route">{useLocation().pathname}</output>;
|
|
334
|
+
const targetHref = `${basename === '/v' ? '' : '/nocobase'}/settings/apps/jhb20/ai`;
|
|
298
335
|
render(
|
|
299
336
|
<MemoryRouter basename={basename} initialEntries={[`${appBase}/admin/a3pq1t1773a`]}>
|
|
300
337
|
{item.label}
|
|
301
|
-
<LocationDisplay />
|
|
302
338
|
</MemoryRouter>,
|
|
303
339
|
);
|
|
304
340
|
|
|
305
341
|
const link = screen.getByRole('link', { name: 'AI employees' });
|
|
306
342
|
expect(link).toHaveAttribute('href', targetHref);
|
|
307
|
-
expect(link).
|
|
308
|
-
|
|
309
|
-
fireEvent(link, click);
|
|
310
|
-
expect(click.defaultPrevented).toBe(true);
|
|
311
|
-
expect(screen.getByLabelText('Current route')).toHaveTextContent(targetHref.slice(basename.length));
|
|
343
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
344
|
+
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
312
345
|
},
|
|
313
346
|
);
|
|
314
347
|
|
|
@@ -331,11 +364,11 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
331
364
|
renderSettingsLabel((items as any[])[0].label, '/admin2/foo');
|
|
332
365
|
|
|
333
366
|
const link = screen.getByRole('link', { name: 'System settings' });
|
|
334
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
367
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/system-settings');
|
|
335
368
|
expect(link).toHaveAttribute('target', '_blank');
|
|
336
369
|
});
|
|
337
370
|
|
|
338
|
-
it('should
|
|
371
|
+
it('should remove the modern basename when building standalone settings hrefs', () => {
|
|
339
372
|
const items = getTopbarPluginSettingsItems({
|
|
340
373
|
canManagePlugins: false,
|
|
341
374
|
t: (key) => key,
|
|
@@ -354,7 +387,30 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
354
387
|
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
355
388
|
|
|
356
389
|
const link = screen.getByRole('link', { name: 'System settings' });
|
|
357
|
-
expect(link).toHaveAttribute('href', '/nocobase/
|
|
390
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/system-settings');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should preserve the new sub-app scope in standalone settings hrefs', () => {
|
|
394
|
+
const items = getTopbarPluginSettingsItems({
|
|
395
|
+
canManagePlugins: false,
|
|
396
|
+
t: (key) => key,
|
|
397
|
+
settings: [
|
|
398
|
+
{
|
|
399
|
+
key: 'system-settings',
|
|
400
|
+
name: 'system-settings',
|
|
401
|
+
title: 'System settings',
|
|
402
|
+
path: '/admin/settings/system-settings',
|
|
403
|
+
icon: null,
|
|
404
|
+
componentLoader: async () => null,
|
|
405
|
+
},
|
|
406
|
+
] as any,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
renderSettingsLabel((items as any[])[0].label, '/_app/a_new/admin/settings/system-settings');
|
|
410
|
+
|
|
411
|
+
const link = screen.getByRole('link', { name: 'System settings' });
|
|
412
|
+
expect(link).toHaveAttribute('href', '/nocobase/settings/_app/a_new/system-settings');
|
|
413
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
358
414
|
});
|
|
359
415
|
|
|
360
416
|
it('should keep external settings opening in a new tab', () => {
|
|
@@ -421,6 +477,34 @@ describe('TopbarActionsBar', () => {
|
|
|
421
477
|
vi.restoreAllMocks();
|
|
422
478
|
});
|
|
423
479
|
|
|
480
|
+
it('should group plugin settings with Help and user center', () => {
|
|
481
|
+
render(
|
|
482
|
+
<TopbarActionsBar
|
|
483
|
+
actions={[
|
|
484
|
+
createAction({ uid: 'notification', actionId: 'notification' }),
|
|
485
|
+
createAction({ uid: 'plugin-settings', actionId: 'plugin-settings' }),
|
|
486
|
+
createAction({ uid: 'user-center', actionId: 'user-center' }),
|
|
487
|
+
]}
|
|
488
|
+
/>,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const notification = screen.getByTestId('flow-model-notification');
|
|
492
|
+
const pluginSettings = screen.getByTestId('flow-model-plugin-settings');
|
|
493
|
+
const help = screen.getByTestId('help-lite');
|
|
494
|
+
const userCenter = screen.getByTestId('flow-model-user-center');
|
|
495
|
+
const mainGroup = notification.closest('.nb-topbar-actions-list');
|
|
496
|
+
const utilityGroup = pluginSettings.closest('.nb-topbar-utility-actions-list');
|
|
497
|
+
|
|
498
|
+
expect(mainGroup).toContainElement(notification);
|
|
499
|
+
expect(mainGroup).not.toContainElement(pluginSettings);
|
|
500
|
+
expect(utilityGroup).not.toBeNull();
|
|
501
|
+
expect(utilityGroup).toContainElement(pluginSettings);
|
|
502
|
+
expect(utilityGroup).toContainElement(help);
|
|
503
|
+
expect(utilityGroup).toContainElement(userCenter);
|
|
504
|
+
expect(pluginSettings.compareDocumentPosition(help) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
505
|
+
expect(help.compareDocumentPosition(userCenter) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
506
|
+
});
|
|
507
|
+
|
|
424
508
|
it('should keep HelpLite rendered when one action fails', () => {
|
|
425
509
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
426
510
|
|
|
@@ -130,7 +130,9 @@ export interface VariableFilterItemProps {
|
|
|
130
130
|
rightAsVariable?: boolean;
|
|
131
131
|
/**
|
|
132
132
|
* 右侧 VariableInput 的 metaTree 提供器;
|
|
133
|
-
*
|
|
133
|
+
* 默认使用视图上下文的 metaTree:useFlowViewContext().getPropertyMetaTree()。
|
|
134
|
+
* 注意它取自视图上下文而非 `model.context`:当调用方传入的 model 与打开设置弹窗的模型不是同一个时,
|
|
135
|
+
* 右侧变量不会跟随 model 变化,需要显式传入本属性。
|
|
134
136
|
*/
|
|
135
137
|
rightMetaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
136
138
|
/**
|
|
@@ -139,9 +141,16 @@ export interface VariableFilterItemProps {
|
|
|
139
141
|
rightVariableConverters?: Pick<Converters, 'resolvePathFromValue' | 'resolveValueFromPath'>;
|
|
140
142
|
ignoreFieldNames?: string[];
|
|
141
143
|
maxAssociationFieldDepth?: number;
|
|
144
|
+
/**
|
|
145
|
+
* 右侧变量树的关联层级上限,默认与 `maxAssociationFieldDepth` 一致。
|
|
146
|
+
* 传 `null` 表示不限制:右侧是变量树而非集合字段树,其深度可能已由调用方约束
|
|
147
|
+
* (例如工作流的变量树由触发器的「预加载关系数据」决定),此时再按左侧的层级上限裁剪
|
|
148
|
+
* 会让已配置好的深层变量选不到。
|
|
149
|
+
*/
|
|
150
|
+
rightMaxAssociationFieldDepth?: number | null;
|
|
142
151
|
}
|
|
143
152
|
|
|
144
|
-
function limitMetaTreeIfNeeded(nodes: MetaTreeNode[], maxAssociationFieldDepth?: number) {
|
|
153
|
+
function limitMetaTreeIfNeeded(nodes: MetaTreeNode[], maxAssociationFieldDepth?: number | null) {
|
|
145
154
|
if (typeof maxAssociationFieldDepth !== 'number') {
|
|
146
155
|
return nodes;
|
|
147
156
|
}
|
|
@@ -367,6 +376,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
367
376
|
rightVariableConverters,
|
|
368
377
|
ignoreFieldNames,
|
|
369
378
|
maxAssociationFieldDepth,
|
|
379
|
+
rightMaxAssociationFieldDepth = maxAssociationFieldDepth,
|
|
370
380
|
}) => {
|
|
371
381
|
// 使用 View 上下文,确保可访问 ctx.view 的异步子树
|
|
372
382
|
const ctx = useFlowViewContext();
|
|
@@ -685,10 +695,10 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
685
695
|
{ title: t('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
686
696
|
...nodes,
|
|
687
697
|
],
|
|
688
|
-
|
|
698
|
+
rightMaxAssociationFieldDepth,
|
|
689
699
|
);
|
|
690
700
|
};
|
|
691
|
-
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t,
|
|
701
|
+
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t, rightMaxAssociationFieldDepth]);
|
|
692
702
|
|
|
693
703
|
// 当启用右侧变量输入时,构造 VariableInput 的 converters:
|
|
694
704
|
// - 变量模式:返回 null 让 VariableInput 渲染 VariableTag
|