@nocobase/client-v2 2.1.0-alpha.25 → 2.1.0-alpha.27
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/blocks/form/QuickEditFormModel.d.ts +7 -1
- package/es/flow/models/fields/JSFieldModel.d.ts +5 -0
- package/es/index.mjs +81 -81
- package/lib/index.js +73 -73
- 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/QuickEditFormModel.tsx +39 -16
- 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
|
@@ -8,13 +8,22 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { SettingOutlined } from '@ant-design/icons';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
AddSubModelButton,
|
|
13
|
+
DragOverlayConfig,
|
|
14
|
+
FlowSettingsButton,
|
|
15
|
+
GridLayoutV2,
|
|
16
|
+
normalizeGridLayout,
|
|
17
|
+
observable,
|
|
18
|
+
projectLayoutToLegacyRows,
|
|
19
|
+
} from '@nocobase/flow-engine';
|
|
12
20
|
import React from 'react';
|
|
13
21
|
import { CollectionBlockModel, GRID_FLOW_KEY, GRID_STEP, GridModel } from '../../base';
|
|
14
22
|
import { getAllDataModels } from '../filter-manager/utils';
|
|
15
23
|
import { FilterFormItemModel } from './FilterFormItemModel';
|
|
16
24
|
|
|
17
25
|
export class FilterFormGridModel extends GridModel {
|
|
26
|
+
private fullLayoutBeforeCollapse?: GridLayoutV2;
|
|
18
27
|
itemSettingsMenuLevel = 2;
|
|
19
28
|
itemFlowSettings = {
|
|
20
29
|
showBackground: true,
|
|
@@ -28,8 +37,8 @@ export class FilterFormGridModel extends GridModel {
|
|
|
28
37
|
dragOverlayConfig: DragOverlayConfig = {
|
|
29
38
|
// 列内插入
|
|
30
39
|
columnInsert: {
|
|
31
|
-
before: { offsetTop: -
|
|
32
|
-
after: { offsetTop:
|
|
40
|
+
before: { offsetTop: -6, height: 24 },
|
|
41
|
+
after: { offsetTop: 3, height: 24 },
|
|
33
42
|
},
|
|
34
43
|
// 列边缘
|
|
35
44
|
columnEdge: {
|
|
@@ -38,8 +47,8 @@ export class FilterFormGridModel extends GridModel {
|
|
|
38
47
|
},
|
|
39
48
|
// 行间隙
|
|
40
49
|
rowGap: {
|
|
41
|
-
above: { offsetTop:
|
|
42
|
-
below: { offsetTop: -
|
|
50
|
+
above: { offsetTop: -2, height: 24 },
|
|
51
|
+
below: { offsetTop: -12, height: 24 },
|
|
43
52
|
},
|
|
44
53
|
};
|
|
45
54
|
|
|
@@ -62,8 +71,16 @@ export class FilterFormGridModel extends GridModel {
|
|
|
62
71
|
*/
|
|
63
72
|
private getFullLayout() {
|
|
64
73
|
const params = this.getStepParams(GRID_FLOW_KEY, GRID_STEP) || {};
|
|
65
|
-
const
|
|
66
|
-
const
|
|
74
|
+
const currentLayout = this.props.layout ? this.getGridLayout() : undefined;
|
|
75
|
+
const savedLayout = params.layout ? this.normalizeLayoutFromSource(params) : undefined;
|
|
76
|
+
const currentProjection = currentLayout
|
|
77
|
+
? projectLayoutToLegacyRows(currentLayout)
|
|
78
|
+
: { rows: this.props.rows || {}, rowOrder: this.props.rowOrder };
|
|
79
|
+
const savedProjection = savedLayout
|
|
80
|
+
? projectLayoutToLegacyRows(savedLayout)
|
|
81
|
+
: { rows: params.rows || {}, rowOrder: params.rowOrder };
|
|
82
|
+
const rawCurrentRows = currentProjection.rows as Record<string, string[][]>;
|
|
83
|
+
const rawSavedRows = (savedProjection.rows || {}) as Record<string, string[][]>;
|
|
67
84
|
const currentCount = Object.keys(rawCurrentRows).length;
|
|
68
85
|
const savedCount = Object.keys(rawSavedRows).length;
|
|
69
86
|
const getItemCount = (rows: Record<string, string[][]>) =>
|
|
@@ -77,10 +94,13 @@ export class FilterFormGridModel extends GridModel {
|
|
|
77
94
|
currentCount > savedCount || (currentCount === savedCount && currentItemCount >= savedItemCount);
|
|
78
95
|
const sourceRows = this.mergeRowsWithItems(useCurrentLayout ? rawCurrentRows : rawSavedRows);
|
|
79
96
|
const sourceRowOrder = useCurrentLayout
|
|
80
|
-
? this.props.rowOrder || params.rowOrder
|
|
81
|
-
: params.rowOrder || this.props.rowOrder;
|
|
97
|
+
? currentProjection.rowOrder || this.props.rowOrder || params.rowOrder
|
|
98
|
+
: savedProjection.rowOrder || params.rowOrder || this.props.rowOrder;
|
|
82
99
|
|
|
83
|
-
return
|
|
100
|
+
return {
|
|
101
|
+
...this.normalizeRowsWithOrder(sourceRows, sourceRowOrder),
|
|
102
|
+
layout: useCurrentLayout ? currentLayout : savedLayout,
|
|
103
|
+
};
|
|
84
104
|
}
|
|
85
105
|
|
|
86
106
|
/**
|
|
@@ -115,17 +135,37 @@ export class FilterFormGridModel extends GridModel {
|
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
toggleFormFieldsCollapse(collapse: boolean, visibleRows: number) {
|
|
118
|
-
const { rows: fullRows, rowOrder } = this.getFullLayout();
|
|
138
|
+
const { rows: fullRows, rowOrder, layout } = this.getFullLayout();
|
|
119
139
|
|
|
120
140
|
if (!collapse) {
|
|
121
|
-
this.
|
|
122
|
-
|
|
141
|
+
const restoredLayout = this.fullLayoutBeforeCollapse || layout;
|
|
142
|
+
if (restoredLayout) {
|
|
143
|
+
this.syncLayoutProps(restoredLayout);
|
|
144
|
+
} else {
|
|
145
|
+
this.setProps('rows', fullRows);
|
|
146
|
+
this.setProps('rowOrder', rowOrder);
|
|
147
|
+
}
|
|
148
|
+
this.fullLayoutBeforeCollapse = undefined;
|
|
123
149
|
return;
|
|
124
150
|
}
|
|
125
151
|
|
|
152
|
+
if (!this.fullLayoutBeforeCollapse) {
|
|
153
|
+
this.fullLayoutBeforeCollapse =
|
|
154
|
+
layout ||
|
|
155
|
+
normalizeGridLayout({
|
|
156
|
+
rows: fullRows,
|
|
157
|
+
rowOrder,
|
|
158
|
+
itemUids: this.getItemUids(),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
126
162
|
const limitedRows = this.limitRowsByVisibleCount(fullRows, rowOrder, visibleRows);
|
|
163
|
+
const limitedLayout = normalizeGridLayout({
|
|
164
|
+
rows: limitedRows,
|
|
165
|
+
rowOrder,
|
|
166
|
+
});
|
|
127
167
|
|
|
128
|
-
this.
|
|
168
|
+
this.syncLayoutProps(limitedLayout);
|
|
129
169
|
this.setProps('rowOrder', rowOrder);
|
|
130
170
|
}
|
|
131
171
|
|
|
@@ -0,0 +1,138 @@
|
|
|
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 '@nocobase/client';
|
|
12
|
+
import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
13
|
+
import { waitFor } from '@testing-library/react';
|
|
14
|
+
import { TableBlockModel } from '../../table/TableBlockModel';
|
|
15
|
+
import { FilterFormBlockModel } from '../FilterFormBlockModel';
|
|
16
|
+
import { FilterFormGridModel } from '../FilterFormGridModel';
|
|
17
|
+
import { FilterFormItemModel } from '../FilterFormItemModel';
|
|
18
|
+
|
|
19
|
+
describe('FilterFormBlockModel cleanup', () => {
|
|
20
|
+
function createFilterFormSetup() {
|
|
21
|
+
const engine = new FlowEngine();
|
|
22
|
+
engine.registerModels({
|
|
23
|
+
FlowModel,
|
|
24
|
+
TableBlockModel,
|
|
25
|
+
FilterFormBlockModel,
|
|
26
|
+
FilterFormGridModel,
|
|
27
|
+
FilterFormItemModel,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const blockGridModel = engine.createModel<FlowModel>({
|
|
31
|
+
uid: 'block-grid',
|
|
32
|
+
use: 'FlowModel',
|
|
33
|
+
subModels: {
|
|
34
|
+
items: [],
|
|
35
|
+
},
|
|
36
|
+
} as any);
|
|
37
|
+
|
|
38
|
+
const tableBlock = blockGridModel.addSubModel('items', {
|
|
39
|
+
uid: 'target-table',
|
|
40
|
+
use: 'TableBlockModel',
|
|
41
|
+
}) as TableBlockModel;
|
|
42
|
+
|
|
43
|
+
const filterForm = blockGridModel.addSubModel('items', {
|
|
44
|
+
uid: 'filter-form',
|
|
45
|
+
use: 'FilterFormBlockModel',
|
|
46
|
+
subModels: {
|
|
47
|
+
grid: {
|
|
48
|
+
uid: 'filter-grid',
|
|
49
|
+
use: 'FilterFormGridModel',
|
|
50
|
+
subModels: {
|
|
51
|
+
items: [],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}) as FilterFormBlockModel;
|
|
56
|
+
|
|
57
|
+
const filterItem = filterForm.subModels.grid.addSubModel('items', {
|
|
58
|
+
uid: 'filter-item',
|
|
59
|
+
use: 'FilterFormItemModel',
|
|
60
|
+
stepParams: {
|
|
61
|
+
filterFormItemSettings: {
|
|
62
|
+
init: {
|
|
63
|
+
defaultTargetUid: tableBlock.uid,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}) as FilterFormItemModel;
|
|
68
|
+
|
|
69
|
+
const removeFilterConfig = vi.fn(async () => {});
|
|
70
|
+
const saveConnectFieldsConfig = vi.fn(async () => {});
|
|
71
|
+
const getConnectFieldsConfig = vi.fn(() => ({
|
|
72
|
+
targets: [
|
|
73
|
+
{
|
|
74
|
+
targetId: tableBlock.uid,
|
|
75
|
+
filterPaths: ['name'],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
filterForm.context.defineProperty('blockGridModel', { value: blockGridModel });
|
|
81
|
+
filterForm.context.defineProperty('filterManager', {
|
|
82
|
+
value: {
|
|
83
|
+
removeFilterConfig,
|
|
84
|
+
saveConnectFieldsConfig,
|
|
85
|
+
getConnectFieldsConfig,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
filterForm.subModels.grid.context.defineProperty('filterManager', {
|
|
89
|
+
value: {
|
|
90
|
+
removeFilterConfig,
|
|
91
|
+
saveConnectFieldsConfig,
|
|
92
|
+
getConnectFieldsConfig,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const destroySpy = vi.spyOn(filterItem, 'destroy').mockResolvedValue(true as any);
|
|
97
|
+
vi.spyOn(filterForm as any, 'applyDefaultsAndInitialFilter').mockResolvedValue(undefined);
|
|
98
|
+
|
|
99
|
+
(filterForm as any).onMount();
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
engine,
|
|
103
|
+
blockGridModel,
|
|
104
|
+
tableBlock,
|
|
105
|
+
filterForm,
|
|
106
|
+
filterItem,
|
|
107
|
+
removeFilterConfig,
|
|
108
|
+
saveConnectFieldsConfig,
|
|
109
|
+
getConnectFieldsConfig,
|
|
110
|
+
destroySpy,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
it('does not remove filter items when target block is only removed during popup teardown', async () => {
|
|
115
|
+
const { engine, blockGridModel, tableBlock, filterForm, removeFilterConfig, destroySpy } = createFilterFormSetup();
|
|
116
|
+
|
|
117
|
+
await Promise.resolve(engine.removeModelWithSubModels(blockGridModel.uid));
|
|
118
|
+
|
|
119
|
+
expect(removeFilterConfig).not.toHaveBeenCalled();
|
|
120
|
+
expect(destroySpy).not.toHaveBeenCalled();
|
|
121
|
+
|
|
122
|
+
(filterForm as any).onUnmount();
|
|
123
|
+
expect(engine.getModel(tableBlock.uid)).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('removes filter items when target block is actually destroyed', async () => {
|
|
127
|
+
const { tableBlock, filterForm, removeFilterConfig, destroySpy } = createFilterFormSetup();
|
|
128
|
+
|
|
129
|
+
await tableBlock.destroy();
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(removeFilterConfig).toHaveBeenCalledWith({ filterId: 'filter-item' });
|
|
133
|
+
expect(destroySpy).toHaveBeenCalledTimes(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
(filterForm as any).onUnmount();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -102,6 +102,51 @@ describe('FilterFormGridModel.toggleFormFieldsCollapse', () => {
|
|
|
102
102
|
expect(model.props.rowOrder).toEqual(['first']);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
it('updates render layout when collapsing a v2 layout', () => {
|
|
106
|
+
const model = engine.createModel<FilterFormGridModel>({
|
|
107
|
+
uid: 'filter-grid-collapse-v2',
|
|
108
|
+
use: 'FilterFormGridModel',
|
|
109
|
+
props: {
|
|
110
|
+
layout: {
|
|
111
|
+
version: 2,
|
|
112
|
+
rows: [
|
|
113
|
+
{
|
|
114
|
+
id: 'first',
|
|
115
|
+
cells: [{ id: 'first-cell', items: ['field-1', 'field-2', 'field-3'] }],
|
|
116
|
+
sizes: [24],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
structure: {} as any,
|
|
122
|
+
});
|
|
123
|
+
(model as any).subModels = {
|
|
124
|
+
items: [
|
|
125
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-1' }),
|
|
126
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-2' }),
|
|
127
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-3' }),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
model.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
|
|
132
|
+
layout: model.props.layout,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
model.toggleFormFieldsCollapse(true, 1);
|
|
136
|
+
|
|
137
|
+
expect(model.props.rows).toEqual({
|
|
138
|
+
first: [['field-1']],
|
|
139
|
+
});
|
|
140
|
+
expect(model.props.layout.rows[0].cells[0].items).toEqual(['field-1']);
|
|
141
|
+
|
|
142
|
+
model.toggleFormFieldsCollapse(false, 1);
|
|
143
|
+
|
|
144
|
+
expect(model.props.rows).toEqual({
|
|
145
|
+
first: [['field-1', 'field-2', 'field-3']],
|
|
146
|
+
});
|
|
147
|
+
expect(model.props.layout.rows[0].cells[0].items).toEqual(['field-1', 'field-2', 'field-3']);
|
|
148
|
+
});
|
|
149
|
+
|
|
105
150
|
it('restores the persisted full layout when current props rows were already truncated', () => {
|
|
106
151
|
const model = engine.createModel<FilterFormGridModel>({
|
|
107
152
|
uid: 'filter-grid-collapse-restore',
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { SettingOutlined } from '@ant-design/icons';
|
|
11
|
-
import { AddSubModelButton,
|
|
11
|
+
import { AddSubModelButton, DragOverlayConfig, FlowSettingsButton } from '@nocobase/flow-engine';
|
|
12
|
+
import { Skeleton } from 'antd';
|
|
12
13
|
import React from 'react';
|
|
13
14
|
import { FieldModel } from '../../base/FieldModel';
|
|
14
15
|
import { GridModel } from '../../base/GridModel';
|
|
15
16
|
import { FormBlockModel } from './FormBlockModel';
|
|
16
|
-
import { Skeleton } from 'antd';
|
|
17
17
|
|
|
18
18
|
export type DefaultFormGridStructure = {
|
|
19
19
|
parent: FormBlockModel;
|
|
@@ -35,8 +35,8 @@ export class FormGridModel<T extends DefaultFormGridStructure = DefaultFormGridS
|
|
|
35
35
|
dragOverlayConfig: DragOverlayConfig = {
|
|
36
36
|
// 列内插入
|
|
37
37
|
columnInsert: {
|
|
38
|
-
before: { offsetTop: -
|
|
39
|
-
after: { offsetTop:
|
|
38
|
+
before: { offsetTop: -6, height: 24 },
|
|
39
|
+
after: { offsetTop: 3, height: 24 },
|
|
40
40
|
},
|
|
41
41
|
// 列边缘
|
|
42
42
|
columnEdge: {
|
|
@@ -45,8 +45,8 @@ export class FormGridModel<T extends DefaultFormGridStructure = DefaultFormGridS
|
|
|
45
45
|
},
|
|
46
46
|
// 行间隙
|
|
47
47
|
rowGap: {
|
|
48
|
-
above: { offsetTop:
|
|
49
|
-
below: { offsetTop: -
|
|
48
|
+
above: { offsetTop: -2, height: 24 },
|
|
49
|
+
below: { offsetTop: -12, height: 24 },
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
52
|
renderAddSubModelButton() {
|
|
@@ -28,6 +28,18 @@ import { FieldModel } from '../../base/FieldModel';
|
|
|
28
28
|
import { FormComponent } from './FormBlockModel';
|
|
29
29
|
import { FormItemModel } from './FormItemModel';
|
|
30
30
|
|
|
31
|
+
export const QUICK_EDIT_POPOVER_MAX_HEIGHT = 'calc(100vh - 96px)';
|
|
32
|
+
export const QUICK_EDIT_FORM_MAX_HEIGHT = 'calc(100vh - 160px)';
|
|
33
|
+
export const QUICK_EDIT_MARKDOWN_HEIGHT = 'min(480px, calc(100vh - 320px))';
|
|
34
|
+
|
|
35
|
+
export function getQuickEditFieldProps(collectionField: CollectionField, fieldProps?: Record<string, any>) {
|
|
36
|
+
const nextProps = { ...collectionField.getComponentProps(), ...(fieldProps || {}) };
|
|
37
|
+
if (['markdown', 'vditor'].includes(collectionField.interface) && nextProps.height == null) {
|
|
38
|
+
nextProps.height = QUICK_EDIT_MARKDOWN_HEIGHT;
|
|
39
|
+
}
|
|
40
|
+
return nextProps;
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
export class QuickEditFormModel extends FlowModel {
|
|
32
44
|
fieldPath: string;
|
|
33
45
|
|
|
@@ -99,7 +111,10 @@ export class QuickEditFormModel extends FlowModel {
|
|
|
99
111
|
placement: 'rightTop',
|
|
100
112
|
styles: {
|
|
101
113
|
body: {
|
|
102
|
-
width:
|
|
114
|
+
width: 420,
|
|
115
|
+
maxHeight: QUICK_EDIT_POPOVER_MAX_HEIGHT,
|
|
116
|
+
overflowY: 'auto',
|
|
117
|
+
overscrollBehavior: 'contain',
|
|
103
118
|
},
|
|
104
119
|
},
|
|
105
120
|
content: (popover) => {
|
|
@@ -160,20 +175,28 @@ export class QuickEditFormModel extends FlowModel {
|
|
|
160
175
|
|
|
161
176
|
return (
|
|
162
177
|
<FormComponent model={this}>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
<div
|
|
179
|
+
style={{
|
|
180
|
+
minHeight: 0,
|
|
181
|
+
overflowY: 'auto',
|
|
182
|
+
maxHeight: QUICK_EDIT_FORM_MAX_HEIGHT,
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{this.mapSubModels('fields', (field) => {
|
|
186
|
+
return (
|
|
187
|
+
<FormItem
|
|
188
|
+
showLabel={false}
|
|
189
|
+
name={this.fieldPath}
|
|
190
|
+
key={field.uid}
|
|
191
|
+
initialValue={this.context.record?.[this.fieldPath]}
|
|
192
|
+
{...this.props}
|
|
193
|
+
>
|
|
194
|
+
<FieldModelRenderer model={field} fallback={<Skeleton.Input size="small" />} />
|
|
195
|
+
</FormItem>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</div>
|
|
199
|
+
<Space style={{ display: 'flex', justifyContent: 'flex-end', flexShrink: 0 }}>
|
|
177
200
|
<Button
|
|
178
201
|
onClick={() => {
|
|
179
202
|
this.viewContainer.close();
|
|
@@ -257,7 +280,7 @@ QuickEditFormModel.registerFlow({
|
|
|
257
280
|
},
|
|
258
281
|
},
|
|
259
282
|
});
|
|
260
|
-
fieldModel.setProps(
|
|
283
|
+
fieldModel.setProps(getQuickEditFieldProps(collectionField, ctx.model._fieldProps));
|
|
261
284
|
fieldModel.setProps({ sourceFieldModelUid: ctx.inputArgs.sourceFieldModelUid });
|
|
262
285
|
ctx.model.context.defineProperty('collectionField', {
|
|
263
286
|
get: () => collectionField,
|
|
@@ -85,6 +85,7 @@ async function setupFormModel() {
|
|
|
85
85
|
{ name: 'assignees', type: 'belongsToMany', target: 'users', interface: 'm2m' },
|
|
86
86
|
{ name: 'note', type: 'string', interface: 'text' },
|
|
87
87
|
{ name: 'status', type: 'string', interface: 'text' },
|
|
88
|
+
{ name: 'rawPayload', type: 'json', filterable: true },
|
|
88
89
|
],
|
|
89
90
|
});
|
|
90
91
|
|
|
@@ -283,6 +284,27 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
|
|
|
283
284
|
expect(params.note).toBeUndefined();
|
|
284
285
|
});
|
|
285
286
|
|
|
287
|
+
it('keeps interfaced fields in formValues meta even when they are not configured in the form grid', async () => {
|
|
288
|
+
const model = await setupFormModel();
|
|
289
|
+
|
|
290
|
+
function HookCaller() {
|
|
291
|
+
model.useHooksBeforeRender();
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
render(React.createElement(HookCaller));
|
|
295
|
+
mockFormGridEnabledFields(model, ['customer', 'note']);
|
|
296
|
+
|
|
297
|
+
const opt = (model.context as any).getPropertyOptions('formValues');
|
|
298
|
+
const meta = await opt.meta();
|
|
299
|
+
const props = await meta.properties();
|
|
300
|
+
|
|
301
|
+
expect(props).toHaveProperty('customer');
|
|
302
|
+
expect(props).toHaveProperty('note');
|
|
303
|
+
expect(props).toHaveProperty('status');
|
|
304
|
+
expect(props).toHaveProperty('assignees');
|
|
305
|
+
expect(props).not.toHaveProperty('rawPayload');
|
|
306
|
+
});
|
|
307
|
+
|
|
286
308
|
it('registers formValuesChange event and eventSettings flow', async () => {
|
|
287
309
|
const engine = new FlowEngine();
|
|
288
310
|
const TestFormModel = await createTestFormModelSubclass();
|
|
@@ -30,6 +30,27 @@ import { TableCustomColumnModel } from './TableCustomColumnModel';
|
|
|
30
30
|
import { CodeEditor } from '../../../components/code-editor';
|
|
31
31
|
import { resolveRunJsParams } from '../../utils/resolveRunJsParams';
|
|
32
32
|
|
|
33
|
+
function getRecordRenderSignature(record: any) {
|
|
34
|
+
if (!record || typeof record !== 'object') {
|
|
35
|
+
return String(record);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const seen = new WeakSet();
|
|
40
|
+
return JSON.stringify(record, (_key, value) => {
|
|
41
|
+
if (value && typeof value === 'object') {
|
|
42
|
+
if (seen.has(value)) {
|
|
43
|
+
return '[Circular]';
|
|
44
|
+
}
|
|
45
|
+
seen.add(value);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return String(record);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
export class JSColumnModel extends TableCustomColumnModel {
|
|
34
55
|
// Stable per‑instance render component to avoid remounts across re-renders
|
|
35
56
|
private _RenderComponent?: React.ComponentType;
|
|
@@ -113,7 +134,13 @@ export class JSColumnModel extends TableCustomColumnModel {
|
|
|
113
134
|
// 使用记录主键作为 fork key,避免分页后 index 复用导致 fork 复用
|
|
114
135
|
const tk = this.context.collection?.getFilterByTK?.(record);
|
|
115
136
|
const forkKey = tk ?? record?.id ?? index;
|
|
137
|
+
const recordSignature = getRecordRenderSignature(record);
|
|
116
138
|
const fork = this.createFork({}, String(forkKey));
|
|
139
|
+
const previousRecordSignature = (fork as any).__recordRenderSignature;
|
|
140
|
+
if (previousRecordSignature !== recordSignature) {
|
|
141
|
+
(fork as any).__recordRenderSignature = recordSignature;
|
|
142
|
+
fork.invalidateFlowCache('beforeRender');
|
|
143
|
+
}
|
|
117
144
|
const recordMeta: PropertyMetaFactory = createRecordMetaFactory(
|
|
118
145
|
() => fork.context.collection,
|
|
119
146
|
fork.context.t('Current record'),
|
|
@@ -137,7 +164,7 @@ export class JSColumnModel extends TableCustomColumnModel {
|
|
|
137
164
|
fork.context.defineProperty('recordIndex', {
|
|
138
165
|
get: () => index,
|
|
139
166
|
});
|
|
140
|
-
return <MemoFlowModelRenderer key={fork.uid} model={fork} />;
|
|
167
|
+
return <MemoFlowModelRenderer key={`${fork.uid}:${recordSignature}`} model={fork} />;
|
|
141
168
|
},
|
|
142
169
|
};
|
|
143
170
|
}
|
|
@@ -264,7 +291,8 @@ JSColumnModel.registerFlow({
|
|
|
264
291
|
|
|
265
292
|
ctx.onRefReady(ctx.ref, async (element) => {
|
|
266
293
|
ctx.defineProperty('element', {
|
|
267
|
-
get: () => new ElementProxy(element),
|
|
294
|
+
get: () => new ElementProxy((ctx.ref?.current as HTMLElement | null) || element),
|
|
295
|
+
cache: false,
|
|
268
296
|
});
|
|
269
297
|
const navigator = createSafeNavigator();
|
|
270
298
|
await ctx.runjs(
|
|
@@ -314,7 +314,14 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
|
|
|
314
314
|
onSuccess: (values) => {
|
|
315
315
|
const collectionField = this.collection.getField(dataIndex);
|
|
316
316
|
record[dataIndex] = values[dataIndex];
|
|
317
|
-
|
|
317
|
+
if (typeof recordIndex === 'number') {
|
|
318
|
+
this.resource.setItem(recordIndex, record);
|
|
319
|
+
} else {
|
|
320
|
+
const nextData = _.cloneDeep(this.resource.getData());
|
|
321
|
+
setNestedValue(nextData, recordIndex, record);
|
|
322
|
+
this.resource.setData(nextData);
|
|
323
|
+
}
|
|
324
|
+
this.resource.emit('refresh');
|
|
318
325
|
// 仅重渲染单元格
|
|
319
326
|
const fork: ForkFlowModel = model.subModels.field.createFork({}, `${recordIndex}`);
|
|
320
327
|
// Provide expandable meta for current row record based on the collection in context
|
|
@@ -313,6 +313,7 @@ export class TableColumnModel extends DisplayItemModel {
|
|
|
313
313
|
? this.fieldPath.replace(`${this.context.prefixFieldPath}.`, '')
|
|
314
314
|
: this.fieldPath;
|
|
315
315
|
const value = get(record, namePath);
|
|
316
|
+
fork.setProps({ value });
|
|
316
317
|
return (
|
|
317
318
|
<FormItem key={field.uid} {...omit(this.props, 'title')} value={value} noStyle={true}>
|
|
318
319
|
<FieldModelRenderer model={fork} />
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { JSColumnModel } from '../JSColumnModel';
|
|
13
|
+
|
|
14
|
+
describe('JSColumnModel row refresh', () => {
|
|
15
|
+
it('changes renderer key and invalidates beforeRender cache when row content changes', () => {
|
|
16
|
+
const engine = new FlowEngine();
|
|
17
|
+
const model = new JSColumnModel({
|
|
18
|
+
uid: 'js-column-row-refresh',
|
|
19
|
+
flowEngine: engine,
|
|
20
|
+
props: {
|
|
21
|
+
width: 200,
|
|
22
|
+
title: 'JS column',
|
|
23
|
+
},
|
|
24
|
+
} as any);
|
|
25
|
+
|
|
26
|
+
engine.context.dataSourceManager.getDataSource('main').addCollection({
|
|
27
|
+
name: 'users',
|
|
28
|
+
filterTargetKey: 'id',
|
|
29
|
+
fields: [
|
|
30
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
31
|
+
{ name: 'age', type: 'integer', interface: 'integer' },
|
|
32
|
+
{ name: 'workyears', type: 'float', interface: 'number' },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const collection = engine.context.dataSourceManager.getCollection('main', 'users');
|
|
37
|
+
model.context.defineProperty('collection', {
|
|
38
|
+
value: collection,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const column = model.getColumnProps();
|
|
42
|
+
const first = column.render(null, { id: 1, age: 3, workyears: 39.2 }, 0) as any;
|
|
43
|
+
const fork = model.getFork('1') as any;
|
|
44
|
+
const invalidateFlowCache = vi.fn();
|
|
45
|
+
fork.invalidateFlowCache = invalidateFlowCache;
|
|
46
|
+
const second = column.render(null, { id: 1, age: 37, workyears: 39.2 }, 0) as any;
|
|
47
|
+
|
|
48
|
+
expect(first.key).not.toBe(second.key);
|
|
49
|
+
expect(invalidateFlowCache).toHaveBeenCalledWith('beforeRender');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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 { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
|
|
13
|
+
describe('TableBlockModel quick edit refresh', () => {
|
|
14
|
+
it('updates table data through a new array reference after quick editing a flat row', () => {
|
|
15
|
+
const engine = new FlowEngine();
|
|
16
|
+
const resource = engine.context.createResource(MultiRecordResource);
|
|
17
|
+
const original = [
|
|
18
|
+
{ id: 1, title: 'old title', status: 'draft' },
|
|
19
|
+
{ id: 2, title: 'other title', status: 'published' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
resource.setData(original);
|
|
23
|
+
const before = resource.getData();
|
|
24
|
+
const editedRecord = { ...before[0], title: 'new title' };
|
|
25
|
+
|
|
26
|
+
resource.setItem(0, editedRecord);
|
|
27
|
+
|
|
28
|
+
expect(resource.getData()).not.toBe(before);
|
|
29
|
+
expect(resource.getData()[0]).toEqual(editedRecord);
|
|
30
|
+
expect(resource.getData()[1]).toEqual(original[1]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('notifies mounted JS fields after quick editing local table data', () => {
|
|
34
|
+
const engine = new FlowEngine();
|
|
35
|
+
const resource = engine.context.createResource(MultiRecordResource);
|
|
36
|
+
const original = [{ id: 1, title: 'old title', status: 'draft' }];
|
|
37
|
+
let refreshCount = 0;
|
|
38
|
+
resource.on('refresh', () => {
|
|
39
|
+
refreshCount += 1;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
resource.setData(original);
|
|
43
|
+
const editedRecord = { ...resource.getData()[0], title: 'new title' };
|
|
44
|
+
resource.setItem(0, editedRecord);
|
|
45
|
+
resource.emit('refresh');
|
|
46
|
+
|
|
47
|
+
expect(refreshCount).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
});
|