@nocobase/client-v2 2.1.0-alpha.45 → 2.1.0-alpha.46
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/authRedirect.d.ts +12 -0
- package/es/components/form/TypedVariableInput.d.ts +75 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- 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/models/base/AssociationFieldGroupModel.d.ts +2 -33
- package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +8 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
- package/es/index.mjs +103 -103
- package/lib/index.js +105 -105
- package/package.json +7 -7
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +5 -0
- package/src/authRedirect.ts +43 -0
- package/src/components/README.md +48 -0
- package/src/components/README.zh-CN.md +48 -0
- package/src/components/form/TypedVariableInput.tsx +441 -0
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +152 -0
- package/src/components/form/index.tsx +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +8 -1
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +100 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +23 -1
- package/src/flow/actions/dataScope.tsx +8 -1
- package/src/flow/actions/linkageRules.tsx +8 -5
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -3
- package/src/flow/actions/setTargetDataScope.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +75 -1
- 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/common/Markdown/Edit.tsx +32 -4
- package/src/flow/components/ConditionBuilder.tsx +12 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
- package/src/flow/components/FieldAssignValueInput.tsx +8 -4
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
- package/src/flow/components/code-editor/__tests__/runjsCompletions.test.ts +272 -27
- package/src/flow/components/code-editor/runjsCompletions.ts +490 -9
- package/src/flow/components/fieldAssignOptions.ts +23 -6
- package/src/flow/components/filter/LinkageFilterItem.tsx +22 -9
- package/src/flow/components/filter/VariableFilterItem.tsx +27 -9
- 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/models/actions/FilterActionModel.tsx +15 -3
- package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +90 -0
- package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
- package/src/flow/models/base/GridModel.tsx +1 -1
- package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
- package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
- package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
- package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +12 -3
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -4
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +216 -0
- package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +142 -40
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +1 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
- package/src/flow/models/blocks/form/FormAssociationItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +27 -2
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +15 -5
- package/src/flow/models/blocks/table/TableCustomColumnModel.tsx +2 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +18 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +6 -7
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +70 -0
|
@@ -12,11 +12,16 @@ import { AddSubModelButton, FlowSettingsButton, tExpr, EditableItemModel } from
|
|
|
12
12
|
import { SettingOutlined } from '@ant-design/icons';
|
|
13
13
|
import { FormGridModel } from '../form/FormGridModel';
|
|
14
14
|
|
|
15
|
+
const MAX_ASSIGN_FORM_ASSOCIATION_FIELD_DEPTH = 2;
|
|
16
|
+
|
|
15
17
|
// 使用范型准确标注 subModels.items 的类型
|
|
16
18
|
export class AssignFormGridModel extends FormGridModel {
|
|
17
19
|
renderAddSubModelButton() {
|
|
18
20
|
const collection = (this.context as any)?.collection;
|
|
19
21
|
const fields = collection?.getFields?.() || [];
|
|
22
|
+
const associationDepth = this.context.prefixFieldPath
|
|
23
|
+
? this.context.prefixFieldPath.split('.').filter(Boolean).length
|
|
24
|
+
: 0;
|
|
20
25
|
// 过滤主键/过滤键字段,避免产生“更新主键”的无效配置
|
|
21
26
|
const pk = typeof collection?.getPrimaryKey === 'function' ? collection.getPrimaryKey() : undefined;
|
|
22
27
|
const filterKey = collection?.filterTargetKey;
|
|
@@ -29,6 +34,12 @@ export class AssignFormGridModel extends FormGridModel {
|
|
|
29
34
|
};
|
|
30
35
|
const items = fields
|
|
31
36
|
.filter((field: any) => !isForbidden(field?.name))
|
|
37
|
+
.filter((field: any) => {
|
|
38
|
+
if (associationDepth < MAX_ASSIGN_FORM_ASSOCIATION_FIELD_DEPTH) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return !(field?.isAssociationField?.() || field?.target || field?.targetCollection);
|
|
42
|
+
})
|
|
32
43
|
.map((field: any) => {
|
|
33
44
|
const fullName = field.name as string;
|
|
34
45
|
const label = field.title || field.name;
|
|
@@ -111,7 +122,7 @@ export class AssignFormGridModel extends FormGridModel {
|
|
|
111
122
|
return;
|
|
112
123
|
}
|
|
113
124
|
const fieldModel = binding.modelName;
|
|
114
|
-
const created = this.
|
|
125
|
+
const created = this.addSubModel('items', {
|
|
115
126
|
use: 'AssignFormItemModel',
|
|
116
127
|
stepParams: {
|
|
117
128
|
fieldSettings: {
|
|
@@ -137,8 +148,6 @@ export class AssignFormGridModel extends FormGridModel {
|
|
|
137
148
|
},
|
|
138
149
|
},
|
|
139
150
|
},
|
|
140
|
-
parentId: this.uid,
|
|
141
|
-
subKey: 'items',
|
|
142
151
|
});
|
|
143
152
|
created['assignValue'] = value;
|
|
144
153
|
}
|
|
@@ -86,6 +86,14 @@ function getAssignFormTempFieldRefreshKey(originField: AssignFormTempOriginField
|
|
|
86
86
|
].join('|');
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function normalizeAssignFormInputValue(value: unknown) {
|
|
90
|
+
if (!value || typeof value !== 'object' || !('target' in value)) {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
const target = (value as { target?: { value?: unknown } }).target;
|
|
94
|
+
return target && 'value' in target ? target.value : value;
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
/**
|
|
90
98
|
* 使用 FormItemModel 的“表单项”包装,内部渲染 VariableInput,并将“常量”映射到临时字段模型。
|
|
91
99
|
*/
|
|
@@ -230,7 +238,6 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
230
238
|
fm?.setProps?.({
|
|
231
239
|
disabled: false,
|
|
232
240
|
readPretty: false,
|
|
233
|
-
pattern: 'editable',
|
|
234
241
|
updateAssociation: false,
|
|
235
242
|
multiple: multi,
|
|
236
243
|
});
|
|
@@ -260,9 +267,12 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
260
267
|
if (fm) {
|
|
261
268
|
fm.setProps?.({
|
|
262
269
|
value: inputProps?.value,
|
|
263
|
-
onChange: (...args:
|
|
264
|
-
const
|
|
265
|
-
|
|
270
|
+
onChange: (...args: unknown[]) => {
|
|
271
|
+
const nextArg = args && args.length ? args[0] : undefined;
|
|
272
|
+
const nextValue = normalizeAssignFormInputValue(nextArg);
|
|
273
|
+
fm.setProps?.({ value: nextValue });
|
|
274
|
+
this.assignValue = nextValue;
|
|
275
|
+
inputProps?.onChange?.(nextValue);
|
|
266
276
|
},
|
|
267
277
|
});
|
|
268
278
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
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 { App, ConfigProvider } from 'antd';
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
|
|
14
|
+
import {
|
|
15
|
+
FlowEngine,
|
|
16
|
+
FlowEngineProvider,
|
|
17
|
+
FlowRuntimeContext,
|
|
18
|
+
FlowSettingsContextProvider,
|
|
19
|
+
type FlowModel,
|
|
20
|
+
type IFlowModelRepository,
|
|
21
|
+
} from '@nocobase/flow-engine';
|
|
22
|
+
import { AssignFormGridModel } from '../AssignFormGridModel';
|
|
23
|
+
import { AssignFormItemModel } from '../AssignFormItemModel';
|
|
24
|
+
import { AssignFormModel } from '../AssignFormModel';
|
|
25
|
+
import { createAssignFieldValuesStep } from '../assignFieldValuesFlow';
|
|
26
|
+
import { InputFieldModel } from '../../../fields/InputFieldModel';
|
|
27
|
+
import { VariableFieldFormModel } from '../../../fields/VariableFieldFormModel';
|
|
28
|
+
|
|
29
|
+
class MockFlowModelRepository implements IFlowModelRepository {
|
|
30
|
+
async findOne(): Promise<Record<string, any> | null> {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async save(model: FlowModel): Promise<Record<string, any>> {
|
|
35
|
+
return { uid: model.uid };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async destroy(): Promise<boolean> {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async move(): Promise<void> {}
|
|
43
|
+
|
|
44
|
+
async duplicate(): Promise<Record<string, any> | null> {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('assignFieldValuesFlow (editor)', () => {
|
|
50
|
+
it('repairs AssignFormModel resource init and clears cached collection', async () => {
|
|
51
|
+
const engine = new FlowEngine();
|
|
52
|
+
engine.setModelRepository(new MockFlowModelRepository());
|
|
53
|
+
engine.registerModels({
|
|
54
|
+
AssignFormModel,
|
|
55
|
+
AssignFormGridModel,
|
|
56
|
+
AssignFormItemModel,
|
|
57
|
+
InputFieldModel,
|
|
58
|
+
VariableFieldFormModel,
|
|
59
|
+
});
|
|
60
|
+
engine.context.defineProperty('location', { value: { search: '' } });
|
|
61
|
+
engine.context.defineProperty('themeToken', { value: { marginLG: 24 } });
|
|
62
|
+
|
|
63
|
+
const dsm = engine.context.dataSourceManager;
|
|
64
|
+
const main = dsm.getDataSource('main');
|
|
65
|
+
main.addCollection({
|
|
66
|
+
name: 'users',
|
|
67
|
+
fields: [{ name: 'nickname', type: 'string', interface: 'input' }],
|
|
68
|
+
});
|
|
69
|
+
const users = dsm.getCollection('main', 'users');
|
|
70
|
+
expect(users?.name).toBe('users');
|
|
71
|
+
|
|
72
|
+
const action = engine.createModel({
|
|
73
|
+
use: 'FlowModel',
|
|
74
|
+
uid: 'act-assign',
|
|
75
|
+
});
|
|
76
|
+
action.setStepParams('assignSettings', 'assignFieldValues', { assignedValues: { nickname: '1111' } });
|
|
77
|
+
|
|
78
|
+
const existing = engine.createModel<AssignFormModel>({
|
|
79
|
+
use: 'AssignFormModel',
|
|
80
|
+
uid: 'form-assign',
|
|
81
|
+
parentId: action.uid,
|
|
82
|
+
subKey: 'assignForm',
|
|
83
|
+
stepParams: {
|
|
84
|
+
resourceSettings: {
|
|
85
|
+
init: {
|
|
86
|
+
dataSourceKey: 'main',
|
|
87
|
+
collectionName: 'missing',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
expect(existing.context.collection).toBeUndefined();
|
|
93
|
+
|
|
94
|
+
action.context.defineProperty('blockModel', { value: { collection: users } });
|
|
95
|
+
|
|
96
|
+
const step = createAssignFieldValuesStep({ settingsFlowKey: 'assignSettings' });
|
|
97
|
+
const schema = step.uiSchema();
|
|
98
|
+
const Editor = schema.editor?.['x-component'] as React.ComponentType;
|
|
99
|
+
expect(Editor).toBeTypeOf('function');
|
|
100
|
+
|
|
101
|
+
const flowSettingsCtx = new FlowRuntimeContext(action as any, 'assignSettings', 'settings');
|
|
102
|
+
|
|
103
|
+
render(
|
|
104
|
+
<FlowEngineProvider engine={engine}>
|
|
105
|
+
<ConfigProvider>
|
|
106
|
+
<App>
|
|
107
|
+
<FlowSettingsContextProvider value={flowSettingsCtx}>
|
|
108
|
+
<Editor />
|
|
109
|
+
</FlowSettingsContextProvider>
|
|
110
|
+
</App>
|
|
111
|
+
</ConfigProvider>
|
|
112
|
+
</FlowEngineProvider>,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
|
|
117
|
+
expect(form?.getStepParams('resourceSettings', 'init')).toEqual({
|
|
118
|
+
dataSourceKey: 'main',
|
|
119
|
+
collectionName: 'users',
|
|
120
|
+
});
|
|
121
|
+
expect(form?.context.collection?.name).toBe('users');
|
|
122
|
+
expect(form?.context.collection?.getFields?.().length).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(screen.getByText('nickname')).toBeInTheDocument();
|
|
127
|
+
const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
|
|
128
|
+
expect(form?.subModels.grid.subModels.items.length).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(screen.getByDisplayValue('1111')).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const input = screen.getByDisplayValue('1111') as HTMLInputElement;
|
|
136
|
+
await userEvent.clear(input);
|
|
137
|
+
await userEvent.type(input, '2222');
|
|
138
|
+
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
|
|
141
|
+
expect(form?.getAssignedValues()).toEqual({ nickname: '2222' });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('does not clear delegated parent resource cache when repairing AssignFormModel init', async () => {
|
|
146
|
+
const engine = new FlowEngine();
|
|
147
|
+
engine.setModelRepository(new MockFlowModelRepository());
|
|
148
|
+
engine.registerModels({ AssignFormModel, AssignFormGridModel, AssignFormItemModel, InputFieldModel });
|
|
149
|
+
engine.context.defineProperty('location', { value: { search: '' } });
|
|
150
|
+
engine.context.defineProperty('themeToken', { value: { marginLG: 24 } });
|
|
151
|
+
|
|
152
|
+
const dsm = engine.context.dataSourceManager;
|
|
153
|
+
const main = dsm.getDataSource('main');
|
|
154
|
+
main.addCollection({
|
|
155
|
+
name: 'users',
|
|
156
|
+
fields: [{ name: 'nickname', type: 'string', interface: 'input' }],
|
|
157
|
+
});
|
|
158
|
+
const users = dsm.getCollection('main', 'users');
|
|
159
|
+
|
|
160
|
+
const action = engine.createModel({
|
|
161
|
+
use: 'FlowModel',
|
|
162
|
+
uid: 'act-assign-parent-resource',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
let resourceCreateCount = 0;
|
|
166
|
+
action.context.defineProperty('resource', {
|
|
167
|
+
get: () => ({ id: ++resourceCreateCount }),
|
|
168
|
+
});
|
|
169
|
+
const cachedResource = action.context.resource;
|
|
170
|
+
|
|
171
|
+
engine.createModel<AssignFormModel>({
|
|
172
|
+
use: 'AssignFormModel',
|
|
173
|
+
uid: 'form-assign-parent-resource',
|
|
174
|
+
parentId: action.uid,
|
|
175
|
+
subKey: 'assignForm',
|
|
176
|
+
stepParams: {
|
|
177
|
+
resourceSettings: {
|
|
178
|
+
init: {
|
|
179
|
+
dataSourceKey: 'main',
|
|
180
|
+
collectionName: 'missing',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
action.context.defineProperty('blockModel', { value: { collection: users } });
|
|
187
|
+
|
|
188
|
+
const step = createAssignFieldValuesStep({ settingsFlowKey: 'assignSettings' });
|
|
189
|
+
const schema = step.uiSchema();
|
|
190
|
+
const Editor = schema.editor?.['x-component'] as React.ComponentType;
|
|
191
|
+
const flowSettingsCtx = new FlowRuntimeContext(action as any, 'assignSettings', 'settings');
|
|
192
|
+
|
|
193
|
+
render(
|
|
194
|
+
<FlowEngineProvider engine={engine}>
|
|
195
|
+
<ConfigProvider>
|
|
196
|
+
<App>
|
|
197
|
+
<FlowSettingsContextProvider value={flowSettingsCtx}>
|
|
198
|
+
<Editor />
|
|
199
|
+
</FlowSettingsContextProvider>
|
|
200
|
+
</App>
|
|
201
|
+
</ConfigProvider>
|
|
202
|
+
</FlowEngineProvider>,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
|
|
207
|
+
expect(form?.getStepParams('resourceSettings', 'init')).toEqual({
|
|
208
|
+
dataSourceKey: 'main',
|
|
209
|
+
collectionName: 'users',
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(action.context.resource).toBe(cachedResource);
|
|
214
|
+
expect(resourceCreateCount).toBe(1);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
useFlowEngine,
|
|
16
16
|
useFlowSettingsContext,
|
|
17
17
|
} from '@nocobase/flow-engine';
|
|
18
|
-
import React, { useEffect
|
|
18
|
+
import React, { useEffect } from 'react';
|
|
19
19
|
import { CollectionActionModel } from '../../base/CollectionActionModel';
|
|
20
20
|
import { RecordActionModel } from '../../base/RecordActionModel';
|
|
21
21
|
import { AssignFormModel } from './AssignFormModel';
|
|
@@ -38,6 +38,9 @@ type AssignFieldValuesModel = {
|
|
|
38
38
|
uid: string;
|
|
39
39
|
assignFormUid?: string;
|
|
40
40
|
context?: AssignFieldValuesContext;
|
|
41
|
+
subModels?: {
|
|
42
|
+
assignForm?: AssignFormModel;
|
|
43
|
+
};
|
|
41
44
|
getStepParams?: (flowKey: string, stepKey: string) => { assignedValues?: AssignedValues } | undefined;
|
|
42
45
|
setStepParams?: (flowKey: string, stepKey: string, params: { assignedValues: AssignedValues }) => void;
|
|
43
46
|
};
|
|
@@ -62,6 +65,81 @@ function getResourceInit(ctx: AssignFieldValuesContext): { dataSourceKey: string
|
|
|
62
65
|
return dsKey && collName ? { dataSourceKey: dsKey, collectionName: collName } : undefined;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
function resolveInitFromFlowSettings(
|
|
69
|
+
blockModel: unknown,
|
|
70
|
+
actionContext: AssignFieldValuesContext | undefined,
|
|
71
|
+
): { dataSourceKey: string; collectionName: string } | undefined {
|
|
72
|
+
const maybeBlockCollection = (blockModel as { collection?: { dataSourceKey?: string; name?: string } } | undefined)
|
|
73
|
+
?.collection;
|
|
74
|
+
const collection = maybeBlockCollection || getContextCollection(actionContext);
|
|
75
|
+
const dsKey = collection?.dataSourceKey;
|
|
76
|
+
const collName = collection?.name;
|
|
77
|
+
return dsKey && collName ? { dataSourceKey: dsKey, collectionName: collName } : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isValidResourceInit(init: unknown): init is {
|
|
81
|
+
dataSourceKey: string;
|
|
82
|
+
collectionName: string;
|
|
83
|
+
} {
|
|
84
|
+
if (!init || typeof init !== 'object') return false;
|
|
85
|
+
const obj = init as { dataSourceKey?: unknown; collectionName?: unknown };
|
|
86
|
+
return (
|
|
87
|
+
typeof obj.dataSourceKey === 'string' &&
|
|
88
|
+
!!obj.dataSourceKey &&
|
|
89
|
+
typeof obj.collectionName === 'string' &&
|
|
90
|
+
!!obj.collectionName
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type LocalCacheContext = {
|
|
95
|
+
_observableCache?: Record<string, unknown>;
|
|
96
|
+
_cache?: Record<string, unknown>;
|
|
97
|
+
_pending?: Record<string, unknown>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function toLocalCacheContext(context: unknown): LocalCacheContext | undefined {
|
|
101
|
+
return context && typeof context === 'object' ? (context as LocalCacheContext) : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clearOwnContextCache(context: unknown, key: string) {
|
|
105
|
+
const localContext = toLocalCacheContext(context);
|
|
106
|
+
if (!localContext) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
delete localContext._observableCache?.[key];
|
|
110
|
+
delete localContext._cache?.[key];
|
|
111
|
+
delete localContext._pending?.[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function clearResourceContextCache(model: { context?: unknown } | undefined) {
|
|
115
|
+
clearOwnContextCache(model?.context, 'dataSource');
|
|
116
|
+
clearOwnContextCache(model?.context, 'collection');
|
|
117
|
+
clearOwnContextCache(model?.context, 'resource');
|
|
118
|
+
clearOwnContextCache(model?.context, 'association');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveAssignFormModel(ctx: {
|
|
122
|
+
model: AssignFieldValuesModel;
|
|
123
|
+
engine: {
|
|
124
|
+
getModel?: (uid: string, fromRoot?: boolean) => AssignFormModel | undefined;
|
|
125
|
+
findModelByParentId?: (parentId: string, subKey: string) => AssignFormModel | undefined | null;
|
|
126
|
+
};
|
|
127
|
+
}) {
|
|
128
|
+
if (ctx.model.assignFormUid) {
|
|
129
|
+
const form = ctx.engine.getModel?.(ctx.model.assignFormUid, true);
|
|
130
|
+
if (form) {
|
|
131
|
+
return form;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const localForm = ctx.model.subModels?.assignForm;
|
|
136
|
+
if (localForm) {
|
|
137
|
+
return localForm;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return ctx.engine.findModelByParentId?.(ctx.model.uid, 'assignForm') || undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
65
143
|
export function createAssignFormSubModelOptions(ctx: AssignFieldValuesContext) {
|
|
66
144
|
return {
|
|
67
145
|
async: true,
|
|
@@ -114,21 +192,52 @@ function AssignFieldsEditor(props: { settingsFlowKey: string; clearRecordContext
|
|
|
114
192
|
const { model, blockModel } = useFlowSettingsContext();
|
|
115
193
|
const action = model as AssignFieldValuesModel;
|
|
116
194
|
const engine = useFlowEngine();
|
|
117
|
-
const initializedRef = useRef(false);
|
|
118
195
|
const [formModel, setFormModel] = React.useState<AssignFormModel | null>(null);
|
|
119
196
|
|
|
120
197
|
useEffect(() => {
|
|
121
198
|
let cancelled = false;
|
|
122
199
|
const loadAssignForm = async () => {
|
|
200
|
+
const init = resolveInitFromFlowSettings(blockModel, action?.context);
|
|
123
201
|
const loaded = (await engine.loadOrCreateModel(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
202
|
+
init
|
|
203
|
+
? {
|
|
204
|
+
parentId: action.uid,
|
|
205
|
+
subKey: 'assignForm',
|
|
206
|
+
use: 'AssignFormModel',
|
|
207
|
+
stepParams: { resourceSettings: { init } },
|
|
208
|
+
}
|
|
209
|
+
: {
|
|
210
|
+
parentId: action.uid,
|
|
211
|
+
subKey: 'assignForm',
|
|
212
|
+
use: 'AssignFormModel',
|
|
213
|
+
},
|
|
129
214
|
{ skipSave: !model.context.flowSettingsEnabled },
|
|
130
215
|
)) as AssignFormModel;
|
|
131
216
|
if (cancelled) return;
|
|
217
|
+
|
|
218
|
+
if (isValidResourceInit(init)) {
|
|
219
|
+
const existingInit = loaded.getStepParams?.('resourceSettings', 'init');
|
|
220
|
+
// Ensure `resourceSettings.init` is available before first render to avoid cached undefined `collection`
|
|
221
|
+
if (
|
|
222
|
+
!isValidResourceInit(existingInit) ||
|
|
223
|
+
existingInit.dataSourceKey !== init.dataSourceKey ||
|
|
224
|
+
existingInit.collectionName !== init.collectionName
|
|
225
|
+
) {
|
|
226
|
+
loaded.setStepParams?.('resourceSettings', 'init', init);
|
|
227
|
+
}
|
|
228
|
+
clearResourceContextCache(loaded);
|
|
229
|
+
clearResourceContextCache(loaded.subModels?.grid);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const prev = action.getStepParams?.(props.settingsFlowKey, ASSIGN_FIELD_VALUES_STEP_KEY) || {};
|
|
233
|
+
loaded.setInitialAssignedValues(prev?.assignedValues || {});
|
|
234
|
+
|
|
235
|
+
const isBulkCollectionAction = model instanceof CollectionActionModel && !(model instanceof RecordActionModel);
|
|
236
|
+
const isBulk = props.clearRecordContext || isBulkCollectionAction;
|
|
237
|
+
if (isBulk && loaded.context?.defineProperty) {
|
|
238
|
+
loaded.context.defineProperty('record', { get: () => undefined, cache: false });
|
|
239
|
+
}
|
|
240
|
+
|
|
132
241
|
setFormModel(loaded);
|
|
133
242
|
action.assignFormUid = loaded?.uid || action.assignFormUid;
|
|
134
243
|
};
|
|
@@ -136,31 +245,15 @@ function AssignFieldsEditor(props: { settingsFlowKey: string; clearRecordContext
|
|
|
136
245
|
return () => {
|
|
137
246
|
cancelled = true;
|
|
138
247
|
};
|
|
139
|
-
}, [
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const collName = coll?.name;
|
|
149
|
-
if (dsKey && collName) {
|
|
150
|
-
formModel.setStepParams('resourceSettings', 'init', {
|
|
151
|
-
dataSourceKey: dsKey,
|
|
152
|
-
collectionName: collName,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
formModel.setInitialAssignedValues(prev?.assignedValues || {});
|
|
156
|
-
|
|
157
|
-
const isBulk =
|
|
158
|
-
props.clearRecordContext || (action instanceof CollectionActionModel && !(action instanceof RecordActionModel));
|
|
159
|
-
if (isBulk && formModel.context?.defineProperty) {
|
|
160
|
-
formModel.context.defineProperty('record', { get: () => undefined, cache: false });
|
|
161
|
-
}
|
|
162
|
-
initializedRef.current = true;
|
|
163
|
-
}, [action, blockModel?.collection, formModel, props.clearRecordContext, props.settingsFlowKey]);
|
|
248
|
+
}, [
|
|
249
|
+
action,
|
|
250
|
+
blockModel,
|
|
251
|
+
engine,
|
|
252
|
+
model,
|
|
253
|
+
model.context.flowSettingsEnabled,
|
|
254
|
+
props.clearRecordContext,
|
|
255
|
+
props.settingsFlowKey,
|
|
256
|
+
]);
|
|
164
257
|
|
|
165
258
|
return formModel ? <FlowModelRenderer model={formModel} showFlowSettings={false} /> : null;
|
|
166
259
|
}
|
|
@@ -187,14 +280,23 @@ export function createAssignFieldValuesStep(options: AssignFieldValuesStepOption
|
|
|
187
280
|
},
|
|
188
281
|
};
|
|
189
282
|
},
|
|
190
|
-
async beforeParamsSave(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
283
|
+
async beforeParamsSave(
|
|
284
|
+
ctx: {
|
|
285
|
+
model: AssignFieldValuesModel;
|
|
286
|
+
engine: {
|
|
287
|
+
getModel?: (uid: string, fromRoot?: boolean) => AssignFormModel | undefined;
|
|
288
|
+
findModelByParentId?: (parentId: string, subKey: string) => AssignFormModel | undefined | null;
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
params?: { assignedValues?: AssignedValues },
|
|
292
|
+
previousParams?: { assignedValues?: AssignedValues },
|
|
293
|
+
) {
|
|
294
|
+
const form = resolveAssignFormModel(ctx);
|
|
295
|
+
if (!form) {
|
|
296
|
+
const assignedValues = params?.assignedValues || previousParams?.assignedValues || {};
|
|
297
|
+
ctx.model.setStepParams?.(options.settingsFlowKey, ASSIGN_FIELD_VALUES_STEP_KEY, { assignedValues });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
198
300
|
if (options.validateBeforeSave) {
|
|
199
301
|
await form.form?.validateFields?.();
|
|
200
302
|
}
|
|
@@ -51,7 +51,7 @@ const getTargetFilterableFields = (field: any, collection?: Collection, model?:
|
|
|
51
51
|
return (targetCollection.getFields() || []).filter((childField: any) => childField?.filterable);
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
const MAX_ASSOCIATION_DEPTH =
|
|
54
|
+
const MAX_ASSOCIATION_DEPTH = 2;
|
|
55
55
|
|
|
56
56
|
const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => {
|
|
57
57
|
const collectionField = fieldModel?.context?.collectionField;
|
package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts
CHANGED
|
@@ -136,9 +136,13 @@ describe('FilterFormItemModel defineChildren association fields', () => {
|
|
|
136
136
|
);
|
|
137
137
|
const managerGroups = await managerAssociation.children();
|
|
138
138
|
const managerFieldsGroup = managerGroups.find((group: any) => group.key === 'department.manager-fields');
|
|
139
|
+
const managerAssociationGroup = managerGroups.find(
|
|
140
|
+
(group: any) => group.key === 'department.manager-relation-fields',
|
|
141
|
+
);
|
|
139
142
|
|
|
140
143
|
const managerFieldKeys = (managerFieldsGroup?.children || []).map((item: any) => item.key);
|
|
141
144
|
expect(managerFieldKeys).toContain('department.manager.name');
|
|
145
|
+
expect(managerAssociationGroup).toBeUndefined();
|
|
142
146
|
|
|
143
147
|
const targetItem = managerFieldsGroup?.children?.find((item: any) => item.key === 'department.manager.name');
|
|
144
148
|
const createOptions = await targetItem.createModelOptions();
|
|
@@ -200,7 +204,8 @@ describe('FilterFormItemModel defineChildren association fields', () => {
|
|
|
200
204
|
});
|
|
201
205
|
|
|
202
206
|
const ds = engine.dataSourceManager.getDataSource('main');
|
|
203
|
-
ds
|
|
207
|
+
expect(ds).toBeTruthy();
|
|
208
|
+
ds?.addCollection({
|
|
204
209
|
name: 'users',
|
|
205
210
|
filterTargetKey: 'id',
|
|
206
211
|
fields: [{ name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } }],
|
|
@@ -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
|
|