@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.
Files changed (76) hide show
  1. package/es/APIClient.d.ts +16 -0
  2. package/es/Application.d.ts +2 -1
  3. package/es/BaseApplication.d.ts +6 -0
  4. package/es/PluginManager.d.ts +2 -0
  5. package/es/authRedirect.d.ts +9 -16
  6. package/es/components/form/EnvVariableInput.d.ts +8 -6
  7. package/es/components/form/VariableInput.d.ts +73 -0
  8. package/es/components/form/index.d.ts +1 -0
  9. package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
  10. package/es/components/form/table/SelectionCell.d.ts +36 -0
  11. package/es/components/form/table/Table.d.ts +82 -0
  12. package/es/components/form/table/constants.d.ts +15 -0
  13. package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
  14. package/es/components/form/table/dnd/index.d.ts +9 -0
  15. package/es/components/form/table/index.d.ts +9 -0
  16. package/es/components/form/table/styles.d.ts +41 -0
  17. package/es/components/form/table/utils.d.ts +44 -0
  18. package/es/components/index.d.ts +2 -0
  19. package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
  21. package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
  22. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
  23. package/es/flow-compat/passwordUtils.d.ts +1 -1
  24. package/es/index.d.ts +1 -0
  25. package/es/index.mjs +166 -99
  26. package/es/json-logic/globalOperators.d.ts +11 -0
  27. package/es/theme/globalStyles.d.ts +9 -0
  28. package/es/theme/index.d.ts +1 -0
  29. package/es/utils/globalDeps.d.ts +7 -0
  30. package/lib/index.js +173 -106
  31. package/package.json +9 -6
  32. package/src/APIClient.ts +68 -0
  33. package/src/Application.tsx +6 -2
  34. package/src/BaseApplication.tsx +8 -0
  35. package/src/PluginManager.ts +2 -0
  36. package/src/__tests__/app.test.tsx +8 -0
  37. package/src/__tests__/authRedirect.test.ts +170 -64
  38. package/src/__tests__/globalDeps.test.ts +2 -0
  39. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
  40. package/src/__tests__/remotePlugins.test.ts +148 -0
  41. package/src/authRedirect.ts +23 -84
  42. package/src/components/form/EnvVariableInput.tsx +11 -46
  43. package/src/components/form/VariableInput.tsx +177 -0
  44. package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
  45. package/src/components/form/index.tsx +1 -0
  46. package/src/components/form/table/RowOverlayPreview.tsx +51 -0
  47. package/src/components/form/table/SelectionCell.tsx +72 -0
  48. package/src/components/form/table/Table.tsx +279 -0
  49. package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
  50. package/src/components/form/table/constants.ts +16 -0
  51. package/src/components/form/table/dnd/SortableRow.tsx +106 -0
  52. package/src/components/form/table/dnd/index.ts +10 -0
  53. package/src/components/form/table/index.tsx +13 -0
  54. package/src/components/form/table/styles.ts +110 -0
  55. package/src/components/form/table/utils.ts +75 -0
  56. package/src/components/index.ts +2 -0
  57. package/src/css-variable/CSSVariableProvider.tsx +1 -1
  58. package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
  59. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
  61. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
  62. package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
  63. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +81 -0
  64. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +34 -2
  65. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
  66. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
  67. package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
  68. package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
  69. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
  70. package/src/index.ts +1 -0
  71. package/src/json-logic/globalOperators.js +731 -0
  72. package/src/nocobase-buildin-plugin/index.tsx +4 -4
  73. package/src/theme/globalStyles.ts +21 -0
  74. package/src/theme/index.tsx +1 -0
  75. package/src/utils/globalDeps.ts +50 -30
  76. 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 runtimeModel = engine.getModel(model.uid, true) || model;
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 hostCtx.runjs(
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
- async applyFormDefaultValues(options?: { force?: boolean }) {
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 (rule.mode && String(rule.mode) !== 'default') continue;
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} />