@nocobase/client-v2 2.1.20 → 2.1.22
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-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -1
- package/es/index.mjs +62 -62
- package/lib/index.js +69 -69
- package/package.json +7 -7
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/components/FieldAssignValueInput.tsx +15 -11
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +134 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +177 -19
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +320 -1
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.22",
|
|
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.
|
|
31
|
-
"@nocobase/flow-engine": "2.1.
|
|
32
|
-
"@nocobase/sdk": "2.1.
|
|
33
|
-
"@nocobase/shared": "2.1.
|
|
34
|
-
"@nocobase/utils": "2.1.
|
|
30
|
+
"@nocobase/evaluators": "2.1.22",
|
|
31
|
+
"@nocobase/flow-engine": "2.1.22",
|
|
32
|
+
"@nocobase/sdk": "2.1.22",
|
|
33
|
+
"@nocobase/shared": "2.1.22",
|
|
34
|
+
"@nocobase/utils": "2.1.22",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"react-i18next": "^11.15.1",
|
|
47
47
|
"react-router-dom": "^6.30.1"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "811fc3d272389739babdfa4338faf843d5bf04ff"
|
|
50
50
|
}
|
|
@@ -115,6 +115,7 @@ export abstract class CollectionFieldInterface {
|
|
|
115
115
|
titleUsable?: boolean;
|
|
116
116
|
usePathOptions?(field: any): any;
|
|
117
117
|
hidden?: boolean;
|
|
118
|
+
creatable?: boolean;
|
|
118
119
|
|
|
119
120
|
addComponentOption(componentOption: CollectionFieldInterfaceComponentOption) {
|
|
120
121
|
if (!this.componentOptions) {
|
package/src/flow/FlowPage.tsx
CHANGED
|
@@ -60,7 +60,9 @@ type FlowPageProps = {
|
|
|
60
60
|
type FlowPageViewContext = FlowEngineContext & {
|
|
61
61
|
view?: {
|
|
62
62
|
inputArgs?: {
|
|
63
|
+
filterByTk?: unknown;
|
|
63
64
|
isMobileLayout?: unknown;
|
|
65
|
+
sourceId?: unknown;
|
|
64
66
|
};
|
|
65
67
|
};
|
|
66
68
|
};
|
|
@@ -102,6 +104,10 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
|
|
|
102
104
|
subType: 'object',
|
|
103
105
|
use: pageModelClass,
|
|
104
106
|
};
|
|
107
|
+
const isRuntimeRecordScopedPage =
|
|
108
|
+
!flowEngine.context.flowSettingsEnabled &&
|
|
109
|
+
(typeof ctx?.view?.inputArgs?.filterByTk !== 'undefined' ||
|
|
110
|
+
typeof ctx?.view?.inputArgs?.sourceId !== 'undefined');
|
|
105
111
|
if (shouldInjectDefaultChildTab) {
|
|
106
112
|
const tabTitle = defaultTabTitle || flowEngine.translate?.('Details');
|
|
107
113
|
options['subModels'] = {
|
|
@@ -128,7 +134,9 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
|
|
|
128
134
|
},
|
|
129
135
|
};
|
|
130
136
|
}
|
|
131
|
-
const data =
|
|
137
|
+
const data =
|
|
138
|
+
(isRuntimeRecordScopedPage && (await flowEngine.loadModel({ ...options, refresh: true }))) ||
|
|
139
|
+
(await flowEngine.loadOrCreateModel(options, { skipSave: !flowEngine.context.flowSettingsEnabled }));
|
|
132
140
|
if (data?.uid && onModelLoaded) {
|
|
133
141
|
data.context.addDelegate(ctx);
|
|
134
142
|
bindViewLayoutState(data, ctx);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { render, waitFor } from '@testing-library/react';
|
|
11
11
|
import React from 'react';
|
|
12
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import { FlowPage } from '../FlowPage';
|
|
14
14
|
|
|
15
15
|
type TestContextProperty = {
|
|
@@ -71,10 +71,15 @@ const createPageModel = (): TestPageModel => {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
const mocks = vi.hoisted(() => ({
|
|
74
|
+
flowSettingsEnabled: false,
|
|
75
|
+
loadModel: vi.fn(),
|
|
76
|
+
loadOrCreateModel: vi.fn(),
|
|
74
77
|
rendererMobileStates: [] as boolean[],
|
|
75
78
|
rendererProps: undefined as RendererProps | undefined,
|
|
76
79
|
model: undefined as TestPageModel | undefined,
|
|
80
|
+
viewFilterByTk: undefined as unknown,
|
|
77
81
|
viewInputMobileLayout: true as boolean | undefined,
|
|
82
|
+
viewSourceId: undefined as unknown,
|
|
78
83
|
contextMobileLayout: undefined as boolean | undefined,
|
|
79
84
|
}));
|
|
80
85
|
|
|
@@ -94,15 +99,20 @@ vi.mock('@nocobase/flow-engine', async () => {
|
|
|
94
99
|
}),
|
|
95
100
|
useFlowEngine: vi.fn(() => ({
|
|
96
101
|
getModelClassAsync: vi.fn(async () => TestPageModelClass),
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
loadModel: mocks.loadModel,
|
|
103
|
+
loadOrCreateModel: mocks.loadOrCreateModel,
|
|
104
|
+
context: {
|
|
105
|
+
flowSettingsEnabled: mocks.flowSettingsEnabled,
|
|
106
|
+
},
|
|
99
107
|
translate: vi.fn((value: string) => value),
|
|
100
108
|
})),
|
|
101
109
|
useFlowModelById: vi.fn(() => mocks.model),
|
|
102
110
|
useFlowViewContext: vi.fn(() => ({
|
|
103
111
|
view: {
|
|
104
112
|
inputArgs: {
|
|
113
|
+
...(typeof mocks.viewFilterByTk !== 'undefined' ? { filterByTk: mocks.viewFilterByTk } : {}),
|
|
105
114
|
...(typeof mocks.viewInputMobileLayout === 'boolean' ? { isMobileLayout: mocks.viewInputMobileLayout } : {}),
|
|
115
|
+
...(typeof mocks.viewSourceId !== 'undefined' ? { sourceId: mocks.viewSourceId } : {}),
|
|
106
116
|
},
|
|
107
117
|
},
|
|
108
118
|
...(typeof mocks.contextMobileLayout === 'boolean' ? { isMobileLayout: mocks.contextMobileLayout } : {}),
|
|
@@ -154,11 +164,21 @@ vi.mock('ahooks', async () => {
|
|
|
154
164
|
});
|
|
155
165
|
|
|
156
166
|
describe('FlowPage', () => {
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
mocks.loadModel.mockImplementation(async () => null);
|
|
169
|
+
mocks.loadOrCreateModel.mockImplementation(async () => mocks.model);
|
|
170
|
+
});
|
|
171
|
+
|
|
157
172
|
afterEach(() => {
|
|
173
|
+
mocks.flowSettingsEnabled = false;
|
|
174
|
+
mocks.loadModel.mockReset();
|
|
175
|
+
mocks.loadOrCreateModel.mockReset();
|
|
158
176
|
mocks.rendererMobileStates = [];
|
|
159
177
|
mocks.rendererProps = undefined;
|
|
160
178
|
mocks.model = undefined;
|
|
179
|
+
mocks.viewFilterByTk = undefined;
|
|
161
180
|
mocks.viewInputMobileLayout = true;
|
|
181
|
+
mocks.viewSourceId = undefined;
|
|
162
182
|
mocks.contextMobileLayout = undefined;
|
|
163
183
|
});
|
|
164
184
|
|
|
@@ -203,4 +223,31 @@ describe('FlowPage', () => {
|
|
|
203
223
|
expect(mocks.rendererMobileStates[0]).toBe(expected);
|
|
204
224
|
},
|
|
205
225
|
);
|
|
226
|
+
|
|
227
|
+
it('reloads runtime page models for record scoped views', async () => {
|
|
228
|
+
mocks.model = createPageModel();
|
|
229
|
+
mocks.viewFilterByTk = 1;
|
|
230
|
+
mocks.loadModel.mockImplementation(async () => mocks.model);
|
|
231
|
+
|
|
232
|
+
render(<FlowPage onModelLoaded={vi.fn()} />);
|
|
233
|
+
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
expect(mocks.loadModel).toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
expect(mocks.loadModel.mock.calls[0]?.[0]).toMatchObject({ refresh: true });
|
|
238
|
+
expect(mocks.loadOrCreateModel).not.toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('keeps configured page model cache behavior in flow settings mode', async () => {
|
|
242
|
+
mocks.flowSettingsEnabled = true;
|
|
243
|
+
mocks.model = createPageModel();
|
|
244
|
+
mocks.viewFilterByTk = 1;
|
|
245
|
+
|
|
246
|
+
render(<FlowPage onModelLoaded={vi.fn()} />);
|
|
247
|
+
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(mocks.loadOrCreateModel).toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
expect(mocks.loadModel).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
206
253
|
});
|
|
@@ -1028,18 +1028,22 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1028
1028
|
collectionField: effectiveCollectionField,
|
|
1029
1029
|
});
|
|
1030
1030
|
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1031
|
+
const sourceContext = resolved?.itemModel?.context || flowCtx.model?.context;
|
|
1032
|
+
const created = engine?.createModel?.(
|
|
1033
|
+
{
|
|
1034
|
+
use: 'VariableFieldFormModel',
|
|
1035
|
+
subModels: {
|
|
1036
|
+
fields: [
|
|
1037
|
+
{
|
|
1038
|
+
use: effectiveFieldModelUse,
|
|
1039
|
+
stepParams: tempFieldStepParams,
|
|
1040
|
+
props: tempFieldProps,
|
|
1041
|
+
},
|
|
1042
|
+
],
|
|
1043
|
+
},
|
|
1041
1044
|
},
|
|
1042
|
-
|
|
1045
|
+
sourceContext ? { delegate: sourceContext } : undefined,
|
|
1046
|
+
);
|
|
1043
1047
|
if (!created) return;
|
|
1044
1048
|
|
|
1045
1049
|
// 注入上下文(集合/数据源/字段/区块/资源)
|
|
@@ -0,0 +1,134 @@
|
|
|
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 { render, waitFor } from '@nocobase/test/client';
|
|
13
|
+
import { FieldAssignValueInput } from '../FieldAssignValueInput';
|
|
14
|
+
|
|
15
|
+
const { mockUseFlowContext, mockGetDefaultBindingByField } = vi.hoisted(() => ({
|
|
16
|
+
mockUseFlowContext: vi.fn(),
|
|
17
|
+
mockGetDefaultBindingByField: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@nocobase/flow-engine', async () => {
|
|
21
|
+
const actual = await vi.importActual<typeof import('@nocobase/flow-engine')>('@nocobase/flow-engine');
|
|
22
|
+
class MockEditableItemModel extends actual.EditableItemModel {}
|
|
23
|
+
MockEditableItemModel.getDefaultBindingByField = mockGetDefaultBindingByField;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
useFlowContext: () => mockUseFlowContext(),
|
|
28
|
+
VariableInput: () => <div data-testid="variable-input" />,
|
|
29
|
+
FlowModelRenderer: () => <div data-testid="flow-model-renderer" />,
|
|
30
|
+
EditableItemModel: MockEditableItemModel,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('FieldAssignValueInput context', () => {
|
|
35
|
+
it('delegates temporary field model context to the source form context', async () => {
|
|
36
|
+
type MockContext = {
|
|
37
|
+
dataSourceManager: { getDataSource: ReturnType<typeof vi.fn> };
|
|
38
|
+
t: (key: string) => string;
|
|
39
|
+
engine?: { createModel: ReturnType<typeof vi.fn> };
|
|
40
|
+
collection?: MockCollection;
|
|
41
|
+
blockModel?: MockFormModel;
|
|
42
|
+
};
|
|
43
|
+
type MockFieldModel = {
|
|
44
|
+
props: Record<string, unknown>;
|
|
45
|
+
setProps: ReturnType<typeof vi.fn>;
|
|
46
|
+
dispatchEvent: ReturnType<typeof vi.fn>;
|
|
47
|
+
remove: ReturnType<typeof vi.fn>;
|
|
48
|
+
};
|
|
49
|
+
type MockCollectionField = {
|
|
50
|
+
name: string;
|
|
51
|
+
interface: string;
|
|
52
|
+
uiSchema: { enum: Array<{ label: string; value: string }> };
|
|
53
|
+
isAssociationField: () => boolean;
|
|
54
|
+
getComponentProps: () => Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
type MockCollection = {
|
|
57
|
+
dataSourceKey: string;
|
|
58
|
+
name: string;
|
|
59
|
+
getField: (name: string) => MockCollectionField | null;
|
|
60
|
+
getFields: () => MockCollectionField[];
|
|
61
|
+
};
|
|
62
|
+
type MockFormModel = {
|
|
63
|
+
context: MockContext;
|
|
64
|
+
collection: MockCollection;
|
|
65
|
+
subModels: Record<string, unknown>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const sourceContext: MockContext = {
|
|
69
|
+
dataSourceManager: {
|
|
70
|
+
getDataSource: vi.fn(() => ({})),
|
|
71
|
+
},
|
|
72
|
+
t: (key: string) => key,
|
|
73
|
+
};
|
|
74
|
+
const fieldModel: MockFieldModel = {
|
|
75
|
+
props: {},
|
|
76
|
+
setProps: vi.fn((props: Record<string, unknown>) => {
|
|
77
|
+
fieldModel.props = { ...fieldModel.props, ...props };
|
|
78
|
+
}),
|
|
79
|
+
dispatchEvent: vi.fn(),
|
|
80
|
+
remove: vi.fn(),
|
|
81
|
+
};
|
|
82
|
+
const tempRoot = {
|
|
83
|
+
context: {
|
|
84
|
+
defineProperty: vi.fn(),
|
|
85
|
+
},
|
|
86
|
+
subModels: {
|
|
87
|
+
fields: [fieldModel],
|
|
88
|
+
},
|
|
89
|
+
setProps: vi.fn(),
|
|
90
|
+
remove: vi.fn(),
|
|
91
|
+
};
|
|
92
|
+
const engine = {
|
|
93
|
+
createModel: vi.fn(() => tempRoot),
|
|
94
|
+
};
|
|
95
|
+
sourceContext.engine = engine;
|
|
96
|
+
|
|
97
|
+
const collectionField: MockCollectionField = {
|
|
98
|
+
name: 'status',
|
|
99
|
+
interface: 'select',
|
|
100
|
+
uiSchema: {
|
|
101
|
+
enum: [{ label: 'Open', value: 'open' }],
|
|
102
|
+
},
|
|
103
|
+
isAssociationField: () => false,
|
|
104
|
+
getComponentProps: () => ({}),
|
|
105
|
+
};
|
|
106
|
+
const collection: MockCollection = {
|
|
107
|
+
dataSourceKey: 'main',
|
|
108
|
+
name: 'tasks',
|
|
109
|
+
getField: (name: string) => (name === 'status' ? collectionField : null),
|
|
110
|
+
getFields: () => [collectionField],
|
|
111
|
+
};
|
|
112
|
+
const formModel: MockFormModel = {
|
|
113
|
+
context: sourceContext,
|
|
114
|
+
collection,
|
|
115
|
+
subModels: {},
|
|
116
|
+
};
|
|
117
|
+
sourceContext.collection = collection;
|
|
118
|
+
sourceContext.blockModel = formModel;
|
|
119
|
+
|
|
120
|
+
mockGetDefaultBindingByField.mockReturnValue({ modelName: 'SelectFieldModel' });
|
|
121
|
+
mockUseFlowContext.mockReturnValue({
|
|
122
|
+
model: formModel,
|
|
123
|
+
t: (key: string) => key,
|
|
124
|
+
getPropertyMetaTree: vi.fn(async () => []),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
render(<FieldAssignValueInput targetPath="status" value="" onChange={vi.fn()} />);
|
|
128
|
+
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(engine.createModel).toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
expect(engine.createModel).toHaveBeenCalledWith(expect.any(Object), { delegate: sourceContext });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -432,6 +432,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
|
|
|
432
432
|
|
|
433
433
|
private canApplyFormDefaultValue(name: string, current: any, force?: boolean) {
|
|
434
434
|
if (force) return true;
|
|
435
|
+
if (this.userEditedFieldNames.has(name)) return false;
|
|
435
436
|
if (isEmptyValue(current)) return true;
|
|
436
437
|
if (!this.lastDefaultValueByFieldName.has(name)) return false;
|
|
437
438
|
return isEqual(current, this.lastDefaultValueByFieldName.get(name));
|
|
@@ -62,9 +62,25 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
62
62
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
interface CollectionFieldWithAssociationMarker {
|
|
66
|
+
isAssociationField?: () => boolean;
|
|
67
|
+
target?: unknown;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hasAssociationMarker(value: unknown): value is CollectionFieldWithAssociationMarker {
|
|
71
|
+
return isRecord(value) && (typeof value.isAssociationField === 'function' || 'target' in value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isAssociationCollectionField = (collectionField: unknown) => {
|
|
75
|
+
if (!hasAssociationMarker(collectionField)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return collectionField.isAssociationField ? collectionField.isAssociationField() : Boolean(collectionField.target);
|
|
79
|
+
};
|
|
80
|
+
|
|
65
81
|
const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => {
|
|
66
82
|
const collectionField = fieldModel?.context?.collectionField;
|
|
67
|
-
if (!collectionField
|
|
83
|
+
if (!isAssociationCollectionField(collectionField)) {
|
|
68
84
|
return value;
|
|
69
85
|
}
|
|
70
86
|
|
|
@@ -171,8 +187,7 @@ const buildFilterFormFieldItem = ({
|
|
|
171
187
|
if (!binding) {
|
|
172
188
|
return;
|
|
173
189
|
}
|
|
174
|
-
const isAssociation =
|
|
175
|
-
typeof field?.isAssociationField === 'function' ? field.isAssociationField() : Boolean(field?.target);
|
|
190
|
+
const isAssociation = isAssociationCollectionField(field);
|
|
176
191
|
const fieldModel =
|
|
177
192
|
isAssociation && ctxWithFlags.engine?.getModelClass?.('FilterFormRecordSelectFieldModel')
|
|
178
193
|
? 'FilterFormRecordSelectFieldModel'
|
|
@@ -507,10 +522,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
|
|
|
507
522
|
const formValue = this.context.form?.getFieldValue(this.props.name);
|
|
508
523
|
const modelValue = fieldModel.getFilterValue ? fieldModel.getFilterValue() : formValue;
|
|
509
524
|
const collectionField = (fieldModel as any)?.context?.collectionField;
|
|
510
|
-
const isAssociationField =
|
|
511
|
-
typeof collectionField?.isAssociationField === 'function'
|
|
512
|
-
? collectionField.isAssociationField()
|
|
513
|
-
: !!collectionField?.target;
|
|
525
|
+
const isAssociationField = isAssociationCollectionField(collectionField);
|
|
514
526
|
const shouldUseFormValue =
|
|
515
527
|
isAssociationField &&
|
|
516
528
|
!this.mounted &&
|
|
@@ -547,11 +559,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
|
|
|
547
559
|
return value;
|
|
548
560
|
}
|
|
549
561
|
const collectionField = (fieldModel as any)?.context?.collectionField;
|
|
550
|
-
|
|
551
|
-
typeof collectionField?.isAssociationField === 'function'
|
|
552
|
-
? collectionField.isAssociationField()
|
|
553
|
-
: !!collectionField?.target;
|
|
554
|
-
if (!isAssociation) {
|
|
562
|
+
if (!isAssociationCollectionField(collectionField)) {
|
|
555
563
|
return value;
|
|
556
564
|
}
|
|
557
565
|
const normalizedFieldNames = normalizeAssociationFieldNames(
|
|
@@ -798,6 +806,13 @@ FilterFormItemModel.registerFlow({
|
|
|
798
806
|
},
|
|
799
807
|
defaultOperator: {
|
|
800
808
|
use: 'defaultOperator',
|
|
809
|
+
hideInSettings(ctx) {
|
|
810
|
+
const collectionField =
|
|
811
|
+
ctx.collectionField ||
|
|
812
|
+
ctx.model?.context?.collectionField ||
|
|
813
|
+
ctx.model?.subModels?.field?.context?.collectionField;
|
|
814
|
+
return isAssociationCollectionField(collectionField);
|
|
815
|
+
},
|
|
801
816
|
},
|
|
802
817
|
operatorComponentProps: {
|
|
803
818
|
use: 'operatorComponentProps',
|
package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts
CHANGED
|
@@ -33,6 +33,42 @@ class DummyCollectionBlockModel extends CollectionBlockModel {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
describe('FilterFormItemModel defineChildren association fields', () => {
|
|
36
|
+
it('hides default operator setting for association filter fields', () => {
|
|
37
|
+
const engine = new FlowEngine();
|
|
38
|
+
engine.registerModels({
|
|
39
|
+
FilterFormItemModel,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const filterItem = engine.createModel<FilterFormItemModel>({
|
|
43
|
+
uid: 'association-filter-item-settings',
|
|
44
|
+
use: 'FilterFormItemModel',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const defaultOperatorStep = filterItem.getFlow('filterFormItemSettings')?.steps?.defaultOperator as {
|
|
48
|
+
hideInSettings?: (ctx: {
|
|
49
|
+
collectionField?: unknown;
|
|
50
|
+
model?: {
|
|
51
|
+
subModels?: {
|
|
52
|
+
field?: {
|
|
53
|
+
context?: {
|
|
54
|
+
collectionField?: unknown;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
}) => boolean;
|
|
60
|
+
};
|
|
61
|
+
expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { isAssociationField: () => true } })).toBe(true);
|
|
62
|
+
expect(
|
|
63
|
+
defaultOperatorStep?.hideInSettings?.({
|
|
64
|
+
model: { subModels: { field: { context: { collectionField: { target: 'departments' } } } } },
|
|
65
|
+
}),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { interface: 'input', type: 'string' } })).toBe(
|
|
68
|
+
false,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
36
72
|
it('groups association target fields and supports recursive paths', async () => {
|
|
37
73
|
const engine = new FlowEngine();
|
|
38
74
|
engine.registerModels({
|
|
@@ -301,6 +301,40 @@ describe('filter-form defaultValues wiring', () => {
|
|
|
301
301
|
expect(values.username_user).toBe('Manual');
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
+
it('does not reapply a filter form default value after user clears the field', async () => {
|
|
305
|
+
const { model, values } = createFilterFormDefaultValuesModel([
|
|
306
|
+
{
|
|
307
|
+
key: 'username-default',
|
|
308
|
+
enable: true,
|
|
309
|
+
targetPath: 'username',
|
|
310
|
+
mode: 'default',
|
|
311
|
+
value: 'admin',
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
316
|
+
expect(values.username_user).toBe('admin');
|
|
317
|
+
|
|
318
|
+
values.username_user = undefined;
|
|
319
|
+
(model as any).handleFilterFormValuesChange({ username_user: undefined }, { username_user: undefined });
|
|
320
|
+
|
|
321
|
+
await waitFor(() => {
|
|
322
|
+
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
323
|
+
'formValuesChange',
|
|
324
|
+
{
|
|
325
|
+
changedValues: {
|
|
326
|
+
username_user: undefined,
|
|
327
|
+
},
|
|
328
|
+
allValues: {
|
|
329
|
+
username_user: undefined,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{ debounce: true },
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
expect(values.username_user).toBeUndefined();
|
|
336
|
+
});
|
|
337
|
+
|
|
304
338
|
it('applies fixed values even when the target filter field already has a value', async () => {
|
|
305
339
|
const { model, values } = createFilterFormDefaultValuesModel(
|
|
306
340
|
[
|