@nocobase/client-v2 2.1.0-beta.44 → 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/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 +178 -100
- 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/AppListRender.tsx +139 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +8 -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/__tests__/AssociationFieldGroupModel.test.ts +86 -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__/FormItemModel.defineChildren.test.ts +27 -2
- 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/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { FilterFormItemModel } from '../../../filter-form/FilterFormItemModel';
|
|
13
|
+
import { customizeFilterRender } from '../customizeFilterRender';
|
|
14
|
+
|
|
15
|
+
type TestFieldModel = {
|
|
16
|
+
props: Record<string, unknown>;
|
|
17
|
+
render: () => React.ReactNode;
|
|
18
|
+
setupReactiveRender: ReturnType<typeof vi.fn>;
|
|
19
|
+
_reactiveWrapperCache?: unknown;
|
|
20
|
+
__originalRender?: () => React.ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const MultipleKeywordsInput = (props: Record<string, unknown>) => {
|
|
24
|
+
return <div data-testid="multiple-keywords-input" {...props} />;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function createFieldModel() {
|
|
28
|
+
return {
|
|
29
|
+
props: {
|
|
30
|
+
value: ['1', '2'],
|
|
31
|
+
placeholder: 'runtime placeholder',
|
|
32
|
+
onCompositionStart: vi.fn(),
|
|
33
|
+
onCompositionEnd: vi.fn(),
|
|
34
|
+
},
|
|
35
|
+
render: () => <span data-testid="original-field" />,
|
|
36
|
+
setupReactiveRender: vi.fn(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createFilterItemModel({
|
|
41
|
+
fieldModel,
|
|
42
|
+
operator = '$in',
|
|
43
|
+
collectionField,
|
|
44
|
+
}: {
|
|
45
|
+
fieldModel: TestFieldModel;
|
|
46
|
+
operator?: string;
|
|
47
|
+
collectionField: Record<string, unknown>;
|
|
48
|
+
}) {
|
|
49
|
+
return {
|
|
50
|
+
uid: 'filter-item',
|
|
51
|
+
operator,
|
|
52
|
+
collectionField,
|
|
53
|
+
context: {
|
|
54
|
+
app: {
|
|
55
|
+
getComponent: (name: string) => (name === 'MultipleKeywordsInput' ? MultipleKeywordsInput : undefined),
|
|
56
|
+
},
|
|
57
|
+
collectionField,
|
|
58
|
+
},
|
|
59
|
+
subModels: {
|
|
60
|
+
field: fieldModel,
|
|
61
|
+
},
|
|
62
|
+
setProps: vi.fn(),
|
|
63
|
+
} as unknown as FilterFormItemModel;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('customizeFilterRender action', () => {
|
|
67
|
+
it('uses operator schema components for number filter fields', () => {
|
|
68
|
+
const fieldModel = createFieldModel();
|
|
69
|
+
const model = createFilterItemModel({
|
|
70
|
+
fieldModel,
|
|
71
|
+
collectionField: {
|
|
72
|
+
interface: 'number',
|
|
73
|
+
type: 'integer',
|
|
74
|
+
filterable: {
|
|
75
|
+
operators: [
|
|
76
|
+
{ label: 'equals', value: '$eq' },
|
|
77
|
+
{
|
|
78
|
+
label: 'is any of',
|
|
79
|
+
value: '$in',
|
|
80
|
+
schema: {
|
|
81
|
+
'x-component': 'MultipleKeywordsInput',
|
|
82
|
+
'x-component-props': {
|
|
83
|
+
fieldInterface: 'number',
|
|
84
|
+
placeholder: 'schema placeholder',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
customizeFilterRender.handler?.({ model } as any);
|
|
94
|
+
|
|
95
|
+
const element = fieldModel.render();
|
|
96
|
+
expect(React.isValidElement(element)).toBe(true);
|
|
97
|
+
expect((element as React.ReactElement).type).toBe(MultipleKeywordsInput);
|
|
98
|
+
expect((element as React.ReactElement).props.fieldInterface).toBe('number');
|
|
99
|
+
expect((element as React.ReactElement).props.placeholder).toBe('runtime placeholder');
|
|
100
|
+
expect((element as React.ReactElement).props.onCompositionStart).toBeNull();
|
|
101
|
+
expect((element as React.ReactElement).props.onCompositionEnd).toBeNull();
|
|
102
|
+
expect(fieldModel.setupReactiveRender).toHaveBeenCalled();
|
|
103
|
+
expect(model.setProps).toHaveBeenCalledWith({ key: 'filter-item-$in' });
|
|
104
|
+
|
|
105
|
+
fieldModel.props = {
|
|
106
|
+
...fieldModel.props,
|
|
107
|
+
value: ['3'],
|
|
108
|
+
placeholder: 'updated runtime placeholder',
|
|
109
|
+
};
|
|
110
|
+
const rerenderedElement = fieldModel.render();
|
|
111
|
+
expect((rerenderedElement as React.ReactElement).props.value).toEqual(['3']);
|
|
112
|
+
expect((rerenderedElement as React.ReactElement).props.placeholder).toBe('updated runtime placeholder');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('restores original render when switching to an operator without a schema component', () => {
|
|
116
|
+
const fieldModel = createFieldModel();
|
|
117
|
+
const originalRender = fieldModel.render;
|
|
118
|
+
const model = createFilterItemModel({
|
|
119
|
+
fieldModel,
|
|
120
|
+
collectionField: {
|
|
121
|
+
interface: 'number',
|
|
122
|
+
type: 'integer',
|
|
123
|
+
filterable: {
|
|
124
|
+
operators: [
|
|
125
|
+
{ label: 'equals', value: '$eq' },
|
|
126
|
+
{
|
|
127
|
+
label: 'is any of',
|
|
128
|
+
value: '$in',
|
|
129
|
+
schema: {
|
|
130
|
+
'x-component': 'MultipleKeywordsInput',
|
|
131
|
+
'x-component-props': {
|
|
132
|
+
fieldInterface: 'number',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
customizeFilterRender.handler?.({ model } as any);
|
|
142
|
+
model.operator = '$eq';
|
|
143
|
+
customizeFilterRender.handler?.({ model } as any);
|
|
144
|
+
|
|
145
|
+
expect(fieldModel.render).toBe(originalRender);
|
|
146
|
+
expect(fieldModel.render()).toEqual(<span data-testid="original-field" />);
|
|
147
|
+
});
|
|
148
|
+
});
|
package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/defaultOperator.test.tsx
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
11
11
|
import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
12
|
+
import { createMockClient, IdFieldInterface, InputFieldInterface } from '@nocobase/client-v2';
|
|
13
|
+
import React from 'react';
|
|
12
14
|
import { defaultOperator } from '../defaultOperator';
|
|
13
15
|
|
|
14
16
|
function createModel() {
|
|
@@ -17,6 +19,12 @@ function createModel() {
|
|
|
17
19
|
return model;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
type OperatorOption = {
|
|
23
|
+
value: string;
|
|
24
|
+
label?: unknown;
|
|
25
|
+
schema?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
|
|
20
28
|
describe('defaultOperator action', () => {
|
|
21
29
|
beforeEach(() => {
|
|
22
30
|
document.body.innerHTML = '';
|
|
@@ -55,4 +63,81 @@ describe('defaultOperator action', () => {
|
|
|
55
63
|
expect(options2.find((o: any) => o.value === '$a')).toBeTruthy();
|
|
56
64
|
expect(options2.find((o: any) => o.value === '$b')).toBeTruthy();
|
|
57
65
|
});
|
|
66
|
+
|
|
67
|
+
it('resolves default operator options from field interfaces when collection field metadata is absent', () => {
|
|
68
|
+
const app = createMockClient();
|
|
69
|
+
app.addFieldInterfaces([InputFieldInterface]);
|
|
70
|
+
app.addFieldInterfaceOperator('input', {
|
|
71
|
+
label: 'plugin in',
|
|
72
|
+
value: '$in',
|
|
73
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
74
|
+
});
|
|
75
|
+
app.addFieldInterfaceOperator('input', {
|
|
76
|
+
label: 'plugin not in',
|
|
77
|
+
value: '$notIn',
|
|
78
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const model = new FlowModel({ uid: 'm-default-operator-interface', flowEngine: app.flowEngine });
|
|
82
|
+
model.context.defineProperty('filterField', {
|
|
83
|
+
value: { name: 'name', title: 'Name', interface: 'input', type: 'string' },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const ui = defaultOperator.uiMode?.({ model } as any);
|
|
87
|
+
const options = (ui?.props?.options || []) as OperatorOption[];
|
|
88
|
+
expect(options.find((item) => item.value === '$in')?.schema?.['x-component']).toBe('MultipleKeywordsInput');
|
|
89
|
+
expect(options.find((item) => item.value === '$notIn')?.schema?.['x-component']).toBe('MultipleKeywordsInput');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('preserves React operator labels', () => {
|
|
93
|
+
const model = createModel();
|
|
94
|
+
const label = <span data-testid="operator-label">plugin label</span>;
|
|
95
|
+
|
|
96
|
+
model.context.defineProperty('collectionField', {
|
|
97
|
+
value: {
|
|
98
|
+
filterable: {
|
|
99
|
+
operators: [{ value: '$in', label, schema: { 'x-component': 'MultipleKeywordsInput' } }],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const ui = defaultOperator.uiMode?.({ model } as any);
|
|
105
|
+
const options = (ui?.props?.options || []) as OperatorOption[];
|
|
106
|
+
expect(options.find((item) => item.value === '$in')?.label).toBe(label);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('falls back to id field interface operators when collection field operators are empty', () => {
|
|
110
|
+
const app = createMockClient();
|
|
111
|
+
app.addFieldInterfaces([IdFieldInterface]);
|
|
112
|
+
app.addFieldInterfaceOperator('id', {
|
|
113
|
+
label: 'plugin in',
|
|
114
|
+
value: '$in',
|
|
115
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
116
|
+
});
|
|
117
|
+
app.addFieldInterfaceOperator('id', {
|
|
118
|
+
label: 'plugin not in',
|
|
119
|
+
value: '$notIn',
|
|
120
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const model = new FlowModel({ uid: 'm-default-operator-id-interface', flowEngine: app.flowEngine });
|
|
124
|
+
model.context.defineProperty('collectionField', {
|
|
125
|
+
value: {
|
|
126
|
+
name: 'id',
|
|
127
|
+
title: 'ID',
|
|
128
|
+
interface: 'id',
|
|
129
|
+
type: 'bigInt',
|
|
130
|
+
filterable: { operators: [] },
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
model.context.defineProperty('filterField', {
|
|
134
|
+
value: { name: 'id', title: 'ID', interface: 'id', type: 'bigInt' },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const ui = defaultOperator.uiMode?.({ model } as any);
|
|
138
|
+
const options = (ui?.props?.options || []) as OperatorOption[];
|
|
139
|
+
expect(options.find((item) => item.value === '$eq')).toBeTruthy();
|
|
140
|
+
expect(options.find((item) => item.value === '$in')?.schema?.['x-component']).toBe('MultipleKeywordsInput');
|
|
141
|
+
expect(options.find((item) => item.value === '$notIn')?.schema?.['x-component']).toBe('MultipleKeywordsInput');
|
|
142
|
+
});
|
|
58
143
|
});
|
|
@@ -40,7 +40,20 @@ export const connectFields = defineAction({
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
// 构建级联选项数据结构
|
|
43
|
-
const
|
|
43
|
+
const MAX_ASSOCIATION_FIELD_DEPTH = 2;
|
|
44
|
+
|
|
45
|
+
const isAssociationField = (field: any) => {
|
|
46
|
+
return Boolean(field?.target || field?.targetCollection);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const buildCascaderOptions = (
|
|
50
|
+
ctx,
|
|
51
|
+
fields: any[],
|
|
52
|
+
prefix = '',
|
|
53
|
+
selectedPaths: string[] = [],
|
|
54
|
+
labelPrefix = '',
|
|
55
|
+
associationDepth = 0,
|
|
56
|
+
) => {
|
|
44
57
|
const selectedPathsArray = selectedPaths.filter(Boolean);
|
|
45
58
|
|
|
46
59
|
return fields
|
|
@@ -48,7 +61,13 @@ const buildCascaderOptions = (ctx, fields: any[], prefix = '', selectedPaths: st
|
|
|
48
61
|
const currentPath = prefix ? `${prefix}.${field.name}` : field.name;
|
|
49
62
|
const label = field.title || field.name;
|
|
50
63
|
const fullLabel = labelPrefix ? `${labelPrefix} / ${label}` : label;
|
|
51
|
-
const
|
|
64
|
+
const isAssociation = isAssociationField(field);
|
|
65
|
+
const nextAssociationDepth = associationDepth + (isAssociation ? 1 : 0);
|
|
66
|
+
if (isAssociation && nextAssociationDepth > MAX_ASSOCIATION_FIELD_DEPTH) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isLeaf = !isAssociation;
|
|
52
71
|
|
|
53
72
|
const option: any = {
|
|
54
73
|
label,
|
|
@@ -59,6 +78,7 @@ const buildCascaderOptions = (ctx, fields: any[], prefix = '', selectedPaths: st
|
|
|
59
78
|
field,
|
|
60
79
|
fullPath: currentPath,
|
|
61
80
|
fullLabel,
|
|
81
|
+
associationDepth: nextAssociationDepth,
|
|
62
82
|
},
|
|
63
83
|
};
|
|
64
84
|
|
|
@@ -68,18 +88,33 @@ const buildCascaderOptions = (ctx, fields: any[], prefix = '', selectedPaths: st
|
|
|
68
88
|
|
|
69
89
|
if (shouldLoadChildren) {
|
|
70
90
|
if (field.filterable?.children?.length) {
|
|
71
|
-
option.children = buildCascaderOptions(
|
|
91
|
+
option.children = buildCascaderOptions(
|
|
92
|
+
ctx,
|
|
93
|
+
field.filterable.children,
|
|
94
|
+
currentPath,
|
|
95
|
+
selectedPaths,
|
|
96
|
+
fullLabel,
|
|
97
|
+
nextAssociationDepth,
|
|
98
|
+
);
|
|
72
99
|
} else {
|
|
73
100
|
const targetCollection = field.targetCollection;
|
|
74
101
|
if (targetCollection) {
|
|
75
102
|
const targetFields = targetCollection.getFields?.() || [];
|
|
76
|
-
option.children = buildCascaderOptions(
|
|
103
|
+
option.children = buildCascaderOptions(
|
|
104
|
+
ctx,
|
|
105
|
+
targetFields,
|
|
106
|
+
currentPath,
|
|
107
|
+
selectedPaths,
|
|
108
|
+
fullLabel,
|
|
109
|
+
nextAssociationDepth,
|
|
110
|
+
);
|
|
77
111
|
}
|
|
78
112
|
}
|
|
79
113
|
}
|
|
80
114
|
|
|
81
115
|
return option;
|
|
82
116
|
})
|
|
117
|
+
.filter(Boolean)
|
|
83
118
|
.sort((a, b) => {
|
|
84
119
|
// isLeaf 为 true 的节点排在前面
|
|
85
120
|
if (a.isLeaf && !b.isLeaf) return -1;
|
|
@@ -259,7 +294,7 @@ function ConnectFields(
|
|
|
259
294
|
<li>{ctx.t('Available "target blocks" are all data blocks on the current page')}</li>
|
|
260
295
|
<li>
|
|
261
296
|
{ctx.t(
|
|
262
|
-
'Support filtering multiple blocks simultaneously, support
|
|
297
|
+
'Support filtering multiple blocks simultaneously, support selection of relationship fields up to two levels (e.g.: User/Department/Name)',
|
|
263
298
|
)}
|
|
264
299
|
</li>
|
|
265
300
|
</ul>
|
|
@@ -469,7 +504,14 @@ function CascaderWrapper(
|
|
|
469
504
|
return;
|
|
470
505
|
}
|
|
471
506
|
|
|
472
|
-
target.children = buildCascaderOptions(
|
|
507
|
+
target.children = buildCascaderOptions(
|
|
508
|
+
ctx,
|
|
509
|
+
childFields,
|
|
510
|
+
fullPath,
|
|
511
|
+
[],
|
|
512
|
+
meta.fullLabel || target.label,
|
|
513
|
+
meta.associationDepth || 0,
|
|
514
|
+
);
|
|
473
515
|
setOptions((prev) => [...prev]);
|
|
474
516
|
};
|
|
475
517
|
|
|
@@ -7,25 +7,13 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import React from 'react';
|
|
11
10
|
import { defineAction } from '@nocobase/flow-engine';
|
|
12
11
|
import { FilterFormItemModel } from '../../filter-form/FilterFormItemModel';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function isOperatorSupported(model: FilterFormItemModel, operator: string) {
|
|
19
|
-
const ops = model.collectionField?.filterable?.operators || [];
|
|
20
|
-
return ops.some((op) => op.value === operator);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// 重新包裹 render,确保仍由 flow-engine 的响应式包装驱动
|
|
24
|
-
function rewrapReactiveRender(fieldModel: any) {
|
|
25
|
-
if (!fieldModel) return;
|
|
26
|
-
fieldModel._reactiveWrapperCache = undefined;
|
|
27
|
-
fieldModel.setupReactiveRender?.();
|
|
28
|
-
}
|
|
12
|
+
import {
|
|
13
|
+
applyOperatorComponentRender,
|
|
14
|
+
restoreOperatorComponentRender,
|
|
15
|
+
} from '../../../../internal/utils/operatorSchemaHelper';
|
|
16
|
+
import { getFilterFormOperatorList } from '../utils';
|
|
29
17
|
|
|
30
18
|
function applyCustomizeFilterRender(model: FilterFormItemModel) {
|
|
31
19
|
const operator = model.operator;
|
|
@@ -37,37 +25,19 @@ function applyCustomizeFilterRender(model: FilterFormItemModel) {
|
|
|
37
25
|
key: `${model.uid}-${operator || ''}`,
|
|
38
26
|
});
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const originalRender = fieldModel['__originalRender'];
|
|
43
|
-
if (typeof originalRender === 'function') {
|
|
44
|
-
// 清理缓存,防止沿用上一轮的 reactive wrapper
|
|
45
|
-
(fieldModel as any)._reactiveWrapperCache = undefined;
|
|
46
|
-
fieldModel.render = originalRender;
|
|
47
|
-
}
|
|
28
|
+
if (!operator) {
|
|
29
|
+
restoreOperatorComponentRender(fieldModel);
|
|
48
30
|
return;
|
|
49
31
|
}
|
|
50
32
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
model.context.app,
|
|
33
|
+
const operatorList = getFilterFormOperatorList(model);
|
|
34
|
+
applyOperatorComponentRender({
|
|
35
|
+
app: model.context.app,
|
|
36
|
+
fieldModel,
|
|
56
37
|
operator,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const { Comp, props: xProps } = resolved;
|
|
61
|
-
|
|
62
|
-
// 缓存一次原始 render/props,用于回退
|
|
63
|
-
if (!fieldModel['__originalRender']) {
|
|
64
|
-
fieldModel['__originalRender'] = fieldModel.render;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
fieldModel.render = () => (
|
|
68
|
-
<Comp {...xProps} {...fieldModel.props} onCompositionStart={null} onCompositionEnd={null} />
|
|
69
|
-
);
|
|
70
|
-
rewrapReactiveRender(fieldModel);
|
|
38
|
+
operators: operatorList,
|
|
39
|
+
propsPriority: 'field',
|
|
40
|
+
});
|
|
71
41
|
}
|
|
72
42
|
|
|
73
43
|
export const customizeFilterRender = defineAction<FilterFormItemModel>({
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { defineAction, tExpr, FlowModel } from '@nocobase/flow-engine';
|
|
11
|
-
import { operators } from '../../../../../flow-compat';
|
|
12
11
|
import { FilterFormFieldModel } from '../../filter-form/fields';
|
|
12
|
+
import { getFilterFormOperatorList } from '../utils';
|
|
13
13
|
|
|
14
14
|
export const defaultOperator: any = defineAction<FilterFormFieldModel>({
|
|
15
15
|
name: 'defaultOperator',
|
|
@@ -37,12 +37,10 @@ export const defaultOperator: any = defineAction<FilterFormFieldModel>({
|
|
|
37
37
|
|
|
38
38
|
function getOperatorOptions(model: FlowModel) {
|
|
39
39
|
const meta = model.context.collectionField || model.context.filterField;
|
|
40
|
-
|
|
41
|
-
model.context.collectionField?.filterable?.operators || operators[model.context.filterField.type];
|
|
42
|
-
return (operatorList || [])
|
|
40
|
+
return getFilterFormOperatorList(model)
|
|
43
41
|
.filter((op) => !op.visible || op.visible(meta))
|
|
44
42
|
.map((op) => ({
|
|
45
43
|
...op,
|
|
46
|
-
label: model.translate(op.label),
|
|
44
|
+
label: typeof op.label === 'string' ? model.translate(op.label) : op.label,
|
|
47
45
|
}));
|
|
48
46
|
}
|
|
@@ -9,20 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { defineAction } from '@nocobase/flow-engine';
|
|
11
11
|
import { FilterFormItemModel } from '../../filter-form/FilterFormItemModel';
|
|
12
|
-
|
|
13
|
-
function getOperatorMeta(model: FilterFormItemModel) {
|
|
14
|
-
const ops = model.collectionField?.filterable?.operators || [];
|
|
15
|
-
if (!ops.length) {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
if (!model.operator) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
return ops.find((op) => op.value === model.operator) || null;
|
|
22
|
-
}
|
|
12
|
+
import { getFilterFormOperatorMeta } from '../utils';
|
|
23
13
|
|
|
24
14
|
function applyOperatorComponentProps(model: FilterFormItemModel) {
|
|
25
|
-
const meta =
|
|
15
|
+
const meta = getFilterFormOperatorMeta(model, model.operator);
|
|
26
16
|
if (!meta) {
|
|
27
17
|
return;
|
|
28
18
|
}
|
|
@@ -8,14 +8,153 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowModel } from '@nocobase/flow-engine';
|
|
11
|
+
import {
|
|
12
|
+
normalizeFilterableOperators,
|
|
13
|
+
type FieldFilterable,
|
|
14
|
+
type FieldFilterOperator,
|
|
15
|
+
} from '../../../../collection-manager/filter-operators';
|
|
16
|
+
import { operators } from '../../../../flow-compat';
|
|
11
17
|
import { BlockGridModel, CollectionBlockModel } from '../../base';
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
type FilterFormFieldMeta = {
|
|
20
|
+
filterable?: FieldFilterable<unknown> | false;
|
|
21
|
+
interface?: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type FilterFormOperatorModel = FlowModel & {
|
|
26
|
+
operator?: string;
|
|
27
|
+
collectionField?: FilterFormFieldMeta;
|
|
28
|
+
filterField?: FilterFormFieldMeta;
|
|
29
|
+
subModels?: {
|
|
30
|
+
field?: {
|
|
31
|
+
operator?: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
getStepParams?: (flowKey: string, stepKey: string) => { value?: string } | undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function getModelContext(model: FlowModel) {
|
|
38
|
+
return model.context as unknown as Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getModelField(model: FlowModel, key: 'collectionField' | 'filterField') {
|
|
42
|
+
const modelWithFields = model as FilterFormOperatorModel;
|
|
43
|
+
const field = modelWithFields[key] || getModelContext(model)?.[key];
|
|
44
|
+
return field && typeof field === 'object' ? (field as FilterFormFieldMeta) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getDataSourceManager(model: FlowModel) {
|
|
48
|
+
const contextManager = getModelContext(model)?.dataSourceManager;
|
|
49
|
+
if (contextManager) {
|
|
50
|
+
return contextManager as Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
return model.flowEngine?.context?.dataSourceManager as unknown as Record<string, unknown> | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cloneFilterable(filterable: FieldFilterable<unknown>): FieldFilterable<unknown> {
|
|
56
|
+
return {
|
|
57
|
+
operatorGroup: filterable.operatorGroup,
|
|
58
|
+
operators: Array.isArray(filterable.operators) ? [...filterable.operators] : filterable.operators,
|
|
59
|
+
operatorOverrides: Array.isArray(filterable.operatorOverrides)
|
|
60
|
+
? [...filterable.operatorOverrides]
|
|
61
|
+
: filterable.operatorOverrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFilterableOperators(filterable?: FieldFilterable<unknown> | false) {
|
|
66
|
+
if (filterable === false) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!filterable || typeof filterable !== 'object') {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const normalized = normalizeFilterableOperators(cloneFilterable(filterable));
|
|
75
|
+
if (Array.isArray(normalized?.operators) && normalized.operators.length > 0) {
|
|
76
|
+
return normalized.operators;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(normalized?.operatorOverrides) && normalized.operatorOverrides.length > 0) {
|
|
80
|
+
return normalized.operatorOverrides;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getFieldInterfaceFilterable(model: FlowModel, field?: FilterFormFieldMeta) {
|
|
85
|
+
const interfaceName = typeof field?.interface === 'string' ? field.interface : undefined;
|
|
86
|
+
if (!interfaceName) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const dataSourceManager = getDataSourceManager(model);
|
|
91
|
+
const fieldInterfaceManager = dataSourceManager?.collectionFieldInterfaceManager as
|
|
92
|
+
| {
|
|
93
|
+
getFieldInterface?: (name: string) => { filterable?: FieldFilterable<unknown> | false } | undefined;
|
|
94
|
+
}
|
|
95
|
+
| undefined;
|
|
96
|
+
const fieldInterface = fieldInterfaceManager?.getFieldInterface?.(interfaceName);
|
|
97
|
+
return fieldInterface?.filterable;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getFallbackOperators(field?: FilterFormFieldMeta) {
|
|
101
|
+
const fallbackKeys = [field?.interface, field?.type].filter((item): item is string => typeof item === 'string');
|
|
102
|
+
const standardOperators = operators as Record<string, FieldFilterOperator<unknown>[] | undefined>;
|
|
103
|
+
for (const key of fallbackKeys) {
|
|
104
|
+
const operatorList = standardOperators[key];
|
|
105
|
+
if (operatorList?.length) {
|
|
106
|
+
return operatorList;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getFilterFormOperatorList(model: FlowModel) {
|
|
113
|
+
const collectionField = getModelField(model, 'collectionField');
|
|
114
|
+
const filterField = getModelField(model, 'filterField');
|
|
115
|
+
const field = collectionField || filterField;
|
|
116
|
+
|
|
117
|
+
if (field?.filterable === false) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fieldOperators = getFilterableOperators(field?.filterable);
|
|
122
|
+
if (fieldOperators?.length) {
|
|
123
|
+
return fieldOperators;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const fieldInterfaceFilterable = getFieldInterfaceFilterable(model, field);
|
|
127
|
+
if (fieldInterfaceFilterable === false) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fieldInterfaceOperators = getFilterableOperators(fieldInterfaceFilterable);
|
|
132
|
+
if (fieldInterfaceOperators?.length) {
|
|
133
|
+
return fieldInterfaceOperators;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return getFallbackOperators(field);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getFilterFormOperatorMeta(model: FlowModel, operator = getDefaultOperator(model)) {
|
|
140
|
+
if (!operator) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const meta = getModelField(model, 'collectionField') || getModelField(model, 'filterField');
|
|
145
|
+
return (
|
|
146
|
+
getFilterFormOperatorList(model).find((op) => op.value === operator && (!op.visible || op.visible(meta))) || null
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getDefaultOperator(model: FilterFormOperatorModel) {
|
|
151
|
+
const operatorList = getFilterFormOperatorList(model);
|
|
14
152
|
return (
|
|
15
153
|
model.operator ||
|
|
16
|
-
model.getStepParams('filterFormItemSettings', 'defaultOperator')?.value ||
|
|
17
|
-
model.subModels
|
|
18
|
-
|
|
154
|
+
model.getStepParams?.('filterFormItemSettings', 'defaultOperator')?.value ||
|
|
155
|
+
model.subModels?.field?.operator ||
|
|
156
|
+
operatorList.find((op) => op.selected)?.value ||
|
|
157
|
+
operatorList[0]?.value ||
|
|
19
158
|
'$includes'
|
|
20
159
|
);
|
|
21
160
|
}
|
|
@@ -22,6 +22,8 @@ import { FieldModel } from '../../base/FieldModel';
|
|
|
22
22
|
import { rebuildFieldSubModel } from '../../../internal/utils/rebuildFieldSubModel';
|
|
23
23
|
import { useJsonTemplateResolver } from '../../../utils/useJsonTemplateResolver';
|
|
24
24
|
|
|
25
|
+
const MAX_FORM_ASSOCIATION_FIELD_DEPTH = 2;
|
|
26
|
+
|
|
25
27
|
function mergeArrays(a, b) {
|
|
26
28
|
if (!b) {
|
|
27
29
|
return a;
|
|
@@ -84,10 +86,17 @@ const AssociationItem = (props) => {
|
|
|
84
86
|
export class FormAssociationItemModel extends DisplayItemModel {
|
|
85
87
|
static defineChildren(ctx: FlowModelContext) {
|
|
86
88
|
const collection = ctx.collection as Collection;
|
|
89
|
+
const associationDepth = ctx.prefixFieldPath ? ctx.prefixFieldPath.split('.').filter(Boolean).length : 0;
|
|
87
90
|
|
|
88
91
|
return collection
|
|
89
92
|
.getFields()
|
|
90
93
|
.map((field) => {
|
|
94
|
+
if (
|
|
95
|
+
associationDepth >= MAX_FORM_ASSOCIATION_FIELD_DEPTH &&
|
|
96
|
+
(field.isAssociationField?.() || field.target || field.targetCollection)
|
|
97
|
+
) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
91
100
|
const binding = this.getDefaultBindingByField(ctx, field, { fallbackToTargetTitleField: true });
|
|
92
101
|
if (!binding) return null;
|
|
93
102
|
const fieldModel = binding.modelName;
|
|
@@ -42,12 +42,21 @@ const interfacesOfUnsupportedDefaultValue = [
|
|
|
42
42
|
'formula',
|
|
43
43
|
];
|
|
44
44
|
|
|
45
|
+
const MAX_FORM_ASSOCIATION_FIELD_DEPTH = 2;
|
|
46
|
+
|
|
45
47
|
export class FormItemModel<T extends DefaultStructure = DefaultStructure> extends EditableItemModel<T> {
|
|
46
48
|
static defineChildren(ctx: FlowModelContext) {
|
|
47
49
|
const collection = ctx.collection as Collection;
|
|
50
|
+
const associationDepth = ctx.prefixFieldPath ? ctx.prefixFieldPath.split('.').filter(Boolean).length : 0;
|
|
48
51
|
return collection
|
|
49
52
|
.getFields()
|
|
50
53
|
.map((field) => {
|
|
54
|
+
if (
|
|
55
|
+
associationDepth >= MAX_FORM_ASSOCIATION_FIELD_DEPTH &&
|
|
56
|
+
(field.isAssociationField?.() || field.target || field.targetCollection)
|
|
57
|
+
) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
51
60
|
const binding = this.getDefaultBindingByField(ctx, field);
|
|
52
61
|
if (!binding) {
|
|
53
62
|
return;
|