@nocobase/client-v2 2.2.0-beta.5 → 2.2.0-beta.7
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/PluginSettingsManager.d.ts +33 -0
- package/es/RouteRepository.d.ts +21 -6
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +145 -119
- package/lib/index.js +148 -122
- package/package.json +7 -7
- package/src/PluginSettingsManager.ts +53 -0
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/PluginSettingsManager.test.ts +13 -0
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/plugin-manager.test.tsx +44 -2
- package/src/__tests__/settings-center.test.tsx +40 -1
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/dataScopeFilter.ts +10 -0
- package/src/flow/actions/linkageRules.tsx +122 -34
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/components/fieldAssignOptions.ts +155 -27
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +12 -1
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
- package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
- package/src/flow/models/fields/InputFieldModel.tsx +14 -22
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- package/src/flow-compat/FieldValidation.tsx +122 -60
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +8 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/nocobase-buildin-plugin/index.tsx +0 -2
- package/src/settings-center/AdminSettingsLayout.tsx +3 -0
- package/src/settings-center/SystemSettingsPage.tsx +0 -1
- package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
- package/src/settings-center/plugin-manager/index.tsx +3 -0
- package/src/settings-center/utils.tsx +0 -6
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ElementProxy, tExpr
|
|
10
|
+
import { ElementProxy, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import React, { useEffect } from 'react';
|
|
12
12
|
import { FieldModel } from '../base/FieldModel';
|
|
13
13
|
import { resolveRunJsParams } from '../utils/resolveRunJsParams';
|
|
@@ -211,12 +211,7 @@ JSFieldModel.registerFlow({
|
|
|
211
211
|
get: () => ctx.model.props?.value,
|
|
212
212
|
cache: false,
|
|
213
213
|
});
|
|
214
|
-
|
|
215
|
-
await ctx.runjs(
|
|
216
|
-
code,
|
|
217
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
218
|
-
{ version },
|
|
219
|
-
);
|
|
214
|
+
await ctx.runjs(code, undefined, { version });
|
|
220
215
|
});
|
|
221
216
|
},
|
|
222
217
|
},
|
|
@@ -7,14 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
ElementProxy,
|
|
12
|
-
FormItem,
|
|
13
|
-
createSafeDocument,
|
|
14
|
-
createSafeWindow,
|
|
15
|
-
createSafeNavigator,
|
|
16
|
-
tExpr,
|
|
17
|
-
} from '@nocobase/flow-engine';
|
|
10
|
+
import { ElementProxy, FormItem, tExpr } from '@nocobase/flow-engine';
|
|
18
11
|
import React from 'react';
|
|
19
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
20
13
|
import { CommonItemModel } from '../base/CommonItemModel';
|
|
@@ -150,12 +143,7 @@ ctx.render(<JsItem />);
|
|
|
150
143
|
ctx.defineProperty('element', {
|
|
151
144
|
get: () => new ElementProxy(element),
|
|
152
145
|
});
|
|
153
|
-
|
|
154
|
-
await ctx.runjs(
|
|
155
|
-
code,
|
|
156
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
157
|
-
{ version },
|
|
158
|
-
);
|
|
146
|
+
await ctx.runjs(code, undefined, { version });
|
|
159
147
|
});
|
|
160
148
|
},
|
|
161
149
|
},
|
|
@@ -142,6 +142,23 @@ describe('InputFieldModel', () => {
|
|
|
142
142
|
expect(input.value).toBe('tét');
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
it('does not overwrite local replacement edit when parent advances to an older value', () => {
|
|
146
|
+
const onChange = vi.fn();
|
|
147
|
+
const createModel = (value: string) =>
|
|
148
|
+
createInputFieldModel({
|
|
149
|
+
value,
|
|
150
|
+
onChange,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const { rerender } = render(<>{createModel('').render()}</>);
|
|
154
|
+
const input = screen.getByRole('textbox') as HTMLInputElement;
|
|
155
|
+
|
|
156
|
+
fireEvent.change(input, { target: { value: 'té' } });
|
|
157
|
+
rerender(<>{createModel('t').render()}</>);
|
|
158
|
+
|
|
159
|
+
expect(input.value).toBe('té');
|
|
160
|
+
});
|
|
161
|
+
|
|
145
162
|
it('renders ScanInput when scan input is enabled', () => {
|
|
146
163
|
const model = createInputFieldModel({ enableScan: true });
|
|
147
164
|
|
|
@@ -14,8 +14,9 @@ import { Button, Dropdown, theme, Tooltip, type ButtonProps, type MenuProps } fr
|
|
|
14
14
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
15
15
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
16
16
|
import { useTranslation } from 'react-i18next';
|
|
17
|
-
import { Link } from 'react-router-dom';
|
|
17
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
18
18
|
import { useACLRoleContext } from '../../../acl';
|
|
19
|
+
import type { BaseApplication } from '../../../BaseApplication';
|
|
19
20
|
import type { PluginSettingsPageType } from '../../../PluginSettingsManager';
|
|
20
21
|
import { useApp } from '../../../hooks/useApp';
|
|
21
22
|
import {
|
|
@@ -64,6 +65,89 @@ const topbarActionTriggerClassName = css`
|
|
|
64
65
|
height: 100%;
|
|
65
66
|
`;
|
|
66
67
|
|
|
68
|
+
type TopbarSettingsAppLike = Pick<BaseApplication<any>, 'router' | 'getPublicPath'>;
|
|
69
|
+
|
|
70
|
+
const normalizeTopbarPath = (pathname?: string) => {
|
|
71
|
+
const trimmed = pathname?.trim();
|
|
72
|
+
if (!trimmed || trimmed === '/') {
|
|
73
|
+
return '/';
|
|
74
|
+
}
|
|
75
|
+
return `/${trimmed.replace(/^\/+/, '')}`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const normalizeTopbarBasePath = (pathname?: string) => {
|
|
79
|
+
const normalized = normalizeTopbarPath(pathname).replace(/\/+$/, '');
|
|
80
|
+
return normalized === '' || normalized === '/' ? '' : normalized;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getTopbarRouterBasePath = (app: TopbarSettingsAppLike | undefined) => {
|
|
84
|
+
return app?.router?.getBasename?.() || app?.router?.basename || app?.getPublicPath?.() || '';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const stripTopbarRouterBasePath = (pathname: string, basename?: string) => {
|
|
88
|
+
const normalizedPath = normalizeTopbarPath(pathname);
|
|
89
|
+
const normalizedBase = normalizeTopbarBasePath(basename);
|
|
90
|
+
|
|
91
|
+
if (!normalizedBase) {
|
|
92
|
+
return normalizedPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (normalizedPath === normalizedBase) {
|
|
96
|
+
return '/';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (normalizedPath.startsWith(`${normalizedBase}/`)) {
|
|
100
|
+
return normalizeTopbarPath(normalizedPath.slice(normalizedBase.length));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return normalizedPath;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const isAdminRuntimePath = (pathname: string) => {
|
|
107
|
+
return pathname === '/admin' || pathname.startsWith('/admin/');
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const buildTopbarDocumentHref = (targetPath: string, basename?: string) => {
|
|
111
|
+
const normalizedTarget = normalizeTopbarPath(targetPath);
|
|
112
|
+
const normalizedBase = normalizeTopbarBasePath(basename);
|
|
113
|
+
|
|
114
|
+
if (!normalizedBase || normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`)) {
|
|
115
|
+
return normalizedTarget;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return `${normalizedBase}${normalizedTarget}`;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function TopbarExternalSettingsLabel(props: { title: React.ReactNode; link: string }) {
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
onClick={() => {
|
|
125
|
+
window.open(props.link, '_blank', 'noopener,noreferrer');
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{props.title}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function TopbarInternalSettingsLabel(props: { title: React.ReactNode; path?: string }) {
|
|
134
|
+
const app = useApp();
|
|
135
|
+
const location = useLocation();
|
|
136
|
+
const targetPath = props.path || '/admin/settings';
|
|
137
|
+
const basename = getTopbarRouterBasePath(app);
|
|
138
|
+
const currentPath = stripTopbarRouterBasePath(location.pathname, basename);
|
|
139
|
+
|
|
140
|
+
if (isAdminRuntimePath(currentPath)) {
|
|
141
|
+
return <Link to={targetPath}>{props.title}</Link>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<a href={buildTopbarDocumentHref(targetPath, basename)} target="_blank" rel="noopener noreferrer">
|
|
146
|
+
{props.title}
|
|
147
|
+
</a>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
67
151
|
/**
|
|
68
152
|
* 将 settings 顶部菜单转换成右上角 dropdown items。
|
|
69
153
|
*
|
|
@@ -112,15 +196,9 @@ export function getTopbarPluginSettingsItems(options: {
|
|
|
112
196
|
title: targetTitle,
|
|
113
197
|
icon: item.icon,
|
|
114
198
|
label: targetLink ? (
|
|
115
|
-
<
|
|
116
|
-
onClick={() => {
|
|
117
|
-
window.open(targetLink, '_blank', 'noopener,noreferrer');
|
|
118
|
-
}}
|
|
119
|
-
>
|
|
120
|
-
{targetTitle}
|
|
121
|
-
</div>
|
|
199
|
+
<TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
|
|
122
200
|
) : (
|
|
123
|
-
<
|
|
201
|
+
<TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
|
|
124
202
|
),
|
|
125
203
|
};
|
|
126
204
|
});
|
|
@@ -131,7 +209,12 @@ export function getTopbarPluginSettingsItems(options: {
|
|
|
131
209
|
items.push({
|
|
132
210
|
key: pluginManagerSetting.key,
|
|
133
211
|
icon: pluginManagerSetting.icon || <ApiOutlined />,
|
|
134
|
-
label:
|
|
212
|
+
label: (
|
|
213
|
+
<TopbarInternalSettingsLabel
|
|
214
|
+
title={pluginManagerSetting.title || t('Plugin manager')}
|
|
215
|
+
path={pluginManagerSetting.path}
|
|
216
|
+
/>
|
|
217
|
+
),
|
|
135
218
|
});
|
|
136
219
|
}
|
|
137
220
|
|
|
@@ -18,7 +18,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
18
18
|
import { FIELDS_VALIDATION_OPTIONS, REQUIRED_RULE_KEY } from './fieldValidationConstants';
|
|
19
19
|
|
|
20
20
|
interface ValidationRule {
|
|
21
|
-
key
|
|
21
|
+
key?: string;
|
|
22
22
|
name: string;
|
|
23
23
|
args?: {
|
|
24
24
|
[key: string]: any;
|
|
@@ -34,14 +34,23 @@ interface ValidationData {
|
|
|
34
34
|
interface FieldValidationProps {
|
|
35
35
|
value?: ValidationData;
|
|
36
36
|
onChange?: (value: ValidationData) => void;
|
|
37
|
+
inheritedValue?: ValidationData;
|
|
37
38
|
type?: string;
|
|
38
39
|
availableValidationOptions?: string[];
|
|
39
40
|
excludeValidationOptions?: string[];
|
|
40
41
|
isAssociation?: boolean;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function formatFallbackRuleLabel(name: string) {
|
|
45
|
+
if (!name) {
|
|
46
|
+
return name;
|
|
47
|
+
}
|
|
48
|
+
return `${name.slice(0, 1).toUpperCase()}${name.slice(1)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
44
|
-
const { value, onChange, type, availableValidationOptions, excludeValidationOptions, isAssociation } =
|
|
52
|
+
const { value, onChange, inheritedValue, type, availableValidationOptions, excludeValidationOptions, isAssociation } =
|
|
53
|
+
props;
|
|
45
54
|
const { t } = useTranslation();
|
|
46
55
|
const { token } = theme.useToken();
|
|
47
56
|
const noCascadeCls = css`
|
|
@@ -57,8 +66,9 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
57
66
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
|
58
67
|
const formilyField: any = useField?.() as any;
|
|
59
68
|
|
|
60
|
-
const validationType = value?.type || type;
|
|
61
|
-
const rules = value?.rules || [];
|
|
69
|
+
const validationType = value?.type || inheritedValue?.type || type;
|
|
70
|
+
const rules = useMemo(() => value?.rules || [], [value?.rules]);
|
|
71
|
+
const inheritedRules = useMemo(() => inheritedValue?.rules || [], [inheritedValue?.rules]);
|
|
62
72
|
|
|
63
73
|
const normalizeArgsForDomainLike = useCallback((ruleName: string, args: Record<string, any> | undefined) => {
|
|
64
74
|
if (!args) return args;
|
|
@@ -163,19 +173,19 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
163
173
|
let hasError = false;
|
|
164
174
|
const invalidRuleKeys: string[] = [];
|
|
165
175
|
try {
|
|
166
|
-
rules.forEach((rule) => {
|
|
176
|
+
rules.forEach((rule, index) => {
|
|
167
177
|
const opt = validationOptions.find((o) => o.key === rule.name);
|
|
168
178
|
if (!opt || !opt.params) return;
|
|
169
179
|
let ruleInvalid = false;
|
|
170
180
|
opt.params.forEach((param) => {
|
|
171
181
|
if (!param.required) return;
|
|
172
|
-
const currentValue =
|
|
173
|
-
if (isValueEmpty(param.componentType
|
|
182
|
+
const currentValue = rule.args?.[param.key] ?? param.defaultValue;
|
|
183
|
+
if (isValueEmpty(param.componentType, currentValue)) {
|
|
174
184
|
hasError = true;
|
|
175
185
|
ruleInvalid = true;
|
|
176
186
|
}
|
|
177
187
|
});
|
|
178
|
-
if (ruleInvalid) invalidRuleKeys.push(rule
|
|
188
|
+
if (ruleInvalid) invalidRuleKeys.push(getRuleKey(rule, index, 'ui'));
|
|
179
189
|
});
|
|
180
190
|
} catch (err) {
|
|
181
191
|
hasError = true;
|
|
@@ -198,17 +208,18 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
198
208
|
|
|
199
209
|
const handleAddRule = (ruleType: string) => {
|
|
200
210
|
const option = validationOptions.find((opt) => opt.key === ruleType);
|
|
211
|
+
const args: Record<string, any> = {};
|
|
201
212
|
const newRule: ValidationRule = {
|
|
202
213
|
key: `r_${Date.now()}`,
|
|
203
214
|
name: ruleType,
|
|
204
|
-
args
|
|
215
|
+
args,
|
|
205
216
|
paramsType: option?.paramsType,
|
|
206
217
|
};
|
|
207
218
|
|
|
208
219
|
if (option?.params) {
|
|
209
220
|
option.params.forEach((param) => {
|
|
210
221
|
if (param.defaultValue !== undefined) {
|
|
211
|
-
|
|
222
|
+
args[param.key] = param.defaultValue;
|
|
212
223
|
}
|
|
213
224
|
});
|
|
214
225
|
}
|
|
@@ -225,7 +236,7 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
225
236
|
};
|
|
226
237
|
|
|
227
238
|
const handleRemoveRule = (ruleKey: string) => {
|
|
228
|
-
const newRules = rules.filter((rule) => rule
|
|
239
|
+
const newRules = rules.filter((rule, index) => getRuleKey(rule, index, 'ui') !== ruleKey);
|
|
229
240
|
onChange?.({
|
|
230
241
|
type: validationType,
|
|
231
242
|
rules: newRules,
|
|
@@ -233,8 +244,8 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
233
244
|
};
|
|
234
245
|
|
|
235
246
|
const handleRuleValueChange = (ruleKey: string, argKey: string, newValue: any) => {
|
|
236
|
-
const newRules = rules.map((rule) => {
|
|
237
|
-
if (rule
|
|
247
|
+
const newRules = rules.map((rule, index) => {
|
|
248
|
+
if (getRuleKey(rule, index, 'ui') === ruleKey) {
|
|
238
249
|
let nextArgs = { ...rule.args, [argKey]: newValue } as Record<string, any>;
|
|
239
250
|
nextArgs = normalizeArgsForDomainLike(rule.name, nextArgs);
|
|
240
251
|
const option = validationOptions.find((item) => item.key === rule.name);
|
|
@@ -261,7 +272,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
261
272
|
}
|
|
262
273
|
};
|
|
263
274
|
|
|
264
|
-
const
|
|
275
|
+
const getRuleKey = (rule: ValidationRule, index: number, source: 'field' | 'ui') =>
|
|
276
|
+
rule.key || `${source}-${rule.name}-${index}`;
|
|
277
|
+
|
|
278
|
+
const renderValidationForm = (rule: ValidationRule, ruleKey: string, readOnly: boolean) => {
|
|
265
279
|
const option = validationOptions.find((opt) => opt.key === rule.name);
|
|
266
280
|
if (!option) return null;
|
|
267
281
|
|
|
@@ -282,7 +296,7 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
282
296
|
) {
|
|
283
297
|
currentValue = deriveTldsModeFromArgs(rule.args);
|
|
284
298
|
}
|
|
285
|
-
const isInvalid = !!param.required && isValueEmpty(param.componentType
|
|
299
|
+
const isInvalid = !readOnly && !!param.required && isValueEmpty(param.componentType, currentValue);
|
|
286
300
|
|
|
287
301
|
return (
|
|
288
302
|
<Form.Item
|
|
@@ -296,33 +310,35 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
296
310
|
>
|
|
297
311
|
{param.componentType === 'checkbox' ? (
|
|
298
312
|
<Checkbox
|
|
313
|
+
disabled={readOnly}
|
|
299
314
|
checked={currentValue || false}
|
|
300
|
-
onChange={(e) => handleRuleValueChange(
|
|
315
|
+
onChange={(e) => handleRuleValueChange(ruleKey, param.key, e.target.checked)}
|
|
301
316
|
>
|
|
302
317
|
{t(param.label)}
|
|
303
318
|
</Checkbox>
|
|
304
319
|
) : param.componentType === 'radio' ? (
|
|
305
320
|
<div>
|
|
306
321
|
<Radio.Group
|
|
322
|
+
disabled={readOnly}
|
|
307
323
|
value={currentValue}
|
|
308
324
|
onChange={(e) => {
|
|
309
325
|
if (param.key === 'tlds' && (option.key === 'email' || option.key === 'domain')) {
|
|
310
326
|
const val = e.target.value as 'iana' | 'disable' | 'allow' | 'deny';
|
|
311
327
|
if (val === 'iana') {
|
|
312
|
-
handleRuleValueChange(
|
|
328
|
+
handleRuleValueChange(ruleKey, 'tlds', { allow: true });
|
|
313
329
|
} else if (val === 'disable') {
|
|
314
|
-
handleRuleValueChange(
|
|
330
|
+
handleRuleValueChange(ruleKey, 'tlds', false);
|
|
315
331
|
} else if (val === 'allow') {
|
|
316
332
|
const prev = rule.args?.tlds;
|
|
317
333
|
const arr = Array.isArray(prev?.allow) ? prev.allow : [];
|
|
318
|
-
handleRuleValueChange(
|
|
334
|
+
handleRuleValueChange(ruleKey, 'tlds', { allow: arr });
|
|
319
335
|
} else if (val === 'deny') {
|
|
320
336
|
const prev = rule.args?.tlds;
|
|
321
337
|
const arr = Array.isArray(prev?.deny) ? prev.deny : [];
|
|
322
|
-
handleRuleValueChange(
|
|
338
|
+
handleRuleValueChange(ruleKey, 'tlds', { deny: arr });
|
|
323
339
|
}
|
|
324
340
|
} else {
|
|
325
|
-
handleRuleValueChange(
|
|
341
|
+
handleRuleValueChange(ruleKey, param.key, e.target.value);
|
|
326
342
|
}
|
|
327
343
|
}}
|
|
328
344
|
>
|
|
@@ -349,6 +365,7 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
349
365
|
: [];
|
|
350
366
|
return (
|
|
351
367
|
<Select
|
|
368
|
+
disabled={readOnly}
|
|
352
369
|
mode="tags"
|
|
353
370
|
value={arrValue}
|
|
354
371
|
onChange={(vals) => {
|
|
@@ -356,9 +373,9 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
356
373
|
new Set((vals as string[]).map((s) => String(s).trim()).filter(Boolean)),
|
|
357
374
|
);
|
|
358
375
|
if (item.value === 'allow') {
|
|
359
|
-
handleRuleValueChange(
|
|
376
|
+
handleRuleValueChange(ruleKey, 'tlds', { allow: cleaned });
|
|
360
377
|
} else {
|
|
361
|
-
handleRuleValueChange(
|
|
378
|
+
handleRuleValueChange(ruleKey, 'tlds', { deny: cleaned });
|
|
362
379
|
}
|
|
363
380
|
}}
|
|
364
381
|
tokenSeparators={[',', ' ', '\n', '\t']}
|
|
@@ -370,9 +387,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
370
387
|
|
|
371
388
|
return (
|
|
372
389
|
<Input
|
|
390
|
+
disabled={readOnly}
|
|
373
391
|
value=""
|
|
374
392
|
onChange={(e) =>
|
|
375
|
-
handleRuleValueChange(
|
|
393
|
+
handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, e.target.value)
|
|
376
394
|
}
|
|
377
395
|
style={{ width: '100%' }}
|
|
378
396
|
/>
|
|
@@ -380,30 +398,33 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
380
398
|
})()
|
|
381
399
|
) : item.componentType === 'inputNumber' ? (
|
|
382
400
|
<InputNumber
|
|
401
|
+
disabled={readOnly}
|
|
383
402
|
precision={allowDecimalInputNumber ? undefined : 0}
|
|
384
403
|
step={allowDecimalInputNumber ? 0.1 : 1}
|
|
385
404
|
value={rule.args?.[`${param.key}_${item.value}`] || ''}
|
|
386
|
-
onChange={(val) => handleRuleValueChange(
|
|
405
|
+
onChange={(val) => handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, val)}
|
|
387
406
|
style={{ width: '100%' }}
|
|
388
407
|
/>
|
|
389
408
|
) : item.componentType === 'checkbox' ? (
|
|
390
409
|
<Checkbox
|
|
410
|
+
disabled={readOnly}
|
|
391
411
|
checked={rule.args?.[`${param.key}_${item.value}`] || false}
|
|
392
412
|
onChange={(e) =>
|
|
393
|
-
handleRuleValueChange(
|
|
413
|
+
handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, e.target.checked)
|
|
394
414
|
}
|
|
395
415
|
>
|
|
396
416
|
{t(item.label)}
|
|
397
417
|
</Checkbox>
|
|
398
418
|
) : item.componentType === 'datePicker' ? (
|
|
399
419
|
<DatePicker
|
|
420
|
+
disabled={readOnly}
|
|
400
421
|
value={
|
|
401
422
|
rule.args?.[`${param.key}_${item.value}`]
|
|
402
423
|
? dayjs(rule.args[`${param.key}_${item.value}`])
|
|
403
424
|
: null
|
|
404
425
|
}
|
|
405
426
|
onChange={(date) =>
|
|
406
|
-
handleRuleValueChange(
|
|
427
|
+
handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, date?.toISOString())
|
|
407
428
|
}
|
|
408
429
|
placeholder={t('Select date')}
|
|
409
430
|
style={{ width: '100%' }}
|
|
@@ -417,8 +438,9 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
417
438
|
</div>
|
|
418
439
|
) : param.componentType === 'singleSelect' ? (
|
|
419
440
|
<Select
|
|
441
|
+
disabled={readOnly}
|
|
420
442
|
value={currentValue}
|
|
421
|
-
onChange={(val) => handleRuleValueChange(
|
|
443
|
+
onChange={(val) => handleRuleValueChange(ruleKey, param.key, val)}
|
|
422
444
|
placeholder={t('Please select')}
|
|
423
445
|
style={{ width: '100%', height: '100%' }}
|
|
424
446
|
allowClear
|
|
@@ -426,9 +448,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
426
448
|
/>
|
|
427
449
|
) : param.componentType === 'multipleSelect' ? (
|
|
428
450
|
<Select
|
|
451
|
+
disabled={readOnly}
|
|
429
452
|
mode="multiple"
|
|
430
453
|
value={currentValue}
|
|
431
|
-
onChange={(val) => handleRuleValueChange(
|
|
454
|
+
onChange={(val) => handleRuleValueChange(ruleKey, param.key, val)}
|
|
432
455
|
placeholder={t('Please select')}
|
|
433
456
|
style={{ width: '100%' }}
|
|
434
457
|
allowClear
|
|
@@ -436,24 +459,27 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
436
459
|
/>
|
|
437
460
|
) : param.componentType === 'inputNumber' ? (
|
|
438
461
|
<InputNumber
|
|
462
|
+
disabled={readOnly}
|
|
439
463
|
precision={allowDecimalInputNumber ? undefined : 0}
|
|
440
464
|
step={allowDecimalInputNumber ? 0.1 : 1}
|
|
441
465
|
value={currentValue !== undefined ? currentValue : ''}
|
|
442
|
-
onChange={(val) => handleRuleValueChange(
|
|
466
|
+
onChange={(val) => handleRuleValueChange(ruleKey, param.key, val)}
|
|
443
467
|
placeholder={t('Enter value')}
|
|
444
468
|
style={{ width: '100%' }}
|
|
445
469
|
/>
|
|
446
470
|
) : param.componentType === 'datePicker' ? (
|
|
447
471
|
<DatePicker
|
|
472
|
+
disabled={readOnly}
|
|
448
473
|
value={currentValue ? dayjs(currentValue) : null}
|
|
449
|
-
onChange={(date) => handleRuleValueChange(
|
|
474
|
+
onChange={(date) => handleRuleValueChange(ruleKey, param.key, date?.toISOString())}
|
|
450
475
|
placeholder={t('Select date')}
|
|
451
476
|
style={{ width: '100%' }}
|
|
452
477
|
/>
|
|
453
478
|
) : (
|
|
454
479
|
<Input
|
|
480
|
+
disabled={readOnly}
|
|
455
481
|
value={currentValue !== undefined ? currentValue : ''}
|
|
456
|
-
onChange={(e) => handleRuleValueChange(
|
|
482
|
+
onChange={(e) => handleRuleValueChange(ruleKey, param.key, e.target.value)}
|
|
457
483
|
placeholder={t('Enter value')}
|
|
458
484
|
/>
|
|
459
485
|
)}
|
|
@@ -465,40 +491,61 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
465
491
|
};
|
|
466
492
|
|
|
467
493
|
const menuItems: MenuProps['items'] = useMemo(() => {
|
|
468
|
-
const addedRuleNames = new Set(rules.map((rule) => rule.name));
|
|
494
|
+
const addedRuleNames = new Set([...inheritedRules, ...rules].map((rule) => rule.name));
|
|
469
495
|
return validationOptions
|
|
470
496
|
.filter((option) => !addedRuleNames.has(option.key))
|
|
471
497
|
.map((option) => ({
|
|
472
498
|
key: option.key,
|
|
473
499
|
label: t(option.label),
|
|
474
500
|
}));
|
|
475
|
-
}, [rules, t, validationOptions]);
|
|
501
|
+
}, [inheritedRules, rules, t, validationOptions]);
|
|
476
502
|
|
|
477
503
|
const menu: MenuProps = {
|
|
478
504
|
items: menuItems,
|
|
479
505
|
onClick: ({ key }) => handleAddRule(key as string),
|
|
480
506
|
};
|
|
481
507
|
|
|
482
|
-
|
|
483
|
-
<div
|
|
484
|
-
{
|
|
508
|
+
const renderSectionTitle = (title: string, extraStyle?: React.CSSProperties) => (
|
|
509
|
+
<div
|
|
510
|
+
style={{
|
|
511
|
+
alignItems: 'center',
|
|
512
|
+
color: token.colorTextSecondary,
|
|
513
|
+
display: 'flex',
|
|
514
|
+
fontSize: token.fontSize,
|
|
515
|
+
fontWeight: 500,
|
|
516
|
+
lineHeight: token.lineHeight,
|
|
517
|
+
marginBottom: token.marginXS,
|
|
518
|
+
...extraStyle,
|
|
519
|
+
}}
|
|
520
|
+
>
|
|
521
|
+
{title}
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const renderRuleList = (dataSource: ValidationRule[], readOnly: boolean, source: 'field' | 'ui') => {
|
|
526
|
+
if (!dataSource.length) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<div style={{ marginBottom: token.marginSM }}>
|
|
485
532
|
<List
|
|
486
533
|
size="small"
|
|
487
534
|
style={{
|
|
488
|
-
backgroundColor: token.colorFillAlter,
|
|
489
|
-
border: `1px solid ${token.colorBorder}`,
|
|
535
|
+
backgroundColor: readOnly ? token.colorFillQuaternary : token.colorFillAlter,
|
|
536
|
+
border: `1px solid ${readOnly ? token.colorBorderSecondary : token.colorBorder}`,
|
|
490
537
|
borderRadius: token.borderRadius,
|
|
491
|
-
marginBottom: token.marginSM,
|
|
492
538
|
}}
|
|
493
|
-
dataSource={
|
|
494
|
-
renderItem={(rule) => {
|
|
539
|
+
dataSource={dataSource}
|
|
540
|
+
renderItem={(rule, index) => {
|
|
541
|
+
const ruleKey = getRuleKey(rule, index, source);
|
|
495
542
|
const option = validationOptions.find((opt) => opt.key === rule.name);
|
|
496
|
-
const ruleLabel = option ? t(option.label) : rule.name;
|
|
543
|
+
const ruleLabel = option ? t(option.label) : formatFallbackRuleLabel(rule.name);
|
|
497
544
|
const hasParams = option?.hasValue && option.params.length > 0;
|
|
498
|
-
const isExpanded = expandedKeys.includes(
|
|
545
|
+
const isExpanded = expandedKeys.includes(ruleKey);
|
|
499
546
|
|
|
500
547
|
return (
|
|
501
|
-
<List.Item style={{ display: 'block', padding: 0 }}>
|
|
548
|
+
<List.Item key={ruleKey} style={{ display: 'block', padding: 0 }}>
|
|
502
549
|
<div
|
|
503
550
|
style={{
|
|
504
551
|
display: 'flex',
|
|
@@ -506,10 +553,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
506
553
|
alignItems: 'center',
|
|
507
554
|
padding: `${token.paddingXS}px ${token.paddingSM}px`,
|
|
508
555
|
cursor: hasParams ? 'pointer' : 'default',
|
|
509
|
-
backgroundColor: token.colorBgContainer,
|
|
556
|
+
backgroundColor: readOnly ? token.colorFillQuaternary : token.colorBgContainer,
|
|
510
557
|
borderRadius: 6,
|
|
511
558
|
}}
|
|
512
|
-
onClick={() => hasParams && handleToggleExpand(
|
|
559
|
+
onClick={() => hasParams && handleToggleExpand(ruleKey)}
|
|
513
560
|
>
|
|
514
561
|
<div style={{ display: 'flex', alignItems: 'center', gap: token.marginXS }}>
|
|
515
562
|
<div
|
|
@@ -533,34 +580,49 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
533
580
|
)}
|
|
534
581
|
</span>
|
|
535
582
|
</div>
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
e
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
583
|
+
{!readOnly && (
|
|
584
|
+
<Button
|
|
585
|
+
type="text"
|
|
586
|
+
size="small"
|
|
587
|
+
icon={<DeleteOutlined />}
|
|
588
|
+
onClick={(e) => {
|
|
589
|
+
e.stopPropagation();
|
|
590
|
+
handleRemoveRule(ruleKey);
|
|
591
|
+
}}
|
|
592
|
+
style={{ color: token.colorTextSecondary }}
|
|
593
|
+
/>
|
|
594
|
+
)}
|
|
546
595
|
</div>
|
|
547
596
|
{hasParams && isExpanded && (
|
|
548
597
|
<div
|
|
549
598
|
style={{
|
|
550
599
|
padding: token.paddingSM,
|
|
551
|
-
backgroundColor: token.colorBgContainer,
|
|
552
|
-
borderTop: `1px solid ${token.colorBorder}`,
|
|
600
|
+
backgroundColor: readOnly ? token.colorFillQuaternary : token.colorBgContainer,
|
|
601
|
+
borderTop: `1px solid ${readOnly ? token.colorBorderSecondary : token.colorBorder}`,
|
|
553
602
|
borderRadius: '0 0 6px 6px',
|
|
554
603
|
}}
|
|
555
604
|
>
|
|
556
|
-
{renderValidationForm(rule)}
|
|
605
|
+
{renderValidationForm(rule, ruleKey, readOnly)}
|
|
557
606
|
</div>
|
|
558
607
|
)}
|
|
559
608
|
</List.Item>
|
|
560
609
|
);
|
|
561
610
|
}}
|
|
562
611
|
/>
|
|
612
|
+
</div>
|
|
613
|
+
);
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
return (
|
|
617
|
+
<div className={noCascadeCls} style={{ marginBottom: token.marginLG }}>
|
|
618
|
+
{inheritedRules.length > 0 && (
|
|
619
|
+
<>
|
|
620
|
+
{renderSectionTitle(t('Server-side field validation rules'))}
|
|
621
|
+
{renderRuleList(inheritedRules, true, 'field')}
|
|
622
|
+
{renderSectionTitle(t('Client-side validation rules'), { marginTop: token.margin })}
|
|
623
|
+
</>
|
|
563
624
|
)}
|
|
625
|
+
{renderRuleList(rules, false, 'ui')}
|
|
564
626
|
|
|
565
627
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
566
628
|
<div>
|