@nocobase/client-v2 2.1.0-beta.29 → 2.1.0-beta.32
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/BaseApplication.d.ts +1 -0
- package/es/PluginManager.d.ts +1 -0
- package/es/components/form/DrawerFormLayout.d.ts +49 -0
- package/es/components/form/EnvVariableInput.d.ts +42 -0
- package/es/components/form/FileSizeInput.d.ts +27 -0
- package/es/components/form/createFormRegistry.d.ts +33 -0
- package/es/components/form/index.d.ts +13 -0
- package/es/components/index.d.ts +1 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +1 -0
- package/es/flow/internal/utils/enumOptionsUtils.d.ts +5 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
- package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +3 -0
- package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +1 -1
- package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.mjs +122 -106
- package/es/utils/remotePlugins.d.ts +0 -4
- package/lib/index.js +121 -105
- package/package.json +6 -5
- package/src/BaseApplication.tsx +14 -8
- package/src/PluginManager.ts +1 -0
- package/src/__tests__/app.test.tsx +28 -1
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
- package/src/__tests__/remotePlugins.test.ts +29 -18
- package/src/__tests__/settings-center.test.tsx +30 -0
- package/src/components/form/DrawerFormLayout.tsx +103 -0
- package/src/components/form/EnvVariableInput.tsx +126 -0
- package/src/components/form/FileSizeInput.tsx +105 -0
- package/src/components/form/createFormRegistry.ts +60 -0
- package/src/components/form/index.tsx +14 -0
- package/src/components/index.ts +1 -1
- package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
- package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
- package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +316 -280
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
- package/src/flow/actions/setTargetDataScope.tsx +32 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
- package/src/flow/components/AdminLayout.tsx +2 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
- package/src/flow/components/FlowRoute.tsx +17 -4
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
- package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
- package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
- package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
- package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
- package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +6 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +6 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
- package/src/flow/models/blocks/table/TableColumnModel.tsx +3 -0
- package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +96 -1
- package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +7 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +4 -1
- package/src/flow/models/fields/ClickableFieldModel.tsx +9 -4
- package/src/flow/models/fields/DisplayTitleFieldModel.tsx +12 -4
- package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +23 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
- package/src/flow/models/utils/displayValueUtils.ts +57 -0
- package/src/flow/system-settings/useSystemSettings.tsx +36 -1
- package/src/utils/globalDeps.ts +2 -0
- package/src/utils/remotePlugins.ts +7 -27
|
@@ -8,15 +8,21 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { BaseApplication } from '../../../BaseApplication';
|
|
11
|
-
import { convertV2AdminPathToLegacy } from '../../../authRedirect';
|
|
12
11
|
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
|
|
13
12
|
|
|
14
13
|
export type AdminRouteNavigationMode = 'spa' | 'document';
|
|
14
|
+
export type AdminRouteRuntimeTargetReason =
|
|
15
|
+
| 'ok'
|
|
16
|
+
| 'missingSchemaUid'
|
|
17
|
+
| 'unsupportedV2Runtime'
|
|
18
|
+
| 'emptyGroup'
|
|
19
|
+
| 'unsupportedRouteType';
|
|
15
20
|
|
|
16
21
|
export type AdminRouteRuntimeTarget = {
|
|
17
22
|
runtimePath: string | null;
|
|
18
23
|
navigationMode: AdminRouteNavigationMode;
|
|
19
24
|
isLegacy: boolean;
|
|
25
|
+
reason: AdminRouteRuntimeTargetReason;
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
const V2_PUBLIC_PATH_SUFFIX = '/v2/';
|
|
@@ -45,6 +51,7 @@ const EMPTY_TARGET: AdminRouteRuntimeTarget = {
|
|
|
45
51
|
runtimePath: null,
|
|
46
52
|
navigationMode: 'spa',
|
|
47
53
|
isLegacy: false,
|
|
54
|
+
reason: 'unsupportedRouteType',
|
|
48
55
|
};
|
|
49
56
|
|
|
50
57
|
function normalizeRootRelativePath(pathname: string) {
|
|
@@ -60,14 +67,8 @@ function normalizePublicPath(value = '/') {
|
|
|
60
67
|
return normalized.endsWith('/') ? normalized : `${normalized}/`;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
*
|
|
66
|
-
* @param {ResolveAdminRouteRuntimeTargetOptions['app']} app 当前应用实例
|
|
67
|
-
* @returns {boolean} 是否启用 v2 到 v1 的经典页跳转
|
|
68
|
-
*/
|
|
69
|
-
function shouldUseLegacyDocumentNavigation(app: ResolveAdminRouteRuntimeTargetOptions['app']) {
|
|
70
|
-
return normalizePublicPath(app.getPublicPath()).endsWith(V2_PUBLIC_PATH_SUFFIX);
|
|
70
|
+
export function isV2AdminRuntime(app?: ResolveAdminRouteRuntimeTargetOptions['app']) {
|
|
71
|
+
return !!app?.getPublicPath && normalizePublicPath(app.getPublicPath()).endsWith(V2_PUBLIC_PATH_SUFFIX);
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function toRouterNavigationPath(pathname: string, basename?: string) {
|
|
@@ -114,12 +115,6 @@ function appendLocationState(pathname: string, location?: LocationLike) {
|
|
|
114
115
|
return `${pathname}${search}${hash}`;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function isSameOrDescendantPath(pathname: string, basePath: string) {
|
|
118
|
-
const normalizedPathname = normalizeRootRelativePath(pathname);
|
|
119
|
-
const normalizedBasePath = normalizeRootRelativePath(basePath);
|
|
120
|
-
return normalizedPathname === normalizedBasePath || normalizedPathname.startsWith(`${normalizedBasePath}/`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
118
|
function logInvalidTarget(
|
|
124
119
|
logger: ResolveAdminRouteRuntimeTargetOptions['log'],
|
|
125
120
|
reason: string,
|
|
@@ -134,6 +129,47 @@ function isSkippableRoute(route: NocoBaseDesktopRoute | undefined) {
|
|
|
134
129
|
);
|
|
135
130
|
}
|
|
136
131
|
|
|
132
|
+
export function isV2MenuRoute(route: NocoBaseDesktopRoute | undefined): boolean {
|
|
133
|
+
if (!route || route.hidden === true || route.hideInMenu === true) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (route.type === NocoBaseDesktopRouteType.flowPage || route.type === NocoBaseDesktopRouteType.link) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (route.type === NocoBaseDesktopRouteType.group) {
|
|
142
|
+
return Array.isArray(route.children) && route.children.some((child) => isV2MenuRoute(child));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function findFirstV2LandingRoute(routes: NocoBaseDesktopRoute[] | undefined): NocoBaseDesktopRoute | undefined {
|
|
149
|
+
if (!Array.isArray(routes)) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const route of routes) {
|
|
154
|
+
if (!route || route.hidden === true || route.hideInMenu === true || route.type === NocoBaseDesktopRouteType.tabs) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (route.type === NocoBaseDesktopRouteType.flowPage) {
|
|
159
|
+
return route;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (route.type === NocoBaseDesktopRouteType.group) {
|
|
163
|
+
const nested = findFirstV2LandingRoute(route.children);
|
|
164
|
+
if (nested) {
|
|
165
|
+
return nested;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
137
173
|
export function findFirstAccessiblePageRoute(
|
|
138
174
|
routes: NocoBaseDesktopRoute[] | undefined,
|
|
139
175
|
): NocoBaseDesktopRoute | undefined {
|
|
@@ -161,64 +197,44 @@ export function findFirstAccessiblePageRoute(
|
|
|
161
197
|
return undefined;
|
|
162
198
|
}
|
|
163
199
|
|
|
164
|
-
function resolvePageRuntimeTarget(
|
|
200
|
+
function resolvePageRuntimeTarget(
|
|
201
|
+
options: ResolveAdminRouteRuntimeTargetOptions,
|
|
202
|
+
route: NocoBaseDesktopRoute,
|
|
203
|
+
): AdminRouteRuntimeTarget {
|
|
165
204
|
const { app, preserveLocationState, location, log = console.warn } = options;
|
|
166
205
|
|
|
167
206
|
if (!route.schemaUid) {
|
|
168
207
|
logInvalidTarget(log, 'Missing schemaUid.', route);
|
|
169
|
-
return EMPTY_TARGET;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (route.type === NocoBaseDesktopRouteType.flowPage) {
|
|
173
208
|
return {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
isLegacy: false,
|
|
209
|
+
...EMPTY_TARGET,
|
|
210
|
+
reason: 'missingSchemaUid',
|
|
177
211
|
};
|
|
178
212
|
}
|
|
179
213
|
|
|
180
|
-
if (
|
|
214
|
+
if (route.type === NocoBaseDesktopRouteType.flowPage) {
|
|
181
215
|
return {
|
|
182
|
-
runtimePath:
|
|
183
|
-
preserveLocationState && location
|
|
184
|
-
? appendLocationState(getV2AdminPath(app, route.schemaUid), location)
|
|
185
|
-
: getV2AdminPath(app, route.schemaUid),
|
|
216
|
+
runtimePath: getV2AdminPath(app, route.schemaUid),
|
|
186
217
|
navigationMode: 'spa' as const,
|
|
187
218
|
isLegacy: false,
|
|
219
|
+
reason: 'ok' as const,
|
|
188
220
|
};
|
|
189
221
|
}
|
|
190
222
|
|
|
191
|
-
|
|
192
|
-
const legacyPath = convertV2AdminPathToLegacy(app, v2RuntimePath);
|
|
193
|
-
|
|
194
|
-
if (!legacyPath) {
|
|
195
|
-
logInvalidTarget(log, 'Failed to resolve legacy runtimePath.', route);
|
|
196
|
-
return EMPTY_TARGET;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (preserveLocationState && location) {
|
|
200
|
-
if (isSameOrDescendantPath(location.pathname, v2RuntimePath)) {
|
|
201
|
-
const correctedCurrentPath = convertV2AdminPathToLegacy(app, location);
|
|
202
|
-
if (correctedCurrentPath) {
|
|
203
|
-
return {
|
|
204
|
-
runtimePath: correctedCurrentPath,
|
|
205
|
-
navigationMode: 'document' as const,
|
|
206
|
-
isLegacy: true,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
223
|
+
if (isV2AdminRuntime(app)) {
|
|
211
224
|
return {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
isLegacy: true,
|
|
225
|
+
...EMPTY_TARGET,
|
|
226
|
+
reason: 'unsupportedV2Runtime',
|
|
215
227
|
};
|
|
216
228
|
}
|
|
217
229
|
|
|
218
230
|
return {
|
|
219
|
-
runtimePath:
|
|
220
|
-
|
|
221
|
-
|
|
231
|
+
runtimePath:
|
|
232
|
+
preserveLocationState && location
|
|
233
|
+
? appendLocationState(getV2AdminPath(app, route.schemaUid), location)
|
|
234
|
+
: getV2AdminPath(app, route.schemaUid),
|
|
235
|
+
navigationMode: 'spa' as const,
|
|
236
|
+
isLegacy: false,
|
|
237
|
+
reason: 'ok' as const,
|
|
222
238
|
};
|
|
223
239
|
}
|
|
224
240
|
|
|
@@ -242,9 +258,14 @@ export function resolveAdminRouteRuntimeTarget(
|
|
|
242
258
|
}
|
|
243
259
|
|
|
244
260
|
if (route.type === NocoBaseDesktopRouteType.group) {
|
|
245
|
-
const firstAccessibleRoute =
|
|
261
|
+
const firstAccessibleRoute = isV2AdminRuntime(options.app)
|
|
262
|
+
? findFirstV2LandingRoute(route.children)
|
|
263
|
+
: findFirstAccessiblePageRoute(route.children);
|
|
246
264
|
if (!firstAccessibleRoute) {
|
|
247
|
-
return
|
|
265
|
+
return {
|
|
266
|
+
...EMPTY_TARGET,
|
|
267
|
+
reason: 'emptyGroup',
|
|
268
|
+
};
|
|
248
269
|
}
|
|
249
270
|
return resolveAdminRouteRuntimeTarget({
|
|
250
271
|
...options,
|
|
@@ -14,7 +14,7 @@ import { useApp } from '../../hooks/useApp';
|
|
|
14
14
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
15
15
|
import { NocoBaseDesktopRouteType } from '../../flow-compat';
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
findFirstV2LandingRoute,
|
|
18
18
|
resolveAdminRouteRuntimeTarget,
|
|
19
19
|
toRouterNavigationPath,
|
|
20
20
|
} from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
|
|
@@ -92,7 +92,7 @@ const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ children })
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const firstAccessibleRoute =
|
|
95
|
+
const firstAccessibleRoute = findFirstV2LandingRoute(routeRepository?.listAccessible?.() || []);
|
|
96
96
|
if (!firstAccessibleRoute) {
|
|
97
97
|
if (active) {
|
|
98
98
|
setReady(true);
|
|
@@ -35,6 +35,7 @@ type CollectionFieldLike = {
|
|
|
35
35
|
title?: unknown;
|
|
36
36
|
type?: unknown;
|
|
37
37
|
interface?: unknown;
|
|
38
|
+
uiSchema?: unknown;
|
|
38
39
|
targetKey?: unknown;
|
|
39
40
|
targetCollectionTitleFieldName?: unknown;
|
|
40
41
|
targetCollection?: any;
|
|
@@ -194,6 +195,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
194
195
|
title,
|
|
195
196
|
type: String(f.type || 'string'),
|
|
196
197
|
interface: fieldInterface,
|
|
198
|
+
uiSchema: (f as any).uiSchema,
|
|
197
199
|
paths: [...basePaths, name],
|
|
198
200
|
};
|
|
199
201
|
|
|
@@ -15,10 +15,12 @@ import { useParams } from 'react-router-dom';
|
|
|
15
15
|
import { useApp } from '../../hooks/useApp';
|
|
16
16
|
import { NocoBaseDesktopRouteType } from '../../flow-compat';
|
|
17
17
|
import { resolveAdminRouteRuntimeTarget } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
|
|
18
|
+
import { AppNotFound } from '../../components';
|
|
18
19
|
|
|
19
20
|
type FlowRouteGuardState = {
|
|
20
21
|
pending: boolean;
|
|
21
22
|
allowBridge: boolean;
|
|
23
|
+
notFound: boolean;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
const BridgeFlowRoute = ({ pageUid }: { pageUid: string }) => {
|
|
@@ -91,6 +93,7 @@ const FlowRoute = () => {
|
|
|
91
93
|
const [guardState, setGuardState] = useState<FlowRouteGuardState>({
|
|
92
94
|
pending: true,
|
|
93
95
|
allowBridge: false,
|
|
96
|
+
notFound: false,
|
|
94
97
|
});
|
|
95
98
|
const replaceTriggeredRef = useRef(false);
|
|
96
99
|
const requestIdRef = useRef(0);
|
|
@@ -104,14 +107,14 @@ const FlowRoute = () => {
|
|
|
104
107
|
const requestId = ++requestIdRef.current;
|
|
105
108
|
|
|
106
109
|
const run = async () => {
|
|
107
|
-
setGuardState({ pending: true, allowBridge: false });
|
|
110
|
+
setGuardState({ pending: true, allowBridge: false, notFound: false });
|
|
108
111
|
|
|
109
112
|
if (!routeRepository?.isAccessibleLoaded?.()) {
|
|
110
113
|
try {
|
|
111
114
|
await routeRepository?.ensureAccessibleLoaded?.();
|
|
112
115
|
} catch (_error) {
|
|
113
116
|
if (active && requestId === requestIdRef.current) {
|
|
114
|
-
setGuardState({ pending: false, allowBridge: true });
|
|
117
|
+
setGuardState({ pending: false, allowBridge: true, notFound: false });
|
|
115
118
|
}
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
@@ -139,10 +142,17 @@ const FlowRoute = () => {
|
|
|
139
142
|
window.location.replace(target.runtimePath);
|
|
140
143
|
return;
|
|
141
144
|
}
|
|
145
|
+
|
|
146
|
+
if (target.reason === 'unsupportedV2Runtime') {
|
|
147
|
+
if (active && requestId === requestIdRef.current) {
|
|
148
|
+
setGuardState({ pending: false, allowBridge: false, notFound: true });
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
if (active && requestId === requestIdRef.current) {
|
|
145
|
-
setGuardState({ pending: false, allowBridge: true });
|
|
155
|
+
setGuardState({ pending: false, allowBridge: true, notFound: false });
|
|
146
156
|
}
|
|
147
157
|
};
|
|
148
158
|
|
|
@@ -159,11 +169,14 @@ const FlowRoute = () => {
|
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
if (!guardState.allowBridge) {
|
|
172
|
+
if (guardState.notFound) {
|
|
173
|
+
return <AppNotFound />;
|
|
174
|
+
}
|
|
162
175
|
return null;
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
return <BridgeFlowRoute pageUid={pageUid} />;
|
|
166
|
-
}, [guardState.allowBridge, guardState.pending, pageUid]);
|
|
179
|
+
}, [guardState.allowBridge, guardState.notFound, guardState.pending, pageUid]);
|
|
167
180
|
|
|
168
181
|
return content;
|
|
169
182
|
};
|
|
@@ -15,7 +15,7 @@ import type { MetaTreeNode } from '@nocobase/flow-engine';
|
|
|
15
15
|
import { FieldAssignRulesEditor, type FieldAssignRuleItem } from '../FieldAssignRulesEditor';
|
|
16
16
|
import { mergeItemMetaTreeForAssignValue } from '../FieldAssignValueInput';
|
|
17
17
|
|
|
18
|
-
const { mockFieldAssignValueInput } = vi.hoisted(() => ({
|
|
18
|
+
const { mockFieldAssignValueInput, mockConditionBuilder } = vi.hoisted(() => ({
|
|
19
19
|
mockFieldAssignValueInput: vi.fn((props: any) => (
|
|
20
20
|
<div
|
|
21
21
|
data-testid="mock-value-input"
|
|
@@ -25,6 +25,9 @@ const { mockFieldAssignValueInput } = vi.hoisted(() => ({
|
|
|
25
25
|
data-date-constant={props?.enableDateVariableAsConstant ? 'yes' : 'no'}
|
|
26
26
|
/>
|
|
27
27
|
)),
|
|
28
|
+
mockConditionBuilder: vi.fn((props: any) => (
|
|
29
|
+
<div data-testid="mock-condition-builder" data-extra={props?.extraMetaTree ? 'yes' : 'no'} />
|
|
30
|
+
)),
|
|
28
31
|
}));
|
|
29
32
|
|
|
30
33
|
vi.mock('../FieldAssignValueInput', async () => {
|
|
@@ -39,9 +42,7 @@ vi.mock('../ConditionBuilder', async () => {
|
|
|
39
42
|
const actual = await vi.importActual<typeof import('../ConditionBuilder')>('../ConditionBuilder');
|
|
40
43
|
return {
|
|
41
44
|
...actual,
|
|
42
|
-
ConditionBuilder:
|
|
43
|
-
<div data-testid="mock-condition-builder" data-extra={props?.extraMetaTree ? 'yes' : 'no'} />
|
|
44
|
-
),
|
|
45
|
+
ConditionBuilder: mockConditionBuilder,
|
|
45
46
|
};
|
|
46
47
|
});
|
|
47
48
|
|
|
@@ -55,6 +56,7 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
55
56
|
|
|
56
57
|
beforeEach(() => {
|
|
57
58
|
mockFieldAssignValueInput.mockClear();
|
|
59
|
+
mockConditionBuilder.mockClear();
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
const createAssociationFixture = () => {
|
|
@@ -574,6 +576,81 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
574
576
|
expect(getByTestId('mock-condition-builder').getAttribute('data-extra')).toBe('yes');
|
|
575
577
|
});
|
|
576
578
|
|
|
579
|
+
it('preserves uiSchema enum for current item attributes in condition extra tree', async () => {
|
|
580
|
+
const aaaField: any = {
|
|
581
|
+
name: 'AAA',
|
|
582
|
+
title: 'AAA',
|
|
583
|
+
type: 'array',
|
|
584
|
+
interface: 'multipleSelect',
|
|
585
|
+
isAssociationField: () => false,
|
|
586
|
+
uiSchema: {
|
|
587
|
+
enum: [
|
|
588
|
+
{ label: 'Test1', value: 'Test1' },
|
|
589
|
+
{ label: 'Test2', value: 'Test2' },
|
|
590
|
+
],
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
const roleNameField: any = {
|
|
594
|
+
name: 'name',
|
|
595
|
+
title: 'Role name',
|
|
596
|
+
type: 'string',
|
|
597
|
+
interface: 'input',
|
|
598
|
+
isAssociationField: () => false,
|
|
599
|
+
};
|
|
600
|
+
const rolesCollection = {
|
|
601
|
+
getField: (name: string) => (name === 'AAA' ? aaaField : name === 'name' ? roleNameField : null),
|
|
602
|
+
getFields: () => [roleNameField, aaaField],
|
|
603
|
+
};
|
|
604
|
+
const rolesField: any = {
|
|
605
|
+
name: 'roles',
|
|
606
|
+
title: 'Roles',
|
|
607
|
+
type: 'belongsToMany',
|
|
608
|
+
interface: 'm2m',
|
|
609
|
+
isAssociationField: () => true,
|
|
610
|
+
targetCollection: rolesCollection,
|
|
611
|
+
};
|
|
612
|
+
const rootCollection = {
|
|
613
|
+
getField: (name: string) => (name === 'roles' ? rolesField : null),
|
|
614
|
+
getFields: () => [rolesField],
|
|
615
|
+
};
|
|
616
|
+
const value: FieldAssignRuleItem[] = [
|
|
617
|
+
{
|
|
618
|
+
key: 'rule-roles-name',
|
|
619
|
+
enable: true,
|
|
620
|
+
targetPath: 'roles.name',
|
|
621
|
+
mode: 'assign',
|
|
622
|
+
condition: { logic: '$and', items: [] },
|
|
623
|
+
},
|
|
624
|
+
];
|
|
625
|
+
|
|
626
|
+
render(
|
|
627
|
+
wrap(
|
|
628
|
+
<FieldAssignRulesEditor t={t} fieldOptions={[]} rootCollection={rootCollection} value={value} showCondition />,
|
|
629
|
+
),
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const conditionProps = mockConditionBuilder.mock.calls[mockConditionBuilder.mock.calls.length - 1]?.[0];
|
|
633
|
+
const extraMetaTree = conditionProps?.extraMetaTree as MetaTreeNode[] | undefined;
|
|
634
|
+
expect(Array.isArray(extraMetaTree)).toBe(true);
|
|
635
|
+
|
|
636
|
+
const itemNode = extraMetaTree?.find((node) => node.name === 'item') as MetaTreeNode | undefined;
|
|
637
|
+
expect(itemNode).toBeTruthy();
|
|
638
|
+
|
|
639
|
+
const itemChildren = (Array.isArray(itemNode?.children) ? itemNode.children : []) as MetaTreeNode[];
|
|
640
|
+
const attributesNode = itemChildren.find((node) => node.name === 'value') as MetaTreeNode | undefined;
|
|
641
|
+
expect(attributesNode).toBeTruthy();
|
|
642
|
+
|
|
643
|
+
const attributeChildren =
|
|
644
|
+
typeof attributesNode?.children === 'function' ? await attributesNode.children() : attributesNode?.children ?? [];
|
|
645
|
+
const aaaNode = (attributeChildren as MetaTreeNode[]).find((node) => node.name === 'AAA') as any;
|
|
646
|
+
|
|
647
|
+
expect(aaaNode?.interface).toBe('multipleSelect');
|
|
648
|
+
expect(aaaNode?.uiSchema?.enum).toEqual([
|
|
649
|
+
{ label: 'Test1', value: 'Test1' },
|
|
650
|
+
{ label: 'Test2', value: 'Test2' },
|
|
651
|
+
]);
|
|
652
|
+
});
|
|
653
|
+
|
|
577
654
|
it('renders empty state when no items', () => {
|
|
578
655
|
const { container } = render(
|
|
579
656
|
wrap(<FieldAssignRulesEditor t={t} fieldOptions={[]} value={[]} showCondition={false} />),
|
|
@@ -22,7 +22,12 @@ import {
|
|
|
22
22
|
observer,
|
|
23
23
|
} from '@nocobase/flow-engine';
|
|
24
24
|
import { NumberPicker } from '@formily/antd-v5';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
enumToOptions,
|
|
27
|
+
normalizeSelectRenderValue,
|
|
28
|
+
translateOptions,
|
|
29
|
+
UiSchemaEnumItem,
|
|
30
|
+
} from '../../internal/utils/enumOptionsUtils';
|
|
26
31
|
import { mergeItemMetaTreeForAssignValue } from '../FieldAssignValueInput';
|
|
27
32
|
import { resolveOperatorComponent } from '../../internal/utils/operatorSchemaHelper';
|
|
28
33
|
|
|
@@ -82,7 +87,9 @@ function createStaticInputRenderer(
|
|
|
82
87
|
} else if (optionsFromEnum) {
|
|
83
88
|
finalProps = { ...finalProps, options: optionsFromEnum };
|
|
84
89
|
}
|
|
85
|
-
return
|
|
90
|
+
return (
|
|
91
|
+
<Select {...finalProps} {...rest} value={normalizeSelectRenderValue(value, finalProps)} onChange={onChange} />
|
|
92
|
+
);
|
|
86
93
|
}
|
|
87
94
|
if (xComponent === 'DateFilterDynamicComponent')
|
|
88
95
|
return <DateFilterDynamicComponentLazy {...commonProps} {...rest} value={value} onChange={onChange} />;
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from '@nocobase/flow-engine';
|
|
26
26
|
import _ from 'lodash';
|
|
27
27
|
import { NumberPicker } from '@formily/antd-v5';
|
|
28
|
-
import { enumToOptions, UiSchemaEnumItem } from '../../internal/utils/enumOptionsUtils';
|
|
28
|
+
import { enumToOptions, normalizeSelectRenderValue, UiSchemaEnumItem } from '../../internal/utils/enumOptionsUtils';
|
|
29
29
|
import { resolveOperatorComponent } from '../../internal/utils/operatorSchemaHelper';
|
|
30
30
|
|
|
31
31
|
const { DateFilterDynamicComponent: DateFilterDynamicComponentLazy } = lazy(
|
|
@@ -155,11 +155,7 @@ function createStaticInputRenderer(
|
|
|
155
155
|
options={selectOptions}
|
|
156
156
|
{...commonProps}
|
|
157
157
|
{...rest}
|
|
158
|
-
value={
|
|
159
|
-
Array.isArray(value) || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
160
|
-
? (value as unknown)
|
|
161
|
-
: undefined
|
|
162
|
-
}
|
|
158
|
+
value={normalizeSelectRenderValue(value, commonProps) as any}
|
|
163
159
|
onChange={(v) => onChange?.(v as unknown as VariableFilterItemValue['value'])}
|
|
164
160
|
/>
|
|
165
161
|
);
|
|
@@ -67,6 +67,9 @@ function createModel() {
|
|
|
67
67
|
return { model, app };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
const getRenderedSelectTexts = (container: HTMLElement) =>
|
|
71
|
+
Array.from(container.querySelectorAll('.ant-select-selection-item')).map((node) => (node.textContent || '').trim());
|
|
72
|
+
|
|
70
73
|
describe('LinkageFilterItem', () => {
|
|
71
74
|
beforeEach(() => {
|
|
72
75
|
document.body.innerHTML = '';
|
|
@@ -201,6 +204,74 @@ describe('LinkageFilterItem', () => {
|
|
|
201
204
|
});
|
|
202
205
|
});
|
|
203
206
|
|
|
207
|
+
it('does not render an empty selected option for constant single select', async () => {
|
|
208
|
+
const value = observable({ path: '', operator: '', value: '' }) as any;
|
|
209
|
+
const { model } = createModel();
|
|
210
|
+
|
|
211
|
+
(globalThis as any).__TEST_META__ = {
|
|
212
|
+
interface: 'select',
|
|
213
|
+
uiSchema: {
|
|
214
|
+
'x-component': 'Select',
|
|
215
|
+
enum: [{ label: 'Published', value: 'published' }],
|
|
216
|
+
'x-filter-operators': [
|
|
217
|
+
{
|
|
218
|
+
value: '$eq',
|
|
219
|
+
label: 'is',
|
|
220
|
+
selected: true,
|
|
221
|
+
schema: { 'x-component': 'Select' },
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
paths: ['collection', 'status'],
|
|
226
|
+
name: 'status',
|
|
227
|
+
title: 'Status',
|
|
228
|
+
type: 'string',
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const view = render(<LinkageFilterItem value={value} model={model} />);
|
|
232
|
+
fireEvent.click(screen.getByTestId('variable-input'));
|
|
233
|
+
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
expect(value.operator).toBe('$eq');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(getRenderedSelectTexts(view.container).filter((text) => text === '')).toHaveLength(0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('does not render an empty selected tag for constant multi select', async () => {
|
|
242
|
+
const value = observable({ path: '', operator: '', value: '' }) as any;
|
|
243
|
+
const { model } = createModel();
|
|
244
|
+
|
|
245
|
+
(globalThis as any).__TEST_META__ = {
|
|
246
|
+
interface: 'select',
|
|
247
|
+
uiSchema: {
|
|
248
|
+
'x-component': 'Select',
|
|
249
|
+
enum: [{ label: 'Published', value: 'published' }],
|
|
250
|
+
'x-filter-operators': [
|
|
251
|
+
{
|
|
252
|
+
value: '$in',
|
|
253
|
+
label: 'is any of',
|
|
254
|
+
selected: true,
|
|
255
|
+
schema: { 'x-component': 'Select', 'x-component-props': { mode: 'tags' } },
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
paths: ['collection', 'status'],
|
|
260
|
+
name: 'status',
|
|
261
|
+
title: 'Status',
|
|
262
|
+
type: 'string',
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const view = render(<LinkageFilterItem value={value} model={model} />);
|
|
266
|
+
fireEvent.click(screen.getByTestId('variable-input'));
|
|
267
|
+
|
|
268
|
+
await waitFor(() => {
|
|
269
|
+
expect(value.operator).toBe('$in');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(getRenderedSelectTexts(view.container).filter((text) => text === '')).toHaveLength(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
204
275
|
it('passes enum options to single select multi-value operators', async () => {
|
|
205
276
|
const value = observable({ path: '', operator: '', value: ['draft'] }) as any;
|
|
206
277
|
const { model, app } = createModel();
|
|
@@ -47,6 +47,9 @@ vi.mock('@nocobase/flow-engine', async () => {
|
|
|
47
47
|
return { ...actual, VariableInput: MockVariableInput };
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
const getRenderedSelectTexts = (root: ParentNode = document.body) =>
|
|
51
|
+
Array.from(root.querySelectorAll('.ant-select-selection-item')).map((node) => (node.textContent || '').trim());
|
|
52
|
+
|
|
50
53
|
function CreateModel() {
|
|
51
54
|
const engine = new FlowEngine();
|
|
52
55
|
const model = new FlowModel({ uid: 'm-variable-filter', flowEngine: engine });
|
|
@@ -262,6 +265,51 @@ describe('VariableFilterItem', () => {
|
|
|
262
265
|
delete (globalThis as any).__TEST_META__;
|
|
263
266
|
});
|
|
264
267
|
|
|
268
|
+
it('does not render an empty selected option for right constant select', async () => {
|
|
269
|
+
const value = observable({ path: '', operator: '', value: '' }) as any;
|
|
270
|
+
const model = CreateModel();
|
|
271
|
+
|
|
272
|
+
const prevMeta = (globalThis as any).__TEST_META__;
|
|
273
|
+
const prevPath = (globalThis as any).__TEST_PATH__;
|
|
274
|
+
(globalThis as any).__TEST_PATH__ = 'status';
|
|
275
|
+
(globalThis as any).__TEST_META__ = {
|
|
276
|
+
interface: 'select',
|
|
277
|
+
uiSchema: {
|
|
278
|
+
'x-component': 'Select',
|
|
279
|
+
enum: [{ label: 'Draft', value: 'draft' }],
|
|
280
|
+
'x-filter-operators': [
|
|
281
|
+
{
|
|
282
|
+
value: '$eq',
|
|
283
|
+
label: 'Equals',
|
|
284
|
+
selected: true,
|
|
285
|
+
schema: { 'x-component': 'Select' },
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
paths: ['collection', 'status'],
|
|
290
|
+
name: 'status',
|
|
291
|
+
title: 'Status',
|
|
292
|
+
type: 'string',
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
render(<VariableFilterItem value={value} model={model} rightAsVariable />);
|
|
296
|
+
fireEvent.click(screen.getAllByTestId('variable-input')[0]);
|
|
297
|
+
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
expect(value.operator).toBe('$eq');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const rightVariableInputProps = (globalThis as any).__LAST_VARIABLE_INPUT_PROPS__;
|
|
303
|
+
const Renderer = rightVariableInputProps?.converters?.renderInputComponent?.({ paths: ['constant'] });
|
|
304
|
+
expect(Renderer).toBeTruthy();
|
|
305
|
+
|
|
306
|
+
const rendered = render(<Renderer value="" onChange={vi.fn()} />);
|
|
307
|
+
expect(getRenderedSelectTexts(rendered.container).filter((text) => text === '')).toHaveLength(0);
|
|
308
|
+
|
|
309
|
+
(globalThis as any).__TEST_META__ = prevMeta;
|
|
310
|
+
(globalThis as any).__TEST_PATH__ = prevPath;
|
|
311
|
+
});
|
|
312
|
+
|
|
265
313
|
it('renders right VariableInput when rightAsVariable=true and hides it for noValue operator', async () => {
|
|
266
314
|
const value = observable({ path: '', operator: '', value: '' }) as any;
|
|
267
315
|
const model = CreateModel();
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect } from 'vitest';
|
|
11
|
-
import { enumToOptions, translateOptions } from '../enumOptionsUtils';
|
|
11
|
+
import { enumToOptions, getSelectedEnumLabels, translateOptions } from '../enumOptionsUtils';
|
|
12
12
|
|
|
13
13
|
// 一个极简的 t:
|
|
14
14
|
// - 直接返回传入 key;
|
|
@@ -55,4 +55,13 @@ describe('enumOptions utils', () => {
|
|
|
55
55
|
{ label: '否', value: false },
|
|
56
56
|
]);
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
it('getSelectedEnumLabels: keeps label for selected value missing from limited options', () => {
|
|
60
|
+
const labels = getSelectedEnumLabels('published', [
|
|
61
|
+
{ label: 'Draft', value: 'draft' },
|
|
62
|
+
{ label: 'Published', value: 'published' },
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
expect(labels).toEqual([{ label: 'Published', value: 'published' }]);
|
|
66
|
+
});
|
|
58
67
|
});
|