@nocobase/client-v2 2.1.0-beta.33 → 2.1.0-beta.35
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/APIClient.d.ts +16 -0
- package/es/Application.d.ts +2 -1
- package/es/BaseApplication.d.ts +6 -0
- package/es/PluginManager.d.ts +2 -0
- package/es/authRedirect.d.ts +9 -16
- package/es/components/form/EnvVariableInput.d.ts +8 -6
- package/es/components/form/VariableInput.d.ts +73 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
- package/es/components/form/table/SelectionCell.d.ts +36 -0
- package/es/components/form/table/Table.d.ts +82 -0
- package/es/components/form/table/constants.d.ts +15 -0
- package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
- package/es/components/form/table/dnd/index.d.ts +9 -0
- package/es/components/form/table/index.d.ts +9 -0
- package/es/components/form/table/styles.d.ts +41 -0
- package/es/components/form/table/utils.d.ts +44 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
- package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +166 -99
- package/es/json-logic/globalOperators.d.ts +11 -0
- package/es/theme/globalStyles.d.ts +9 -0
- package/es/theme/index.d.ts +1 -0
- package/es/utils/globalDeps.d.ts +7 -0
- package/lib/index.js +173 -106
- package/package.json +9 -6
- package/src/APIClient.ts +68 -0
- package/src/Application.tsx +6 -2
- package/src/BaseApplication.tsx +8 -0
- package/src/PluginManager.ts +2 -0
- package/src/__tests__/app.test.tsx +8 -0
- package/src/__tests__/authRedirect.test.ts +170 -64
- package/src/__tests__/globalDeps.test.ts +2 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
- package/src/__tests__/remotePlugins.test.ts +148 -0
- package/src/authRedirect.ts +23 -84
- package/src/components/form/EnvVariableInput.tsx +11 -46
- package/src/components/form/VariableInput.tsx +177 -0
- package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/form/table/RowOverlayPreview.tsx +51 -0
- package/src/components/form/table/SelectionCell.tsx +72 -0
- package/src/components/form/table/Table.tsx +279 -0
- package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
- package/src/components/form/table/constants.ts +16 -0
- package/src/components/form/table/dnd/SortableRow.tsx +106 -0
- package/src/components/form/table/dnd/index.ts +10 -0
- package/src/components/form/table/index.tsx +13 -0
- package/src/components/form/table/styles.ts +110 -0
- package/src/components/form/table/utils.ts +75 -0
- package/src/components/index.ts +2 -0
- package/src/css-variable/CSSVariableProvider.tsx +1 -1
- package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
- package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +81 -0
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +34 -2
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
- package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
- package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
- package/src/index.ts +1 -0
- package/src/json-logic/globalOperators.js +731 -0
- package/src/nocobase-buildin-plugin/index.tsx +4 -4
- package/src/theme/globalStyles.ts +21 -0
- package/src/theme/index.tsx +1 -0
- package/src/utils/globalDeps.ts +50 -30
- package/src/utils/remotePlugins.ts +107 -6
|
@@ -14,6 +14,7 @@ import { render, waitFor } from '@testing-library/react';
|
|
|
14
14
|
import { App, ConfigProvider } from 'antd';
|
|
15
15
|
import { useCodeRunner } from '../hooks/useCodeRunner';
|
|
16
16
|
import {
|
|
17
|
+
FlowContext,
|
|
17
18
|
FlowEngine,
|
|
18
19
|
FlowModel,
|
|
19
20
|
FlowEngineProvider,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
ElementProxy,
|
|
22
23
|
createSafeWindow,
|
|
23
24
|
createSafeDocument,
|
|
25
|
+
createViewScopedEngine,
|
|
24
26
|
} from '@nocobase/flow-engine';
|
|
25
27
|
import { JSEditableFieldModel } from '../../../models/fields/JSEditableFieldModel';
|
|
26
28
|
|
|
@@ -31,6 +33,20 @@ class DummyJsAutoModel extends FlowModel {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function registerRunJsPreviewFlow(model: FlowModel) {
|
|
37
|
+
model.registerFlow('jsSettings', {
|
|
38
|
+
steps: {
|
|
39
|
+
runJs: {
|
|
40
|
+
useRawParams: true,
|
|
41
|
+
async handler(ctx) {
|
|
42
|
+
const code = ctx?.inputArgs?.preview?.code || '';
|
|
43
|
+
return ctx.runjs(code, undefined, { preprocessTemplates: true });
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
describe('useCodeRunner (beforeRender)', () => {
|
|
35
51
|
it('logs success and captures console output', async () => {
|
|
36
52
|
const engine = new FlowEngine();
|
|
@@ -192,6 +208,71 @@ describe('useCodeRunner (beforeRender)', () => {
|
|
|
192
208
|
});
|
|
193
209
|
});
|
|
194
210
|
|
|
211
|
+
it('keeps popup context when a top scoped engine has another model with the same uid', async () => {
|
|
212
|
+
const engine = new FlowEngine();
|
|
213
|
+
engine.registerModels({ DummyJsAutoModel });
|
|
214
|
+
const model = engine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'same-popup-uid' });
|
|
215
|
+
model.context.defineProperty('popup', {
|
|
216
|
+
value: {
|
|
217
|
+
uid: 'popup-view',
|
|
218
|
+
record: { username: 'alice' },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
registerRunJsPreviewFlow(model);
|
|
222
|
+
|
|
223
|
+
const scopedEngine = createViewScopedEngine(engine);
|
|
224
|
+
const topModel = scopedEngine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'same-popup-uid' });
|
|
225
|
+
registerRunJsPreviewFlow(topModel);
|
|
226
|
+
|
|
227
|
+
const { result } = renderHook(() => useCodeRunner(model.context, 'v1'));
|
|
228
|
+
let runResult: any;
|
|
229
|
+
await act(async () => {
|
|
230
|
+
runResult = await result.current.run(`
|
|
231
|
+
const currentUsername = await ctx.getVar('ctx.popup.record.username');
|
|
232
|
+
console.log(currentUsername);
|
|
233
|
+
return currentUsername;
|
|
234
|
+
`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(runResult?.success).toBe(true);
|
|
238
|
+
expect(runResult?.value).toBe('alice');
|
|
239
|
+
expect(result.current.logs.some((l) => l.level === 'log' && l.msg.includes('alice'))).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('runs direct event-flow previews against the popup-bound model context when the settings view has no popup', async () => {
|
|
243
|
+
const engine = new FlowEngine();
|
|
244
|
+
engine.registerModels({ DummyJsAutoModel });
|
|
245
|
+
const model = engine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'direct-popup-uid' });
|
|
246
|
+
model.context.defineProperty('popup', {
|
|
247
|
+
value: {
|
|
248
|
+
uid: 'popup-view',
|
|
249
|
+
record: { username: 'alice' },
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const scopedEngine = createViewScopedEngine(engine);
|
|
254
|
+
scopedEngine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'direct-popup-uid' });
|
|
255
|
+
|
|
256
|
+
const settingsCtx = new FlowContext();
|
|
257
|
+
settingsCtx.defineProperty('engine', { value: scopedEngine });
|
|
258
|
+
settingsCtx.defineProperty('popup', { value: undefined });
|
|
259
|
+
settingsCtx.addDelegate(model.context);
|
|
260
|
+
|
|
261
|
+
const { result } = renderHook(() => useCodeRunner(settingsCtx as any, 'v1'));
|
|
262
|
+
let runResult: any;
|
|
263
|
+
await act(async () => {
|
|
264
|
+
runResult = await result.current.run(`
|
|
265
|
+
const currentUsername = await ctx.getVar('ctx.popup.record.username');
|
|
266
|
+
console.log(currentUsername);
|
|
267
|
+
return currentUsername;
|
|
268
|
+
`);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(runResult?.success).toBe(true);
|
|
272
|
+
expect(runResult?.value).toBe('alice');
|
|
273
|
+
expect(result.current.logs.some((l) => l.level === 'log' && l.msg.includes('alice'))).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
195
276
|
it('compiles JSX in preview and renders antd Input without syntax error', async () => {
|
|
196
277
|
const engine = new FlowEngine();
|
|
197
278
|
engine.registerModels({ DummyJsAutoModel });
|
|
@@ -112,6 +112,31 @@ function createLoggerWrapperFactory(append: (level: RunLog['level'], args: any[]
|
|
|
112
112
|
return wrap;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function hasPopupViewMarkers(view: any): boolean {
|
|
116
|
+
const inputArgs = view?.inputArgs || {};
|
|
117
|
+
const openerUids = inputArgs?.openerUids;
|
|
118
|
+
const viewStack = view?.navigation?.viewStack;
|
|
119
|
+
|
|
120
|
+
return (Array.isArray(openerUids) && openerUids.length > 0) || (Array.isArray(viewStack) && viewStack.length >= 2);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function hasPreviewPopupContext(ctx: any): Promise<boolean> {
|
|
124
|
+
if (!ctx) return false;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const popup = await ctx.popup;
|
|
128
|
+
if (popup) return true;
|
|
129
|
+
} catch (_) {
|
|
130
|
+
// ignore unavailable popup getters
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
return hasPopupViewMarkers(await ctx.view);
|
|
135
|
+
} catch (_) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
116
141
|
const [logs, setLogs] = useState<RunLog[]>([]);
|
|
117
142
|
const [running, setRunning] = useState(false);
|
|
@@ -131,7 +156,14 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
|
131
156
|
const model = hostCtx?.model;
|
|
132
157
|
if (!model) throw new Error('No model in FlowContext');
|
|
133
158
|
const engine = hostCtx.engine;
|
|
134
|
-
const
|
|
159
|
+
const globalRuntimeModel = engine.getModel(model.uid, true) || model;
|
|
160
|
+
const [hostHasPopupContext, modelHasPopupContext] = await Promise.all([
|
|
161
|
+
hasPreviewPopupContext(hostCtx),
|
|
162
|
+
hasPreviewPopupContext(model.context),
|
|
163
|
+
]);
|
|
164
|
+
const shouldPreservePopupModel = hostHasPopupContext || modelHasPopupContext;
|
|
165
|
+
const runtimeModel = shouldPreservePopupModel ? model : globalRuntimeModel;
|
|
166
|
+
const directRunCtx = hostHasPopupContext ? hostCtx : modelHasPopupContext ? model.context : hostCtx;
|
|
135
167
|
|
|
136
168
|
const nativeConsole: Record<RunLog['level'], (...args: any[]) => void> = {
|
|
137
169
|
log: (...args) => console.log(...args),
|
|
@@ -255,7 +287,7 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
|
255
287
|
if (!flow) {
|
|
256
288
|
// 无可用流程(典型场景:联动规则里的 RunJS 预览),直接在当前上下文执行代码
|
|
257
289
|
const navigator = createSafeNavigator();
|
|
258
|
-
await
|
|
290
|
+
await directRunCtx.runjs(
|
|
259
291
|
code,
|
|
260
292
|
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
261
293
|
{ version },
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { SettingOutlined } from '@ant-design/icons';
|
|
11
11
|
import { FormButtonGroup } from '@formily/antd-v5';
|
|
12
|
+
import type { CollectionField, PropertyMeta, PropertyMetaFactory } from '@nocobase/flow-engine';
|
|
12
13
|
import {
|
|
13
14
|
AddSubModelButton,
|
|
14
15
|
DndProvider,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
FlowSettingsButton,
|
|
23
24
|
} from '@nocobase/flow-engine';
|
|
24
25
|
import { Form } from 'antd';
|
|
26
|
+
import { isEqual } from 'lodash';
|
|
25
27
|
import React from 'react';
|
|
26
28
|
import { commonConditionHandler, ConditionBuilder } from '../../../components/ConditionBuilder';
|
|
27
29
|
import {
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
import { BlockSceneEnum } from '../../base/BlockModel';
|
|
32
34
|
import { FilterBlockModel } from '../../base/FilterBlockModel';
|
|
33
35
|
import { FormComponent } from '../form/FormBlockModel';
|
|
36
|
+
import { evaluateCondition } from '../form/value-runtime/conditions';
|
|
34
37
|
import { isEmptyValue } from '../form/value-runtime/utils';
|
|
35
38
|
import { FilterManager, type RefreshTargetsByFilterOptions } from '../filter-manager/FilterManager';
|
|
36
39
|
import { FilterFormItemModel } from './FilterFormItemModel';
|
|
@@ -40,6 +43,177 @@ import { FormItemModel } from '../form/FormItemModel';
|
|
|
40
43
|
import { getDefaultOperator } from '../filter-manager/utils';
|
|
41
44
|
import { normalizeFilterValueByOperator } from './valueNormalization';
|
|
42
45
|
|
|
46
|
+
const RELATION_FIELD_TYPES = ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany', 'belongsToArray'];
|
|
47
|
+
const NUMERIC_FIELD_TYPES = ['integer', 'float', 'double', 'decimal'];
|
|
48
|
+
|
|
49
|
+
function getFilterFormFieldMetaType(field: CollectionField) {
|
|
50
|
+
if (RELATION_FIELD_TYPES.includes(field.type)) {
|
|
51
|
+
return 'object';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (NUMERIC_FIELD_TYPES.includes(field.type)) {
|
|
55
|
+
return 'number';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
switch (field.type) {
|
|
59
|
+
case 'boolean':
|
|
60
|
+
return 'boolean';
|
|
61
|
+
case 'json':
|
|
62
|
+
return 'object';
|
|
63
|
+
case 'array':
|
|
64
|
+
return 'array';
|
|
65
|
+
default:
|
|
66
|
+
return 'string';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shouldShowFilterFormFieldMeta(field: CollectionField) {
|
|
71
|
+
return Boolean(field?.interface);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createFilterFormFieldMeta(field: CollectionField): PropertyMeta {
|
|
75
|
+
const baseMeta = {
|
|
76
|
+
title: field.title || field.name,
|
|
77
|
+
interface: field.interface,
|
|
78
|
+
options: field.options,
|
|
79
|
+
uiSchema: field.uiSchema || {},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (!field.isAssociationField?.()) {
|
|
83
|
+
return {
|
|
84
|
+
type: getFilterFormFieldMetaType(field),
|
|
85
|
+
...baseMeta,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const targetCollection = field.targetCollection;
|
|
90
|
+
if (!targetCollection) {
|
|
91
|
+
return {
|
|
92
|
+
type: 'object',
|
|
93
|
+
...baseMeta,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
type: 'object',
|
|
99
|
+
...baseMeta,
|
|
100
|
+
properties: async () => {
|
|
101
|
+
const properties: Record<string, PropertyMeta> = {};
|
|
102
|
+
targetCollection.fields.forEach((subField) => {
|
|
103
|
+
if (shouldShowFilterFormFieldMeta(subField)) {
|
|
104
|
+
properties[subField.name] = createFilterFormFieldMeta(subField);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return properties;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getFilterFormItemFieldName(itemModel: any) {
|
|
113
|
+
const name = itemModel?.props?.name;
|
|
114
|
+
if (typeof name === 'string' && name) {
|
|
115
|
+
return name;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return itemModel?.fieldPath && itemModel?.uid ? `${itemModel.fieldPath}_${itemModel.uid}` : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toFilterByTk(value: any, primaryKey: string | string[] | undefined) {
|
|
122
|
+
if (value == null) return undefined;
|
|
123
|
+
if (Array.isArray(primaryKey)) {
|
|
124
|
+
if (typeof value !== 'object') return undefined;
|
|
125
|
+
const filterByTk: Record<string, any> = {};
|
|
126
|
+
for (const key of primaryKey) {
|
|
127
|
+
const item = value?.[key];
|
|
128
|
+
if (item == null) return undefined;
|
|
129
|
+
filterByTk[key] = item;
|
|
130
|
+
}
|
|
131
|
+
return filterByTk;
|
|
132
|
+
}
|
|
133
|
+
if (typeof value !== 'object') return value;
|
|
134
|
+
const key = Array.isArray(primaryKey) ? primaryKey[0] : primaryKey;
|
|
135
|
+
return key ? value?.[key] : value?.id;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function setValueByPath(target: Record<string, any>, path: string, value: any) {
|
|
139
|
+
const segments = path.split('.').filter(Boolean);
|
|
140
|
+
if (!segments.length) return;
|
|
141
|
+
|
|
142
|
+
let cursor = target;
|
|
143
|
+
segments.forEach((segment, index) => {
|
|
144
|
+
if (index === segments.length - 1) {
|
|
145
|
+
cursor[segment] = value;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!cursor[segment] || typeof cursor[segment] !== 'object' || Array.isArray(cursor[segment])) {
|
|
150
|
+
cursor[segment] = {};
|
|
151
|
+
}
|
|
152
|
+
cursor = cursor[segment];
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function setMetaByPath(target: Record<string, PropertyMeta>, path: string, meta: PropertyMeta) {
|
|
157
|
+
const segments = path.split('.').filter(Boolean);
|
|
158
|
+
if (!segments.length) return;
|
|
159
|
+
|
|
160
|
+
let cursor = target;
|
|
161
|
+
segments.forEach((segment, index) => {
|
|
162
|
+
if (index === segments.length - 1) {
|
|
163
|
+
cursor[segment] = meta;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const current = cursor[segment];
|
|
168
|
+
if (!current || typeof current !== 'object') {
|
|
169
|
+
cursor[segment] = {
|
|
170
|
+
type: 'object',
|
|
171
|
+
title: segment,
|
|
172
|
+
properties: {},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const properties = cursor[segment].properties;
|
|
176
|
+
if (!properties || typeof properties === 'function') {
|
|
177
|
+
cursor[segment].properties = {};
|
|
178
|
+
}
|
|
179
|
+
cursor = cursor[segment].properties as Record<string, PropertyMeta>;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getFilterFormValues(form: any, items: any[]) {
|
|
184
|
+
const formValues = form?.getFieldsValue?.() || {};
|
|
185
|
+
const values = { ...formValues };
|
|
186
|
+
|
|
187
|
+
for (const itemModel of items) {
|
|
188
|
+
const fieldName = getFilterFormItemFieldName(itemModel);
|
|
189
|
+
if (!fieldName || !fieldName.includes('.') || !(fieldName in formValues)) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
setValueByPath(values, fieldName, formValues[fieldName]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return values;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isFilterFormFieldSubPath(fieldName: string, subPath: string) {
|
|
199
|
+
return subPath === fieldName || subPath.startsWith(`${fieldName}.`) || subPath.startsWith(`${fieldName}[`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isFilterFormFieldDeepSubPath(fieldName: string, subPath: string) {
|
|
203
|
+
return subPath.startsWith(`${fieldName}.`) || subPath.startsWith(`${fieldName}[`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function findFilterFormItemByVariableSubPath(items: any[], subPath: string) {
|
|
207
|
+
if (!subPath) return null;
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
items.find((itemModel) => {
|
|
211
|
+
const fieldName = getFilterFormItemFieldName(itemModel);
|
|
212
|
+
return fieldName && isFilterFormFieldSubPath(fieldName, subPath);
|
|
213
|
+
}) || null
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
43
217
|
export class FilterFormBlockModel extends FilterBlockModel<{
|
|
44
218
|
subModels: {
|
|
45
219
|
grid: any; // Replace with actual type if available
|
|
@@ -56,6 +230,8 @@ export class FilterFormBlockModel extends FilterBlockModel<{
|
|
|
56
230
|
private removeTargetBlockListener?: () => void;
|
|
57
231
|
private initialDefaultsPromise?: Promise<void>;
|
|
58
232
|
private initialRefreshHandledTargetIds = new Set<string>();
|
|
233
|
+
private lastDefaultValueByFieldName = new Map<string, any>();
|
|
234
|
+
private defaultValuesRefreshSeq = 0;
|
|
59
235
|
|
|
60
236
|
get form() {
|
|
61
237
|
return this.context.form;
|
|
@@ -65,10 +241,98 @@ export class FilterFormBlockModel extends FilterBlockModel<{
|
|
|
65
241
|
return 'Filter form';
|
|
66
242
|
}
|
|
67
243
|
|
|
244
|
+
protected createFormValuesMetaFactory(): PropertyMetaFactory {
|
|
245
|
+
const factory: PropertyMetaFactory = async () => ({
|
|
246
|
+
type: 'object',
|
|
247
|
+
title: this.translate('Current form'),
|
|
248
|
+
properties: async () => {
|
|
249
|
+
const properties: Record<string, PropertyMeta> = {};
|
|
250
|
+
const items = this.subModels?.grid?.subModels?.items || [];
|
|
251
|
+
|
|
252
|
+
for (const itemModel of items) {
|
|
253
|
+
const fieldName = getFilterFormItemFieldName(itemModel);
|
|
254
|
+
const collectionField = itemModel?.subModels?.field?.context?.collectionField || itemModel?.collectionField;
|
|
255
|
+
if (!fieldName || !collectionField || !shouldShowFilterFormFieldMeta(collectionField)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
setMetaByPath(properties, fieldName, createFilterFormFieldMeta(collectionField));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return properties;
|
|
262
|
+
},
|
|
263
|
+
buildVariablesParams: () => {
|
|
264
|
+
const formValues = this.form?.getFieldsValue?.() || {};
|
|
265
|
+
const items = this.subModels?.grid?.subModels?.items || [];
|
|
266
|
+
const params: Record<string, any> = {};
|
|
267
|
+
|
|
268
|
+
for (const itemModel of items) {
|
|
269
|
+
const fieldName = getFilterFormItemFieldName(itemModel);
|
|
270
|
+
const collectionField = itemModel?.subModels?.field?.context?.collectionField || itemModel?.collectionField;
|
|
271
|
+
if (!fieldName || !collectionField?.isAssociationField?.()) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const targetCollection = collectionField.targetCollection;
|
|
276
|
+
const target = collectionField.target;
|
|
277
|
+
if (!targetCollection || !target) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const fieldValue = formValues[fieldName];
|
|
282
|
+
const primaryKey = targetCollection.filterTargetKey;
|
|
283
|
+
if (Array.isArray(fieldValue)) {
|
|
284
|
+
const filterByTk = fieldValue.map((item) => toFilterByTk(item, primaryKey)).filter((item) => item != null);
|
|
285
|
+
if (filterByTk.length) {
|
|
286
|
+
setValueByPath(params, fieldName, {
|
|
287
|
+
collection: target,
|
|
288
|
+
dataSourceKey: targetCollection.dataSourceKey,
|
|
289
|
+
filterByTk,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const filterByTk = toFilterByTk(fieldValue, primaryKey);
|
|
296
|
+
if (filterByTk != null) {
|
|
297
|
+
setValueByPath(params, fieldName, {
|
|
298
|
+
collection: target,
|
|
299
|
+
dataSourceKey: targetCollection.dataSourceKey,
|
|
300
|
+
filterByTk,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return params;
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
factory.title = this.translate('Current form');
|
|
309
|
+
return factory;
|
|
310
|
+
}
|
|
311
|
+
|
|
68
312
|
useHooksBeforeRender() {
|
|
69
313
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
70
314
|
const [form] = Form.useForm();
|
|
71
315
|
this.context.defineProperty('form', { get: () => form, cache: false });
|
|
316
|
+
this.context.defineProperty('formValues', {
|
|
317
|
+
get: () => getFilterFormValues(this.form, this.subModels?.grid?.subModels?.items || []),
|
|
318
|
+
cache: false,
|
|
319
|
+
meta: this.createFormValuesMetaFactory(),
|
|
320
|
+
resolveOnServer: (subPath: string) => {
|
|
321
|
+
const items = this.subModels?.grid?.subModels?.items || [];
|
|
322
|
+
const itemModel = findFilterFormItemByVariableSubPath(items, subPath);
|
|
323
|
+
if (!itemModel) return false;
|
|
324
|
+
|
|
325
|
+
const fieldName = getFilterFormItemFieldName(itemModel);
|
|
326
|
+
const collectionField = itemModel?.subModels?.field?.context?.collectionField || itemModel?.collectionField;
|
|
327
|
+
return Boolean(
|
|
328
|
+
fieldName &&
|
|
329
|
+
isFilterFormFieldDeepSubPath(fieldName, subPath) &&
|
|
330
|
+
collectionField?.isAssociationField?.() &&
|
|
331
|
+
collectionField?.targetCollection,
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
serverOnlyWhenContextParams: true,
|
|
335
|
+
});
|
|
72
336
|
}
|
|
73
337
|
|
|
74
338
|
async saveStepParams() {
|
|
@@ -165,14 +429,38 @@ export class FilterFormBlockModel extends FilterBlockModel<{
|
|
|
165
429
|
}
|
|
166
430
|
}
|
|
167
431
|
|
|
168
|
-
|
|
432
|
+
private canApplyFormDefaultValue(name: string, current: any, force?: boolean) {
|
|
433
|
+
if (force) return true;
|
|
434
|
+
if (isEmptyValue(current)) return true;
|
|
435
|
+
if (!this.lastDefaultValueByFieldName.has(name)) return false;
|
|
436
|
+
return isEqual(current, this.lastDefaultValueByFieldName.get(name));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private async matchDefaultValueCondition(condition: any) {
|
|
440
|
+
if (!condition) return true;
|
|
441
|
+
|
|
442
|
+
let resolvedCondition = condition;
|
|
443
|
+
try {
|
|
444
|
+
const nextCondition = await (this.context as any).resolveJsonTemplate?.(condition);
|
|
445
|
+
if (typeof nextCondition !== 'undefined') {
|
|
446
|
+
resolvedCondition = nextCondition;
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
resolvedCondition = condition;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return evaluateCondition(this.context, resolvedCondition);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async applyFormDefaultValues(options?: { force?: boolean; refreshSeq?: number }) {
|
|
456
|
+
const appliedValues: Record<string, any> = {};
|
|
169
457
|
const form = this.form;
|
|
170
|
-
if (!form) return;
|
|
458
|
+
if (!form) return appliedValues;
|
|
171
459
|
|
|
172
460
|
const force = options?.force === true;
|
|
173
461
|
const params = this.getStepParams?.('formFilterBlockModelSettings', 'defaultValues');
|
|
174
462
|
const rules = (params?.value || []) as any[];
|
|
175
|
-
if (!Array.isArray(rules) || rules.length === 0) return;
|
|
463
|
+
if (!Array.isArray(rules) || rules.length === 0) return appliedValues;
|
|
176
464
|
|
|
177
465
|
const resolveValue = async (raw: any) => {
|
|
178
466
|
// RunJS support
|
|
@@ -188,7 +476,8 @@ export class FilterFormBlockModel extends FilterBlockModel<{
|
|
|
188
476
|
for (const rule of rules) {
|
|
189
477
|
if (!rule || typeof rule !== 'object') continue;
|
|
190
478
|
if (rule.enable === false) continue;
|
|
191
|
-
if (
|
|
479
|
+
if (!(await this.matchDefaultValueCondition(rule.condition))) continue;
|
|
480
|
+
if (options?.refreshSeq && options.refreshSeq !== this.defaultValuesRefreshSeq) return appliedValues;
|
|
192
481
|
|
|
193
482
|
const targetPath = rule.targetPath ? String(rule.targetPath).trim() : '';
|
|
194
483
|
const fieldUid = rule.field ? String(rule.field).trim() : '';
|
|
@@ -203,20 +492,54 @@ export class FilterFormBlockModel extends FilterBlockModel<{
|
|
|
203
492
|
if (!name) continue;
|
|
204
493
|
|
|
205
494
|
const current = (form as any).getFieldValue?.(name);
|
|
206
|
-
if (!force && !isEmptyValue(current)) continue;
|
|
207
495
|
|
|
208
496
|
const resolved = await resolveValue(rule.value);
|
|
497
|
+
if (options?.refreshSeq && options.refreshSeq !== this.defaultValuesRefreshSeq) return appliedValues;
|
|
209
498
|
if (typeof resolved === 'undefined') continue;
|
|
210
499
|
|
|
211
500
|
const operator = getDefaultOperator(itemModel as any);
|
|
212
501
|
const normalized = normalizeFilterValueByOperator(operator, resolved);
|
|
502
|
+
const mode = String(rule.mode || 'default') === 'assign' ? 'assign' : 'default';
|
|
503
|
+
if (mode === 'default' && !this.canApplyFormDefaultValue(String(name), current, force)) continue;
|
|
504
|
+
if (isEqual(current, normalized)) {
|
|
505
|
+
if (mode === 'default') {
|
|
506
|
+
this.lastDefaultValueByFieldName.set(String(name), normalized);
|
|
507
|
+
} else {
|
|
508
|
+
this.lastDefaultValueByFieldName.delete(String(name));
|
|
509
|
+
}
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
213
512
|
|
|
214
513
|
if (typeof (form as any).setFieldValue === 'function') {
|
|
215
514
|
(form as any).setFieldValue(name, normalized);
|
|
216
515
|
} else {
|
|
217
516
|
(form as any).setFieldsValue?.({ [String(name)]: normalized });
|
|
218
517
|
}
|
|
518
|
+
if (mode === 'default') {
|
|
519
|
+
this.lastDefaultValueByFieldName.set(String(name), normalized);
|
|
520
|
+
} else {
|
|
521
|
+
this.lastDefaultValueByFieldName.delete(String(name));
|
|
522
|
+
}
|
|
523
|
+
appliedValues[String(name)] = normalized;
|
|
219
524
|
}
|
|
525
|
+
|
|
526
|
+
return appliedValues;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private handleFilterFormValuesChange(changedValues: any, allValues: any) {
|
|
530
|
+
const refreshSeq = ++this.defaultValuesRefreshSeq;
|
|
531
|
+
void (async () => {
|
|
532
|
+
const appliedValues = await this.applyFormDefaultValues({ refreshSeq });
|
|
533
|
+
if (refreshSeq !== this.defaultValuesRefreshSeq) return;
|
|
534
|
+
|
|
535
|
+
const finalChangedValues = { ...(changedValues || {}), ...(appliedValues || {}) };
|
|
536
|
+
const finalAllValues = this.form?.getFieldsValue?.() || allValues;
|
|
537
|
+
const payload = { changedValues: finalChangedValues, allValues: finalAllValues };
|
|
538
|
+
this.dispatchEvent('formValuesChange', payload, { debounce: true });
|
|
539
|
+
this.emitter.emit('formValuesChange', payload);
|
|
540
|
+
})().catch((error) => {
|
|
541
|
+
console.error('Failed to refresh filter form default values:', error);
|
|
542
|
+
});
|
|
220
543
|
}
|
|
221
544
|
|
|
222
545
|
private async handleTargetBlockRemoved(targetUid: string) {
|
|
@@ -264,6 +587,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
|
|
|
264
587
|
onFinish={() => {
|
|
265
588
|
this.context.refreshTargets();
|
|
266
589
|
}}
|
|
590
|
+
onValuesChange={(changedValues, allValues) => this.handleFilterFormValuesChange(changedValues, allValues)}
|
|
267
591
|
layoutProps={{ colon, labelAlign, labelWidth, labelWrap, layout }}
|
|
268
592
|
>
|
|
269
593
|
<FlowModelRenderer model={this.subModels.grid} showFlowSettings={false} />
|