@nocobase/client-v2 2.1.10 → 2.1.12
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/flow/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/index.mjs +100 -84
- package/lib/index.js +126 -110
- package/package.json +7 -7
- package/src/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- package/src/flow/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/dataScopeFilter.ts +10 -0
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/openView.tsx +38 -4
- package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
- package/src/flow/components/FieldAssignRulesEditor.tsx +66 -9
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +427 -0
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
- package/src/flow/components/fieldAssignOptions.ts +155 -27
- package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
- package/src/flow/models/base/ActionModelCore.tsx +6 -4
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/js-block/JSBlock.tsx +223 -2
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +35 -12
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -14
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +57 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- package/src/flow/resolveViewParamsToViewList.tsx +11 -3
- package/src/settings-center/SystemSettingsPage.tsx +1 -1
|
@@ -12,6 +12,12 @@ import { transformFilter } from '@nocobase/utils/client';
|
|
|
12
12
|
import _ from 'lodash';
|
|
13
13
|
|
|
14
14
|
const PRESERVE_NULL = { __nocobaseDataScopeNull__: true };
|
|
15
|
+
const SERVER_CURRENT_ROLE_VARIABLE = '{{$nRole}}';
|
|
16
|
+
const CURRENT_ROLE_EXPRESSION_RE = /^\s*\{\{\s*ctx\.role\s*\}\}\s*$/;
|
|
17
|
+
|
|
18
|
+
function isCurrentRoleExpression(value: unknown) {
|
|
19
|
+
return typeof value === 'string' && CURRENT_ROLE_EXPRESSION_RE.test(value);
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
function isPreserveNull(value: any) {
|
|
17
23
|
return (
|
|
@@ -45,6 +51,10 @@ function markEmptyVariableValues(rawNode: any, resolvedNode: any) {
|
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
if ('path' in rawNode && 'operator' in rawNode) {
|
|
54
|
+
if (isCurrentRoleExpression(rawNode.value)) {
|
|
55
|
+
resolvedNode.value = SERVER_CURRENT_ROLE_VARIABLE;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
48
58
|
if (
|
|
49
59
|
isVariableExpression(rawNode.value) &&
|
|
50
60
|
(resolvedNode.value === undefined || resolvedNode.value === null || resolvedNode.value === '')
|
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
defineAction,
|
|
12
|
+
tExpr,
|
|
13
|
+
FlowModelContext,
|
|
14
|
+
FlowModel,
|
|
15
|
+
FlowExitAllException,
|
|
16
|
+
createOpenViewRouteState,
|
|
17
|
+
RUNJS_OPEN_VIEW_ROUTE_STATE,
|
|
18
|
+
} from '@nocobase/flow-engine';
|
|
11
19
|
import React from 'react';
|
|
12
20
|
import { FlowPage } from '../FlowPage';
|
|
13
21
|
import { PageModel, RootPageModel } from '../models';
|
|
@@ -291,8 +299,32 @@ export const openView = defineAction({
|
|
|
291
299
|
? (inputArgs as any).associationName
|
|
292
300
|
: (params as any)?.associationName;
|
|
293
301
|
const mergedTabUid = typeof inputArgs.tabUid !== 'undefined' ? inputArgs.tabUid : params.tabUid;
|
|
294
|
-
|
|
295
|
-
const
|
|
302
|
+
const hasRunJSOpenViewRouteState = Object.prototype.hasOwnProperty.call(inputArgs, RUNJS_OPEN_VIEW_ROUTE_STATE);
|
|
303
|
+
const runJSOpenViewRouteStateInput = (inputArgs as Record<PropertyKey, unknown>)[RUNJS_OPEN_VIEW_ROUTE_STATE];
|
|
304
|
+
const runJSOpenViewRouteState = hasRunJSOpenViewRouteState
|
|
305
|
+
? createOpenViewRouteState(
|
|
306
|
+
runJSOpenViewRouteStateInput && typeof runJSOpenViewRouteStateInput === 'object'
|
|
307
|
+
? (runJSOpenViewRouteStateInput as { mode?: unknown; size?: unknown })
|
|
308
|
+
: {
|
|
309
|
+
mode: inputArgs.mode,
|
|
310
|
+
size: inputArgs.size,
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
: undefined;
|
|
314
|
+
const replayOpenViewRouteStateInput = (inputArgs as { openViewRouteState?: unknown }).openViewRouteState;
|
|
315
|
+
const replayOpenViewRouteState =
|
|
316
|
+
replayOpenViewRouteStateInput && typeof replayOpenViewRouteStateInput === 'object'
|
|
317
|
+
? createOpenViewRouteState(replayOpenViewRouteStateInput as { mode?: unknown; size?: unknown })
|
|
318
|
+
: undefined;
|
|
319
|
+
const runtimeOpenViewRouteState = runJSOpenViewRouteState || replayOpenViewRouteState;
|
|
320
|
+
// 移动端中只需要显示子页面。本次 dispatch 参数优先于持久化默认值。
|
|
321
|
+
const openMode = ctx.inputArgs?.isMobileLayout
|
|
322
|
+
? 'embed'
|
|
323
|
+
: runtimeOpenViewRouteState?.mode || ctx.inputArgs?.mode || params.mode || 'drawer';
|
|
324
|
+
const effectiveRunJSOpenViewRouteState =
|
|
325
|
+
runJSOpenViewRouteState && ctx.inputArgs?.isMobileLayout
|
|
326
|
+
? createOpenViewRouteState({ ...runJSOpenViewRouteState, mode: openMode })
|
|
327
|
+
: runJSOpenViewRouteState;
|
|
296
328
|
let navigation = typeof inputArgs.navigation !== 'undefined' ? inputArgs.navigation : params.navigation;
|
|
297
329
|
|
|
298
330
|
// 传递了上下文就必须禁用路由,否则下次路由打开会缺少上下文
|
|
@@ -313,6 +345,7 @@ export const openView = defineAction({
|
|
|
313
345
|
sourceId: mergedSourceId,
|
|
314
346
|
tabUid: mergedTabUid,
|
|
315
347
|
viewUid: ctx.model.context?.inputArgs?.viewUid || ctx.model.uid,
|
|
348
|
+
...(effectiveRunJSOpenViewRouteState ? { openViewRouteState: effectiveRunJSOpenViewRouteState } : {}),
|
|
316
349
|
} as Record<string, unknown>;
|
|
317
350
|
const pendingView = {
|
|
318
351
|
type: pendingType,
|
|
@@ -328,6 +361,7 @@ export const openView = defineAction({
|
|
|
328
361
|
filterByTk: mergedFilterByTk,
|
|
329
362
|
sourceId: mergedSourceId,
|
|
330
363
|
tabUid: mergedTabUid,
|
|
364
|
+
...(effectiveRunJSOpenViewRouteState ? { openViewRouteState: effectiveRunJSOpenViewRouteState } : {}),
|
|
331
365
|
};
|
|
332
366
|
ctx.view.navigation.navigateTo(nextView);
|
|
333
367
|
return;
|
|
@@ -373,7 +407,7 @@ export const openView = defineAction({
|
|
|
373
407
|
|
|
374
408
|
const pageModelClass =
|
|
375
409
|
ctx.inputArgs.pageModelClass || params.pageModelClass || ctx.layout?.childPageModelClass || 'ChildPageModel';
|
|
376
|
-
const size = ctx.inputArgs.size || params.size || 'medium';
|
|
410
|
+
const size = runtimeOpenViewRouteState?.size || ctx.inputArgs.size || params.size || 'medium';
|
|
377
411
|
let pageModelUid: string | null = null;
|
|
378
412
|
let pageModelRef: FlowModel | null = null;
|
|
379
413
|
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { define, observable } from '@formily/reactive';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
decodeOpenViewRouteState,
|
|
13
|
+
parsePathnameToViewParams,
|
|
14
|
+
type FlowEngine,
|
|
15
|
+
FlowModel,
|
|
16
|
+
type ViewParam,
|
|
17
|
+
} from '@nocobase/flow-engine';
|
|
12
18
|
import {
|
|
13
19
|
BaseLayoutRouteCoordinator,
|
|
14
20
|
type BaseLayoutRouteCoordinatorOptions,
|
|
@@ -116,20 +122,36 @@ const isStandardLayoutRelativePath = (relativePath: string) => {
|
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
let i = 1;
|
|
125
|
+
let currentViewUid = segments[0];
|
|
119
126
|
while (i < segments.length) {
|
|
120
127
|
const segment = segments[i];
|
|
128
|
+
|
|
121
129
|
if (segment === 'view') {
|
|
122
130
|
if (!segments[i + 1]) {
|
|
123
131
|
return false;
|
|
124
132
|
}
|
|
133
|
+
currentViewUid = segments[i + 1];
|
|
134
|
+
i += 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (segment === 'opts') {
|
|
139
|
+
if (!segments[i + 1] || !decodeOpenViewRouteState(currentViewUid, segments[i + 1])) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
i += 2;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isKnownViewParamName(segment) && segments[i + 1]) {
|
|
125
147
|
i += 2;
|
|
126
148
|
continue;
|
|
127
149
|
}
|
|
128
150
|
|
|
129
|
-
if (!
|
|
151
|
+
if (!segments[i + 1]) {
|
|
130
152
|
return false;
|
|
131
153
|
}
|
|
132
|
-
|
|
154
|
+
return false;
|
|
133
155
|
}
|
|
134
156
|
|
|
135
157
|
return true;
|
|
@@ -272,7 +272,7 @@ export class BaseLayoutRouteCoordinator {
|
|
|
272
272
|
runtime.viewState[getKey(viewItem)]?.destroy?.(true);
|
|
273
273
|
delete runtime.viewState[getKey(viewItem)];
|
|
274
274
|
});
|
|
275
|
-
updateViewListHidden(viewList);
|
|
275
|
+
updateViewListHidden(viewList, !!this.layoutContext?.isMobileLayout);
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
if (viewsToOpen.length) {
|
|
@@ -299,7 +299,7 @@ export class BaseLayoutRouteCoordinator {
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
if (runtime.meta.active) {
|
|
302
|
-
updateViewListHidden(viewList);
|
|
302
|
+
updateViewListHidden(viewList, !!this.layoutContext?.isMobileLayout);
|
|
303
303
|
return;
|
|
304
304
|
}
|
|
305
305
|
|
|
@@ -390,6 +390,7 @@ export class BaseLayoutRouteCoordinator {
|
|
|
390
390
|
openViewParams?.associationName && !hasUsableSourceId(viewItem.params.sourceId)
|
|
391
391
|
? null
|
|
392
392
|
: openViewParams?.associationName;
|
|
393
|
+
const openViewRouteState = viewItem.params.openViewRouteState;
|
|
393
394
|
const openerUids = viewList.slice(0, viewItem.index).map((item) => item.params.viewUid);
|
|
394
395
|
const navigation = new ViewNavigation(
|
|
395
396
|
this.flowEngine.context,
|
|
@@ -408,6 +409,8 @@ export class BaseLayoutRouteCoordinator {
|
|
|
408
409
|
deactivateRef,
|
|
409
410
|
openerUids,
|
|
410
411
|
...viewItem.params,
|
|
412
|
+
...(openViewRouteState?.mode ? { mode: openViewRouteState.mode } : {}),
|
|
413
|
+
...(openViewRouteState?.size ? { size: openViewRouteState.size } : {}),
|
|
411
414
|
pageActive: runtime.meta.active,
|
|
412
415
|
activationControlledByLayout: true,
|
|
413
416
|
navigation,
|
|
@@ -121,6 +121,19 @@ describe('AdminLayoutRouteCoordinator', () => {
|
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
it('passes RunJS openView route state as runtime mode and size during route replay', () => {
|
|
125
|
+
const { dispatchEvent } = setupRouteReplay({
|
|
126
|
+
openViewRouteState: { mode: 'dialog', size: 'large' },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
|
|
130
|
+
mode: 'dialog',
|
|
131
|
+
size: 'large',
|
|
132
|
+
openViewRouteState: { mode: 'dialog', size: 'large' },
|
|
133
|
+
triggerByRouter: true,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
124
137
|
it('uses layout content element before route page placeholder as view target', () => {
|
|
125
138
|
const engine = new FlowEngine();
|
|
126
139
|
engine.registerModels({ RouteModel });
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
FlowEngine,
|
|
40
40
|
FlowEngineProvider,
|
|
41
41
|
FlowModelRenderer,
|
|
42
|
+
encodeOpenViewRouteState,
|
|
42
43
|
useFlowEngine,
|
|
43
44
|
type FlowModel,
|
|
44
45
|
} from '@nocobase/flow-engine';
|
|
@@ -213,6 +214,87 @@ describe('AdminLayoutModel runtime', () => {
|
|
|
213
214
|
});
|
|
214
215
|
});
|
|
215
216
|
|
|
217
|
+
it('should parse RunJS openView route params into layout route view stack state', async () => {
|
|
218
|
+
const token = encodeOpenViewRouteState('popup', { mode: 'dialog', size: 'large' });
|
|
219
|
+
if (!token) {
|
|
220
|
+
throw new Error('Expected openView route state token.');
|
|
221
|
+
}
|
|
222
|
+
const engine = new FlowEngine();
|
|
223
|
+
engine.context.defineProperty('routeRepository', {
|
|
224
|
+
value: {
|
|
225
|
+
getRouteBySchemaUid: (pageUid: string) => ({ title: pageUid }),
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
render(
|
|
230
|
+
<FlowEngineProvider engine={engine}>
|
|
231
|
+
<TestAdminLayoutHost />
|
|
232
|
+
</FlowEngineProvider>,
|
|
233
|
+
);
|
|
234
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
235
|
+
expect(model).toBeTruthy();
|
|
236
|
+
|
|
237
|
+
act(() => {
|
|
238
|
+
model.syncLayoutRoute({
|
|
239
|
+
name: getLayoutPageViewRouteName('admin'),
|
|
240
|
+
pathname: `/admin/page-1/view/popup/opts/${token}/filterbytk/1`,
|
|
241
|
+
layoutBasePathname: '/admin',
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await waitFor(() => {
|
|
246
|
+
expect(model.context.layoutRoute).toMatchObject({
|
|
247
|
+
type: 'page',
|
|
248
|
+
pageUid: 'page-1',
|
|
249
|
+
viewStack: [
|
|
250
|
+
{ viewUid: 'page-1' },
|
|
251
|
+
{
|
|
252
|
+
viewUid: 'popup',
|
|
253
|
+
openViewRouteState: { mode: 'dialog', size: 'large' },
|
|
254
|
+
filterByTk: '1',
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should reject malformed RunJS openView route params', async () => {
|
|
262
|
+
const wrongViewToken = encodeOpenViewRouteState('other-popup', { mode: 'dialog', size: 'large' });
|
|
263
|
+
if (!wrongViewToken) {
|
|
264
|
+
throw new Error('Expected openView route state token.');
|
|
265
|
+
}
|
|
266
|
+
const engine = new FlowEngine();
|
|
267
|
+
|
|
268
|
+
render(
|
|
269
|
+
<FlowEngineProvider engine={engine}>
|
|
270
|
+
<TestAdminLayoutHost />
|
|
271
|
+
</FlowEngineProvider>,
|
|
272
|
+
);
|
|
273
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
274
|
+
expect(model).toBeTruthy();
|
|
275
|
+
|
|
276
|
+
[
|
|
277
|
+
'/admin/page-1/sourceid',
|
|
278
|
+
'/admin/page-1/AbCdEfGh/filterbytk/1',
|
|
279
|
+
'/admin/page-1/view/popup/AbCdEfGh/filterbytk/1',
|
|
280
|
+
'/admin/page-1/view/popup/opts/AbCdEfGh/filterbytk/1',
|
|
281
|
+
'/admin/page-1/view/popup/openviewmode/dialog',
|
|
282
|
+
'/admin/page-1/view/popup/openviewsize/large',
|
|
283
|
+
`/admin/page-1/view/popup/opts/${wrongViewToken}/filterbytk/1`,
|
|
284
|
+
].forEach((pathname) => {
|
|
285
|
+
expect(
|
|
286
|
+
model.resolveLayoutRoute({
|
|
287
|
+
name: getLayoutPageViewRouteName('admin'),
|
|
288
|
+
pathname,
|
|
289
|
+
layoutBasePathname: '/admin',
|
|
290
|
+
}),
|
|
291
|
+
).toMatchObject({
|
|
292
|
+
type: 'notFound',
|
|
293
|
+
pathname,
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
216
298
|
it('should not consume global routes that belong to nested layouts', async () => {
|
|
217
299
|
const engine = new FlowEngine();
|
|
218
300
|
const routeRef = observable.ref({
|
|
@@ -113,6 +113,48 @@ export interface FieldAssignRulesEditorProps {
|
|
|
113
113
|
|
|
114
114
|
const ALL_ASSIGN_MODES: AssignMode[] = ['default', 'override', 'assign'];
|
|
115
115
|
|
|
116
|
+
function cloneCascaderOption(option: FieldAssignCascaderOption): FieldAssignCascaderOption {
|
|
117
|
+
return {
|
|
118
|
+
...option,
|
|
119
|
+
children: option.children?.map((child) => cloneCascaderOption(child)),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mergeCascaderOptionInto(options: FieldAssignCascaderOption[], source: FieldAssignCascaderOption) {
|
|
124
|
+
const value = source?.value ? String(source.value) : '';
|
|
125
|
+
if (!value) return;
|
|
126
|
+
|
|
127
|
+
const existing = options.find((item) => String(item?.value || '') === value);
|
|
128
|
+
if (!existing) {
|
|
129
|
+
options.push(cloneCascaderOption(source));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!existing.label && source.label) {
|
|
134
|
+
existing.label = source.label;
|
|
135
|
+
}
|
|
136
|
+
if (source.isLeaf === false || existing.children?.length || source.children?.length) {
|
|
137
|
+
existing.isLeaf = false;
|
|
138
|
+
}
|
|
139
|
+
if (source.loading) {
|
|
140
|
+
existing.loading = source.loading;
|
|
141
|
+
}
|
|
142
|
+
if (source.children?.length) {
|
|
143
|
+
existing.children = mergeCascaderOptions(existing.children || [], source.children);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mergeCascaderOptions(
|
|
148
|
+
configured: FieldAssignCascaderOption[],
|
|
149
|
+
allFields: FieldAssignCascaderOption[],
|
|
150
|
+
): FieldAssignCascaderOption[] {
|
|
151
|
+
const result = (Array.isArray(configured) ? configured : []).map((item) => cloneCascaderOption(item));
|
|
152
|
+
for (const item of Array.isArray(allFields) ? allFields : []) {
|
|
153
|
+
mergeCascaderOptionInto(result, item);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
116
158
|
function isAssignMode(mode: unknown): mode is AssignMode {
|
|
117
159
|
return mode === 'default' || mode === 'override' || mode === 'assign';
|
|
118
160
|
}
|
|
@@ -171,13 +213,21 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
171
213
|
},
|
|
172
214
|
[normalizeItemMode, onChange],
|
|
173
215
|
);
|
|
216
|
+
|
|
217
|
+
const normalizeFieldOptions = React.useCallback((options: FieldAssignRulesEditorProps['fieldOptions']) => {
|
|
218
|
+
return Array.isArray(options)
|
|
219
|
+
? (options as FieldAssignCascaderOption[]).map((option) => cloneCascaderOption(option))
|
|
220
|
+
: [];
|
|
221
|
+
}, []);
|
|
174
222
|
const [cascaderOptions, setCascaderOptions] = React.useState<FieldAssignCascaderOption[]>(() =>
|
|
175
|
-
|
|
223
|
+
normalizeFieldOptions(fieldOptions),
|
|
176
224
|
);
|
|
225
|
+
const loadedCascaderOptionsRef = React.useRef<WeakSet<FieldAssignCascaderOption>>(new WeakSet());
|
|
177
226
|
|
|
178
227
|
React.useEffect(() => {
|
|
179
|
-
|
|
180
|
-
|
|
228
|
+
loadedCascaderOptionsRef.current = new WeakSet();
|
|
229
|
+
setCascaderOptions(normalizeFieldOptions(fieldOptions));
|
|
230
|
+
}, [fieldOptions, normalizeFieldOptions, rootCollection, maxAssociationFieldDepth]);
|
|
181
231
|
|
|
182
232
|
const getRuleKey = React.useCallback((item: FieldAssignRuleItem, index: number) => item?.key || String(index), []);
|
|
183
233
|
const [titleFieldDraftMap, setTitleFieldDraftMap] = React.useState<Record<string, string | undefined>>({});
|
|
@@ -597,23 +647,26 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
597
647
|
const opts = selectedOptions || [];
|
|
598
648
|
const target = opts[opts.length - 1];
|
|
599
649
|
if (!target) return;
|
|
600
|
-
if (target.children && Array.isArray(target.children) && target.children.length) return;
|
|
601
650
|
if (target.isLeaf) return;
|
|
651
|
+
if (loadedCascaderOptionsRef.current.has(target)) return;
|
|
602
652
|
|
|
603
653
|
const segments = opts.map((o) => String(o?.value)).filter(Boolean);
|
|
604
654
|
const targetCollection = resolveTargetCollectionBySegments(segments);
|
|
605
655
|
if (!targetCollection) {
|
|
606
|
-
target.isLeaf =
|
|
656
|
+
target.isLeaf = !(target.children && target.children.length);
|
|
657
|
+
loadedCascaderOptionsRef.current.add(target);
|
|
607
658
|
setCascaderOptions((prev) => [...prev]);
|
|
608
659
|
return;
|
|
609
660
|
}
|
|
610
661
|
|
|
611
662
|
const children = buildChildrenFromCollection(targetCollection, segments.length);
|
|
612
663
|
if (!children.length) {
|
|
613
|
-
target.isLeaf =
|
|
664
|
+
target.isLeaf = !(target.children && target.children.length);
|
|
614
665
|
} else {
|
|
615
|
-
target.children = children;
|
|
666
|
+
target.children = mergeCascaderOptions(target.children || [], children);
|
|
667
|
+
target.isLeaf = false;
|
|
616
668
|
}
|
|
669
|
+
loadedCascaderOptionsRef.current.add(target);
|
|
617
670
|
setCascaderOptions((prev) => [...prev]);
|
|
618
671
|
},
|
|
619
672
|
[buildChildrenFromCollection, resolveTargetCollectionBySegments],
|
|
@@ -629,12 +682,14 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
629
682
|
const hit = options.find((o) => String(o?.value) === seg);
|
|
630
683
|
if (!hit) return;
|
|
631
684
|
selected.push(hit);
|
|
685
|
+
if (!hit.isLeaf && !loadedCascaderOptionsRef.current.has(hit)) {
|
|
686
|
+
await loadCascaderData(selected);
|
|
687
|
+
}
|
|
632
688
|
if (hit.children?.length) {
|
|
633
689
|
options = hit.children;
|
|
634
690
|
continue;
|
|
635
691
|
}
|
|
636
692
|
if (hit.isLeaf) return;
|
|
637
|
-
await loadCascaderData(selected);
|
|
638
693
|
options = hit.children || [];
|
|
639
694
|
}
|
|
640
695
|
},
|
|
@@ -665,7 +720,9 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
665
720
|
await preloadCascaderPath(segs);
|
|
666
721
|
}
|
|
667
722
|
};
|
|
668
|
-
|
|
723
|
+
run().catch((error) => {
|
|
724
|
+
console.warn('[FieldAssignRulesEditor] Failed to preload cascader path', error);
|
|
725
|
+
});
|
|
669
726
|
return () => {
|
|
670
727
|
cancelled = true;
|
|
671
728
|
};
|