@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
|
@@ -28,6 +28,13 @@ type FlowRouteGuardState = {
|
|
|
28
28
|
|
|
29
29
|
export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
|
|
30
30
|
|
|
31
|
+
type FlowRouteLayoutContext = {
|
|
32
|
+
authCheck?: boolean;
|
|
33
|
+
routePath?: string;
|
|
34
|
+
routeName?: string;
|
|
35
|
+
uid?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
31
38
|
export type FlowRouteProps = {
|
|
32
39
|
pageUid?: string;
|
|
33
40
|
active?: boolean;
|
|
@@ -38,7 +45,7 @@ export type FlowRouteProps = {
|
|
|
38
45
|
const getDefaultAdminLayoutModel = (flowEngine: FlowEngine) =>
|
|
39
46
|
getAdminLayoutModel<AdminLayoutModel>(flowEngine, { required: true });
|
|
40
47
|
|
|
41
|
-
const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?:
|
|
48
|
+
const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: FlowRouteLayoutContext) => {
|
|
42
49
|
const layout = contextLayout || flowEngine.context.layout;
|
|
43
50
|
|
|
44
51
|
if (layout?.uid) {
|
|
@@ -48,7 +55,10 @@ const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: any) => {
|
|
|
48
55
|
return getDefaultAdminLayoutModel(flowEngine);
|
|
49
56
|
};
|
|
50
57
|
|
|
51
|
-
const getDefaultLegacyPageBehavior = (
|
|
58
|
+
const getDefaultLegacyPageBehavior = (
|
|
59
|
+
flowEngine: FlowEngine,
|
|
60
|
+
contextLayout?: FlowRouteLayoutContext,
|
|
61
|
+
): LegacyPageBehavior => {
|
|
52
62
|
const layout = contextLayout || flowEngine.context.layout;
|
|
53
63
|
|
|
54
64
|
if (layout?.routeName && layout.routeName !== 'admin') {
|
|
@@ -58,6 +68,8 @@ const getDefaultLegacyPageBehavior = (flowEngine: FlowEngine, contextLayout?: an
|
|
|
58
68
|
return 'redirect';
|
|
59
69
|
};
|
|
60
70
|
|
|
71
|
+
const shouldRequireAccessibleRoute = (contextLayout?: FlowRouteLayoutContext) => contextLayout?.authCheck !== false;
|
|
72
|
+
|
|
61
73
|
const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
|
|
62
74
|
if (flowEngine.getModel(pageUid)) {
|
|
63
75
|
return true;
|
|
@@ -148,18 +160,46 @@ const BridgeFlowRoute = ({
|
|
|
148
160
|
* @throws {Error} 当缺少 `route.params.name` 时抛出异常
|
|
149
161
|
*/
|
|
150
162
|
const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
163
|
+
const {
|
|
164
|
+
active,
|
|
165
|
+
getLayoutModel: getLayoutModelProp,
|
|
166
|
+
legacyPageBehavior: legacyPageBehaviorProp,
|
|
167
|
+
pageUid: pageUidProp,
|
|
168
|
+
} = props;
|
|
151
169
|
const flowEngine = useFlowEngine();
|
|
152
|
-
const flowContext = useFlowContext<
|
|
170
|
+
const flowContext = useFlowContext<{ layout?: FlowRouteLayoutContext }>();
|
|
153
171
|
const contextLayout = flowContext?.layout;
|
|
172
|
+
const propsLayoutModel = useMemo(() => getLayoutModelProp?.(flowEngine), [flowEngine, getLayoutModelProp]);
|
|
173
|
+
const rawRouteLayout = contextLayout || propsLayoutModel?.layout;
|
|
174
|
+
const routeLayoutUid = rawRouteLayout?.uid;
|
|
175
|
+
const routeLayoutRouteName = rawRouteLayout?.routeName;
|
|
176
|
+
const routeLayoutRoutePath = rawRouteLayout?.routePath;
|
|
177
|
+
const routeLayoutAuthCheck = rawRouteLayout?.authCheck;
|
|
178
|
+
const routeLayout = useMemo(() => {
|
|
179
|
+
if (
|
|
180
|
+
!routeLayoutUid &&
|
|
181
|
+
!routeLayoutRouteName &&
|
|
182
|
+
!routeLayoutRoutePath &&
|
|
183
|
+
typeof routeLayoutAuthCheck === 'undefined'
|
|
184
|
+
) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
authCheck: routeLayoutAuthCheck,
|
|
189
|
+
routeName: routeLayoutRouteName,
|
|
190
|
+
routePath: routeLayoutRoutePath,
|
|
191
|
+
uid: routeLayoutUid,
|
|
192
|
+
};
|
|
193
|
+
}, [routeLayoutAuthCheck, routeLayoutRouteName, routeLayoutRoutePath, routeLayoutUid]);
|
|
154
194
|
const getLayoutModel = useMemo(
|
|
155
|
-
() =>
|
|
156
|
-
[
|
|
195
|
+
() => getLayoutModelProp || ((engine: FlowEngine) => getDefaultLayoutModel(engine, routeLayout)),
|
|
196
|
+
[getLayoutModelProp, routeLayout],
|
|
157
197
|
);
|
|
158
|
-
const legacyPageBehavior =
|
|
198
|
+
const legacyPageBehavior = legacyPageBehaviorProp || getDefaultLegacyPageBehavior(flowEngine, routeLayout);
|
|
159
199
|
const app = useApp();
|
|
160
200
|
const routeRepository = flowEngine.context.routeRepository;
|
|
161
201
|
const params = useParams();
|
|
162
|
-
const pageUid =
|
|
202
|
+
const pageUid = pageUidProp || params?.name;
|
|
163
203
|
const skipRouteRepositoryCheck = !routeRepository;
|
|
164
204
|
const [guardState, setGuardState] = useState<FlowRouteGuardState>({
|
|
165
205
|
pending: true,
|
|
@@ -196,6 +236,11 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
196
236
|
}
|
|
197
237
|
|
|
198
238
|
const route = skipRouteRepositoryCheck ? undefined : routeRepository?.getRouteBySchemaUid?.(pageUid);
|
|
239
|
+
if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
|
|
240
|
+
setGuardState({ pending: false, allowBridge: false, notFound: true });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
199
244
|
if (!route && legacyPageBehavior === 'notFound') {
|
|
200
245
|
const flowModelExists = await hasFlowModel(flowEngine, pageUid);
|
|
201
246
|
if (active && requestId === requestIdRef.current) {
|
|
@@ -245,12 +290,12 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
245
290
|
}
|
|
246
291
|
};
|
|
247
292
|
|
|
248
|
-
|
|
293
|
+
run();
|
|
249
294
|
|
|
250
295
|
return () => {
|
|
251
296
|
active = false;
|
|
252
297
|
};
|
|
253
|
-
}, [app, flowEngine, legacyPageBehavior, pageUid, routeRepository, skipRouteRepositoryCheck]);
|
|
298
|
+
}, [app, flowEngine, legacyPageBehavior, pageUid, routeLayout, routeRepository, skipRouteRepositoryCheck]);
|
|
254
299
|
|
|
255
300
|
const content = useMemo(() => {
|
|
256
301
|
if (guardState.pending) {
|
|
@@ -265,8 +310,8 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
265
310
|
return null;
|
|
266
311
|
}
|
|
267
312
|
|
|
268
|
-
return <BridgeFlowRoute pageUid={pageUid} active={
|
|
269
|
-
}, [getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid
|
|
313
|
+
return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
|
|
314
|
+
}, [active, getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid]);
|
|
270
315
|
|
|
271
316
|
return content;
|
|
272
317
|
};
|
|
@@ -285,6 +285,433 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
285
285
|
});
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
+
it('preloads full association children when configured options already contain partial children', async () => {
|
|
289
|
+
const nameField = { name: 'name', title: 'Name', interface: 'input' };
|
|
290
|
+
const nicknameField = { name: 'nickname', title: 'Nickname', interface: 'input' };
|
|
291
|
+
const profileCollection = {
|
|
292
|
+
getField: (name: string) => {
|
|
293
|
+
if (name === 'name') return nameField;
|
|
294
|
+
if (name === 'nickname') return nicknameField;
|
|
295
|
+
return null;
|
|
296
|
+
},
|
|
297
|
+
getFields: vi.fn(() => [nameField, nicknameField]),
|
|
298
|
+
};
|
|
299
|
+
const profileField = {
|
|
300
|
+
name: 'profile',
|
|
301
|
+
title: 'Profile',
|
|
302
|
+
type: 'belongsTo',
|
|
303
|
+
interface: 'm2o',
|
|
304
|
+
target: 'profiles',
|
|
305
|
+
targetCollection: profileCollection,
|
|
306
|
+
};
|
|
307
|
+
const rootCollection = {
|
|
308
|
+
getField: (name: string) => (name === 'profile' ? profileField : null),
|
|
309
|
+
getFields: () => [profileField],
|
|
310
|
+
};
|
|
311
|
+
const fieldOptions = [
|
|
312
|
+
{
|
|
313
|
+
label: 'Profile',
|
|
314
|
+
value: 'profile',
|
|
315
|
+
isLeaf: false,
|
|
316
|
+
children: [{ label: 'Name', value: 'name', isLeaf: true }],
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
const value: FieldAssignRuleItem[] = [
|
|
320
|
+
{
|
|
321
|
+
key: 'rule-profile-nickname',
|
|
322
|
+
enable: true,
|
|
323
|
+
targetPath: 'profile.nickname',
|
|
324
|
+
mode: 'assign',
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
render(
|
|
329
|
+
wrap(
|
|
330
|
+
<FieldAssignRulesEditor
|
|
331
|
+
t={t}
|
|
332
|
+
fieldOptions={fieldOptions}
|
|
333
|
+
rootCollection={rootCollection}
|
|
334
|
+
value={value}
|
|
335
|
+
showCondition={false}
|
|
336
|
+
/>,
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
await waitFor(() => {
|
|
341
|
+
expect(profileCollection.getFields).toHaveBeenCalled();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('keeps configured child paths expandable when target collection cannot be resolved', async () => {
|
|
346
|
+
const configuredParent = {
|
|
347
|
+
label: 'Legacy profile',
|
|
348
|
+
value: 'legacyProfile',
|
|
349
|
+
isLeaf: false,
|
|
350
|
+
children: [{ label: 'Nickname', value: 'nickname', isLeaf: true }],
|
|
351
|
+
};
|
|
352
|
+
const rootCollection = {
|
|
353
|
+
getField: vi.fn(() => null),
|
|
354
|
+
getFields: () => [],
|
|
355
|
+
};
|
|
356
|
+
const value: FieldAssignRuleItem[] = [
|
|
357
|
+
{
|
|
358
|
+
key: 'rule-legacy-profile-nickname',
|
|
359
|
+
enable: true,
|
|
360
|
+
targetPath: 'legacyProfile.nickname',
|
|
361
|
+
mode: 'assign',
|
|
362
|
+
},
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
render(
|
|
366
|
+
wrap(
|
|
367
|
+
<FieldAssignRulesEditor
|
|
368
|
+
t={t}
|
|
369
|
+
fieldOptions={[configuredParent]}
|
|
370
|
+
rootCollection={rootCollection}
|
|
371
|
+
value={value}
|
|
372
|
+
showCondition={false}
|
|
373
|
+
/>,
|
|
374
|
+
),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
await waitFor(() => {
|
|
378
|
+
expect(rootCollection.getField).toHaveBeenCalled();
|
|
379
|
+
expect(configuredParent.isLeaf).toBe(false);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('preloads the latest fieldOptions after props switch', async () => {
|
|
384
|
+
const nameField = { name: 'name', title: 'Name', interface: 'input' };
|
|
385
|
+
const nicknameField = { name: 'nickname', title: 'Nickname', interface: 'input' };
|
|
386
|
+
const profileCollection = {
|
|
387
|
+
getField: (name: string) => {
|
|
388
|
+
if (name === 'name') return nameField;
|
|
389
|
+
if (name === 'nickname') return nicknameField;
|
|
390
|
+
return null;
|
|
391
|
+
},
|
|
392
|
+
getFields: vi.fn(() => [nameField, nicknameField]),
|
|
393
|
+
};
|
|
394
|
+
const profileField = {
|
|
395
|
+
name: 'profile',
|
|
396
|
+
title: 'Profile',
|
|
397
|
+
type: 'belongsTo',
|
|
398
|
+
interface: 'm2o',
|
|
399
|
+
target: 'profiles',
|
|
400
|
+
targetCollection: profileCollection,
|
|
401
|
+
};
|
|
402
|
+
const rootCollection = {
|
|
403
|
+
getField: (name: string) => (name === 'profile' ? profileField : null),
|
|
404
|
+
getFields: () => [profileField],
|
|
405
|
+
};
|
|
406
|
+
const oldConfiguredParent = {
|
|
407
|
+
label: 'Profile',
|
|
408
|
+
value: 'profile',
|
|
409
|
+
isLeaf: false,
|
|
410
|
+
children: [{ label: 'Name', value: 'name', isLeaf: true }],
|
|
411
|
+
};
|
|
412
|
+
const latestConfiguredParent = {
|
|
413
|
+
label: 'Profile',
|
|
414
|
+
value: 'profile',
|
|
415
|
+
isLeaf: false,
|
|
416
|
+
children: [{ label: 'Name', value: 'name', isLeaf: true }],
|
|
417
|
+
};
|
|
418
|
+
const value: FieldAssignRuleItem[] = [
|
|
419
|
+
{
|
|
420
|
+
key: 'rule-profile-nickname',
|
|
421
|
+
enable: true,
|
|
422
|
+
targetPath: 'profile.nickname',
|
|
423
|
+
mode: 'assign',
|
|
424
|
+
},
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
const { container, rerender } = render(
|
|
428
|
+
wrap(
|
|
429
|
+
<FieldAssignRulesEditor
|
|
430
|
+
t={t}
|
|
431
|
+
fieldOptions={[oldConfiguredParent]}
|
|
432
|
+
rootCollection={rootCollection}
|
|
433
|
+
value={[]}
|
|
434
|
+
showCondition={false}
|
|
435
|
+
/>,
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
rerender(
|
|
440
|
+
wrap(
|
|
441
|
+
<FieldAssignRulesEditor
|
|
442
|
+
t={t}
|
|
443
|
+
fieldOptions={[latestConfiguredParent]}
|
|
444
|
+
rootCollection={rootCollection}
|
|
445
|
+
value={value}
|
|
446
|
+
showCondition={false}
|
|
447
|
+
/>,
|
|
448
|
+
),
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
await waitFor(() => {
|
|
452
|
+
expect(profileCollection.getFields).toHaveBeenCalled();
|
|
453
|
+
});
|
|
454
|
+
const selector = container.querySelector('.ant-select-selector') as HTMLElement | null;
|
|
455
|
+
expect(selector).not.toBeNull();
|
|
456
|
+
await userEvent.click(selector as HTMLElement);
|
|
457
|
+
expect(await screen.findByText('Nickname')).toBeInTheDocument();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('does not mutate fieldOptions props while preloading children', async () => {
|
|
461
|
+
const nameField = { name: 'name', title: 'Name', interface: 'input' };
|
|
462
|
+
const nicknameField = { name: 'nickname', title: 'Nickname', interface: 'input' };
|
|
463
|
+
const profileCollection = {
|
|
464
|
+
getField: (name: string) => {
|
|
465
|
+
if (name === 'name') return nameField;
|
|
466
|
+
if (name === 'nickname') return nicknameField;
|
|
467
|
+
return null;
|
|
468
|
+
},
|
|
469
|
+
getFields: vi.fn(() => [nameField, nicknameField]),
|
|
470
|
+
};
|
|
471
|
+
const profileField = {
|
|
472
|
+
name: 'profile',
|
|
473
|
+
title: 'Profile',
|
|
474
|
+
type: 'belongsTo',
|
|
475
|
+
interface: 'm2o',
|
|
476
|
+
target: 'profiles',
|
|
477
|
+
targetCollection: profileCollection,
|
|
478
|
+
};
|
|
479
|
+
const rootCollection = {
|
|
480
|
+
getField: (name: string) => (name === 'profile' ? profileField : null),
|
|
481
|
+
getFields: () => [profileField],
|
|
482
|
+
};
|
|
483
|
+
const configuredParent = {
|
|
484
|
+
label: 'Profile',
|
|
485
|
+
value: 'profile',
|
|
486
|
+
isLeaf: false,
|
|
487
|
+
children: [{ label: 'Name', value: 'name', isLeaf: true }],
|
|
488
|
+
};
|
|
489
|
+
const value: FieldAssignRuleItem[] = [
|
|
490
|
+
{
|
|
491
|
+
key: 'rule-profile-nickname',
|
|
492
|
+
enable: true,
|
|
493
|
+
targetPath: 'profile.nickname',
|
|
494
|
+
mode: 'assign',
|
|
495
|
+
},
|
|
496
|
+
];
|
|
497
|
+
|
|
498
|
+
render(
|
|
499
|
+
wrap(
|
|
500
|
+
<FieldAssignRulesEditor
|
|
501
|
+
t={t}
|
|
502
|
+
fieldOptions={[configuredParent]}
|
|
503
|
+
rootCollection={rootCollection}
|
|
504
|
+
value={value}
|
|
505
|
+
showCondition={false}
|
|
506
|
+
/>,
|
|
507
|
+
),
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
await waitFor(() => {
|
|
511
|
+
expect(profileCollection.getFields).toHaveBeenCalled();
|
|
512
|
+
});
|
|
513
|
+
expect(configuredParent.children.some((child) => child.value === 'nickname')).toBe(false);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('retries preload when rootCollection becomes resolvable with same fieldOptions', async () => {
|
|
517
|
+
const nicknameField = { name: 'nickname', title: 'Nickname', interface: 'input' };
|
|
518
|
+
const profileCollection = {
|
|
519
|
+
getField: (name: string) => (name === 'nickname' ? nicknameField : null),
|
|
520
|
+
getFields: vi.fn(() => [nicknameField]),
|
|
521
|
+
};
|
|
522
|
+
const profileField = {
|
|
523
|
+
name: 'profile',
|
|
524
|
+
title: 'Profile',
|
|
525
|
+
type: 'belongsTo',
|
|
526
|
+
interface: 'm2o',
|
|
527
|
+
target: 'profiles',
|
|
528
|
+
targetCollection: profileCollection,
|
|
529
|
+
};
|
|
530
|
+
const unresolvedCollection = {
|
|
531
|
+
getField: () => null,
|
|
532
|
+
getFields: () => [],
|
|
533
|
+
};
|
|
534
|
+
const resolvedCollection = {
|
|
535
|
+
getField: (name: string) => (name === 'profile' ? profileField : null),
|
|
536
|
+
getFields: () => [profileField],
|
|
537
|
+
};
|
|
538
|
+
const configuredParent = {
|
|
539
|
+
label: 'Profile',
|
|
540
|
+
value: 'profile',
|
|
541
|
+
isLeaf: false,
|
|
542
|
+
children: [{ label: 'Name', value: 'name', isLeaf: true }],
|
|
543
|
+
};
|
|
544
|
+
const value: FieldAssignRuleItem[] = [
|
|
545
|
+
{
|
|
546
|
+
key: 'rule-profile-nickname',
|
|
547
|
+
enable: true,
|
|
548
|
+
targetPath: 'profile.nickname',
|
|
549
|
+
mode: 'assign',
|
|
550
|
+
},
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
const { rerender } = render(
|
|
554
|
+
wrap(
|
|
555
|
+
<FieldAssignRulesEditor
|
|
556
|
+
t={t}
|
|
557
|
+
fieldOptions={[configuredParent]}
|
|
558
|
+
rootCollection={unresolvedCollection}
|
|
559
|
+
value={value}
|
|
560
|
+
showCondition={false}
|
|
561
|
+
/>,
|
|
562
|
+
),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
rerender(
|
|
566
|
+
wrap(
|
|
567
|
+
<FieldAssignRulesEditor
|
|
568
|
+
t={t}
|
|
569
|
+
fieldOptions={[configuredParent]}
|
|
570
|
+
rootCollection={resolvedCollection}
|
|
571
|
+
value={value}
|
|
572
|
+
showCondition={false}
|
|
573
|
+
/>,
|
|
574
|
+
),
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
await waitFor(() => {
|
|
578
|
+
expect(profileCollection.getFields).toHaveBeenCalled();
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('handles preload errors without leaving unhandled rejections', async () => {
|
|
583
|
+
const loadError = new Error('failed to load fields');
|
|
584
|
+
const profileCollection = {
|
|
585
|
+
getFields: vi.fn(() => {
|
|
586
|
+
throw loadError;
|
|
587
|
+
}),
|
|
588
|
+
};
|
|
589
|
+
const profileField = {
|
|
590
|
+
name: 'profile',
|
|
591
|
+
title: 'Profile',
|
|
592
|
+
type: 'belongsTo',
|
|
593
|
+
interface: 'm2o',
|
|
594
|
+
target: 'profiles',
|
|
595
|
+
targetCollection: profileCollection,
|
|
596
|
+
};
|
|
597
|
+
const rootCollection = {
|
|
598
|
+
getField: (name: string) => (name === 'profile' ? profileField : null),
|
|
599
|
+
getFields: () => [profileField],
|
|
600
|
+
};
|
|
601
|
+
const configuredParent = {
|
|
602
|
+
label: 'Profile',
|
|
603
|
+
value: 'profile',
|
|
604
|
+
isLeaf: false,
|
|
605
|
+
};
|
|
606
|
+
const value: FieldAssignRuleItem[] = [
|
|
607
|
+
{
|
|
608
|
+
key: 'rule-profile-nickname',
|
|
609
|
+
enable: true,
|
|
610
|
+
targetPath: 'profile.nickname',
|
|
611
|
+
mode: 'assign',
|
|
612
|
+
},
|
|
613
|
+
];
|
|
614
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
615
|
+
|
|
616
|
+
render(
|
|
617
|
+
wrap(
|
|
618
|
+
<FieldAssignRulesEditor
|
|
619
|
+
t={t}
|
|
620
|
+
fieldOptions={[configuredParent]}
|
|
621
|
+
rootCollection={rootCollection}
|
|
622
|
+
value={value}
|
|
623
|
+
showCondition={false}
|
|
624
|
+
/>,
|
|
625
|
+
),
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
await waitFor(() => {
|
|
630
|
+
expect(warnSpy).toHaveBeenCalledWith('[FieldAssignRulesEditor] Failed to preload cascader path', loadError);
|
|
631
|
+
});
|
|
632
|
+
} finally {
|
|
633
|
+
warnSpy.mockRestore();
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('keeps loaded cascader children when translation function identity changes', async () => {
|
|
638
|
+
const nameField = { name: 'name', title: 'Name', interface: 'input' };
|
|
639
|
+
const nicknameField = { name: 'nickname', title: 'Nickname', interface: 'input' };
|
|
640
|
+
const profileCollection = {
|
|
641
|
+
getField: (name: string) => {
|
|
642
|
+
if (name === 'name') return nameField;
|
|
643
|
+
if (name === 'nickname') return nicknameField;
|
|
644
|
+
return null;
|
|
645
|
+
},
|
|
646
|
+
getFields: vi.fn(() => [nameField, nicknameField]),
|
|
647
|
+
};
|
|
648
|
+
const profileField = {
|
|
649
|
+
name: 'profile',
|
|
650
|
+
title: 'Profile',
|
|
651
|
+
type: 'belongsTo',
|
|
652
|
+
interface: 'm2o',
|
|
653
|
+
target: 'profiles',
|
|
654
|
+
targetCollection: profileCollection,
|
|
655
|
+
};
|
|
656
|
+
const rootCollection = {
|
|
657
|
+
getField: (name: string) => (name === 'profile' ? profileField : null),
|
|
658
|
+
getFields: () => [profileField],
|
|
659
|
+
};
|
|
660
|
+
const fieldOptions = [
|
|
661
|
+
{
|
|
662
|
+
label: 'Profile',
|
|
663
|
+
value: 'profile',
|
|
664
|
+
isLeaf: false,
|
|
665
|
+
},
|
|
666
|
+
];
|
|
667
|
+
const value: FieldAssignRuleItem[] = [
|
|
668
|
+
{
|
|
669
|
+
key: 'rule-profile',
|
|
670
|
+
enable: true,
|
|
671
|
+
targetPath: 'profile',
|
|
672
|
+
mode: 'assign',
|
|
673
|
+
},
|
|
674
|
+
];
|
|
675
|
+
|
|
676
|
+
const { container, rerender } = render(
|
|
677
|
+
wrap(
|
|
678
|
+
<FieldAssignRulesEditor
|
|
679
|
+
t={(key) => key}
|
|
680
|
+
fieldOptions={fieldOptions}
|
|
681
|
+
rootCollection={rootCollection}
|
|
682
|
+
value={value}
|
|
683
|
+
showCondition={false}
|
|
684
|
+
/>,
|
|
685
|
+
),
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
const selector = container.querySelector('.ant-select-selector') as HTMLElement | null;
|
|
689
|
+
expect(selector).not.toBeNull();
|
|
690
|
+
await userEvent.click(selector as HTMLElement);
|
|
691
|
+
const profileOption = (await screen.findAllByText('Profile')).find((item) =>
|
|
692
|
+
item.closest('.ant-cascader-menu-item'),
|
|
693
|
+
);
|
|
694
|
+
expect(profileOption).toBeTruthy();
|
|
695
|
+
await userEvent.click(profileOption as HTMLElement);
|
|
696
|
+
expect(await screen.findByText('Nickname')).toBeInTheDocument();
|
|
697
|
+
await userEvent.keyboard('{Escape}');
|
|
698
|
+
|
|
699
|
+
rerender(
|
|
700
|
+
wrap(
|
|
701
|
+
<FieldAssignRulesEditor
|
|
702
|
+
t={(key) => key}
|
|
703
|
+
fieldOptions={fieldOptions}
|
|
704
|
+
rootCollection={rootCollection}
|
|
705
|
+
value={value}
|
|
706
|
+
showCondition={false}
|
|
707
|
+
/>,
|
|
708
|
+
),
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
await userEvent.click(selector as HTMLElement);
|
|
712
|
+
expect(await screen.findByText('Nickname')).toBeInTheDocument();
|
|
713
|
+
});
|
|
714
|
+
|
|
288
715
|
it('saves selected title field into current assign rule without syncing', async () => {
|
|
289
716
|
const { rootCollection, value } = createAssociationFixture();
|
|
290
717
|
const onChange = vi.fn();
|
|
@@ -795,15 +1222,23 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
795
1222
|
);
|
|
796
1223
|
|
|
797
1224
|
expect(screen.getByLabelText('Default value')).toBeInTheDocument();
|
|
1225
|
+
expect(screen.getByLabelText('Override value')).toBeInTheDocument();
|
|
798
1226
|
expect(screen.getByLabelText('Fixed value')).toBeInTheDocument();
|
|
799
1227
|
expect(screen.getByLabelText('Fixed value')).toBeChecked();
|
|
800
1228
|
await userEvent.click(screen.getByLabelText('Default value'));
|
|
801
1229
|
await waitFor(() => {
|
|
802
1230
|
expect(onChange).toHaveBeenCalled();
|
|
803
1231
|
});
|
|
804
|
-
|
|
805
|
-
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
1232
|
+
let lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
806
1233
|
expect(lastCall?.[0]?.mode).toBe('default');
|
|
1234
|
+
|
|
1235
|
+
onChange.mockClear();
|
|
1236
|
+
await userEvent.click(screen.getByLabelText('Override value'));
|
|
1237
|
+
await waitFor(() => {
|
|
1238
|
+
expect(onChange).toHaveBeenCalled();
|
|
1239
|
+
});
|
|
1240
|
+
lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
1241
|
+
expect(lastCall?.[0]?.mode).toBe('override');
|
|
807
1242
|
});
|
|
808
1243
|
|
|
809
1244
|
it('shows assignment mode tooltips for each option', async () => {
|
|
@@ -821,7 +1256,7 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
821
1256
|
);
|
|
822
1257
|
|
|
823
1258
|
const questionIcons = Array.from(container.querySelectorAll('.anticon-question-circle'));
|
|
824
|
-
expect(questionIcons).toHaveLength(
|
|
1259
|
+
expect(questionIcons).toHaveLength(3);
|
|
825
1260
|
|
|
826
1261
|
await userEvent.hover(questionIcons[0] as Element);
|
|
827
1262
|
await waitFor(() => {
|
|
@@ -835,11 +1270,80 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
835
1270
|
await userEvent.unhover(questionIcons[0] as Element);
|
|
836
1271
|
|
|
837
1272
|
await userEvent.hover(questionIcons[1] as Element);
|
|
1273
|
+
await waitFor(() => {
|
|
1274
|
+
expect(screen.getByText('Overrides existing values until the user edits it.')).toBeInTheDocument();
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
await userEvent.unhover(questionIcons[1] as Element);
|
|
1278
|
+
|
|
1279
|
+
await userEvent.hover(questionIcons[2] as Element);
|
|
838
1280
|
await waitFor(() => {
|
|
839
1281
|
expect(screen.getByText('A system-set value. Read-only.')).toBeInTheDocument();
|
|
840
1282
|
});
|
|
841
1283
|
});
|
|
842
1284
|
|
|
1285
|
+
it('honors allowed assignment modes', () => {
|
|
1286
|
+
const value: FieldAssignRuleItem[] = [
|
|
1287
|
+
{
|
|
1288
|
+
key: '1',
|
|
1289
|
+
enable: true,
|
|
1290
|
+
targetPath: 'title',
|
|
1291
|
+
mode: 'override',
|
|
1292
|
+
},
|
|
1293
|
+
];
|
|
1294
|
+
|
|
1295
|
+
render(
|
|
1296
|
+
wrap(
|
|
1297
|
+
<FieldAssignRulesEditor
|
|
1298
|
+
t={t}
|
|
1299
|
+
fieldOptions={[]}
|
|
1300
|
+
value={value}
|
|
1301
|
+
allowedModes={['default', 'assign']}
|
|
1302
|
+
showCondition={false}
|
|
1303
|
+
showEnable={false}
|
|
1304
|
+
/>,
|
|
1305
|
+
),
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
expect(screen.getByLabelText('Default value')).toBeInTheDocument();
|
|
1309
|
+
expect(screen.getByLabelText('Fixed value')).toBeInTheDocument();
|
|
1310
|
+
expect(screen.queryByLabelText('Override value')).toBeNull();
|
|
1311
|
+
expect(screen.getByLabelText('Fixed value')).toBeChecked();
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it('normalizes disallowed assignment mode before emitting changes', async () => {
|
|
1315
|
+
const onChange = vi.fn();
|
|
1316
|
+
const value: FieldAssignRuleItem[] = [
|
|
1317
|
+
{
|
|
1318
|
+
key: '1',
|
|
1319
|
+
enable: true,
|
|
1320
|
+
targetPath: 'title',
|
|
1321
|
+
mode: 'override',
|
|
1322
|
+
},
|
|
1323
|
+
];
|
|
1324
|
+
|
|
1325
|
+
render(
|
|
1326
|
+
wrap(
|
|
1327
|
+
<FieldAssignRulesEditor
|
|
1328
|
+
t={t}
|
|
1329
|
+
fieldOptions={[]}
|
|
1330
|
+
value={value}
|
|
1331
|
+
onChange={onChange}
|
|
1332
|
+
allowedModes={['default', 'assign']}
|
|
1333
|
+
showCondition={false}
|
|
1334
|
+
/>,
|
|
1335
|
+
),
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
await userEvent.click(screen.getByRole('switch'));
|
|
1339
|
+
|
|
1340
|
+
await waitFor(() => {
|
|
1341
|
+
expect(onChange).toHaveBeenCalled();
|
|
1342
|
+
});
|
|
1343
|
+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
1344
|
+
expect(lastCall?.[0]?.mode).toBe('assign');
|
|
1345
|
+
});
|
|
1346
|
+
|
|
843
1347
|
it('merges extra item tree into base item while keeping order', () => {
|
|
844
1348
|
const base: MetaTreeNode[] = [
|
|
845
1349
|
{ name: 'formValues', title: 'Current form', type: 'object', paths: ['formValues'] },
|
|
@@ -897,7 +1401,7 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
897
1401
|
|
|
898
1402
|
const itemNode = merged.find((node) => node.name === 'item');
|
|
899
1403
|
expect(itemNode).toBeTruthy();
|
|
900
|
-
const parent = (itemNode
|
|
1404
|
+
const parent = ((itemNode?.children as MetaTreeNode[]) || []).find((node) => node.name === 'parentItem');
|
|
901
1405
|
expect(parent).toBeTruthy();
|
|
902
1406
|
|
|
903
1407
|
const nestedParent = ((parent?.children as MetaTreeNode[]) || []).find((node) => node.name === 'parentItem');
|