@nocobase/client-v2 2.1.0-beta.24 → 2.1.0-beta.25

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 (27) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/flow/actions/dataScopeFilter.d.ts +9 -0
  3. package/es/flow/internal/utils/rebuildFieldSubModel.d.ts +2 -1
  4. package/es/flow/models/fields/JSFieldModel.d.ts +5 -0
  5. package/es/index.mjs +100 -100
  6. package/lib/index.js +89 -89
  7. package/package.json +6 -5
  8. package/src/BaseApplication.tsx +4 -0
  9. package/src/__tests__/globalDeps.test.ts +1 -0
  10. package/src/__tests__/remotePlugins.test.ts +27 -0
  11. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +158 -0
  12. package/src/flow/actions/dataScope.tsx +6 -4
  13. package/src/flow/actions/dataScopeFilter.ts +70 -0
  14. package/src/flow/actions/setTargetDataScope.tsx +6 -5
  15. package/src/flow/internal/utils/__tests__/rebuildFieldSubModel.test.ts +77 -2
  16. package/src/flow/internal/utils/rebuildFieldSubModel.ts +21 -5
  17. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +9 -5
  18. package/src/flow/models/blocks/filter-form/__tests__/FilterFormBlockModel.cleanup.test.ts +138 -0
  19. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +22 -0
  20. package/src/flow/models/blocks/table/JSColumnModel.tsx +30 -2
  21. package/src/flow/models/blocks/table/TableBlockModel.tsx +8 -1
  22. package/src/flow/models/blocks/table/TableColumnModel.tsx +1 -0
  23. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +51 -0
  24. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +49 -0
  25. package/src/flow/models/fields/JSFieldModel.tsx +54 -14
  26. package/src/utils/globalDeps.ts +4 -0
  27. package/src/utils/requirejs.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.0-beta.24",
3
+ "version": "2.1.0-beta.25",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -24,9 +24,10 @@
24
24
  "@formily/antd-v5": "1.2.3",
25
25
  "@formily/react": "^2.2.27",
26
26
  "@formily/shared": "^2.2.27",
27
- "@nocobase/flow-engine": "2.1.0-beta.24",
28
- "@nocobase/sdk": "2.1.0-beta.24",
29
- "@nocobase/shared": "2.1.0-beta.24",
27
+ "@nocobase/flow-engine": "2.1.0-beta.25",
28
+ "@nocobase/sdk": "2.1.0-beta.25",
29
+ "@nocobase/shared": "2.1.0-beta.25",
30
+ "ahooks": "^3.7.2",
30
31
  "antd": "5.24.2",
31
32
  "classnames": "^2.3.1",
32
33
  "dayjs": "^1.11.10",
@@ -35,5 +36,5 @@
35
36
  "react-i18next": "^11.15.1",
36
37
  "react-router-dom": "^6.30.1"
37
38
  },
38
- "gitHead": "f77b85530a2d127d9bfe4dca3a26fbb02c1139ba"
39
+ "gitHead": "824f8b8200e9fe086135768934d3ef427b212446"
39
40
  }
@@ -189,6 +189,7 @@ export abstract class BaseApplication<
189
189
  this.configureContext();
190
190
  this.addBaseProviders();
191
191
  this.addCustomProviders();
192
+ this.addFinalProviders();
192
193
  this.addReactRouterComponents();
193
194
  this.addProviders(options.providers || []);
194
195
  this.ws = this.createWebSocketClient(options);
@@ -338,6 +339,9 @@ export abstract class BaseApplication<
338
339
  this.use(FlowEngineProvider, { engine: this.flowEngine });
339
340
  this.use(GlobalThemeProvider);
340
341
  this.use(AntdAppProvider);
342
+ }
343
+
344
+ protected addFinalProviders() {
341
345
  this.use(FlowEngineGlobalsContextProvider);
342
346
  }
343
347
 
@@ -23,5 +23,6 @@ describe('client-v2 defineGlobalDeps', () => {
23
23
  expect(define).toHaveBeenCalledWith('@nocobase/utils/client', expect.any(Function));
24
24
  expect(define).toHaveBeenCalledWith('@nocobase/client-v2', expect.any(Function));
25
25
  expect(define).toHaveBeenCalledWith('@nocobase/flow-engine', expect.any(Function));
26
+ expect(define).toHaveBeenCalledWith('ahooks', expect.any(Function));
26
27
  });
27
28
  });
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { Plugin } from '../Plugin';
11
+ import { getRequireJs } from '../utils/requirejs';
11
12
  import { defineDevPlugins, definePluginClient, getPlugins } from '../utils/remotePlugins';
12
13
 
13
14
  describe('client-v2 remotePlugins', () => {
@@ -69,4 +70,30 @@ describe('client-v2 remotePlugins', () => {
69
70
  expect(mockDefine).toHaveBeenCalledWith('@nocobase/demo/client-v2', expect.any(Function));
70
71
  expect(mockDefine).not.toHaveBeenCalledWith('@nocobase/demo/client', expect.any(Function));
71
72
  });
73
+
74
+ it('should not append duplicate .js for plugin URLs without query strings', () => {
75
+ const requirejs = getRequireJs();
76
+
77
+ requirejs.requirejs.config({
78
+ paths: {
79
+ '@nocobase/demo': '/static/plugins/@nocobase/demo/dist/client-v2/index.js',
80
+ },
81
+ });
82
+
83
+ expect(requirejs.requirejs.toUrl('@nocobase/demo')).toBe('/static/plugins/@nocobase/demo/dist/client-v2/index.js');
84
+ });
85
+
86
+ it('should keep hashed plugin URLs unchanged', () => {
87
+ const requirejs = getRequireJs();
88
+
89
+ requirejs.requirejs.config({
90
+ paths: {
91
+ '@nocobase/demo': '/static/plugins/@nocobase/demo/dist/client-v2/index.js?hash=12345678',
92
+ },
93
+ });
94
+
95
+ expect(requirejs.requirejs.toUrl('@nocobase/demo')).toBe(
96
+ '/static/plugins/@nocobase/demo/dist/client-v2/index.js?hash=12345678',
97
+ );
98
+ });
72
99
  });
@@ -0,0 +1,158 @@
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 { describe, expect, it, vi } from 'vitest';
11
+ import { dataScope } from '../dataScope';
12
+ import { normalizeDataScopeFilter } from '../dataScopeFilter';
13
+ import { setTargetDataScope } from '../setTargetDataScope';
14
+
15
+ describe('normalizeDataScopeFilter', () => {
16
+ it('keeps null when a right-side variable resolves to empty', () => {
17
+ const rawFilter = {
18
+ logic: '$and',
19
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.formValues.department.id }}' }],
20
+ };
21
+ const resolvedFilter = {
22
+ logic: '$and',
23
+ items: [{ path: 'departmentId', operator: '$eq', value: undefined }],
24
+ };
25
+
26
+ expect(normalizeDataScopeFilter(rawFilter, resolvedFilter)).toEqual({
27
+ $and: [{ departmentId: { $eq: null } }],
28
+ });
29
+ });
30
+
31
+ it('keeps null when a right-side variable resolves to an empty string', () => {
32
+ const rawFilter = {
33
+ logic: '$and',
34
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.formValues.department.id }}' }],
35
+ };
36
+ const resolvedFilter = {
37
+ logic: '$and',
38
+ items: [{ path: 'departmentId', operator: '$eq', value: '' }],
39
+ };
40
+
41
+ expect(normalizeDataScopeFilter(rawFilter, resolvedFilter)).toEqual({
42
+ $and: [{ departmentId: { $eq: null } }],
43
+ });
44
+ });
45
+
46
+ it('still prunes empty constant values', () => {
47
+ const filter = {
48
+ logic: '$and',
49
+ items: [{ path: 'departmentId', operator: '$eq', value: undefined }],
50
+ };
51
+
52
+ expect(normalizeDataScopeFilter(filter, filter)).toBeUndefined();
53
+ });
54
+
55
+ it('handles nested groups and keeps non-empty values unchanged', () => {
56
+ const rawFilter = {
57
+ logic: '$and',
58
+ items: [
59
+ { path: 'status', operator: '$eq', value: 'active' },
60
+ {
61
+ logic: '$or',
62
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.formValues.department.id }}' }],
63
+ },
64
+ ],
65
+ };
66
+ const resolvedFilter = {
67
+ logic: '$and',
68
+ items: [
69
+ { path: 'status', operator: '$eq', value: 'active' },
70
+ {
71
+ logic: '$or',
72
+ items: [{ path: 'departmentId', operator: '$eq', value: null }],
73
+ },
74
+ ],
75
+ };
76
+
77
+ expect(normalizeDataScopeFilter(rawFilter, resolvedFilter)).toEqual({
78
+ $and: [{ status: { $eq: 'active' } }, { $or: [{ departmentId: { $eq: null } }] }],
79
+ });
80
+ });
81
+
82
+ it('does not preserve explicit null constants', () => {
83
+ const filter = {
84
+ logic: '$and',
85
+ items: [{ path: 'departmentId', operator: '$eq', value: null }],
86
+ };
87
+
88
+ expect(normalizeDataScopeFilter(filter, filter)).toBeUndefined();
89
+ });
90
+
91
+ it('dataScope handler sends null for empty variable dependencies', async () => {
92
+ const resource = {
93
+ addFilterGroup: vi.fn(),
94
+ removeFilterGroup: vi.fn(),
95
+ };
96
+ const ctx = {
97
+ model: {
98
+ uid: 'field-1',
99
+ resource,
100
+ },
101
+ resolveJsonTemplate: vi.fn(async (template) => ({
102
+ ...template,
103
+ items: [{ ...template.items[0], value: undefined }],
104
+ })),
105
+ };
106
+ const params = {
107
+ filter: {
108
+ logic: '$and',
109
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.formValues.department.id }}' }],
110
+ },
111
+ };
112
+
113
+ await (dataScope as any).handler(ctx, params);
114
+
115
+ expect(resource.addFilterGroup).toHaveBeenCalledWith('field-1', {
116
+ $and: [{ departmentId: { $eq: null } }],
117
+ });
118
+ expect(resource.removeFilterGroup).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it('setTargetDataScope handler sends null for empty variable dependencies', async () => {
122
+ const resource = {
123
+ addFilterGroup: vi.fn(),
124
+ removeFilterGroup: vi.fn(),
125
+ hasData: vi.fn(() => false),
126
+ refresh: vi.fn(),
127
+ };
128
+ const targetModel = { resource };
129
+ const ctx = {
130
+ model: {
131
+ uid: 'action-1',
132
+ scheduleModelOperation: vi.fn((_uid, callback) => callback(targetModel)),
133
+ },
134
+ resolveJsonTemplate: vi.fn(async (template) => ({
135
+ ...template,
136
+ filter: {
137
+ ...template.filter,
138
+ items: [{ ...template.filter.items[0], value: undefined }],
139
+ },
140
+ })),
141
+ };
142
+ const params = {
143
+ targetBlockUid: 'target-1',
144
+ filter: {
145
+ logic: '$and',
146
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.formValues.department.id }}' }],
147
+ },
148
+ };
149
+
150
+ await (setTargetDataScope as any).handler(ctx, params);
151
+
152
+ expect(ctx.model.scheduleModelOperation).toHaveBeenCalledWith('target-1', expect.any(Function));
153
+ expect(resource.addFilterGroup).toHaveBeenCalledWith('setTargetDataScope_action-1', {
154
+ $and: [{ departmentId: { $eq: null } }],
155
+ });
156
+ expect(resource.removeFilterGroup).not.toHaveBeenCalled();
157
+ });
158
+ });
@@ -7,12 +7,12 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { defineAction, MultiRecordResource, pruneFilter, tExpr, useFlowSettingsContext } from '@nocobase/flow-engine';
11
- import { isEmptyFilter, transformFilter } from '@nocobase/utils/client';
12
- import _ from 'lodash';
10
+ import { defineAction, MultiRecordResource, tExpr, useFlowSettingsContext } from '@nocobase/flow-engine';
11
+ import { isEmptyFilter } from '@nocobase/utils/client';
13
12
  import React from 'react';
14
13
  import { FilterGroup, VariableFilterItem } from '../components/filter';
15
14
  import { FieldModel } from '../models/base/FieldModel';
15
+ import { normalizeDataScopeFilter } from './dataScopeFilter';
16
16
 
17
17
  export const dataScope = defineAction({
18
18
  name: 'dataScope',
@@ -43,6 +43,7 @@ export const dataScope = defineAction({
43
43
  filter: { logic: '$and', items: [] },
44
44
  };
45
45
  },
46
+ useRawParams: true,
46
47
  async handler(ctx, params) {
47
48
  // @ts-ignore
48
49
  const resource = ctx.model?.resource as MultiRecordResource;
@@ -50,7 +51,8 @@ export const dataScope = defineAction({
50
51
  return;
51
52
  }
52
53
 
53
- const filter = pruneFilter(transformFilter(params.filter));
54
+ const resolvedFilter = await ctx.resolveJsonTemplate(params.filter);
55
+ const filter = normalizeDataScopeFilter(params.filter, resolvedFilter);
54
56
 
55
57
  if (isEmptyFilter(filter)) {
56
58
  resource.removeFilterGroup(ctx.model.uid);
@@ -0,0 +1,70 @@
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 { isVariableExpression, pruneFilter } from '@nocobase/flow-engine';
11
+ import { transformFilter } from '@nocobase/utils/client';
12
+ import _ from 'lodash';
13
+
14
+ const PRESERVE_NULL = { __nocobaseDataScopeNull__: true };
15
+
16
+ function isPreserveNull(value: any) {
17
+ return (
18
+ value &&
19
+ typeof value === 'object' &&
20
+ !Array.isArray(value) &&
21
+ value.__nocobaseDataScopeNull__ === true &&
22
+ Object.keys(value).length === 1
23
+ );
24
+ }
25
+
26
+ function restorePreservedNull(value: any): any {
27
+ if (isPreserveNull(value)) {
28
+ return null;
29
+ }
30
+ if (Array.isArray(value)) {
31
+ return value.map((item) => restorePreservedNull(item));
32
+ }
33
+ if (value && typeof value === 'object') {
34
+ return Object.keys(value).reduce<Record<string, any>>((result, key) => {
35
+ result[key] = restorePreservedNull(value[key]);
36
+ return result;
37
+ }, {});
38
+ }
39
+ return value;
40
+ }
41
+
42
+ function markEmptyVariableValues(rawNode: any, resolvedNode: any) {
43
+ if (!rawNode || !resolvedNode || typeof rawNode !== 'object' || typeof resolvedNode !== 'object') {
44
+ return;
45
+ }
46
+
47
+ if ('path' in rawNode && 'operator' in rawNode) {
48
+ if (
49
+ isVariableExpression(rawNode.value) &&
50
+ (resolvedNode.value === undefined || resolvedNode.value === null || resolvedNode.value === '')
51
+ ) {
52
+ resolvedNode.value = PRESERVE_NULL;
53
+ }
54
+ return;
55
+ }
56
+
57
+ if (Array.isArray(rawNode.items) && Array.isArray(resolvedNode.items)) {
58
+ rawNode.items.forEach((rawItem, index) => {
59
+ markEmptyVariableValues(rawItem, resolvedNode.items[index]);
60
+ });
61
+ }
62
+ }
63
+
64
+ export function normalizeDataScopeFilter(rawFilter: any, resolvedFilter: any) {
65
+ const filterForTransform = _.cloneDeep(resolvedFilter);
66
+ markEmptyVariableValues(rawFilter, filterForTransform);
67
+
68
+ const filter = pruneFilter(transformFilter(filterForTransform));
69
+ return restorePreservedNull(filter);
70
+ }
@@ -12,14 +12,13 @@ import {
12
12
  defineAction,
13
13
  FlowModel,
14
14
  MultiRecordResource,
15
- pruneFilter,
16
15
  useFlowContext,
17
16
  tExpr,
18
17
  } from '@nocobase/flow-engine';
19
- import { isEmptyFilter, transformFilter } from '@nocobase/utils/client';
20
- import _ from 'lodash';
18
+ import { isEmptyFilter } from '@nocobase/utils/client';
21
19
  import React from 'react';
22
20
  import { FilterGroup, VariableFilterItem } from '../components/filter';
21
+ import { normalizeDataScopeFilter } from './dataScopeFilter';
23
22
 
24
23
  export const setTargetDataScope = defineAction({
25
24
  name: 'setTargetDataScope',
@@ -62,8 +61,10 @@ export const setTargetDataScope = defineAction({
62
61
  filter: { logic: '$and', items: [] },
63
62
  };
64
63
  },
64
+ useRawParams: true,
65
65
  async handler(ctx, params) {
66
- const targetBlockUid = params.targetBlockUid;
66
+ const resolvedParams = await ctx.resolveJsonTemplate(params);
67
+ const targetBlockUid = resolvedParams.targetBlockUid;
67
68
  if (!targetBlockUid) {
68
69
  return;
69
70
  }
@@ -74,7 +75,7 @@ export const setTargetDataScope = defineAction({
74
75
  return;
75
76
  }
76
77
 
77
- const filter = pruneFilter(transformFilter(params.filter));
78
+ const filter = normalizeDataScopeFilter(params.filter, resolvedParams.filter);
78
79
 
79
80
  if (isEmptyFilter(filter)) {
80
81
  resource.removeFilterGroup(`setTargetDataScope_${ctx.model.uid}`);
@@ -35,7 +35,7 @@ describe('rebuildFieldSubModel', () => {
35
35
  });
36
36
  });
37
37
 
38
- test('rebuilds field with same uid and updates binding use', async () => {
38
+ test('rebuilds field with same uid and direct target field model use', async () => {
39
39
  const staleClickHandler = () => null;
40
40
  const parent = engine.createModel<DummyParentModel>({
41
41
  use: DummyParentModel,
@@ -66,7 +66,8 @@ describe('rebuildFieldSubModel', () => {
66
66
 
67
67
  expect(rebuilt).toBeInstanceOf(DummyTargetFieldModel);
68
68
  expect(rebuilt.uid).toBe('field-1');
69
- expect(getFieldBindingUse(rebuilt)).toBe('DummyTargetFieldModel');
69
+ expect(getFieldBindingUse(rebuilt)).toBeUndefined();
70
+ expect(rebuilt.use).toBe('DummyTargetFieldModel');
70
71
  expect(rebuilt.props).toMatchObject({ added: 'yes', pattern: 'readPretty' });
71
72
  expect((rebuilt.props as any).onClick).toBeUndefined();
72
73
 
@@ -106,4 +107,78 @@ describe('rebuildFieldSubModel', () => {
106
107
  expect(Array.isArray(cols)).toBe(true);
107
108
  expect(cols.map((c) => c.uid)).toEqual(['col-1', 'col-2']);
108
109
  });
110
+
111
+ test('preserves compatible step params when rebuilding with the same field model use', async () => {
112
+ const parent = engine.createModel<DummyParentModel>({
113
+ use: DummyParentModel,
114
+ uid: 'parent-3',
115
+ subModels: {
116
+ field: {
117
+ use: DummyTargetFieldModel,
118
+ uid: 'field-3',
119
+ stepParams: {
120
+ fieldSettings: { init: { initKey: true } },
121
+ displayFieldSettings: {
122
+ overflowMode: {
123
+ overflowMode: true,
124
+ },
125
+ },
126
+ } as any,
127
+ },
128
+ },
129
+ });
130
+
131
+ await rebuildFieldSubModel({
132
+ parentModel: parent,
133
+ targetUse: 'DummyTargetFieldModel',
134
+ fieldSettingsInit: { fieldPath: 'title' },
135
+ });
136
+
137
+ const rebuilt = parent.subModels.field as DummyTargetFieldModel;
138
+ expect(rebuilt.stepParams).toEqual({
139
+ fieldSettings: {
140
+ init: { fieldPath: 'title' },
141
+ },
142
+ displayFieldSettings: {
143
+ overflowMode: {
144
+ overflowMode: true,
145
+ },
146
+ },
147
+ });
148
+ });
149
+
150
+ test('drops incompatible step params when rebuilding to a different field model use', async () => {
151
+ const parent = engine.createModel<DummyParentModel>({
152
+ use: DummyParentModel,
153
+ uid: 'parent-4',
154
+ subModels: {
155
+ field: {
156
+ use: FieldModel,
157
+ uid: 'field-4',
158
+ stepParams: {
159
+ fieldBinding: { use: 'FieldModel' },
160
+ fieldSettings: { init: { initKey: true } },
161
+ numberSettings: {
162
+ format: {
163
+ separator: '0,0.00',
164
+ },
165
+ },
166
+ } as any,
167
+ },
168
+ },
169
+ });
170
+
171
+ await rebuildFieldSubModel({
172
+ parentModel: parent,
173
+ targetUse: 'DummyTargetFieldModel',
174
+ fieldSettingsInit: { fieldPath: 'title' },
175
+ });
176
+
177
+ const rebuilt = parent.subModels.field as DummyTargetFieldModel;
178
+ expect(rebuilt.stepParams).toEqual({
179
+ fieldSettings: {
180
+ init: { fieldPath: 'title' },
181
+ },
182
+ });
183
+ });
109
184
  });
@@ -10,8 +10,9 @@
10
10
  /**
11
11
  * 通用的字段子模型重建工具:
12
12
  * - 保留原有 uid
13
- * - 通过 FieldModel 入口 + fieldBinding.use 动态选择目标字段类
13
+ * - 直接重建为目标字段类,保持与 defineChildren 初始创建逻辑一致
14
14
  * - 支持同步父项模式(pattern)
15
+ * - 同一字段模型类型下保留已有字段设置;切换到其他字段模型类型时丢弃不兼容设置
15
16
  * - 重建后触发 beforeRender(useCache: false)
16
17
  */
17
18
  import { FieldModel } from '../../models/base/FieldModel';
@@ -39,6 +40,16 @@ type RebuildOptions = {
39
40
  fieldSettingsInit?: unknown;
40
41
  };
41
42
 
43
+ function normalizeModelUse(value: unknown): string | undefined {
44
+ if (typeof value === 'string') {
45
+ return value;
46
+ }
47
+ if (typeof value === 'function' && value.name) {
48
+ return value.name;
49
+ }
50
+ return undefined;
51
+ }
52
+
42
53
  export function getFieldBindingUse(fieldModel?: FieldModel): string | undefined {
43
54
  const bindingUse = (fieldModel?.stepParams as FieldStepParams | undefined)?.fieldBinding?.use;
44
55
  return typeof bindingUse === 'string' ? bindingUse : undefined;
@@ -61,13 +72,18 @@ export async function rebuildFieldSubModel({
61
72
  delete prevSubModels[key];
62
73
  }
63
74
  }
64
- const prevStepParams: FieldStepParams = (fieldModel?.stepParams as FieldStepParams) || {};
75
+ const currentUse = normalizeModelUse(getFieldBindingUse(fieldModel) || fieldModel?.use);
76
+ const shouldPreserveStepParams = currentUse === targetUse;
77
+ const prevStepParams: FieldStepParams = shouldPreserveStepParams
78
+ ? (fieldModel?.stepParams as FieldStepParams) || {}
79
+ : {};
65
80
  const nextFieldSettingsInit = fieldSettingsInit ?? parentModel.getFieldSettingsInitParams?.();
81
+ const { fieldBinding: _fieldBinding, ...restStepParams } = prevStepParams;
66
82
 
67
83
  const nextStepParams: FieldStepParams = {
68
- ...prevStepParams,
69
- fieldBinding: { ...prevStepParams.fieldBinding, use: targetUse },
84
+ ...restStepParams,
70
85
  fieldSettings: {
86
+ ...(restStepParams.fieldSettings || {}),
71
87
  init: nextFieldSettingsInit,
72
88
  },
73
89
  };
@@ -81,7 +97,7 @@ export async function rebuildFieldSubModel({
81
97
 
82
98
  const subModel = parentModel.setSubModel('field', {
83
99
  uid: fieldUid,
84
- use: FieldModel,
100
+ use: targetUse,
85
101
  props: { ...(defaultProps || {}), ...(pattern ? { pattern } : {}) },
86
102
  stepParams: nextStepParams as StepParams,
87
103
  // Preserve existing subModels (e.g. SubTable columns) so switching field component back and forth
@@ -102,15 +102,19 @@ export class FilterFormBlockModel extends FilterBlockModel<{
102
102
  // 首次进入页面:等待子模型 beforeRender 完成(例如 name 初始化),再应用表单级默认值并触发筛选
103
103
  void this.applyDefaultsAndInitialFilter();
104
104
 
105
- // 监听页面区块删除,自动清理已失效的筛选字段
105
+ // 监听页面区块删除,自动清理已失效的筛选字段。
106
+ // 这里使用 onSubModelDestroyed 而不是 onSubModelRemoved,避免弹窗关闭时
107
+ // 的临时模型卸载被误判成“用户删除了目标区块”。
106
108
  const blockGridModel = this.context.blockGridModel;
107
109
  if (blockGridModel?.emitter) {
108
- const handleTargetRemoved = (model) => {
110
+ const handleTargetDestroyed = (model) => {
109
111
  if (!model?.uid || model.uid === this.uid) return;
110
- this.handleTargetBlockRemoved(model.uid);
112
+ void this.handleTargetBlockRemoved(model.uid).catch((error) => {
113
+ console.error('Failed to handle destroyed target block in FilterFormBlockModel:', error);
114
+ });
111
115
  };
112
- blockGridModel.emitter.on('onSubModelRemoved', handleTargetRemoved);
113
- this.removeTargetBlockListener = () => blockGridModel.emitter.off('onSubModelRemoved', handleTargetRemoved);
116
+ blockGridModel.emitter.on('onSubModelDestroyed', handleTargetDestroyed);
117
+ this.removeTargetBlockListener = () => blockGridModel.emitter.off('onSubModelDestroyed', handleTargetDestroyed);
114
118
  }
115
119
  }
116
120