@nocobase/client-v2 2.1.0-beta.43 → 2.1.0-beta.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/BaseApplication.d.ts +9 -0
- package/es/authRedirect.d.ts +12 -0
- package/es/collection-manager/filter-operators/index.d.ts +1 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +5 -1
- package/es/components/form/filter/FilterValueInput.d.ts +5 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +11 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +3 -2
- package/es/flow/components/ConditionBuilder.d.ts +1 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
- package/es/flow/components/fieldAssignOptions.d.ts +10 -0
- package/es/flow/components/filter/LinkageFilterItem.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
- package/es/flow/components/filter/metaTreeAssociationDepth.d.ts +13 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +26 -3
- package/es/flow/models/base/AssociationFieldGroupModel.d.ts +2 -33
- package/es/flow/models/blocks/filter-manager/utils.d.ts +23 -1
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
- package/es/index.mjs +179 -101
- package/lib/index.js +187 -109
- package/package.json +7 -7
- package/src/BaseApplication.tsx +13 -0
- package/src/__tests__/browserChecker.test.ts +103 -0
- package/src/__tests__/dataSourceRuntime.test.tsx +22 -1
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +5 -0
- package/src/authRedirect.ts +43 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +14 -7
- package/src/collection-manager/filter-operators/index.ts +29 -2
- package/src/components/form/filter/CollectionFilterItem.tsx +10 -2
- package/src/components/form/filter/FilterValueInput.tsx +27 -2
- package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +39 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +8 -1
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +369 -2
- package/src/flow/actions/dataScope.tsx +8 -1
- package/src/flow/actions/linkageRules.tsx +56 -1
- package/src/flow/actions/setTargetDataScope.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +31 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +139 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +75 -1
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +11 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -4
- package/src/flow/admin-shell/admin-layout/useApplications.tsx +34 -1
- package/src/flow/common/Markdown/Edit.tsx +32 -4
- package/src/flow/components/ConditionBuilder.tsx +12 -2
- package/src/flow/components/DefaultValue.tsx +14 -8
- package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
- package/src/flow/components/FieldAssignValueInput.tsx +14 -14
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
- package/src/flow/components/fieldAssignOptions.ts +23 -6
- package/src/flow/components/filter/LinkageFilterItem.tsx +26 -12
- package/src/flow/components/filter/VariableFilterItem.tsx +32 -13
- package/src/flow/components/filter/__tests__/fieldsToOptions.test.ts +48 -0
- package/src/flow/components/filter/__tests__/metaTreeAssociationDepth.test.ts +60 -0
- package/src/flow/components/filter/fieldsToOptions.ts +9 -4
- package/src/flow/components/filter/metaTreeAssociationDepth.ts +69 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +112 -5
- package/src/flow/models/actions/FilterActionModel.tsx +15 -3
- package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
- package/src/flow/models/base/GridModel.tsx +1 -1
- package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
- package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
- package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
- package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +11 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +14 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
- package/src/flow/models/blocks/filter-manager/__tests__/getDefaultOperator.test.ts +35 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +148 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/defaultOperator.test.tsx +85 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
- package/src/flow/models/blocks/filter-manager/flow-actions/customizeFilterRender.tsx +14 -44
- package/src/flow/models/blocks/filter-manager/flow-actions/defaultOperator.tsx +3 -5
- package/src/flow/models/blocks/filter-manager/flow-actions/operatorComponentProps.tsx +2 -12
- package/src/flow/models/blocks/filter-manager/utils.ts +143 -4
- package/src/flow/models/blocks/form/FormAssociationItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +27 -2
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +15 -5
- package/src/flow/models/blocks/table/TableCustomColumnModel.tsx +2 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +18 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +6 -7
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +70 -0
|
@@ -24,7 +24,7 @@ import type { CascaderProps, CollapseProps } from 'antd';
|
|
|
24
24
|
import React from 'react';
|
|
25
25
|
import { ConditionBuilder } from './ConditionBuilder';
|
|
26
26
|
import { FieldAssignValueInput } from './FieldAssignValueInput';
|
|
27
|
-
import type
|
|
27
|
+
import { buildFieldAssignCascaderOptionsFromCollection, type FieldAssignCascaderOption } from './fieldAssignOptions';
|
|
28
28
|
import { isRunJSValue, isVariableExpression, type MetaTreeNode } from '@nocobase/flow-engine';
|
|
29
29
|
import { isToManyAssociationField } from '../internal/utils/modelUtils';
|
|
30
30
|
|
|
@@ -36,6 +36,7 @@ type CollectionFieldLike = {
|
|
|
36
36
|
type?: unknown;
|
|
37
37
|
interface?: unknown;
|
|
38
38
|
uiSchema?: unknown;
|
|
39
|
+
target?: unknown;
|
|
39
40
|
targetKey?: unknown;
|
|
40
41
|
targetCollectionTitleFieldName?: unknown;
|
|
41
42
|
targetCollection?: any;
|
|
@@ -105,6 +106,7 @@ export interface FieldAssignRulesEditorProps {
|
|
|
105
106
|
onSyncAssociationTitleField?: (params: SyncAssociationTitleFieldParams) => Promise<void> | void;
|
|
106
107
|
/** 在日期字段下启用“日期变量替换 Constant 位”。 */
|
|
107
108
|
enableDateVariableAsConstant?: boolean;
|
|
109
|
+
maxAssociationFieldDepth?: number;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (props) => {
|
|
@@ -123,9 +125,10 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
123
125
|
isTitleFieldCandidate,
|
|
124
126
|
onSyncAssociationTitleField,
|
|
125
127
|
enableDateVariableAsConstant = false,
|
|
128
|
+
maxAssociationFieldDepth = 2,
|
|
126
129
|
} = props;
|
|
127
130
|
|
|
128
|
-
const value = Array.isArray(rawValue) ? rawValue : [];
|
|
131
|
+
const value = React.useMemo(() => (Array.isArray(rawValue) ? rawValue : []), [rawValue]);
|
|
129
132
|
const [cascaderOptions, setCascaderOptions] = React.useState<FieldAssignCascaderOption[]>(() =>
|
|
130
133
|
Array.isArray(fieldOptions) ? (fieldOptions as FieldAssignCascaderOption[]) : [],
|
|
131
134
|
);
|
|
@@ -171,6 +174,19 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
171
174
|
}
|
|
172
175
|
}, [getRuleKey, syncingRuleKey, value]);
|
|
173
176
|
|
|
177
|
+
const parseTargetPathToSegments = React.useCallback((targetPath?: string): string[] => {
|
|
178
|
+
const raw = String(targetPath || '');
|
|
179
|
+
if (!raw) return [];
|
|
180
|
+
return raw
|
|
181
|
+
.split('.')
|
|
182
|
+
.map((s) => s.trim())
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
}, []);
|
|
185
|
+
|
|
186
|
+
const isAssociationFieldLike = React.useCallback((field?: CollectionFieldLike | null) => {
|
|
187
|
+
return !!(field?.isAssociationField?.() || field?.target || field?.targetCollection);
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
174
190
|
const buildCollectionMetaTreeNodes = React.useCallback(
|
|
175
191
|
(collection: any, basePaths: string[], visited: Set<any>): MetaTreeNode[] => {
|
|
176
192
|
if (!collection?.getFields) return [];
|
|
@@ -199,7 +215,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
199
215
|
paths: [...basePaths, name],
|
|
200
216
|
};
|
|
201
217
|
|
|
202
|
-
const isAssoc =
|
|
218
|
+
const isAssoc = isAssociationFieldLike(f);
|
|
203
219
|
const isToMany = isToManyAssociationField(f);
|
|
204
220
|
if (isAssoc && !isToMany && f.targetCollection) {
|
|
205
221
|
node.children = async () =>
|
|
@@ -211,7 +227,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
211
227
|
|
|
212
228
|
return nodes;
|
|
213
229
|
},
|
|
214
|
-
[t],
|
|
230
|
+
[isAssociationFieldLike, t],
|
|
215
231
|
);
|
|
216
232
|
|
|
217
233
|
const buildItemMetaTree = React.useCallback(
|
|
@@ -226,7 +242,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
226
242
|
for (let i = 0; i < segs.length; i++) {
|
|
227
243
|
const seg = segs[i] as string;
|
|
228
244
|
const field = current?.getField?.(seg);
|
|
229
|
-
if (!field
|
|
245
|
+
if (!isAssociationFieldLike(field)) break;
|
|
230
246
|
const toMany = isToManyAssociationField(field);
|
|
231
247
|
const nextCollection = field?.targetCollection;
|
|
232
248
|
if (!nextCollection) break;
|
|
@@ -291,7 +307,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
291
307
|
|
|
292
308
|
return [buildObjectNode(levels.length - 1, ['item'], 'item', 'Current item')];
|
|
293
309
|
},
|
|
294
|
-
[buildCollectionMetaTreeNodes, rootCollection, t],
|
|
310
|
+
[buildCollectionMetaTreeNodes, isAssociationFieldLike, parseTargetPathToSegments, rootCollection, t],
|
|
295
311
|
);
|
|
296
312
|
|
|
297
313
|
const patchItem = (index: number, patch: Partial<FieldAssignRuleItem>) => {
|
|
@@ -381,15 +397,6 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
381
397
|
[t],
|
|
382
398
|
);
|
|
383
399
|
|
|
384
|
-
const parseTargetPathToSegments = React.useCallback((targetPath?: string): string[] => {
|
|
385
|
-
const raw = String(targetPath || '');
|
|
386
|
-
if (!raw) return [];
|
|
387
|
-
return raw
|
|
388
|
-
.split('.')
|
|
389
|
-
.map((s) => s.trim())
|
|
390
|
-
.filter(Boolean);
|
|
391
|
-
}, []);
|
|
392
|
-
|
|
393
400
|
const getFieldLabel = (targetPath?: string) => {
|
|
394
401
|
const segs = parseTargetPathToSegments(targetPath);
|
|
395
402
|
if (!segs.length) return undefined;
|
|
@@ -413,7 +420,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
413
420
|
: seg,
|
|
414
421
|
),
|
|
415
422
|
);
|
|
416
|
-
if (field
|
|
423
|
+
if (isAssociationFieldLike(field as CollectionFieldLike) && field?.targetCollection) {
|
|
417
424
|
collection = field.targetCollection;
|
|
418
425
|
}
|
|
419
426
|
}
|
|
@@ -435,7 +442,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
435
442
|
|
|
436
443
|
const isLast = i === segs.length - 1;
|
|
437
444
|
if (isLast) {
|
|
438
|
-
if (!field
|
|
445
|
+
if (!isAssociationFieldLike(field) || !field?.targetCollection) return null;
|
|
439
446
|
|
|
440
447
|
const targetCollection = field.targetCollection;
|
|
441
448
|
const targetFields =
|
|
@@ -502,7 +509,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
502
509
|
};
|
|
503
510
|
}
|
|
504
511
|
|
|
505
|
-
if (!field
|
|
512
|
+
if (!isAssociationFieldLike(field) || !field?.targetCollection) {
|
|
506
513
|
return null;
|
|
507
514
|
}
|
|
508
515
|
collection = field.targetCollection;
|
|
@@ -510,7 +517,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
510
517
|
|
|
511
518
|
return null;
|
|
512
519
|
},
|
|
513
|
-
[isTitleFieldCandidate, parseTargetPathToSegments, rootCollection, t],
|
|
520
|
+
[isAssociationFieldLike, isTitleFieldCandidate, parseTargetPathToSegments, rootCollection, t],
|
|
514
521
|
);
|
|
515
522
|
|
|
516
523
|
const resolveTargetCollectionBySegments = React.useCallback(
|
|
@@ -519,42 +526,24 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
519
526
|
let collection = rootCollection;
|
|
520
527
|
for (const seg of segments) {
|
|
521
528
|
const field = collection?.getField?.(seg);
|
|
522
|
-
if (!field
|
|
529
|
+
if (!isAssociationFieldLike(field) || !field?.targetCollection) {
|
|
523
530
|
return null;
|
|
524
531
|
}
|
|
525
532
|
collection = field.targetCollection;
|
|
526
533
|
}
|
|
527
534
|
return collection || null;
|
|
528
535
|
},
|
|
529
|
-
[rootCollection],
|
|
536
|
+
[isAssociationFieldLike, rootCollection],
|
|
530
537
|
);
|
|
531
538
|
|
|
532
539
|
const buildChildrenFromCollection = React.useCallback(
|
|
533
|
-
(collection: any): FieldAssignCascaderOption[] => {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const f = rawField as CollectionFieldLike;
|
|
539
|
-
const fieldInterface = typeof f.interface === 'string' ? f.interface : undefined;
|
|
540
|
-
if (!fieldInterface) continue;
|
|
541
|
-
if (fieldInterface === 'formula') continue;
|
|
542
|
-
|
|
543
|
-
const name = String(f.name || '');
|
|
544
|
-
if (!name) continue;
|
|
545
|
-
|
|
546
|
-
const title = t(typeof f.title === 'string' ? f.title : name);
|
|
547
|
-
const isAssoc = !!f.isAssociationField?.();
|
|
548
|
-
const hasTarget = !!f.targetCollection;
|
|
549
|
-
out.push({
|
|
550
|
-
label: title,
|
|
551
|
-
value: name,
|
|
552
|
-
isLeaf: !(isAssoc && hasTarget),
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
return out;
|
|
540
|
+
(collection: any, associationDepth: number): FieldAssignCascaderOption[] => {
|
|
541
|
+
return buildFieldAssignCascaderOptionsFromCollection(collection, t, {
|
|
542
|
+
associationDepth,
|
|
543
|
+
maxAssociationFieldDepth,
|
|
544
|
+
});
|
|
556
545
|
},
|
|
557
|
-
[t],
|
|
546
|
+
[maxAssociationFieldDepth, t],
|
|
558
547
|
);
|
|
559
548
|
|
|
560
549
|
const loadCascaderData = React.useCallback<NonNullable<CascaderProps<FieldAssignCascaderOption>['loadData']>>(
|
|
@@ -573,7 +562,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
573
562
|
return;
|
|
574
563
|
}
|
|
575
564
|
|
|
576
|
-
const children = buildChildrenFromCollection(targetCollection);
|
|
565
|
+
const children = buildChildrenFromCollection(targetCollection, segments.length);
|
|
577
566
|
if (!children.length) {
|
|
578
567
|
target.isLeaf = true;
|
|
579
568
|
} else {
|
|
@@ -889,6 +878,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
889
878
|
value={item.value}
|
|
890
879
|
onChange={(v) => patchItem(index, { value: v })}
|
|
891
880
|
extraMetaTree={extraMetaTree}
|
|
881
|
+
maxAssociationFieldDepth={maxAssociationFieldDepth}
|
|
892
882
|
{...(getValueInputProps?.(item, index) || {})}
|
|
893
883
|
enableDateVariableAsConstant={enableDateVariableAsConstant}
|
|
894
884
|
associationFieldNamesOverride={
|
|
@@ -959,6 +949,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
959
949
|
value={item.condition || { logic: '$and', items: [] }}
|
|
960
950
|
onChange={(condition) => patchItem(index, { condition })}
|
|
961
951
|
extraMetaTree={extraMetaTree}
|
|
952
|
+
maxAssociationFieldDepth={2}
|
|
962
953
|
/>
|
|
963
954
|
</div>
|
|
964
955
|
)}
|
|
@@ -36,10 +36,11 @@ import {
|
|
|
36
36
|
isToManyAssociationField,
|
|
37
37
|
} from '../internal/utils/modelUtils';
|
|
38
38
|
import { RunJSValueEditor } from './RunJSValueEditor';
|
|
39
|
-
import { resolveOperatorComponent } from '../internal/utils/operatorSchemaHelper';
|
|
39
|
+
import { pickOperatorStyle as pickStyle, resolveOperatorComponent } from '../internal/utils/operatorSchemaHelper';
|
|
40
40
|
import { InputFieldModel } from '../models/fields/InputFieldModel';
|
|
41
41
|
import { normalizeFilterValueByOperator } from '../models/blocks/filter-form/valueNormalization';
|
|
42
42
|
import { FieldAssignExactDatePicker, type ExactDatePickerMode } from './FieldAssignExactDatePicker';
|
|
43
|
+
import { limitAssociationMetaTree } from './filter/metaTreeAssociationDepth';
|
|
43
44
|
|
|
44
45
|
const DATE_FIELD_INTERFACES = new Set(['date', 'datetime', 'datetimeNoTz', 'createdAt', 'updatedAt', 'unixTimestamp']);
|
|
45
46
|
|
|
@@ -340,6 +341,7 @@ interface Props {
|
|
|
340
341
|
* 默认 false,保持历史行为。
|
|
341
342
|
*/
|
|
342
343
|
enableDateVariableAsConstant?: boolean;
|
|
344
|
+
maxAssociationFieldDepth?: number;
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
type ResolvedFieldContext = {
|
|
@@ -352,14 +354,6 @@ type ResolvedFieldContext = {
|
|
|
352
354
|
collectionField: CollectionField | null;
|
|
353
355
|
};
|
|
354
356
|
|
|
355
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
356
|
-
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function pickStyle(value: unknown): React.CSSProperties | undefined {
|
|
360
|
-
return isPlainObject(value) ? (value as React.CSSProperties) : undefined;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
357
|
function withFullWidthStyle(style?: React.CSSProperties): React.CSSProperties {
|
|
364
358
|
return { ...style, width: '100%', minWidth: 0 };
|
|
365
359
|
}
|
|
@@ -718,6 +712,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
718
712
|
preferFormItemFieldModel,
|
|
719
713
|
associationFieldNamesOverride,
|
|
720
714
|
enableDateVariableAsConstant = false,
|
|
715
|
+
maxAssociationFieldDepth = 2,
|
|
721
716
|
}) => {
|
|
722
717
|
const flowCtx = useFlowContext<FlowModelContext>();
|
|
723
718
|
const normalizeEventValue = React.useCallback((eventOrValue: unknown) => {
|
|
@@ -859,7 +854,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
859
854
|
fieldName,
|
|
860
855
|
collectionField: nested.collectionField || null,
|
|
861
856
|
};
|
|
862
|
-
}, [flowCtx
|
|
857
|
+
}, [flowCtx, itemModel, resolveNestedAssociationField, targetPath]);
|
|
863
858
|
|
|
864
859
|
const { collection, dataSource, blockModel, fieldPath, fieldName, collectionField: cf } = resolved;
|
|
865
860
|
const itemCollectionField = (resolved?.itemModel as any)?.context?.collectionField;
|
|
@@ -1172,7 +1167,11 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1172
1167
|
<Comp
|
|
1173
1168
|
{...fieldModel.props}
|
|
1174
1169
|
{...xProps}
|
|
1175
|
-
style={{
|
|
1170
|
+
style={{
|
|
1171
|
+
width: '100%',
|
|
1172
|
+
...pickStyle((fieldModel.props as Record<string, unknown>)?.style),
|
|
1173
|
+
...pickStyle(xProps?.style),
|
|
1174
|
+
}}
|
|
1176
1175
|
/>
|
|
1177
1176
|
);
|
|
1178
1177
|
rewrapReactiveRender(fieldModel);
|
|
@@ -1232,7 +1231,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1232
1231
|
);
|
|
1233
1232
|
};
|
|
1234
1233
|
return C;
|
|
1235
|
-
}, [placeholder, tempRoot, coerceEmptyValueForRenderer, normalizeEventValue]);
|
|
1234
|
+
}, [placeholder, tempRoot, coerceEmptyValueForRenderer, normalizeEventValue, operator]);
|
|
1236
1235
|
|
|
1237
1236
|
const DateVariableConstantEditor = React.useMemo(() => {
|
|
1238
1237
|
const C: React.FC<any> = (inputProps) => {
|
|
@@ -1408,6 +1407,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1408
1407
|
const extra = extraMetaTreeRef.current;
|
|
1409
1408
|
const extraTree = Array.isArray(extra) ? extra : [];
|
|
1410
1409
|
const mergedBase = mergeItemMetaTreeForAssignValue(base as MetaTreeNode[], extraTree as MetaTreeNode[]);
|
|
1410
|
+
const limitedBase = limitAssociationMetaTree(mergedBase, { maxAssociationDepth: maxAssociationFieldDepth });
|
|
1411
1411
|
return [
|
|
1412
1412
|
{
|
|
1413
1413
|
title: tExpr('Constant'),
|
|
@@ -1418,10 +1418,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1418
1418
|
},
|
|
1419
1419
|
{ title: tExpr('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
1420
1420
|
{ title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent },
|
|
1421
|
-
...
|
|
1421
|
+
...limitedBase,
|
|
1422
1422
|
];
|
|
1423
1423
|
};
|
|
1424
|
-
}, [flowCtx, ConstantEditor, NullComponent, RunJSComponent]);
|
|
1424
|
+
}, [flowCtx, ConstantEditor, NullComponent, RunJSComponent, maxAssociationFieldDepth]);
|
|
1425
1425
|
|
|
1426
1426
|
const displayValue = React.useMemo(() => {
|
|
1427
1427
|
if (!useDateVariableConstant) {
|
|
@@ -108,6 +108,54 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
108
108
|
};
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
const createAssociationFixtureWithoutIsAssociationField = () => {
|
|
112
|
+
const profileCollection = {
|
|
113
|
+
titleField: 'name',
|
|
114
|
+
filterTargetKey: 'id',
|
|
115
|
+
getField: (name: string) => {
|
|
116
|
+
if (name === 'name') {
|
|
117
|
+
return { name: 'name', title: 'Name', interface: 'input' };
|
|
118
|
+
}
|
|
119
|
+
if (name === 'nickname') {
|
|
120
|
+
return { name: 'nickname', title: 'Nickname', interface: 'input' };
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
},
|
|
124
|
+
getFields: () => [
|
|
125
|
+
{
|
|
126
|
+
name: 'name',
|
|
127
|
+
title: 'Name',
|
|
128
|
+
interface: 'input',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'nickname',
|
|
132
|
+
title: 'Nickname',
|
|
133
|
+
interface: 'input',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const profileField: any = {
|
|
139
|
+
name: 'profile',
|
|
140
|
+
title: 'Profile',
|
|
141
|
+
type: 'belongsTo',
|
|
142
|
+
interface: 'm2o',
|
|
143
|
+
target: 'profiles',
|
|
144
|
+
targetKey: 'id',
|
|
145
|
+
targetCollection: profileCollection,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const rootCollection = {
|
|
149
|
+
getField: (name: string) => (name === 'profile' ? profileField : null),
|
|
150
|
+
getFields: () => [profileField],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
profileCollection,
|
|
155
|
+
rootCollection,
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
111
159
|
const openAdvancedPanel = async () => {
|
|
112
160
|
await userEvent.click(screen.getByRole('button', { name: /Advanced/i }));
|
|
113
161
|
await screen.findByText('Title field');
|
|
@@ -175,6 +223,68 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
175
223
|
});
|
|
176
224
|
});
|
|
177
225
|
|
|
226
|
+
it('recognizes lightweight association fields without isAssociationField()', async () => {
|
|
227
|
+
const { rootCollection } = createAssociationFixtureWithoutIsAssociationField();
|
|
228
|
+
const value: FieldAssignRuleItem[] = [
|
|
229
|
+
{
|
|
230
|
+
key: 'rule-profile',
|
|
231
|
+
enable: true,
|
|
232
|
+
targetPath: 'profile',
|
|
233
|
+
mode: 'assign',
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
const { container } = render(
|
|
238
|
+
wrap(
|
|
239
|
+
<FieldAssignRulesEditor
|
|
240
|
+
t={t}
|
|
241
|
+
fieldOptions={[]}
|
|
242
|
+
rootCollection={rootCollection}
|
|
243
|
+
value={value}
|
|
244
|
+
showCondition={false}
|
|
245
|
+
isTitleFieldCandidate={() => true}
|
|
246
|
+
/>,
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
const input = container.querySelector('[data-testid="mock-value-input"]');
|
|
252
|
+
expect(input?.getAttribute('data-assoc-label')).toBe('name');
|
|
253
|
+
expect(input?.getAttribute('data-assoc-value')).toBe('id');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('resolves nested labels and item meta tree through lightweight association fields', async () => {
|
|
258
|
+
const { rootCollection } = createAssociationFixtureWithoutIsAssociationField();
|
|
259
|
+
const value: FieldAssignRuleItem[] = [
|
|
260
|
+
{
|
|
261
|
+
key: 'rule-profile-nickname',
|
|
262
|
+
enable: true,
|
|
263
|
+
targetPath: 'profile.nickname',
|
|
264
|
+
mode: 'assign',
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
const { container } = render(
|
|
269
|
+
wrap(
|
|
270
|
+
<FieldAssignRulesEditor
|
|
271
|
+
t={t}
|
|
272
|
+
fieldOptions={[]}
|
|
273
|
+
rootCollection={rootCollection}
|
|
274
|
+
value={value}
|
|
275
|
+
showCondition={false}
|
|
276
|
+
isTitleFieldCandidate={() => true}
|
|
277
|
+
/>,
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(container.textContent).toContain('Profile / Nickname');
|
|
283
|
+
const input = container.querySelector('[data-testid="mock-value-input"]');
|
|
284
|
+
expect(input?.getAttribute('data-extra')).toBe('yes');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
178
288
|
it('saves selected title field into current assign rule without syncing', async () => {
|
|
179
289
|
const { rootCollection, value } = createAssociationFixture();
|
|
180
290
|
const onChange = vi.fn();
|
|
@@ -785,7 +895,8 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
785
895
|
expect(merged.map((node) => node.name)).toEqual(['formValues', 'item', 'user']);
|
|
786
896
|
expect(merged.filter((node) => node.name === 'item')).toHaveLength(1);
|
|
787
897
|
|
|
788
|
-
const itemNode = merged.find((node) => node.name === 'item')
|
|
898
|
+
const itemNode = merged.find((node) => node.name === 'item');
|
|
899
|
+
expect(itemNode).toBeTruthy();
|
|
789
900
|
const parent = (itemNode.children as MetaTreeNode[]).find((node) => node.name === 'parentItem');
|
|
790
901
|
expect(parent).toBeTruthy();
|
|
791
902
|
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it } from 'vitest';
|
|
11
11
|
import { buildCustomFieldTargetPath } from '../../internal/utils/modelUtils';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
buildFieldAssignCascaderOptionsFromCollection,
|
|
14
|
+
collectFieldAssignCascaderOptions,
|
|
15
|
+
} from '../fieldAssignOptions';
|
|
13
16
|
|
|
14
17
|
describe('fieldAssignOptions', () => {
|
|
15
18
|
it('marks subform node as non-leaf so Cascader can lazy-load target collection fields', () => {
|
|
@@ -94,4 +97,71 @@ describe('fieldAssignOptions', () => {
|
|
|
94
97
|
options.some((o: any) => o?.value === customTargetPath && o?.label === 'Custom age' && o?.isLeaf === true),
|
|
95
98
|
).toBe(true);
|
|
96
99
|
});
|
|
100
|
+
|
|
101
|
+
it('hides configured association fields deeper than relation / relation / field', () => {
|
|
102
|
+
const makeItem = (fieldPath: string, field: any, label: string) => ({
|
|
103
|
+
props: { label },
|
|
104
|
+
getStepParams: (flowKey: string, stepKey: string) => {
|
|
105
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') return { fieldPath };
|
|
106
|
+
return {};
|
|
107
|
+
},
|
|
108
|
+
subModels: {
|
|
109
|
+
field: {
|
|
110
|
+
context: { collectionField: field },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const companyField = {
|
|
116
|
+
name: 'company',
|
|
117
|
+
title: 'Company',
|
|
118
|
+
interface: 'm2o',
|
|
119
|
+
target: 'companies',
|
|
120
|
+
isAssociationField: () => true,
|
|
121
|
+
targetCollection: { getFields: () => [] },
|
|
122
|
+
};
|
|
123
|
+
const nameField = {
|
|
124
|
+
name: 'name',
|
|
125
|
+
title: 'Name',
|
|
126
|
+
interface: 'input',
|
|
127
|
+
isAssociationField: () => false,
|
|
128
|
+
};
|
|
129
|
+
const formBlockModel = {
|
|
130
|
+
context: { collection: null },
|
|
131
|
+
subModels: {
|
|
132
|
+
grid: {
|
|
133
|
+
subModels: {
|
|
134
|
+
items: [
|
|
135
|
+
makeItem('users.department.name', nameField, 'Department name'),
|
|
136
|
+
makeItem('users.department.company', companyField, 'Company'),
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const options = collectFieldAssignCascaderOptions({ formBlockModel, t: (s) => s });
|
|
144
|
+
|
|
145
|
+
expect(options.map((item) => item.value)).toContain('name');
|
|
146
|
+
expect(options.map((item) => item.value)).not.toContain('company');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('hides lazy-loaded association selector options after two association levels', () => {
|
|
150
|
+
const collection = {
|
|
151
|
+
getFields: () => [
|
|
152
|
+
{ name: 'name', title: 'Name', interface: 'input' },
|
|
153
|
+
{
|
|
154
|
+
name: 'company',
|
|
155
|
+
title: 'Company',
|
|
156
|
+
interface: 'm2o',
|
|
157
|
+
target: 'companies',
|
|
158
|
+
targetCollection: { getFields: () => [] },
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const options = buildFieldAssignCascaderOptionsFromCollection(collection, (s) => s, { associationDepth: 2 });
|
|
164
|
+
|
|
165
|
+
expect(options.map((item) => item.value)).toEqual(['name']);
|
|
166
|
+
});
|
|
97
167
|
});
|
|
@@ -76,10 +76,15 @@ function getCollectionFromFormBlockModel(model: any): CollectionLike | null {
|
|
|
76
76
|
return collection && typeof collection === 'object' ? (collection as CollectionLike) : null;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function
|
|
79
|
+
export function buildFieldAssignCascaderOptionsFromCollection(
|
|
80
80
|
collection: CollectionLike | null,
|
|
81
81
|
t: (key: string) => string,
|
|
82
|
+
options: {
|
|
83
|
+
associationDepth?: number;
|
|
84
|
+
maxAssociationFieldDepth?: number;
|
|
85
|
+
} = {},
|
|
82
86
|
): FieldAssignCascaderOption[] {
|
|
87
|
+
const { associationDepth = 0, maxAssociationFieldDepth = 2 } = options;
|
|
83
88
|
const fields = typeof collection?.getFields === 'function' ? collection.getFields() || [] : [];
|
|
84
89
|
const out: FieldAssignCascaderOption[] = [];
|
|
85
90
|
for (const rawField of fields) {
|
|
@@ -88,6 +93,7 @@ function buildRootOptionsFromCollection(
|
|
|
88
93
|
name?: unknown;
|
|
89
94
|
title?: unknown;
|
|
90
95
|
interface?: unknown;
|
|
96
|
+
target?: unknown;
|
|
91
97
|
isAssociationField?: () => boolean;
|
|
92
98
|
targetCollection?: any;
|
|
93
99
|
};
|
|
@@ -99,8 +105,11 @@ function buildRootOptionsFromCollection(
|
|
|
99
105
|
if (!name) continue;
|
|
100
106
|
const title = t(typeof f.title === 'string' ? f.title : name);
|
|
101
107
|
|
|
102
|
-
const isAssoc = !!f.isAssociationField?.();
|
|
108
|
+
const isAssoc = !!(f.isAssociationField?.() || f.target || f.targetCollection);
|
|
103
109
|
const hasTarget = !!f.targetCollection;
|
|
110
|
+
if (isAssoc && associationDepth >= maxAssociationFieldDepth) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
104
113
|
|
|
105
114
|
out.push({
|
|
106
115
|
label: title,
|
|
@@ -138,8 +147,9 @@ export function collectFieldAssignCascaderOptions(options: {
|
|
|
138
147
|
t: (key: string) => string;
|
|
139
148
|
/** 子表单模型递归深度(FormItemModel 层级);默认不限制(只受实际配置与循环引用约束) */
|
|
140
149
|
maxFormItemDepth?: number;
|
|
150
|
+
maxAssociationFieldDepth?: number;
|
|
141
151
|
}): FieldAssignCascaderOption[] {
|
|
142
|
-
const { formBlockModel, t, maxFormItemDepth = Number.POSITIVE_INFINITY } = options;
|
|
152
|
+
const { formBlockModel, t, maxFormItemDepth = Number.POSITIVE_INFINITY, maxAssociationFieldDepth = 2 } = options;
|
|
143
153
|
|
|
144
154
|
const rootItems = formBlockModel?.subModels?.grid?.subModels?.items || [];
|
|
145
155
|
const rootCollection = getCollectionFromFormBlockModel(formBlockModel);
|
|
@@ -167,9 +177,14 @@ export function collectFieldAssignCascaderOptions(options: {
|
|
|
167
177
|
|
|
168
178
|
const fieldModel = item?.subModels?.field;
|
|
169
179
|
const childItems = fieldModel?.subModels?.grid?.subModels?.items;
|
|
170
|
-
const cf: CollectionField | undefined =
|
|
171
|
-
|
|
180
|
+
const cf: (CollectionField & { target?: unknown; targetCollection?: unknown }) | undefined =
|
|
181
|
+
fieldModel?.context?.collectionField;
|
|
182
|
+
const isAssociation = !!(cf?.isAssociationField?.() || cf?.target || cf?.targetCollection);
|
|
172
183
|
const hasTargetCollection = !!cf?.targetCollection;
|
|
184
|
+
const associationDepth = isAssociation ? targetPath.split('.').filter(Boolean).length : 0;
|
|
185
|
+
if (isAssociation && associationDepth > maxAssociationFieldDepth) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
173
188
|
|
|
174
189
|
// 1) 子表单/子表单列表:递归展开已配置字段(支持无限深度)。
|
|
175
190
|
//
|
|
@@ -207,6 +222,8 @@ export function collectFieldAssignCascaderOptions(options: {
|
|
|
207
222
|
};
|
|
208
223
|
|
|
209
224
|
const configuredOptions = walkItems(rootItems, 1);
|
|
210
|
-
const allFieldOptions =
|
|
225
|
+
const allFieldOptions = buildFieldAssignCascaderOptionsFromCollection(rootCollection, t, {
|
|
226
|
+
maxAssociationFieldDepth,
|
|
227
|
+
});
|
|
211
228
|
return mergeRootOptions(configuredOptions, allFieldOptions);
|
|
212
229
|
}
|