@nocobase/client-v2 2.1.0-alpha.26 → 2.1.0-alpha.28
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/components/Grid/index.d.ts +5 -3
- package/es/flow/components/code-editor/types.d.ts +1 -0
- package/es/flow/internal/utils/rebuildFieldSubModel.d.ts +2 -1
- package/es/flow/models/base/GridModel.d.ts +19 -2
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/fields/JSFieldModel.d.ts +5 -0
- package/es/index.mjs +83 -83
- package/lib/index.js +83 -83
- package/package.json +5 -5
- package/src/BaseApplication.tsx +4 -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/components/Grid/index.tsx +66 -20
- package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
- package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
- package/src/flow/components/code-editor/index.tsx +18 -17
- package/src/flow/components/code-editor/linter.ts +222 -158
- package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
- package/src/flow/components/code-editor/types.ts +1 -0
- package/src/flow/internal/utils/__tests__/rebuildFieldSubModel.test.ts +77 -2
- package/src/flow/internal/utils/rebuildFieldSubModel.ts +21 -5
- package/src/flow/models/base/BlockGridModel.tsx +2 -2
- package/src/flow/models/base/GridModel.tsx +428 -195
- package/src/flow/models/base/__tests__/BlockGridModel.dragOverlayConfig.test.ts +44 -0
- package/src/flow/models/base/__tests__/GridModel.computeOverlayRect.test.ts +29 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +181 -2
- package/src/flow/models/base/__tests__/GridModel.resizeLayout.test.ts +124 -0
- package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +55 -15
- package/src/flow/models/blocks/details/DetailsGridModel.tsx +6 -6
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +9 -5
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +54 -14
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormBlockModel.cleanup.test.ts +138 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +45 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +6 -6
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.0-alpha.
|
|
3
|
+
"version": "2.1.0-alpha.28",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -24,9 +24,9 @@
|
|
|
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-alpha.
|
|
28
|
-
"@nocobase/sdk": "2.1.0-alpha.
|
|
29
|
-
"@nocobase/shared": "2.1.0-alpha.
|
|
27
|
+
"@nocobase/flow-engine": "2.1.0-alpha.28",
|
|
28
|
+
"@nocobase/sdk": "2.1.0-alpha.28",
|
|
29
|
+
"@nocobase/shared": "2.1.0-alpha.28",
|
|
30
30
|
"antd": "5.24.2",
|
|
31
31
|
"classnames": "^2.3.1",
|
|
32
32
|
"dayjs": "^1.11.10",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"react-i18next": "^11.15.1",
|
|
36
36
|
"react-router-dom": "^6.30.1"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "e38293c4a1ce4d043d2b17a3067fd4f0341a7a2d"
|
|
39
39
|
}
|
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
|
|
|
@@ -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}`);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { EMPTY_COLUMN_UID } from '@nocobase/flow-engine';
|
|
10
|
+
import { EMPTY_COLUMN_UID, GridLayoutPath, GridLayoutV2, normalizeGridLayout } from '@nocobase/flow-engine';
|
|
11
11
|
import { Col, Row } from 'antd';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
|
|
@@ -16,37 +16,63 @@ interface DragOverlayRect {
|
|
|
16
16
|
readonly left: number;
|
|
17
17
|
readonly width: number;
|
|
18
18
|
readonly height: number;
|
|
19
|
-
readonly type: 'column' | 'column-edge' | 'row-gap' | 'empty-row' | 'empty-column';
|
|
19
|
+
readonly type: 'column' | 'column-edge' | 'row-gap' | 'empty-row' | 'empty-column' | 'item-edge';
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
interface GridProps {
|
|
23
|
-
readonly rows
|
|
23
|
+
readonly rows?: Record<string, string[][]>;
|
|
24
24
|
readonly sizes?: Record<string, number[]>;
|
|
25
|
+
readonly layout?: GridLayoutV2;
|
|
25
26
|
readonly renderItem: (uid: string) => React.ReactNode;
|
|
26
27
|
readonly rowGap?: number;
|
|
27
28
|
readonly colGap?: number;
|
|
28
29
|
readonly dragOverlayRect?: DragOverlayRect | null;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
export function Grid({
|
|
32
|
-
|
|
32
|
+
export function Grid({
|
|
33
|
+
rows = {},
|
|
34
|
+
sizes = {},
|
|
35
|
+
layout,
|
|
36
|
+
renderItem,
|
|
37
|
+
rowGap = 16,
|
|
38
|
+
colGap = 16,
|
|
39
|
+
dragOverlayRect,
|
|
40
|
+
}: GridProps) {
|
|
41
|
+
const normalizedLayout = layout || normalizeGridLayout({ rows, sizes });
|
|
42
|
+
if (!normalizedLayout.rows.length) {
|
|
33
43
|
return (
|
|
34
|
-
<div style={{ position: 'relative' }} data-grid-empty-container>
|
|
44
|
+
<div style={{ position: 'relative' }} data-grid-root data-grid-empty-container>
|
|
35
45
|
{dragOverlayRect && <GridDragOverlay rect={dragOverlayRect} />}
|
|
36
46
|
</div>
|
|
37
47
|
);
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
return (
|
|
41
|
-
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: rowGap }}>
|
|
51
|
+
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: rowGap }} data-grid-root>
|
|
42
52
|
{dragOverlayRect && <GridDragOverlay rect={dragOverlayRect} />}
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
<GridRows rows={normalizedLayout.rows} renderItem={renderItem} rowGap={rowGap} colGap={colGap} parentPath={[]} />
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface GridRowsProps {
|
|
59
|
+
readonly rows: GridLayoutV2['rows'];
|
|
60
|
+
readonly renderItem: (uid: string) => React.ReactNode;
|
|
61
|
+
readonly rowGap: number;
|
|
62
|
+
readonly colGap: number;
|
|
63
|
+
readonly parentPath: GridLayoutPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function GridRows({ rows, renderItem, rowGap, colGap, parentPath }: GridRowsProps) {
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
{rows.map((row) => {
|
|
70
|
+
const colCount = row.cells.length;
|
|
71
|
+
const rowSizes = row.sizes || [];
|
|
46
72
|
const hasAnySize = rowSizes.some((v) => v != null && v !== undefined);
|
|
73
|
+
const rowPath = [...parentPath, { rowId: row.id }];
|
|
47
74
|
|
|
48
|
-
|
|
49
|
-
const spans = cells.map((_, cellIdx) => {
|
|
75
|
+
const spans = row.cells.map((_, cellIdx) => {
|
|
50
76
|
if (hasAnySize) {
|
|
51
77
|
const assigned = rowSizes.reduce((sum, v) => sum + (v || 0), 0);
|
|
52
78
|
const unassignedCount = colCount - rowSizes.filter(Boolean).length;
|
|
@@ -58,22 +84,24 @@ export function Grid({ rows, sizes = {}, renderItem, rowGap = 16, colGap = 16, d
|
|
|
58
84
|
});
|
|
59
85
|
|
|
60
86
|
return (
|
|
61
|
-
<Row key={
|
|
62
|
-
{cells.map((cell, cellIdx) => (
|
|
87
|
+
<Row key={row.id} gutter={colGap} data-grid-row-id={row.id} data-grid-path={JSON.stringify(rowPath)}>
|
|
88
|
+
{row.cells.map((cell, cellIdx) => (
|
|
63
89
|
<GridColumn
|
|
64
|
-
key={
|
|
65
|
-
rowKey={
|
|
90
|
+
key={cell.id}
|
|
91
|
+
rowKey={row.id}
|
|
66
92
|
columnIndex={cellIdx}
|
|
67
93
|
span={spans[cellIdx]}
|
|
68
94
|
rowGap={rowGap}
|
|
95
|
+
colGap={colGap}
|
|
69
96
|
cell={cell}
|
|
97
|
+
path={[...rowPath.slice(0, -1), { rowId: row.id, cellId: cell.id }]}
|
|
70
98
|
renderItem={renderItem}
|
|
71
99
|
/>
|
|
72
100
|
))}
|
|
73
101
|
</Row>
|
|
74
102
|
);
|
|
75
103
|
})}
|
|
76
|
-
|
|
104
|
+
</>
|
|
77
105
|
);
|
|
78
106
|
}
|
|
79
107
|
|
|
@@ -82,7 +110,9 @@ interface GridColumnProps {
|
|
|
82
110
|
readonly columnIndex: number;
|
|
83
111
|
readonly span: number;
|
|
84
112
|
readonly rowGap: number;
|
|
85
|
-
readonly
|
|
113
|
+
readonly colGap: number;
|
|
114
|
+
readonly cell: GridLayoutV2['rows'][number]['cells'][number];
|
|
115
|
+
readonly path: GridLayoutPath;
|
|
86
116
|
readonly renderItem: (uid: string) => React.ReactNode;
|
|
87
117
|
}
|
|
88
118
|
|
|
@@ -91,13 +121,21 @@ const GridColumn = React.memo(function GridColumn({
|
|
|
91
121
|
columnIndex,
|
|
92
122
|
span,
|
|
93
123
|
rowGap,
|
|
124
|
+
colGap,
|
|
94
125
|
cell,
|
|
126
|
+
path,
|
|
95
127
|
renderItem,
|
|
96
128
|
}: GridColumnProps) {
|
|
129
|
+
const items = cell.items || [];
|
|
97
130
|
return (
|
|
98
|
-
<Col
|
|
131
|
+
<Col
|
|
132
|
+
span={span}
|
|
133
|
+
data-grid-column-row-id={rowKey}
|
|
134
|
+
data-grid-column-index={columnIndex}
|
|
135
|
+
data-grid-path={JSON.stringify(path)}
|
|
136
|
+
>
|
|
99
137
|
<div style={{ display: 'flex', flexDirection: 'column', gap: rowGap }}>
|
|
100
|
-
{
|
|
138
|
+
{items.map((uid, itemIdx) => {
|
|
101
139
|
if (uid === EMPTY_COLUMN_UID) {
|
|
102
140
|
return null;
|
|
103
141
|
}
|
|
@@ -108,11 +146,15 @@ const GridColumn = React.memo(function GridColumn({
|
|
|
108
146
|
data-grid-column-index={columnIndex}
|
|
109
147
|
data-grid-item-index={itemIdx}
|
|
110
148
|
data-grid-item-uid={uid}
|
|
149
|
+
data-grid-path={JSON.stringify(path)}
|
|
111
150
|
>
|
|
112
151
|
{renderItem(uid)}
|
|
113
152
|
</div>
|
|
114
153
|
);
|
|
115
154
|
})}
|
|
155
|
+
{cell.rows?.length ? (
|
|
156
|
+
<GridRows rows={cell.rows} renderItem={renderItem} rowGap={rowGap} colGap={colGap} parentPath={path} />
|
|
157
|
+
) : null}
|
|
116
158
|
</div>
|
|
117
159
|
</Col>
|
|
118
160
|
);
|
|
@@ -152,6 +194,10 @@ function GridDragOverlay({ rect }: Readonly<{ rect: DragOverlayRect }>) {
|
|
|
152
194
|
border: '2px dashed var(--colorPrimary)',
|
|
153
195
|
backgroundColor: 'rgba(22, 119, 255, 0.04)',
|
|
154
196
|
},
|
|
197
|
+
'item-edge': {
|
|
198
|
+
border: '2px solid var(--colorPrimary)',
|
|
199
|
+
backgroundColor: 'rgba(22, 119, 255, 0.12)',
|
|
200
|
+
},
|
|
155
201
|
};
|
|
156
202
|
|
|
157
203
|
return <div style={{ ...baseStyle, ...typeStyles[rect.type] }} />;
|
|
@@ -39,6 +39,24 @@ describe('code-editor linter', () => {
|
|
|
39
39
|
);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
it('does not warn for callback parameters used inside JSX', () => {
|
|
43
|
+
const code = `
|
|
44
|
+
const columns = [
|
|
45
|
+
{
|
|
46
|
+
render: (roles, record) => (
|
|
47
|
+
<div>
|
|
48
|
+
{roles.map((role) => <Tag key={role.name}>{record.nickname || role.title}</Tag>)}
|
|
49
|
+
</div>
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
`;
|
|
54
|
+
const diags = computeDiagnosticsFromText(code);
|
|
55
|
+
expect(diags.some((d) => d.message.includes('Possible undefined variable: roles'))).toBe(false);
|
|
56
|
+
expect(diags.some((d) => d.message.includes('Possible undefined variable: record'))).toBe(false);
|
|
57
|
+
expect(diags.some((d) => d.message.includes('Possible undefined variable: role'))).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
42
60
|
it('reports non-callable expression warning', () => {
|
|
43
61
|
const code = `(1+2)()`;
|
|
44
62
|
const diags = computeDiagnosticsFromText(code);
|
|
@@ -38,6 +38,29 @@ describe('runjsDiagnostics', () => {
|
|
|
38
38
|
expect(res.issues.some((i) => i.type === 'lint' && i.ruleId === 'no-noncallable-call')).toBe(true);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
it('does not report JSX callback parameters as undefined variables', async () => {
|
|
42
|
+
const ctx = createTestCtx();
|
|
43
|
+
const code = `
|
|
44
|
+
const { Tag } = ctx.libs.antd;
|
|
45
|
+
const columns = [
|
|
46
|
+
{
|
|
47
|
+
render: (roles, record) => (
|
|
48
|
+
<div>
|
|
49
|
+
{roles.map((role) => <Tag key={role.name}>{record.nickname || role.title}</Tag>)}
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
ctx.render(<div />);
|
|
55
|
+
`;
|
|
56
|
+
const res = await diagnoseRunJS(code, ctx);
|
|
57
|
+
expect(
|
|
58
|
+
res.issues.some(
|
|
59
|
+
(i) => i.type === 'lint' && i.ruleId === 'possible-undefined-variable' && /roles|record|role/.test(i.message),
|
|
60
|
+
),
|
|
61
|
+
).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
41
64
|
it('reports suspicious short ctx member call as a lint issue', async () => {
|
|
42
65
|
const ctx = createTestCtx();
|
|
43
66
|
const res = await diagnoseRunJS('ctx.fw();', ctx);
|
|
@@ -133,6 +133,19 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|
|
133
133
|
return createRunJSCompletionSource({ hostCtx, staticOptions: finalExtra });
|
|
134
134
|
}, [hostCtx, finalExtra]);
|
|
135
135
|
|
|
136
|
+
const runCurrentCode = useCallback(async () => {
|
|
137
|
+
const code = viewRef.current?.state.doc.toString() || '';
|
|
138
|
+
clearDiagnostics(viewRef.current);
|
|
139
|
+
const res = await run(code);
|
|
140
|
+
if (!res?.success) {
|
|
141
|
+
const rawErr = res?.error;
|
|
142
|
+
const errText = res?.timeout ? tr('Execution timed out') : String(rawErr || tr('Unknown error'));
|
|
143
|
+
const pos = parseErrorLineColumn(rawErr);
|
|
144
|
+
if (pos && viewRef.current) markErrorAt(viewRef.current, pos.line, pos.column, errText);
|
|
145
|
+
}
|
|
146
|
+
return res;
|
|
147
|
+
}, [run, tr]);
|
|
148
|
+
|
|
136
149
|
// JSX 转换支持暂时移除:直接按原样运行代码
|
|
137
150
|
|
|
138
151
|
// 错误标注相关工具已提取至 errorHelpers.ts
|
|
@@ -153,6 +166,9 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|
|
153
166
|
const v = viewRef.current;
|
|
154
167
|
return v ? v.state.doc.toString() : '';
|
|
155
168
|
},
|
|
169
|
+
run() {
|
|
170
|
+
return Promise.resolve(undefined);
|
|
171
|
+
},
|
|
156
172
|
|
|
157
173
|
buttonGroupHeight: 0,
|
|
158
174
|
snippetEntries: [],
|
|
@@ -160,6 +176,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|
|
160
176
|
});
|
|
161
177
|
extraEditorRef.current.snippetEntries = snippetEntries;
|
|
162
178
|
extraEditorRef.current.logs = logs;
|
|
179
|
+
extraEditorRef.current.run = runCurrentCode;
|
|
163
180
|
|
|
164
181
|
// snippet group display handled in SnippetsDrawer
|
|
165
182
|
|
|
@@ -210,23 +227,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|
|
210
227
|
{tr('Snippets')}
|
|
211
228
|
</Button>
|
|
212
229
|
<>
|
|
213
|
-
<Button
|
|
214
|
-
size="small"
|
|
215
|
-
loading={running}
|
|
216
|
-
onClick={async () => {
|
|
217
|
-
const code = viewRef.current?.state.doc.toString() || '';
|
|
218
|
-
clearDiagnostics(viewRef.current);
|
|
219
|
-
const res = await run(code);
|
|
220
|
-
if (!res?.success) {
|
|
221
|
-
const rawErr = res?.error;
|
|
222
|
-
const errText = res?.timeout
|
|
223
|
-
? tr('Execution timed out')
|
|
224
|
-
: String(rawErr || tr('Unknown error'));
|
|
225
|
-
const pos = parseErrorLineColumn(rawErr);
|
|
226
|
-
if (pos && viewRef.current) markErrorAt(viewRef.current, pos.line, pos.column, errText);
|
|
227
|
-
}
|
|
228
|
-
}}
|
|
229
|
-
>
|
|
230
|
+
<Button size="small" loading={running} onClick={runCurrentCode}>
|
|
230
231
|
{tr('Run')}
|
|
231
232
|
</Button>
|
|
232
233
|
</>
|