@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
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
UiSchemaEnumItem,
|
|
30
30
|
} from '../../internal/utils/enumOptionsUtils';
|
|
31
31
|
import { mergeItemMetaTreeForAssignValue } from '../FieldAssignValueInput';
|
|
32
|
-
import { resolveOperatorComponent } from '../../internal/utils/operatorSchemaHelper';
|
|
32
|
+
import { pickOperatorStyle as pickStyle, resolveOperatorComponent } from '../../internal/utils/operatorSchemaHelper';
|
|
33
|
+
import { limitAssociationMetaTree } from './metaTreeAssociationDepth';
|
|
33
34
|
|
|
34
35
|
const { DateFilterDynamicComponent: DateFilterDynamicComponentLazy } = lazy(
|
|
35
36
|
() => import('../../models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent'),
|
|
@@ -49,6 +50,14 @@ export interface LinkageFilterItemProps {
|
|
|
49
50
|
model: FlowModel;
|
|
50
51
|
/** 向变量树额外注入节点(置于根部) */
|
|
51
52
|
extraMetaTree?: MetaTreeNode[];
|
|
53
|
+
maxAssociationFieldDepth?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function limitMetaTreeIfNeeded(nodes: MetaTreeNode[], maxAssociationFieldDepth?: number) {
|
|
57
|
+
if (typeof maxAssociationFieldDepth !== 'number') {
|
|
58
|
+
return nodes;
|
|
59
|
+
}
|
|
60
|
+
return limitAssociationMetaTree(nodes, { maxAssociationDepth: maxAssociationFieldDepth });
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
function createStaticInputRenderer(
|
|
@@ -221,7 +230,7 @@ export async function enhanceMetaTreeWithFilterableChildren(
|
|
|
221
230
|
* LinkageFilterItem:左/右均为可变量输入,适用于联动规则等“前端逻辑”场景
|
|
222
231
|
*/
|
|
223
232
|
export const LinkageFilterItem: React.FC<LinkageFilterItemProps> = observer((props) => {
|
|
224
|
-
const { value, model, extraMetaTree } = props;
|
|
233
|
+
const { value, model, extraMetaTree, maxAssociationFieldDepth } = props;
|
|
225
234
|
const ctx = useFlowViewContext();
|
|
226
235
|
const t = model.translate;
|
|
227
236
|
const { path: leftPath, operator: selectedOperator, value: rightOperandValue } = value || {};
|
|
@@ -345,7 +354,8 @@ export const LinkageFilterItem: React.FC<LinkageFilterItemProps> = observer((pro
|
|
|
345
354
|
return (inputProps: { value?: any; onChange?: (v: any) => void } & Record<string, any>) => {
|
|
346
355
|
const { value: inputValue, onChange, ...rest } = inputProps || {};
|
|
347
356
|
const nextProps = { ...componentProps };
|
|
348
|
-
|
|
357
|
+
const options = Array.isArray(nextProps.options) ? nextProps.options : undefined;
|
|
358
|
+
if ((!options || options.length === 0) && enumOptions.length > 0) {
|
|
349
359
|
nextProps.options = enumOptions;
|
|
350
360
|
}
|
|
351
361
|
const normalizedValue = Array.isArray(inputValue)
|
|
@@ -363,7 +373,7 @@ export const LinkageFilterItem: React.FC<LinkageFilterItemProps> = observer((pro
|
|
|
363
373
|
{...rest}
|
|
364
374
|
value={normalizedValue}
|
|
365
375
|
onChange={onChange}
|
|
366
|
-
style={{ width: 200, ...(nextProps
|
|
376
|
+
style={{ width: 200, ...pickStyle(nextProps.style), ...pickStyle(rest?.style) }}
|
|
367
377
|
/>
|
|
368
378
|
);
|
|
369
379
|
|
|
@@ -386,13 +396,16 @@ export const LinkageFilterItem: React.FC<LinkageFilterItemProps> = observer((pro
|
|
|
386
396
|
return async () => {
|
|
387
397
|
const baseMetaTree = (model?.context.getPropertyMetaTree() || ctx.getPropertyMetaTree()) as MetaTreeNode[];
|
|
388
398
|
const mergedMetaTree = mergeExtraMetaTreeWithBase(baseMetaTree, extraMetaTree);
|
|
389
|
-
return
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
399
|
+
return limitMetaTreeIfNeeded(
|
|
400
|
+
[
|
|
401
|
+
{ title: t('Constant'), name: 'constant', type: 'string', paths: ['constant'] },
|
|
402
|
+
{ title: t('Null'), name: 'null', type: 'object', paths: ['null'] },
|
|
403
|
+
...mergedMetaTree,
|
|
404
|
+
],
|
|
405
|
+
maxAssociationFieldDepth,
|
|
406
|
+
);
|
|
394
407
|
};
|
|
395
|
-
}, [ctx, model, t, extraMetaTree]);
|
|
408
|
+
}, [ctx, model, t, extraMetaTree, maxAssociationFieldDepth]);
|
|
396
409
|
|
|
397
410
|
// 左侧变量树:默认整棵 ctx(不追加 Constant/Null),确保可获取字段 interface
|
|
398
411
|
const leftMetaTreeGetter = useCallback(async () => {
|
|
@@ -403,8 +416,9 @@ export const LinkageFilterItem: React.FC<LinkageFilterItemProps> = observer((pro
|
|
|
403
416
|
getFlowFieldInterfaceOptions(name, model.context.dataSourceManager, model.context.app?.dataSourceManager) as
|
|
404
417
|
| FieldInterfaceDef
|
|
405
418
|
| undefined;
|
|
406
|
-
|
|
407
|
-
|
|
419
|
+
const enhanced = await enhanceMetaTreeWithFilterableChildren(merged, getFieldInterface);
|
|
420
|
+
return limitMetaTreeIfNeeded(enhanced, maxAssociationFieldDepth);
|
|
421
|
+
}, [ctx, model, extraMetaTree, maxAssociationFieldDepth]);
|
|
408
422
|
|
|
409
423
|
// 右侧 converters:常量/空值特殊处理;变量沿用默认(表达式)
|
|
410
424
|
const rightSideConverters = useMemo<Converters>(() => {
|
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
import _ from 'lodash';
|
|
27
27
|
import { NumberPicker } from '@formily/antd-v5';
|
|
28
28
|
import { enumToOptions, normalizeSelectRenderValue, UiSchemaEnumItem } from '../../internal/utils/enumOptionsUtils';
|
|
29
|
-
import { resolveOperatorComponent } from '../../internal/utils/operatorSchemaHelper';
|
|
29
|
+
import { pickOperatorStyle as pickStyle, resolveOperatorComponent } from '../../internal/utils/operatorSchemaHelper';
|
|
30
|
+
import { limitAssociationMetaTree } from './metaTreeAssociationDepth';
|
|
30
31
|
|
|
31
32
|
const { DateFilterDynamicComponent: DateFilterDynamicComponentLazy } = lazy(
|
|
32
33
|
() => import('../../models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent'),
|
|
@@ -85,6 +86,14 @@ export interface VariableFilterItemProps {
|
|
|
85
86
|
*/
|
|
86
87
|
rightMetaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
87
88
|
ignoreFieldNames?: string[];
|
|
89
|
+
maxAssociationFieldDepth?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function limitMetaTreeIfNeeded(nodes: MetaTreeNode[], maxAssociationFieldDepth?: number) {
|
|
93
|
+
if (typeof maxAssociationFieldDepth !== 'number') {
|
|
94
|
+
return nodes;
|
|
95
|
+
}
|
|
96
|
+
return limitAssociationMetaTree(nodes, { maxAssociationDepth: maxAssociationFieldDepth });
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
function createStaticInputRenderer(
|
|
@@ -260,7 +269,7 @@ function normalizeRightValueInput(input: unknown) {
|
|
|
260
269
|
* 上下文筛选项组件
|
|
261
270
|
*/
|
|
262
271
|
export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
263
|
-
({ value, model, rightAsVariable, rightMetaTree, ignoreFieldNames }) => {
|
|
272
|
+
({ value, model, rightAsVariable, rightMetaTree, ignoreFieldNames, maxAssociationFieldDepth }) => {
|
|
264
273
|
// 使用 View 上下文,确保可访问 ctx.view 的异步子树
|
|
265
274
|
const ctx = useFlowViewContext();
|
|
266
275
|
const t = model.translate;
|
|
@@ -486,14 +495,15 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
486
495
|
if (resolved && supportKeyword) {
|
|
487
496
|
const { Comp, props: xProps } = resolved;
|
|
488
497
|
const nextProps = { ...xProps };
|
|
489
|
-
|
|
498
|
+
const options = Array.isArray(nextProps.options) ? nextProps.options : undefined;
|
|
499
|
+
if ((!options || options.length === 0) && enumOptions?.length) {
|
|
490
500
|
nextProps.options = enumOptions;
|
|
491
501
|
}
|
|
492
502
|
const style = {
|
|
493
503
|
flex: '1 1 40%',
|
|
494
504
|
minWidth: 160,
|
|
495
505
|
maxWidth: '100%',
|
|
496
|
-
...(nextProps
|
|
506
|
+
...pickStyle(nextProps.style),
|
|
497
507
|
};
|
|
498
508
|
const normalized =
|
|
499
509
|
Array.isArray(rightValue) && rightValue.every((v) => typeof v === 'string' || typeof v === 'number')
|
|
@@ -508,7 +518,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
508
518
|
<div style={style}>
|
|
509
519
|
<Comp
|
|
510
520
|
{...nextProps}
|
|
511
|
-
style={{ width: '100%', ...(nextProps
|
|
521
|
+
style={{ width: '100%', ...pickStyle(nextProps.style) }}
|
|
512
522
|
value={normalized}
|
|
513
523
|
onChange={(vals: any) => setRightValue(vals)}
|
|
514
524
|
/>
|
|
@@ -539,13 +549,22 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
539
549
|
const nodes: MetaTreeNode[] = Array.isArray(raw)
|
|
540
550
|
? (raw as MetaTreeNode[])
|
|
541
551
|
: await (raw as () => Promise<MetaTreeNode[]>)();
|
|
542
|
-
return
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
552
|
+
return limitMetaTreeIfNeeded(
|
|
553
|
+
[
|
|
554
|
+
{
|
|
555
|
+
title: t('Constant'),
|
|
556
|
+
name: 'constant',
|
|
557
|
+
type: 'string',
|
|
558
|
+
paths: ['constant'],
|
|
559
|
+
render: staticInputRenderer,
|
|
560
|
+
},
|
|
561
|
+
{ title: t('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
562
|
+
...nodes,
|
|
563
|
+
],
|
|
564
|
+
maxAssociationFieldDepth,
|
|
565
|
+
);
|
|
547
566
|
};
|
|
548
|
-
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t]);
|
|
567
|
+
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t, maxAssociationFieldDepth]);
|
|
549
568
|
|
|
550
569
|
// 当启用右侧变量输入时,构造 VariableInput 的 converters:
|
|
551
570
|
// - 变量模式:返回 null 让 VariableInput 渲染 VariableTag
|
|
@@ -629,9 +648,9 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
629
648
|
for (const n of nodes) {
|
|
630
649
|
out.push(await enhanceNode(n));
|
|
631
650
|
}
|
|
632
|
-
return out;
|
|
651
|
+
return limitMetaTreeIfNeeded(out, maxAssociationFieldDepth);
|
|
633
652
|
};
|
|
634
|
-
}, [getFieldInterface, model]);
|
|
653
|
+
}, [getFieldInterface, model, maxAssociationFieldDepth]);
|
|
635
654
|
|
|
636
655
|
return (
|
|
637
656
|
<Space wrap style={{ width: '100%' }}>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { fieldsToOptions } from '../fieldsToOptions';
|
|
13
|
+
|
|
14
|
+
describe('fieldsToOptions', () => {
|
|
15
|
+
it('skips nested fields when target collection is unavailable', () => {
|
|
16
|
+
const field = {
|
|
17
|
+
name: 'space',
|
|
18
|
+
type: 'belongsTo',
|
|
19
|
+
target: 'spaces',
|
|
20
|
+
title: 'Space',
|
|
21
|
+
interface: 'm2o',
|
|
22
|
+
filterable: true,
|
|
23
|
+
uiSchema: {},
|
|
24
|
+
getInterfaceOptions: () => ({
|
|
25
|
+
filterable: {
|
|
26
|
+
nested: true,
|
|
27
|
+
operators: [{ value: '$eq', label: 'is' }],
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
getFields: () => {
|
|
31
|
+
throw new Error('Target collection spaces not found for field space');
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
expect(() => fieldsToOptions([field], 1, [], (key: string) => key)).not.toThrow();
|
|
36
|
+
expect(fieldsToOptions([field], 1, [], (key: string) => key)).toEqual([
|
|
37
|
+
{
|
|
38
|
+
name: 'space',
|
|
39
|
+
type: 'belongsTo',
|
|
40
|
+
target: 'spaces',
|
|
41
|
+
title: 'Space',
|
|
42
|
+
schema: {},
|
|
43
|
+
operators: [{ value: '$eq', label: 'is' }],
|
|
44
|
+
children: [],
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import type { MetaTreeNode } from '@nocobase/flow-engine';
|
|
12
|
+
import { limitAssociationMetaTree } from '../metaTreeAssociationDepth';
|
|
13
|
+
|
|
14
|
+
describe('limitAssociationMetaTree', () => {
|
|
15
|
+
it('keeps relation / relation / normal field and removes deeper relation fields', async () => {
|
|
16
|
+
const tree: MetaTreeNode[] = [
|
|
17
|
+
{
|
|
18
|
+
name: 'user',
|
|
19
|
+
title: 'User',
|
|
20
|
+
type: 'object',
|
|
21
|
+
interface: 'm2o',
|
|
22
|
+
options: { target: 'users' },
|
|
23
|
+
paths: ['collection', 'user'],
|
|
24
|
+
children: async () => [
|
|
25
|
+
{
|
|
26
|
+
name: 'department',
|
|
27
|
+
title: 'Department',
|
|
28
|
+
type: 'object',
|
|
29
|
+
interface: 'm2o',
|
|
30
|
+
options: { target: 'departments' },
|
|
31
|
+
paths: ['collection', 'user', 'department'],
|
|
32
|
+
children: async () => [
|
|
33
|
+
{
|
|
34
|
+
name: 'name',
|
|
35
|
+
title: 'Name',
|
|
36
|
+
type: 'string',
|
|
37
|
+
interface: 'input',
|
|
38
|
+
paths: ['collection', 'user', 'department', 'name'],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'company',
|
|
42
|
+
title: 'Company',
|
|
43
|
+
type: 'object',
|
|
44
|
+
interface: 'm2o',
|
|
45
|
+
options: { target: 'companies' },
|
|
46
|
+
paths: ['collection', 'user', 'department', 'company'],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const limited = limitAssociationMetaTree(tree);
|
|
55
|
+
const userChildren = await (limited[0].children as () => Promise<MetaTreeNode[]>)();
|
|
56
|
+
const departmentChildren = await (userChildren[0].children as () => Promise<MetaTreeNode[]>)();
|
|
57
|
+
|
|
58
|
+
expect(departmentChildren.map((node) => node.name)).toEqual(['name']);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -59,10 +59,15 @@ const field2option = (field: CollectionField, depth, nonfilterable, t) => {
|
|
|
59
59
|
option['children'] = children;
|
|
60
60
|
}
|
|
61
61
|
if (nested) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
let targetFields: CollectionField[] = [];
|
|
63
|
+
try {
|
|
64
|
+
targetFields = field.getFields().filter((f) => {
|
|
65
|
+
// 过滤掉附件字段,因为会报错:Target collection attachments not found for field xxx
|
|
66
|
+
return f.target !== 'attachments' && f.interface !== 'formula';
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
targetFields = [];
|
|
70
|
+
}
|
|
66
71
|
const options = fieldsToOptions(targetFields, depth + 1, nonfilterable, t).filter(Boolean);
|
|
67
72
|
option['children'] = option['children'] || [];
|
|
68
73
|
option['children'].push(...options);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { MetaTreeNode } from '@nocobase/flow-engine';
|
|
11
|
+
|
|
12
|
+
const ASSOCIATION_FIELD_INTERFACES = new Set(['m2o', 'o2o', 'oho', 'obo', 'o2m', 'm2m', 'linkTo']);
|
|
13
|
+
|
|
14
|
+
function isAssociationMetaNode(node: MetaTreeNode) {
|
|
15
|
+
const options = (node as any)?.options || {};
|
|
16
|
+
return Boolean(
|
|
17
|
+
options.target ||
|
|
18
|
+
options.targetCollection ||
|
|
19
|
+
options.foreignKey ||
|
|
20
|
+
options.through ||
|
|
21
|
+
ASSOCIATION_FIELD_INTERFACES.has(String(node?.interface || '')),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function limitNodeAssociationDepth(node: MetaTreeNode, associationDepth: number, maxAssociationDepth: number) {
|
|
26
|
+
const isAssociation = isAssociationMetaNode(node);
|
|
27
|
+
const nextAssociationDepth = associationDepth + (isAssociation ? 1 : 0);
|
|
28
|
+
|
|
29
|
+
if (isAssociation && nextAssociationDepth > maxAssociationDepth) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const wrapChildren = (children: MetaTreeNode[] | (() => Promise<MetaTreeNode[]>)) => {
|
|
34
|
+
if (Array.isArray(children)) {
|
|
35
|
+
return limitAssociationMetaTree(children, {
|
|
36
|
+
associationDepth: nextAssociationDepth,
|
|
37
|
+
maxAssociationDepth,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return async () => {
|
|
42
|
+
const loaded = await children();
|
|
43
|
+
return limitAssociationMetaTree(Array.isArray(loaded) ? loaded : [], {
|
|
44
|
+
associationDepth: nextAssociationDepth,
|
|
45
|
+
maxAssociationDepth,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...node,
|
|
52
|
+
children: node.children ? wrapChildren(node.children as any) : undefined,
|
|
53
|
+
} as MetaTreeNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function limitAssociationMetaTree(
|
|
57
|
+
nodes: MetaTreeNode[] | undefined,
|
|
58
|
+
options: {
|
|
59
|
+
associationDepth?: number;
|
|
60
|
+
maxAssociationDepth?: number;
|
|
61
|
+
} = {},
|
|
62
|
+
): MetaTreeNode[] {
|
|
63
|
+
const { associationDepth = 0, maxAssociationDepth = 2 } = options;
|
|
64
|
+
if (!Array.isArray(nodes)) return [];
|
|
65
|
+
|
|
66
|
+
return nodes
|
|
67
|
+
.map((node) => limitNodeAssociationDepth(node, associationDepth, maxAssociationDepth))
|
|
68
|
+
.filter(Boolean) as MetaTreeNode[];
|
|
69
|
+
}
|
|
@@ -7,24 +7,131 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import React, { type ComponentType, type CSSProperties, type ReactNode } from 'react';
|
|
10
11
|
import { DateFilterDynamicComponent } from '../../models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent';
|
|
11
12
|
|
|
13
|
+
type OperatorMeta = {
|
|
14
|
+
value?: string;
|
|
15
|
+
schema?: {
|
|
16
|
+
'x-component'?: string;
|
|
17
|
+
'x-component-props'?: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ComponentRegistry = {
|
|
22
|
+
getComponent?: (name: string) => ComponentType<Record<string, unknown>> | undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type OperatorComponentFieldModel = {
|
|
26
|
+
props?: Record<string, unknown>;
|
|
27
|
+
render?: () => ReactNode;
|
|
28
|
+
__originalRender?: () => ReactNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type OperatorComponentRenderOptions = {
|
|
32
|
+
app?: ComponentRegistry;
|
|
33
|
+
fieldModel: unknown;
|
|
34
|
+
operator: string;
|
|
35
|
+
operators?: OperatorMeta[];
|
|
36
|
+
propsPriority?: 'field' | 'operator';
|
|
37
|
+
style?: CSSProperties;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function pickOperatorStyle(value: unknown): CSSProperties | undefined {
|
|
45
|
+
return isRecord(value) ? (value as CSSProperties) : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getFieldModel(fieldModel: unknown) {
|
|
49
|
+
return fieldModel as OperatorComponentFieldModel & {
|
|
50
|
+
_reactiveWrapperCache?: unknown;
|
|
51
|
+
setupReactiveRender?: () => void;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rewrapReactiveRender(fieldModel: unknown) {
|
|
56
|
+
const mutableFieldModel = getFieldModel(fieldModel);
|
|
57
|
+
mutableFieldModel._reactiveWrapperCache = undefined;
|
|
58
|
+
mutableFieldModel.setupReactiveRender?.();
|
|
59
|
+
}
|
|
60
|
+
|
|
12
61
|
/**
|
|
13
62
|
* 根据操作符的 schema 定位组件及其 props。
|
|
14
63
|
*/
|
|
15
|
-
export function resolveOperatorComponent(
|
|
64
|
+
export function resolveOperatorComponent(
|
|
65
|
+
app: ComponentRegistry | undefined,
|
|
66
|
+
operator: string,
|
|
67
|
+
operators?: OperatorMeta[],
|
|
68
|
+
) {
|
|
16
69
|
if (!operators || !Array.isArray(operators)) return null;
|
|
17
70
|
const op = operators.find((item) => item?.value === operator);
|
|
18
71
|
const schema = op?.schema;
|
|
19
72
|
const xComp = schema?.['x-component'];
|
|
20
73
|
if (!xComp) return null;
|
|
21
|
-
let Comp;
|
|
74
|
+
let Comp: ComponentType<Record<string, unknown>> | undefined;
|
|
22
75
|
if (xComp === 'DateFilterDynamicComponent') {
|
|
23
|
-
Comp = DateFilterDynamicComponent
|
|
76
|
+
Comp = DateFilterDynamicComponent as ComponentType<Record<string, unknown>>;
|
|
24
77
|
} else {
|
|
25
|
-
Comp = app?.getComponent?.(xComp
|
|
78
|
+
Comp = app?.getComponent?.(xComp);
|
|
26
79
|
}
|
|
27
80
|
if (!Comp) return null;
|
|
28
|
-
const props = (schema?.['x-component-props']
|
|
81
|
+
const props = isRecord(schema?.['x-component-props']) ? schema['x-component-props'] : {};
|
|
29
82
|
return { Comp, props };
|
|
30
83
|
}
|
|
84
|
+
|
|
85
|
+
export function restoreOperatorComponentRender(fieldModel?: unknown) {
|
|
86
|
+
const mutableFieldModel = getFieldModel(fieldModel);
|
|
87
|
+
if (!mutableFieldModel || typeof mutableFieldModel.__originalRender !== 'function') {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
mutableFieldModel.render = mutableFieldModel.__originalRender;
|
|
92
|
+
rewrapReactiveRender(mutableFieldModel);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function applyOperatorComponentRender({
|
|
97
|
+
app,
|
|
98
|
+
fieldModel,
|
|
99
|
+
operator,
|
|
100
|
+
operators,
|
|
101
|
+
propsPriority = 'field',
|
|
102
|
+
style,
|
|
103
|
+
}: OperatorComponentRenderOptions) {
|
|
104
|
+
const mutableFieldModel = getFieldModel(fieldModel);
|
|
105
|
+
const resolved = resolveOperatorComponent(app, operator, operators);
|
|
106
|
+
if (!resolved) {
|
|
107
|
+
restoreOperatorComponentRender(mutableFieldModel);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!mutableFieldModel.__originalRender && typeof mutableFieldModel.render === 'function') {
|
|
112
|
+
mutableFieldModel.__originalRender = mutableFieldModel.render;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { Comp, props: operatorProps } = resolved;
|
|
116
|
+
const operatorStyle = pickOperatorStyle(operatorProps.style);
|
|
117
|
+
|
|
118
|
+
mutableFieldModel.render = () => {
|
|
119
|
+
const fieldProps = mutableFieldModel.props || {};
|
|
120
|
+
const fieldStyle = pickOperatorStyle(fieldProps.style);
|
|
121
|
+
const componentProps =
|
|
122
|
+
propsPriority === 'operator' ? { ...fieldProps, ...operatorProps } : { ...operatorProps, ...fieldProps };
|
|
123
|
+
const componentStyle =
|
|
124
|
+
propsPriority === 'operator' ? { ...fieldStyle, ...operatorStyle } : { ...operatorStyle, ...fieldStyle };
|
|
125
|
+
const mergedStyle = { ...(style || {}), ...componentStyle };
|
|
126
|
+
|
|
127
|
+
return React.createElement(Comp, {
|
|
128
|
+
...componentProps,
|
|
129
|
+
...(Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {}),
|
|
130
|
+
onCompositionStart: null,
|
|
131
|
+
onCompositionEnd: null,
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
rewrapReactiveRender(mutableFieldModel);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
@@ -75,7 +75,12 @@ export class FilterActionModel extends ActionModel {
|
|
|
75
75
|
value={this.props.filterValue || this.props.defaultFilterValue}
|
|
76
76
|
ctx={this.context}
|
|
77
77
|
FilterItem={(props) => (
|
|
78
|
-
<VariableFilterItem
|
|
78
|
+
<VariableFilterItem
|
|
79
|
+
{...props}
|
|
80
|
+
model={this}
|
|
81
|
+
ignoreFieldNames={this.getIgnoreFieldNames()}
|
|
82
|
+
maxAssociationFieldDepth={2}
|
|
83
|
+
/>
|
|
79
84
|
)}
|
|
80
85
|
/>
|
|
81
86
|
<div style={{ height: 150 }}></div>
|
|
@@ -94,7 +99,12 @@ export class FilterActionModel extends ActionModel {
|
|
|
94
99
|
value={this.props.filterValue || this.props.defaultFilterValue}
|
|
95
100
|
ctx={this.context}
|
|
96
101
|
FilterItem={(props) => (
|
|
97
|
-
<VariableFilterItem
|
|
102
|
+
<VariableFilterItem
|
|
103
|
+
{...props}
|
|
104
|
+
model={this}
|
|
105
|
+
ignoreFieldNames={this.getIgnoreFieldNames()}
|
|
106
|
+
maxAssociationFieldDepth={2}
|
|
107
|
+
/>
|
|
98
108
|
)}
|
|
99
109
|
/>
|
|
100
110
|
}
|
|
@@ -187,7 +197,9 @@ FilterActionModel.registerFlow({
|
|
|
187
197
|
return (
|
|
188
198
|
<FilterGroup
|
|
189
199
|
value={props.value || {}}
|
|
190
|
-
FilterItem={(props) =>
|
|
200
|
+
FilterItem={(props) => (
|
|
201
|
+
<VariableFilterItem {...props} model={modelInstance} maxAssociationFieldDepth={2} />
|
|
202
|
+
)}
|
|
191
203
|
/>
|
|
192
204
|
);
|
|
193
205
|
},
|
|
@@ -11,13 +11,20 @@ import { Collection, DisplayItemModel, FlowModel, FlowModelContext } from '@noco
|
|
|
11
11
|
|
|
12
12
|
export class AssociationFieldGroupModel extends FlowModel {
|
|
13
13
|
static itemModelName = 'DetailsItemModel';
|
|
14
|
+
static maxAssociationFieldDepth = 2;
|
|
14
15
|
static defineChildren(ctx: FlowModelContext) {
|
|
15
16
|
const itemModel = this.itemModelName;
|
|
16
|
-
const
|
|
17
|
+
const maxAssociationFieldDepth = this.maxAssociationFieldDepth ?? 2;
|
|
18
|
+
const displayAssociationFields = (targetCollection: Collection, fieldPath = '', associationDepth = 0) => {
|
|
19
|
+
if (associationDepth >= maxAssociationFieldDepth) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
return targetCollection
|
|
18
24
|
.getToOneAssociationFields()
|
|
19
25
|
.map((field) => {
|
|
20
26
|
const fPath = fieldPath ? `${fieldPath}.${field.name}` : field.name;
|
|
27
|
+
const nextAssociationDepth = associationDepth + 1;
|
|
21
28
|
if (!field.targetCollection) {
|
|
22
29
|
console.error(
|
|
23
30
|
`AssociationFieldGroupModel: target collection ${field.target} not found for field ${field.name}`,
|
|
@@ -37,6 +44,12 @@ export class AssociationFieldGroupModel extends FlowModel {
|
|
|
37
44
|
children: field.targetCollection
|
|
38
45
|
.getFields()
|
|
39
46
|
.map((f) => {
|
|
47
|
+
if (
|
|
48
|
+
nextAssociationDepth >= maxAssociationFieldDepth &&
|
|
49
|
+
(f.isAssociationField?.() || f.target || f.targetCollection)
|
|
50
|
+
) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
40
53
|
const fp = `${fPath}.${f.name}`;
|
|
41
54
|
const binding = DisplayItemModel.getDefaultBindingByField(ctx, f, {
|
|
42
55
|
fallbackToTargetTitleField: true,
|
|
@@ -81,9 +94,13 @@ export class AssociationFieldGroupModel extends FlowModel {
|
|
|
81
94
|
key: `${fPath}-children-associationField`,
|
|
82
95
|
label: 'Display association fields',
|
|
83
96
|
type: 'group',
|
|
84
|
-
children: (
|
|
97
|
+
children: (
|
|
98
|
+
displayAssociationFields(field.targetCollection, fPath, nextAssociationDepth) || []
|
|
99
|
+
).filter(Boolean),
|
|
85
100
|
},
|
|
86
|
-
]
|
|
101
|
+
].filter((item) => {
|
|
102
|
+
return item.key !== `${fPath}-children-associationField` || (item.children as any[])?.length > 0;
|
|
103
|
+
});
|
|
87
104
|
},
|
|
88
105
|
};
|
|
89
106
|
})
|
|
@@ -1006,7 +1006,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
1006
1006
|
<Space
|
|
1007
1007
|
ref={this.gridContainerRef}
|
|
1008
1008
|
direction={'vertical'}
|
|
1009
|
-
style={{ width: '100%', marginBottom: this.props.rowGap }}
|
|
1009
|
+
style={{ width: '100%', marginBottom: this.props.rowGap, display: 'flex' }}
|
|
1010
1010
|
size={this.props.rowGap}
|
|
1011
1011
|
>
|
|
1012
1012
|
<DndProvider
|
|
@@ -49,7 +49,11 @@ export class ChildPageModel extends PageModel {
|
|
|
49
49
|
throw new Error('Invalid drag event');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
if (event.active.id === event.over.id) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await this.flowEngine.moveModel(event.active.id, event.over.id);
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
render() {
|
|
@@ -107,6 +107,18 @@ describe('ChildPageModel', () => {
|
|
|
107
107
|
expect(mockFlowEngine.moveModel).toHaveBeenCalledWith('active-model-id', 'over-model-id');
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
it('should ignore self-drop', async () => {
|
|
111
|
+
await childPageModel.handleDragEnd({
|
|
112
|
+
...mockDragEndEvent,
|
|
113
|
+
over: {
|
|
114
|
+
...(mockDragEndEvent.over as NonNullable<DragEndEvent['over']>),
|
|
115
|
+
id: 'active-model-id',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(mockFlowEngine.moveModel).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
110
122
|
it('should handle case when over is null', async () => {
|
|
111
123
|
const eventWithNullOver = {
|
|
112
124
|
...mockDragEndEvent,
|