@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.
- package/es/BaseApplication.d.ts +1 -0
- package/es/flow/actions/dataScopeFilter.d.ts +9 -0
- package/es/flow/internal/utils/rebuildFieldSubModel.d.ts +2 -1
- package/es/flow/models/fields/JSFieldModel.d.ts +5 -0
- package/es/index.mjs +100 -100
- package/lib/index.js +89 -89
- package/package.json +6 -5
- package/src/BaseApplication.tsx +4 -0
- package/src/__tests__/globalDeps.test.ts +1 -0
- package/src/__tests__/remotePlugins.test.ts +27 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +158 -0
- package/src/flow/actions/dataScope.tsx +6 -4
- package/src/flow/actions/dataScopeFilter.ts +70 -0
- package/src/flow/actions/setTargetDataScope.tsx +6 -5
- package/src/flow/internal/utils/__tests__/rebuildFieldSubModel.test.ts +77 -2
- package/src/flow/internal/utils/rebuildFieldSubModel.ts +21 -5
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +9 -5
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormBlockModel.cleanup.test.ts +138 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +22 -0
- package/src/flow/models/blocks/table/JSColumnModel.tsx +30 -2
- package/src/flow/models/blocks/table/TableBlockModel.tsx +8 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +1 -0
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +51 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +49 -0
- package/src/flow/models/fields/JSFieldModel.tsx +54 -14
- package/src/utils/globalDeps.ts +4 -0
- 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.
|
|
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.
|
|
28
|
-
"@nocobase/sdk": "2.1.0-beta.
|
|
29
|
-
"@nocobase/shared": "2.1.0-beta.
|
|
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": "
|
|
39
|
+
"gitHead": "824f8b8200e9fe086135768934d3ef427b212446"
|
|
39
40
|
}
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -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,
|
|
11
|
-
import { isEmptyFilter
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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)).
|
|
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
|
-
* -
|
|
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
|
|
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
|
-
...
|
|
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:
|
|
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
|
|
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('
|
|
113
|
-
this.removeTargetBlockListener = () => blockGridModel.emitter.off('
|
|
116
|
+
blockGridModel.emitter.on('onSubModelDestroyed', handleTargetDestroyed);
|
|
117
|
+
this.removeTargetBlockListener = () => blockGridModel.emitter.off('onSubModelDestroyed', handleTargetDestroyed);
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
|