@nocobase/client-v2 2.1.0-alpha.46 → 2.1.0-alpha.47
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/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/internal/utils/operatorSchemaHelper.d.ts +26 -3
- package/es/flow/models/blocks/filter-manager/utils.d.ts +23 -1
- package/es/index.mjs +77 -77
- package/lib/index.js +82 -82
- package/package.json +7 -7
- package/src/__tests__/browserChecker.test.ts +103 -0
- package/src/__tests__/dataSourceRuntime.test.tsx +22 -1
- 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/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +369 -2
- package/src/flow/actions/linkageRules.tsx +53 -0
- package/src/flow/components/DefaultValue.tsx +14 -8
- package/src/flow/components/FieldAssignValueInput.tsx +6 -10
- package/src/flow/components/filter/LinkageFilterItem.tsx +4 -3
- package/src/flow/components/filter/VariableFilterItem.tsx +5 -4
- package/src/flow/internal/utils/operatorSchemaHelper.ts +112 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +13 -11
- 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/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
|
@@ -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
|
+
}
|
|
@@ -22,7 +22,12 @@ import { CollectionBlockModel, FieldModel } from '../../base';
|
|
|
22
22
|
import { RecordSelectFieldModel } from '../../fields/AssociationFieldModel/RecordSelectFieldModel';
|
|
23
23
|
import { normalizeAssociationFieldNames } from '../../fields/AssociationFieldModel/recordSelectShared';
|
|
24
24
|
import { FilterManager } from '../filter-manager';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
getAllDataModels,
|
|
27
|
+
getDefaultOperator,
|
|
28
|
+
getFilterFormOperatorMeta,
|
|
29
|
+
isFilterValueEmpty,
|
|
30
|
+
} from '../filter-manager/utils';
|
|
26
31
|
import { FilterFormFieldModel } from './fields';
|
|
27
32
|
import { normalizeFilterValueByOperator } from './valueNormalization';
|
|
28
33
|
|
|
@@ -53,6 +58,10 @@ const getTargetFilterableFields = (field: any, collection?: Collection, model?:
|
|
|
53
58
|
|
|
54
59
|
const MAX_ASSOCIATION_DEPTH = 2;
|
|
55
60
|
|
|
61
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
62
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => {
|
|
57
66
|
const collectionField = fieldModel?.context?.collectionField;
|
|
58
67
|
if (!collectionField?.isAssociationField?.()) {
|
|
@@ -449,15 +458,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
|
|
|
449
458
|
|
|
450
459
|
private getCurrentOperatorMeta() {
|
|
451
460
|
const operator = getDefaultOperator(this);
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const operatorList = this.collectionField?.filterable?.operators;
|
|
455
|
-
|
|
456
|
-
if (!Array.isArray(operatorList)) {
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return operatorList.find((op) => op.value === operator) || null;
|
|
461
|
+
return getFilterFormOperatorMeta(this, operator);
|
|
461
462
|
}
|
|
462
463
|
|
|
463
464
|
onInit(options: any) {
|
|
@@ -530,7 +531,8 @@ export class FilterFormItemModel extends FilterableItemModel<{
|
|
|
530
531
|
rawValue = this.normalizeAssociationFilterValue(rawValue, fieldModel);
|
|
531
532
|
const operatorMeta = this.getCurrentOperatorMeta();
|
|
532
533
|
if (operatorMeta?.noValue) {
|
|
533
|
-
const
|
|
534
|
+
const componentProps = operatorMeta?.schema?.['x-component-props'];
|
|
535
|
+
const options = isRecord(componentProps) ? componentProps.options : undefined;
|
|
534
536
|
if (Array.isArray(options)) {
|
|
535
537
|
return rawValue;
|
|
536
538
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
11
|
|
|
12
|
-
import { getDefaultOperator } from '../utils';
|
|
12
|
+
import { getDefaultOperator, getFilterFormOperatorList } from '../utils';
|
|
13
13
|
|
|
14
14
|
describe('getDefaultOperator', () => {
|
|
15
15
|
it('returns model.operator when it exists', () => {
|
|
@@ -66,4 +66,38 @@ describe('getDefaultOperator', () => {
|
|
|
66
66
|
expect(result).toBe('$includes');
|
|
67
67
|
expect(getStepParams).toHaveBeenCalledWith('filterFormItemSettings', 'defaultOperator');
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it('falls back to field interface operators when field operators are empty', () => {
|
|
71
|
+
const getStepParams = vi.fn().mockReturnValue(undefined);
|
|
72
|
+
const model = {
|
|
73
|
+
getStepParams,
|
|
74
|
+
subModels: {},
|
|
75
|
+
collectionField: {
|
|
76
|
+
interface: 'id',
|
|
77
|
+
type: 'bigInt',
|
|
78
|
+
filterable: { operators: [] },
|
|
79
|
+
},
|
|
80
|
+
context: {
|
|
81
|
+
dataSourceManager: {
|
|
82
|
+
collectionFieldInterfaceManager: {
|
|
83
|
+
getFieldInterface: vi.fn(() => ({
|
|
84
|
+
filterable: {
|
|
85
|
+
operators: [
|
|
86
|
+
{ label: 'is', value: '$eq', selected: true },
|
|
87
|
+
{
|
|
88
|
+
label: 'is any of',
|
|
89
|
+
value: '$in',
|
|
90
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
})),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
} as any;
|
|
99
|
+
|
|
100
|
+
expect(getDefaultOperator(model)).toBe('$eq');
|
|
101
|
+
expect(getFilterFormOperatorList(model).map((item) => item.value)).toEqual(['$eq', '$in']);
|
|
102
|
+
});
|
|
69
103
|
});
|
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
|
});
|
|
@@ -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
|
}
|