@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.0-alpha.
|
|
3
|
+
"version": "2.1.0-alpha.47",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.1.0-alpha.
|
|
31
|
-
"@nocobase/flow-engine": "2.1.0-alpha.
|
|
32
|
-
"@nocobase/sdk": "2.1.0-alpha.
|
|
33
|
-
"@nocobase/shared": "2.1.0-alpha.
|
|
34
|
-
"@nocobase/utils": "2.1.0-alpha.
|
|
30
|
+
"@nocobase/evaluators": "2.1.0-alpha.47",
|
|
31
|
+
"@nocobase/flow-engine": "2.1.0-alpha.47",
|
|
32
|
+
"@nocobase/sdk": "2.1.0-alpha.47",
|
|
33
|
+
"@nocobase/shared": "2.1.0-alpha.47",
|
|
34
|
+
"@nocobase/utils": "2.1.0-alpha.47",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"react-i18next": "^11.15.1",
|
|
46
46
|
"react-router-dom": "^6.30.1"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "66196b57f9043ea0ac6ebdafbc732bfb98af1396"
|
|
49
49
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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 { readFileSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import vm from 'node:vm';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
type BrowserCheckerCase = {
|
|
16
|
+
pathname: string;
|
|
17
|
+
publicPath: string;
|
|
18
|
+
expectedRedirect?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const testDir = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const browserCheckerCases = [
|
|
23
|
+
{
|
|
24
|
+
label: 'v2 public browser-checker',
|
|
25
|
+
scriptPath: path.resolve(testDir, '../../../app/client-v2/public/browser-checker.js'),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: 'v1 public browser-checker',
|
|
29
|
+
scriptPath: path.resolve(testDir, '../../../app/client/public/browser-checker.js'),
|
|
30
|
+
},
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
|
|
34
|
+
const replace = vi.fn();
|
|
35
|
+
const consoleMock = {
|
|
36
|
+
debug: vi.fn(),
|
|
37
|
+
log: vi.fn(),
|
|
38
|
+
};
|
|
39
|
+
const context = {
|
|
40
|
+
showLog: false,
|
|
41
|
+
window: {
|
|
42
|
+
__nocobase_public_path__: input.publicPath,
|
|
43
|
+
location: {
|
|
44
|
+
origin: 'http://c.local.nocobase.com',
|
|
45
|
+
pathname: input.pathname,
|
|
46
|
+
search: '',
|
|
47
|
+
hash: '',
|
|
48
|
+
replace,
|
|
49
|
+
},
|
|
50
|
+
console: consoleMock,
|
|
51
|
+
outerWidth: 1280,
|
|
52
|
+
outerHeight: 720,
|
|
53
|
+
devicePixelRatio: 1,
|
|
54
|
+
onresize: undefined,
|
|
55
|
+
},
|
|
56
|
+
document: {
|
|
57
|
+
documentElement: {
|
|
58
|
+
className: '',
|
|
59
|
+
clientWidth: 1280,
|
|
60
|
+
clientHeight: 720,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
navigator: {
|
|
64
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 Chrome/126.0 Safari/537.36',
|
|
65
|
+
},
|
|
66
|
+
console: consoleMock,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
vm.runInNewContext(readFileSync(scriptPath, 'utf8'), context, {
|
|
70
|
+
filename: scriptPath,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return replace;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
|
77
|
+
it('normalizes a relative public path before redirecting to the trailing-slash entry', () => {
|
|
78
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
79
|
+
pathname: '/nocobase/v',
|
|
80
|
+
publicPath: 'nocobase/v/',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('prefixes outside paths with a root-relative basename instead of duplicating a relative segment', () => {
|
|
87
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
88
|
+
pathname: '/foo',
|
|
89
|
+
publicPath: 'nocobase/v/',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/foo');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not redirect when the current path is already under the normalized basename', () => {
|
|
96
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
97
|
+
pathname: '/nocobase/v/',
|
|
98
|
+
publicPath: 'nocobase/v/',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(replace).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createMockClient } from '@nocobase/client-v2';
|
|
10
|
+
import { CollectionFieldInterface, createMockClient } from '@nocobase/client-v2';
|
|
11
11
|
import { describe, expect, it } from 'vitest';
|
|
12
12
|
|
|
13
13
|
class InputInterface {
|
|
@@ -16,6 +16,12 @@ class InputInterface {
|
|
|
16
16
|
titleUsable = true;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
class OperatorOnlyInterface extends CollectionFieldInterface {
|
|
20
|
+
name = 'operatorOnly';
|
|
21
|
+
group = 'basic';
|
|
22
|
+
filterable = {};
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
describe('data source runtime', () => {
|
|
20
26
|
it('shares one dataSourceManager instance between app and flow context', () => {
|
|
21
27
|
const app = createMockClient();
|
|
@@ -31,4 +37,19 @@ describe('data source runtime', () => {
|
|
|
31
37
|
|
|
32
38
|
expect(app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface('input')?.titleUsable).toBe(true);
|
|
33
39
|
});
|
|
40
|
+
|
|
41
|
+
it('exposes operators registered on interfaces without predefined operator lists', () => {
|
|
42
|
+
const app = createMockClient();
|
|
43
|
+
const operator = {
|
|
44
|
+
label: 'plugin operator',
|
|
45
|
+
value: '$plugin',
|
|
46
|
+
schema: { 'x-component': 'PluginInput' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
app.addFieldInterfaces([OperatorOnlyInterface as any]);
|
|
50
|
+
app.addFieldInterfaceOperator('operatorOnly', operator);
|
|
51
|
+
|
|
52
|
+
const fieldInterface = app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface('operatorOnly');
|
|
53
|
+
expect(fieldInterface?.filterable?.operators).toEqual([operator]);
|
|
54
|
+
});
|
|
34
55
|
});
|
|
@@ -14,7 +14,6 @@ import type { ComponentType, ReactNode } from 'react';
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
normalizeFilterableOperators,
|
|
17
|
-
resolveFilterOperators,
|
|
18
17
|
type FieldFilterable,
|
|
19
18
|
type FieldFilterOperator,
|
|
20
19
|
} from '../collection-manager/filter-operators';
|
|
@@ -181,14 +180,22 @@ export abstract class CollectionFieldInterface {
|
|
|
181
180
|
}
|
|
182
181
|
|
|
183
182
|
addOperator(operatorOption: FieldFilterOperator) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
if (!this.filterable) {
|
|
184
|
+
set(this, 'filterable', {});
|
|
185
|
+
}
|
|
187
186
|
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
const operatorOverrides = Array.isArray(this.filterable.operatorOverrides)
|
|
188
|
+
? [...this.filterable.operatorOverrides]
|
|
189
|
+
: [];
|
|
190
|
+
|
|
191
|
+
const existingIndex = operatorOverrides.findIndex((item) => item.value === operatorOption.value);
|
|
192
|
+
if (existingIndex !== -1) {
|
|
193
|
+
operatorOverrides[existingIndex] = operatorOption;
|
|
194
|
+
} else {
|
|
195
|
+
operatorOverrides.push(operatorOption);
|
|
190
196
|
}
|
|
191
197
|
|
|
192
|
-
|
|
198
|
+
set(this, 'filterable.operatorOverrides', operatorOverrides);
|
|
199
|
+
normalizeFilterableOperators(this.filterable);
|
|
193
200
|
}
|
|
194
201
|
}
|
|
@@ -23,6 +23,7 @@ export type FieldFilterOperatorList<TMeta = unknown> = FieldFilterOperator<TMeta
|
|
|
23
23
|
export type FieldFilterable<TMeta = unknown> = {
|
|
24
24
|
operators?: FieldFilterOperatorList<TMeta>;
|
|
25
25
|
operatorGroup?: string;
|
|
26
|
+
operatorOverrides?: FieldFilterOperator<TMeta>[];
|
|
26
27
|
children?: unknown[];
|
|
27
28
|
nested?: boolean;
|
|
28
29
|
[key: string]: unknown;
|
|
@@ -120,14 +121,20 @@ export function normalizeFilterableOperators<TMeta = unknown>(filterable?: Field
|
|
|
120
121
|
return filterable;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
const operatorOverrides = Array.isArray(filterable.operatorOverrides) ? filterable.operatorOverrides : [];
|
|
125
|
+
|
|
123
126
|
if (filterable.operatorGroup) {
|
|
124
|
-
filterable.operators = resolveFilterOperators(filterable.operatorGroup);
|
|
127
|
+
filterable.operators = mergeOperatorOverrides(resolveFilterOperators(filterable.operatorGroup), operatorOverrides);
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
const rawOperators = (filterable as { operators?: unknown }).operators;
|
|
128
131
|
if (typeof rawOperators === 'string') {
|
|
129
132
|
filterable.operatorGroup = rawOperators;
|
|
130
|
-
filterable.operators = resolveFilterOperators(rawOperators);
|
|
133
|
+
filterable.operators = mergeOperatorOverrides(resolveFilterOperators(rawOperators), operatorOverrides);
|
|
134
|
+
} else if (Array.isArray(rawOperators)) {
|
|
135
|
+
filterable.operators = mergeOperatorOverrides(rawOperators, operatorOverrides);
|
|
136
|
+
} else if (operatorOverrides.length > 0) {
|
|
137
|
+
filterable.operators = [...operatorOverrides];
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
if (Array.isArray(filterable.children)) {
|
|
@@ -141,6 +148,26 @@ export function normalizeFilterableOperators<TMeta = unknown>(filterable?: Field
|
|
|
141
148
|
return filterable;
|
|
142
149
|
}
|
|
143
150
|
|
|
151
|
+
function mergeOperatorOverrides<TMeta = unknown>(
|
|
152
|
+
operators: FieldFilterOperator<TMeta>[] = [],
|
|
153
|
+
operatorOverrides: FieldFilterOperator<TMeta>[] = [],
|
|
154
|
+
) {
|
|
155
|
+
if (operatorOverrides.length === 0) {
|
|
156
|
+
return operators;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = [...operators];
|
|
160
|
+
operatorOverrides.forEach((operatorOverride) => {
|
|
161
|
+
const existingIndex = result.findIndex((item) => item.value === operatorOverride.value);
|
|
162
|
+
if (existingIndex === -1) {
|
|
163
|
+
result.push(operatorOverride);
|
|
164
|
+
} else {
|
|
165
|
+
result[existingIndex] = operatorOverride;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
144
171
|
export function createFilterable<TPreset extends FieldFilterOperatorGroupName>(
|
|
145
172
|
preset: TPreset,
|
|
146
173
|
options: Omit<FieldFilterable, 'operators'> = {},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { css } from '@emotion/css';
|
|
11
|
-
import { Collection, observer } from '@nocobase/flow-engine';
|
|
11
|
+
import { Collection, observer, useFlowEngine } from '@nocobase/flow-engine';
|
|
12
12
|
import { Cascader, Select, Space } from 'antd';
|
|
13
13
|
import React, { FC, useMemo } from 'react';
|
|
14
14
|
import { FilterOption, useFilterOptions } from '../../../flow/components/filter/useFilterOptions';
|
|
@@ -56,6 +56,8 @@ export interface CollectionFilterItemProps {
|
|
|
56
56
|
noIgnore?: boolean;
|
|
57
57
|
/** Translator; defaults to identity so callers can omit it. */
|
|
58
58
|
t?: (key: string) => string;
|
|
59
|
+
/** Optional v2 app registry used to resolve plugin-provided operator components. */
|
|
60
|
+
app?: { getComponent?: (name: string) => React.ComponentType<any> | undefined };
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
const identity = (s: string) => s;
|
|
@@ -94,6 +96,8 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
94
96
|
(props) => {
|
|
95
97
|
const { collection, filterableFieldNames, nonfilterableFieldNames, noIgnore = false, t = identity } = props;
|
|
96
98
|
const { path: leftValue, operator, value: rightValue } = props.value;
|
|
99
|
+
const flowEngine = useFlowEngine({ throwError: false }) as any;
|
|
100
|
+
const app = props.app || flowEngine?.context?.app;
|
|
97
101
|
|
|
98
102
|
const options = useFilterOptions(collection, { filterableFieldNames, nonfilterableFieldNames, noIgnore, t });
|
|
99
103
|
|
|
@@ -160,6 +164,7 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
160
164
|
onChange={handleValueChange}
|
|
161
165
|
placeholder={t('Enter value')}
|
|
162
166
|
t={t}
|
|
167
|
+
app={app}
|
|
163
168
|
/>
|
|
164
169
|
</Space>
|
|
165
170
|
);
|
|
@@ -172,7 +177,10 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
172
177
|
*/
|
|
173
178
|
export function createCollectionFilterItem(
|
|
174
179
|
collection: Collection,
|
|
175
|
-
bound?: Pick<
|
|
180
|
+
bound?: Pick<
|
|
181
|
+
CollectionFilterItemProps,
|
|
182
|
+
'filterableFieldNames' | 'nonfilterableFieldNames' | 'noIgnore' | 't' | 'app'
|
|
183
|
+
>,
|
|
176
184
|
) {
|
|
177
185
|
const Component: FC<{ value: CollectionFilterItemValue }> = (props) => (
|
|
178
186
|
<CollectionFilterItem {...bound} value={props.value} collection={collection} />
|
|
@@ -13,6 +13,10 @@ import type { FilterOperator, FilterOption } from '../../../flow/components/filt
|
|
|
13
13
|
import { PasswordInput } from '../PasswordInput';
|
|
14
14
|
import { DateFilterDynamicComponent } from './DateFilterDynamicComponent';
|
|
15
15
|
|
|
16
|
+
type ComponentRegistryApp = {
|
|
17
|
+
getComponent?: (name: string) => React.ComponentType<any> | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
16
20
|
export interface FilterValueInputProps {
|
|
17
21
|
/** The currently selected leaf field option from the field picker. */
|
|
18
22
|
field?: FilterOption;
|
|
@@ -26,17 +30,20 @@ export interface FilterValueInputProps {
|
|
|
26
30
|
t?: (key: string) => string;
|
|
27
31
|
/** Optional placeholder for the fallback `Input`. */
|
|
28
32
|
placeholder?: string;
|
|
33
|
+
/** Optional v2 app registry used to resolve plugin-provided operator components. */
|
|
34
|
+
app?: ComponentRegistryApp;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const identity = (s: string) => s;
|
|
32
38
|
|
|
33
39
|
type EffectiveSchema = {
|
|
34
|
-
'x-component'?: string
|
|
40
|
+
'x-component'?: string | React.ComponentType<any>;
|
|
35
41
|
'x-component-props'?: Record<string, any>;
|
|
36
42
|
enum?: Array<{ value: any; label: string }> | any[];
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
const baseStyle = { minWidth: 200 } as const;
|
|
46
|
+
const fallbackInputComponents = new Set(['Input', 'Input.URL', 'NanoIDInput']);
|
|
40
47
|
|
|
41
48
|
/** Resolve operator-level schema → field uiSchema → fallback Input. */
|
|
42
49
|
const resolveSchema = (field?: FilterOption, operator?: FilterOperator): EffectiveSchema => {
|
|
@@ -54,7 +61,7 @@ const resolveSchema = (field?: FilterOption, operator?: FilterOperator): Effecti
|
|
|
54
61
|
* Interface-aware value renderer for filter rows. Returns `null` for `noValue` operators (`$empty`, `$notEmpty`). Otherwise dispatches the effective `x-component` (operator schema > field uiSchema > Input) to a small registry of antd controls.
|
|
55
62
|
*/
|
|
56
63
|
export const FilterValueInput: React.FC<FilterValueInputProps> = (props) => {
|
|
57
|
-
const { field, operator, value, onChange, t = identity, placeholder } = props;
|
|
64
|
+
const { field, operator, value, onChange, t = identity, placeholder, app } = props;
|
|
58
65
|
|
|
59
66
|
if (operator?.noValue) {
|
|
60
67
|
return null;
|
|
@@ -64,6 +71,12 @@ export const FilterValueInput: React.FC<FilterValueInputProps> = (props) => {
|
|
|
64
71
|
const componentName = schema['x-component'];
|
|
65
72
|
const componentProps = schema['x-component-props'] || {};
|
|
66
73
|
const enumOptions = (schema as any).enum || (field?.schema as any)?.enum;
|
|
74
|
+
const customComponentProps = {
|
|
75
|
+
value,
|
|
76
|
+
onChange,
|
|
77
|
+
...componentProps,
|
|
78
|
+
style: { ...baseStyle, ...(componentProps.style || {}) },
|
|
79
|
+
};
|
|
67
80
|
|
|
68
81
|
switch (componentName) {
|
|
69
82
|
case 'DateFilterDynamicComponent':
|
|
@@ -183,6 +196,18 @@ export const FilterValueInput: React.FC<FilterValueInputProps> = (props) => {
|
|
|
183
196
|
case 'Input.URL':
|
|
184
197
|
case 'NanoIDInput':
|
|
185
198
|
default:
|
|
199
|
+
if (typeof componentName === 'function') {
|
|
200
|
+
const Component = componentName;
|
|
201
|
+
return <Component {...customComponentProps} />;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (typeof componentName === 'string' && !fallbackInputComponents.has(componentName)) {
|
|
205
|
+
const Component = app?.getComponent?.(componentName);
|
|
206
|
+
if (Component) {
|
|
207
|
+
return <Component {...customComponentProps} />;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
186
211
|
return (
|
|
187
212
|
<Input
|
|
188
213
|
value={value}
|
|
@@ -222,6 +222,45 @@ describe('FilterValueInput schema precedence', () => {
|
|
|
222
222
|
expect(document.querySelector('.ant-select')).not.toBeNull();
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
+
it('operator schema can render an app-registered custom component', () => {
|
|
226
|
+
const onChange = vi.fn();
|
|
227
|
+
const CustomFilterInput = (props: any) => (
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
data-testid="custom-filter-input"
|
|
231
|
+
data-field-interface={props.fieldInterface}
|
|
232
|
+
data-value={JSON.stringify(props.value)}
|
|
233
|
+
onClick={() => props.onChange?.(['alpha', 'beta'])}
|
|
234
|
+
>
|
|
235
|
+
custom-filter-input
|
|
236
|
+
</button>
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
render(
|
|
240
|
+
<FilterValueInput
|
|
241
|
+
operator={opOf({
|
|
242
|
+
value: '$in',
|
|
243
|
+
schema: {
|
|
244
|
+
'x-component': 'MultipleKeywordsInput',
|
|
245
|
+
'x-component-props': { fieldInterface: 'input' },
|
|
246
|
+
},
|
|
247
|
+
})}
|
|
248
|
+
value={['foo', 'bar']}
|
|
249
|
+
onChange={onChange}
|
|
250
|
+
app={{
|
|
251
|
+
getComponent: (name) => (name === 'MultipleKeywordsInput' ? CustomFilterInput : undefined),
|
|
252
|
+
}}
|
|
253
|
+
/>,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const input = screen.getByTestId('custom-filter-input');
|
|
257
|
+
expect(input.getAttribute('data-field-interface')).toBe('input');
|
|
258
|
+
expect(input.getAttribute('data-value')).toBe(JSON.stringify(['foo', 'bar']));
|
|
259
|
+
|
|
260
|
+
fireEvent.click(input);
|
|
261
|
+
expect(onChange).toHaveBeenCalledWith(['alpha', 'beta']);
|
|
262
|
+
});
|
|
263
|
+
|
|
225
264
|
it('field uiSchema is used when operator has none', () => {
|
|
226
265
|
const { container } = render(
|
|
227
266
|
<FilterValueInput
|