@nocobase/client-v2 2.2.0-beta.8 → 2.3.0-alpha.1
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/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +3 -0
- package/es/RouteRepository.d.ts +8 -0
- package/es/RouterManager.d.ts +15 -0
- package/es/authRedirect.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -1
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +1 -0
- package/es/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +6 -0
- package/es/index.mjs +279 -158
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +280 -159
- package/lib/locale/languageCodes.js +2 -1
- package/package.json +8 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +13 -6
- package/src/RouteRepository.ts +25 -0
- package/src/RouterManager.tsx +142 -1
- package/src/__tests__/RouteRepository.test.ts +23 -0
- package/src/__tests__/RouterManager.test.ts +125 -0
- package/src/__tests__/app.test.tsx +73 -0
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
- package/src/authRedirect.ts +38 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +23 -6
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +180 -3
- package/src/components/form/ScanInput/useCodeScanner.ts +170 -5
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +1 -0
- package/src/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/FlowPage.tsx +38 -3
- package/src/flow/__tests__/FlowPage.test.tsx +201 -27
- package/src/flow/__tests__/FlowRoute.test.tsx +549 -4
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/dateTimeFormat.tsx +2 -2
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +86 -11
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/actions/openView.tsx +21 -1
- package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -5
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
- package/src/flow/common/Markdown/Display.tsx +14 -3
- package/src/flow/common/Markdown/Edit.tsx +19 -7
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +91 -20
- package/src/flow/components/FlowRoute.tsx +134 -18
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +1 -0
- package/src/flow/internal/components/Markdown/util.ts +2 -1
- package/src/flow/models/base/ActionModel.tsx +10 -8
- package/src/flow/models/base/ActionModelCore.tsx +5 -0
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +184 -3
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +572 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
- package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
- package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +6 -0
- package/src/locale/languageCodes.ts +2 -1
- package/src/nocobase-buildin-plugin/index.tsx +19 -1
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
- package/src/utils/markdownSanitize.ts +88 -0
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
|
|
11
11
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
12
|
import { deviceType } from 'react-device-detect';
|
|
13
|
-
import { useParams } from 'react-router-dom';
|
|
13
|
+
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
14
14
|
import { useApp } from '../../hooks/useApp';
|
|
15
|
-
import { NocoBaseDesktopRouteType } from '../../flow-compat';
|
|
16
|
-
import {
|
|
15
|
+
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
|
|
16
|
+
import {
|
|
17
|
+
resolveAdminRouteRuntimeTarget,
|
|
18
|
+
toRouterNavigationPath,
|
|
19
|
+
} from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
|
|
17
20
|
import { getAdminLayoutModel, type AdminLayoutModel } from '../admin-shell/admin-layout/AdminLayoutModel';
|
|
18
21
|
import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutModel';
|
|
19
22
|
import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
|
|
@@ -21,6 +24,7 @@ import { AppNotFound } from '../../components';
|
|
|
21
24
|
import { useKeepAlive } from '../../components/KeepAlive';
|
|
22
25
|
|
|
23
26
|
type FlowRouteGuardState = {
|
|
27
|
+
pageUid?: string;
|
|
24
28
|
pending: boolean;
|
|
25
29
|
allowBridge: boolean;
|
|
26
30
|
notFound: boolean;
|
|
@@ -35,6 +39,14 @@ type FlowRouteLayoutContext = {
|
|
|
35
39
|
uid?: string;
|
|
36
40
|
};
|
|
37
41
|
|
|
42
|
+
type FlowRouteRepositoryLike = {
|
|
43
|
+
refreshAccessible?: () => Promise<unknown>;
|
|
44
|
+
isAccessibleLoaded?: () => boolean;
|
|
45
|
+
ensureAccessibleLoaded?: () => Promise<unknown>;
|
|
46
|
+
getRouteBySchemaUid?: (schemaUid: string) => NocoBaseDesktopRoute | undefined;
|
|
47
|
+
listAccessible?: () => NocoBaseDesktopRoute[];
|
|
48
|
+
};
|
|
49
|
+
|
|
38
50
|
export type FlowRouteProps = {
|
|
39
51
|
pageUid?: string;
|
|
40
52
|
active?: boolean;
|
|
@@ -70,6 +82,44 @@ const getDefaultLegacyPageBehavior = (
|
|
|
70
82
|
|
|
71
83
|
const shouldRequireAccessibleRoute = (contextLayout?: FlowRouteLayoutContext) => contextLayout?.authCheck !== false;
|
|
72
84
|
|
|
85
|
+
const findAccessibleRouteByIdentity = (
|
|
86
|
+
routes: NocoBaseDesktopRoute[] | undefined,
|
|
87
|
+
pageUid: string,
|
|
88
|
+
): NocoBaseDesktopRoute | undefined => {
|
|
89
|
+
if (!Array.isArray(routes)) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const route of routes) {
|
|
94
|
+
if (
|
|
95
|
+
(route.type !== NocoBaseDesktopRouteType.group && route.schemaUid === pageUid) ||
|
|
96
|
+
(route.type === NocoBaseDesktopRouteType.group && route.id != null && String(route.id) === pageUid)
|
|
97
|
+
) {
|
|
98
|
+
return route;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const matched = findAccessibleRouteByIdentity(route.children, pageUid);
|
|
102
|
+
if (matched) {
|
|
103
|
+
return matched;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return undefined;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const getAccessibleRouteByPageUid = (
|
|
111
|
+
routeRepository: FlowRouteRepositoryLike | undefined,
|
|
112
|
+
pageUid: string,
|
|
113
|
+
): NocoBaseDesktopRoute | undefined => {
|
|
114
|
+
const routeBySchemaUid = routeRepository?.getRouteBySchemaUid?.(pageUid);
|
|
115
|
+
|
|
116
|
+
if (routeBySchemaUid && routeBySchemaUid.type !== NocoBaseDesktopRouteType.group) {
|
|
117
|
+
return routeBySchemaUid;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return findAccessibleRouteByIdentity(routeRepository?.listAccessible?.(), pageUid);
|
|
121
|
+
};
|
|
122
|
+
|
|
73
123
|
const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
|
|
74
124
|
if (flowEngine.getModel(pageUid)) {
|
|
75
125
|
return true;
|
|
@@ -197,11 +247,15 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
197
247
|
);
|
|
198
248
|
const legacyPageBehavior = legacyPageBehaviorProp || getDefaultLegacyPageBehavior(flowEngine, routeLayout);
|
|
199
249
|
const app = useApp();
|
|
200
|
-
const
|
|
250
|
+
const location = useLocation();
|
|
251
|
+
const navigate = useNavigate();
|
|
252
|
+
const routeRepository = flowEngine.context.routeRepository as FlowRouteRepositoryLike | undefined;
|
|
201
253
|
const params = useParams();
|
|
202
254
|
const pageUid = pageUidProp || params?.name;
|
|
255
|
+
const hasNestedRoutePath = typeof params?.tabUid !== 'undefined' || typeof params?.['*'] !== 'undefined';
|
|
203
256
|
const skipRouteRepositoryCheck = !routeRepository;
|
|
204
257
|
const [guardState, setGuardState] = useState<FlowRouteGuardState>({
|
|
258
|
+
pageUid: undefined,
|
|
205
259
|
pending: true,
|
|
206
260
|
allowBridge: false,
|
|
207
261
|
notFound: false,
|
|
@@ -213,19 +267,24 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
213
267
|
throw new Error('[NocoBase] FlowRoute requires pageUid or route.params.name.');
|
|
214
268
|
}
|
|
215
269
|
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
replaceTriggeredRef.current = false;
|
|
272
|
+
}, [location.hash, location.pathname, location.search, pageUid]);
|
|
273
|
+
|
|
216
274
|
useEffect(() => {
|
|
217
275
|
let active = true;
|
|
218
276
|
const requestId = ++requestIdRef.current;
|
|
277
|
+
const requiresAccessibleRoute = shouldRequireAccessibleRoute(routeLayout);
|
|
219
278
|
|
|
220
279
|
const run = async () => {
|
|
221
|
-
setGuardState({ pending: true, allowBridge: false, notFound: false });
|
|
280
|
+
setGuardState({ pageUid, pending: true, allowBridge: false, notFound: false });
|
|
222
281
|
|
|
223
|
-
if (!skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
|
|
282
|
+
if (requiresAccessibleRoute && !skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
|
|
224
283
|
try {
|
|
225
284
|
await routeRepository?.ensureAccessibleLoaded?.();
|
|
226
285
|
} catch (_error) {
|
|
227
286
|
if (active && requestId === requestIdRef.current) {
|
|
228
|
-
setGuardState({ pending: false, allowBridge: true, notFound: false });
|
|
287
|
+
setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
|
|
229
288
|
}
|
|
230
289
|
return;
|
|
231
290
|
}
|
|
@@ -235,28 +294,67 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
235
294
|
return;
|
|
236
295
|
}
|
|
237
296
|
|
|
238
|
-
const route =
|
|
239
|
-
|
|
240
|
-
|
|
297
|
+
const route =
|
|
298
|
+
skipRouteRepositoryCheck || !requiresAccessibleRoute
|
|
299
|
+
? undefined
|
|
300
|
+
: getAccessibleRouteByPageUid(routeRepository, pageUid);
|
|
301
|
+
if (!route && !skipRouteRepositoryCheck && requiresAccessibleRoute) {
|
|
302
|
+
setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
|
|
241
303
|
return;
|
|
242
304
|
}
|
|
243
305
|
|
|
244
306
|
if (!route && legacyPageBehavior === 'notFound') {
|
|
245
307
|
const flowModelExists = await hasFlowModel(flowEngine, pageUid);
|
|
246
308
|
if (active && requestId === requestIdRef.current) {
|
|
247
|
-
setGuardState({ pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
|
|
309
|
+
setGuardState({ pageUid, pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (route?.type === NocoBaseDesktopRouteType.group) {
|
|
315
|
+
if (hasNestedRoutePath) {
|
|
316
|
+
setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const target = resolveAdminRouteRuntimeTarget({
|
|
321
|
+
app,
|
|
322
|
+
route,
|
|
323
|
+
layout: routeLayout,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (target.reason === 'emptyGroup') {
|
|
327
|
+
setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (target.runtimePath) {
|
|
332
|
+
if (replaceTriggeredRef.current) {
|
|
333
|
+
setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
replaceTriggeredRef.current = true;
|
|
338
|
+
if (target.navigationMode === 'document') {
|
|
339
|
+
window.location.replace(target.runtimePath);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
navigate(toRouterNavigationPath(target.runtimePath, app.router?.getBasename?.()), { replace: true });
|
|
343
|
+
return;
|
|
248
344
|
}
|
|
345
|
+
|
|
346
|
+
setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
|
|
249
347
|
return;
|
|
250
348
|
}
|
|
251
349
|
|
|
252
350
|
if (route?.type === NocoBaseDesktopRouteType.page) {
|
|
253
351
|
if (legacyPageBehavior === 'notFound') {
|
|
254
|
-
setGuardState({ pending: false, allowBridge: false, notFound: true });
|
|
352
|
+
setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
|
|
255
353
|
return;
|
|
256
354
|
}
|
|
257
355
|
|
|
258
356
|
if (legacyPageBehavior === 'bridge') {
|
|
259
|
-
setGuardState({ pending: false, allowBridge: true, notFound: false });
|
|
357
|
+
setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
|
|
260
358
|
return;
|
|
261
359
|
}
|
|
262
360
|
|
|
@@ -279,14 +377,14 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
279
377
|
|
|
280
378
|
if (target.reason === 'unsupportedV2Runtime') {
|
|
281
379
|
if (active && requestId === requestIdRef.current) {
|
|
282
|
-
setGuardState({ pending: false, allowBridge: false, notFound: true });
|
|
380
|
+
setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
|
|
283
381
|
}
|
|
284
382
|
return;
|
|
285
383
|
}
|
|
286
384
|
}
|
|
287
385
|
|
|
288
386
|
if (active && requestId === requestIdRef.current) {
|
|
289
|
-
setGuardState({ pending: false, allowBridge: true, notFound: false });
|
|
387
|
+
setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
|
|
290
388
|
}
|
|
291
389
|
};
|
|
292
390
|
|
|
@@ -295,10 +393,20 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
295
393
|
return () => {
|
|
296
394
|
active = false;
|
|
297
395
|
};
|
|
298
|
-
}, [
|
|
396
|
+
}, [
|
|
397
|
+
app,
|
|
398
|
+
flowEngine,
|
|
399
|
+
hasNestedRoutePath,
|
|
400
|
+
legacyPageBehavior,
|
|
401
|
+
navigate,
|
|
402
|
+
pageUid,
|
|
403
|
+
routeLayout,
|
|
404
|
+
routeRepository,
|
|
405
|
+
skipRouteRepositoryCheck,
|
|
406
|
+
]);
|
|
299
407
|
|
|
300
408
|
const content = useMemo(() => {
|
|
301
|
-
if (guardState.pending) {
|
|
409
|
+
if (guardState.pageUid !== pageUid || guardState.pending) {
|
|
302
410
|
return null;
|
|
303
411
|
}
|
|
304
412
|
|
|
@@ -311,7 +419,15 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
311
419
|
}
|
|
312
420
|
|
|
313
421
|
return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
|
|
314
|
-
}, [
|
|
422
|
+
}, [
|
|
423
|
+
active,
|
|
424
|
+
getLayoutModel,
|
|
425
|
+
guardState.allowBridge,
|
|
426
|
+
guardState.notFound,
|
|
427
|
+
guardState.pageUid,
|
|
428
|
+
guardState.pending,
|
|
429
|
+
pageUid,
|
|
430
|
+
]);
|
|
315
431
|
|
|
316
432
|
return content;
|
|
317
433
|
};
|
|
@@ -15,6 +15,7 @@ export interface RunJSValueEditorProps {
|
|
|
15
15
|
t?: (key: string) => string;
|
|
16
16
|
value?: unknown;
|
|
17
17
|
onChange?: (value: RunJSValue) => void;
|
|
18
|
+
disabled?: boolean;
|
|
18
19
|
height?: string;
|
|
19
20
|
scene?: string;
|
|
20
21
|
containerStyle?: React.CSSProperties;
|
|
@@ -25,6 +26,7 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
|
|
|
25
26
|
t,
|
|
26
27
|
value,
|
|
27
28
|
onChange,
|
|
29
|
+
disabled,
|
|
28
30
|
height = '200px',
|
|
29
31
|
scene = 'formValue',
|
|
30
32
|
containerStyle = { flex: 1, minWidth: 0 },
|
|
@@ -38,9 +40,15 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
|
|
|
38
40
|
<div style={containerStyle}>
|
|
39
41
|
<CodeEditor
|
|
40
42
|
value={current.code}
|
|
41
|
-
onChange={(code) =>
|
|
43
|
+
onChange={(code) => {
|
|
44
|
+
if (disabled) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
onChange?.({ ...current, code });
|
|
48
|
+
}}
|
|
42
49
|
version={current.version}
|
|
43
50
|
height={height}
|
|
51
|
+
readonly={disabled}
|
|
44
52
|
enableLinter
|
|
45
53
|
placeholder={placeholderText}
|
|
46
54
|
scene={scene}
|
|
@@ -0,0 +1,216 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { render, waitFor } from '@nocobase/test/client';
|
|
13
|
+
import { FieldAssignValueInput } from '../FieldAssignValueInput';
|
|
14
|
+
|
|
15
|
+
const { mockUseFlowContext, mockGetDefaultBindingByField, mockVariableInput } = vi.hoisted(() => ({
|
|
16
|
+
mockUseFlowContext: vi.fn(),
|
|
17
|
+
mockGetDefaultBindingByField: vi.fn(),
|
|
18
|
+
mockVariableInput: vi.fn(() => <div data-testid="variable-input" />),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('@nocobase/flow-engine', async () => {
|
|
22
|
+
const actual = await vi.importActual<typeof import('@nocobase/flow-engine')>('@nocobase/flow-engine');
|
|
23
|
+
class MockEditableItemModel extends actual.EditableItemModel {}
|
|
24
|
+
MockEditableItemModel.getDefaultBindingByField = mockGetDefaultBindingByField;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
useFlowContext: () => mockUseFlowContext(),
|
|
29
|
+
VariableInput: mockVariableInput,
|
|
30
|
+
FlowModelRenderer: () => <div data-testid="flow-model-renderer" />,
|
|
31
|
+
EditableItemModel: MockEditableItemModel,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('FieldAssignValueInput context', () => {
|
|
36
|
+
it('delegates temporary field model context to the source form context', async () => {
|
|
37
|
+
type MockContext = {
|
|
38
|
+
dataSourceManager: { getDataSource: ReturnType<typeof vi.fn> };
|
|
39
|
+
t: (key: string) => string;
|
|
40
|
+
engine?: { createModel: ReturnType<typeof vi.fn> };
|
|
41
|
+
collection?: MockCollection;
|
|
42
|
+
blockModel?: MockFormModel;
|
|
43
|
+
};
|
|
44
|
+
type MockFieldModel = {
|
|
45
|
+
props: Record<string, unknown>;
|
|
46
|
+
setProps: ReturnType<typeof vi.fn>;
|
|
47
|
+
dispatchEvent: ReturnType<typeof vi.fn>;
|
|
48
|
+
remove: ReturnType<typeof vi.fn>;
|
|
49
|
+
};
|
|
50
|
+
type MockCollectionField = {
|
|
51
|
+
name: string;
|
|
52
|
+
interface: string;
|
|
53
|
+
uiSchema: { enum: Array<{ label: string; value: string }> };
|
|
54
|
+
isAssociationField: () => boolean;
|
|
55
|
+
getComponentProps: () => Record<string, unknown>;
|
|
56
|
+
};
|
|
57
|
+
type MockCollection = {
|
|
58
|
+
dataSourceKey: string;
|
|
59
|
+
name: string;
|
|
60
|
+
getField: (name: string) => MockCollectionField | null;
|
|
61
|
+
getFields: () => MockCollectionField[];
|
|
62
|
+
};
|
|
63
|
+
type MockFormModel = {
|
|
64
|
+
context: MockContext;
|
|
65
|
+
collection: MockCollection;
|
|
66
|
+
subModels: Record<string, unknown>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const sourceContext: MockContext = {
|
|
70
|
+
dataSourceManager: {
|
|
71
|
+
getDataSource: vi.fn(() => ({})),
|
|
72
|
+
},
|
|
73
|
+
t: (key: string) => key,
|
|
74
|
+
};
|
|
75
|
+
const fieldModel: MockFieldModel = {
|
|
76
|
+
props: {},
|
|
77
|
+
setProps: vi.fn((props: Record<string, unknown>) => {
|
|
78
|
+
fieldModel.props = { ...fieldModel.props, ...props };
|
|
79
|
+
}),
|
|
80
|
+
dispatchEvent: vi.fn(),
|
|
81
|
+
remove: vi.fn(),
|
|
82
|
+
};
|
|
83
|
+
const tempRoot = {
|
|
84
|
+
context: {
|
|
85
|
+
defineProperty: vi.fn(),
|
|
86
|
+
},
|
|
87
|
+
subModels: {
|
|
88
|
+
fields: [fieldModel],
|
|
89
|
+
},
|
|
90
|
+
setProps: vi.fn(),
|
|
91
|
+
remove: vi.fn(),
|
|
92
|
+
};
|
|
93
|
+
const engine = {
|
|
94
|
+
createModel: vi.fn(() => tempRoot),
|
|
95
|
+
};
|
|
96
|
+
sourceContext.engine = engine;
|
|
97
|
+
|
|
98
|
+
const collectionField: MockCollectionField = {
|
|
99
|
+
name: 'status',
|
|
100
|
+
interface: 'select',
|
|
101
|
+
uiSchema: {
|
|
102
|
+
enum: [{ label: 'Open', value: 'open' }],
|
|
103
|
+
},
|
|
104
|
+
isAssociationField: () => false,
|
|
105
|
+
getComponentProps: () => ({}),
|
|
106
|
+
};
|
|
107
|
+
const collection: MockCollection = {
|
|
108
|
+
dataSourceKey: 'main',
|
|
109
|
+
name: 'tasks',
|
|
110
|
+
getField: (name: string) => (name === 'status' ? collectionField : null),
|
|
111
|
+
getFields: () => [collectionField],
|
|
112
|
+
};
|
|
113
|
+
const formModel: MockFormModel = {
|
|
114
|
+
context: sourceContext,
|
|
115
|
+
collection,
|
|
116
|
+
subModels: {},
|
|
117
|
+
};
|
|
118
|
+
sourceContext.collection = collection;
|
|
119
|
+
sourceContext.blockModel = formModel;
|
|
120
|
+
|
|
121
|
+
mockGetDefaultBindingByField.mockReturnValue({ modelName: 'SelectFieldModel' });
|
|
122
|
+
mockUseFlowContext.mockReturnValue({
|
|
123
|
+
model: formModel,
|
|
124
|
+
t: (key: string) => key,
|
|
125
|
+
getPropertyMetaTree: vi.fn(async () => []),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
render(<FieldAssignValueInput targetPath="status" value="" onChange={vi.fn()} />);
|
|
129
|
+
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(engine.createModel).toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
expect(engine.createModel).toHaveBeenCalledWith(expect.any(Object), { delegate: sourceContext });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('lets callers override variable path parsing for domain-specific stored formats', async () => {
|
|
137
|
+
mockVariableInput.mockClear();
|
|
138
|
+
const sourceContext = {
|
|
139
|
+
dataSourceManager: {
|
|
140
|
+
getDataSource: vi.fn(() => ({})),
|
|
141
|
+
},
|
|
142
|
+
t: (key: string) => key,
|
|
143
|
+
};
|
|
144
|
+
const fieldModel = {
|
|
145
|
+
props: {},
|
|
146
|
+
setProps: vi.fn(),
|
|
147
|
+
dispatchEvent: vi.fn(),
|
|
148
|
+
remove: vi.fn(),
|
|
149
|
+
};
|
|
150
|
+
const tempRoot = {
|
|
151
|
+
context: {
|
|
152
|
+
defineProperty: vi.fn(),
|
|
153
|
+
},
|
|
154
|
+
subModels: {
|
|
155
|
+
fields: [fieldModel],
|
|
156
|
+
},
|
|
157
|
+
setProps: vi.fn(),
|
|
158
|
+
remove: vi.fn(),
|
|
159
|
+
};
|
|
160
|
+
const engine = {
|
|
161
|
+
createModel: vi.fn(() => tempRoot),
|
|
162
|
+
};
|
|
163
|
+
const collectionField = {
|
|
164
|
+
name: 'status',
|
|
165
|
+
interface: 'input',
|
|
166
|
+
uiSchema: { 'x-component': 'Input' },
|
|
167
|
+
isAssociationField: () => false,
|
|
168
|
+
getComponentProps: () => ({}),
|
|
169
|
+
};
|
|
170
|
+
const collection = {
|
|
171
|
+
dataSourceKey: 'main',
|
|
172
|
+
name: 'tasks',
|
|
173
|
+
getField: (name: string) => (name === 'status' ? collectionField : null),
|
|
174
|
+
getFields: () => [collectionField],
|
|
175
|
+
};
|
|
176
|
+
const formModel = {
|
|
177
|
+
context: { ...sourceContext, engine, collection, blockModel: null as any },
|
|
178
|
+
collection,
|
|
179
|
+
subModels: {},
|
|
180
|
+
};
|
|
181
|
+
formModel.context.blockModel = formModel;
|
|
182
|
+
|
|
183
|
+
mockGetDefaultBindingByField.mockReturnValue({ modelName: 'InputFieldModel' });
|
|
184
|
+
mockUseFlowContext.mockReturnValue({
|
|
185
|
+
model: formModel,
|
|
186
|
+
t: (key: string) => key,
|
|
187
|
+
getPropertyMetaTree: vi.fn(async () => []),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const variableConverters = {
|
|
191
|
+
resolvePathFromValue: vi.fn((value: string) =>
|
|
192
|
+
value === '{{$context.data.updatedAt}}' ? ['$context', 'data', 'updatedAt'] : undefined,
|
|
193
|
+
),
|
|
194
|
+
resolveValueFromPath: vi.fn(() => undefined),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
render(
|
|
198
|
+
<FieldAssignValueInput
|
|
199
|
+
targetPath="status"
|
|
200
|
+
value="{{$context.data.updatedAt}}"
|
|
201
|
+
onChange={vi.fn()}
|
|
202
|
+
variableConverters={variableConverters}
|
|
203
|
+
/>,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(engine.createModel).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
const latestVariableInputCall = mockVariableInput.mock.calls.at(-1)?.[0];
|
|
210
|
+
expect(latestVariableInputCall.converters.resolvePathFromValue('{{$context.data.updatedAt}}')).toEqual([
|
|
211
|
+
'$context',
|
|
212
|
+
'data',
|
|
213
|
+
'updatedAt',
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -45,6 +45,7 @@ interface FilterGroupProps {
|
|
|
45
45
|
value: Record<string, any>;
|
|
46
46
|
/** 自定义筛选项组件 */
|
|
47
47
|
FilterItem?: React.FC<FilterItemProps>;
|
|
48
|
+
disabled?: boolean;
|
|
48
49
|
closeIcon?: ReactNode;
|
|
49
50
|
/** 是否显示边框 */
|
|
50
51
|
showBorder?: boolean;
|
|
@@ -89,6 +90,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
89
90
|
const {
|
|
90
91
|
value = { logic: '$and', items: [] },
|
|
91
92
|
FilterItem,
|
|
93
|
+
disabled = false,
|
|
92
94
|
showBorder = false,
|
|
93
95
|
onRemove,
|
|
94
96
|
onChange,
|
|
@@ -120,11 +122,17 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
120
122
|
};
|
|
121
123
|
|
|
122
124
|
const handleLogicChange = (newLogic: '$and' | '$or') => {
|
|
125
|
+
if (disabled) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
123
128
|
value.logic = newLogic;
|
|
124
129
|
onChange?.(value);
|
|
125
130
|
};
|
|
126
131
|
|
|
127
132
|
const handleAddCondition = () => {
|
|
133
|
+
if (disabled) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
128
136
|
items.push({
|
|
129
137
|
path: '',
|
|
130
138
|
operator: '',
|
|
@@ -134,6 +142,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
134
142
|
};
|
|
135
143
|
|
|
136
144
|
const handleAddConditionGroup = () => {
|
|
145
|
+
if (disabled) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
137
148
|
items.push({
|
|
138
149
|
logic: '$and',
|
|
139
150
|
items: [],
|
|
@@ -142,6 +153,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
142
153
|
};
|
|
143
154
|
|
|
144
155
|
const handleRemoveItem = (index: number) => {
|
|
156
|
+
if (disabled) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
145
159
|
items.splice(index, 1);
|
|
146
160
|
onChange?.(value);
|
|
147
161
|
};
|
|
@@ -160,11 +174,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
160
174
|
<button
|
|
161
175
|
type="button"
|
|
162
176
|
aria-label="icon-close"
|
|
177
|
+
disabled={disabled}
|
|
163
178
|
style={{
|
|
164
179
|
position: 'absolute',
|
|
165
180
|
right: 10,
|
|
166
181
|
top: 10,
|
|
167
|
-
cursor: 'pointer',
|
|
182
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
168
183
|
background: 'transparent',
|
|
169
184
|
border: 0,
|
|
170
185
|
padding: 0,
|
|
@@ -182,6 +197,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
182
197
|
data-testid="filter-select-all-or-any"
|
|
183
198
|
style={{ width: 'auto' }}
|
|
184
199
|
value={logic}
|
|
200
|
+
disabled={disabled}
|
|
185
201
|
onChange={handleLogicChange}
|
|
186
202
|
>
|
|
187
203
|
<Select.Option value="$and">{t('All')}</Select.Option>
|
|
@@ -200,6 +216,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
200
216
|
key={getFilterItemKey(item)}
|
|
201
217
|
value={item}
|
|
202
218
|
FilterItem={FilterItem}
|
|
219
|
+
disabled={disabled}
|
|
203
220
|
showBorder={true}
|
|
204
221
|
onRemove={() => handleRemoveItem(index)}
|
|
205
222
|
onChange={(v) => {
|
|
@@ -221,11 +238,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
221
238
|
<button
|
|
222
239
|
type="button"
|
|
223
240
|
aria-label="icon-close"
|
|
241
|
+
disabled={disabled}
|
|
224
242
|
style={{
|
|
225
243
|
marginLeft: 8,
|
|
226
244
|
marginRight: 8,
|
|
227
245
|
flex: '0 0 auto',
|
|
228
|
-
cursor: 'pointer',
|
|
246
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
229
247
|
background: 'transparent',
|
|
230
248
|
border: 0,
|
|
231
249
|
padding: 0,
|
|
@@ -251,11 +269,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
251
269
|
<button
|
|
252
270
|
type="button"
|
|
253
271
|
aria-label="icon-close"
|
|
272
|
+
disabled={disabled}
|
|
254
273
|
style={{
|
|
255
274
|
marginLeft: 8,
|
|
256
275
|
marginRight: 8,
|
|
257
276
|
flex: '0 0 auto',
|
|
258
|
-
cursor: 'pointer',
|
|
277
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
259
278
|
background: 'transparent',
|
|
260
279
|
border: 0,
|
|
261
280
|
padding: 0,
|
|
@@ -277,11 +296,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
277
296
|
<button
|
|
278
297
|
type="button"
|
|
279
298
|
aria-label="icon-close"
|
|
299
|
+
disabled={disabled}
|
|
280
300
|
style={{
|
|
281
301
|
marginLeft: 8,
|
|
282
302
|
marginRight: 8,
|
|
283
303
|
flex: '0 0 auto',
|
|
284
|
-
cursor: 'pointer',
|
|
304
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
285
305
|
background: 'transparent',
|
|
286
306
|
border: 0,
|
|
287
307
|
padding: 0,
|
|
@@ -298,7 +318,14 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
298
318
|
</div>
|
|
299
319
|
|
|
300
320
|
<Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
|
|
301
|
-
<Button
|
|
321
|
+
<Button
|
|
322
|
+
style={{ padding: 0 }}
|
|
323
|
+
type="link"
|
|
324
|
+
size="small"
|
|
325
|
+
icon={<PlusOutlined />}
|
|
326
|
+
onClick={handleAddCondition}
|
|
327
|
+
disabled={disabled}
|
|
328
|
+
>
|
|
302
329
|
{t('Add condition')}
|
|
303
330
|
</Button>
|
|
304
331
|
<Button
|
|
@@ -307,6 +334,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
307
334
|
size="small"
|
|
308
335
|
icon={<PlusOutlined />}
|
|
309
336
|
onClick={handleAddConditionGroup}
|
|
337
|
+
disabled={disabled}
|
|
310
338
|
>
|
|
311
339
|
{t('Add condition group')}
|
|
312
340
|
</Button>
|