@nocobase/client-v2 2.1.0-beta.25 → 2.1.0-beta.26
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/flow/actions/linkageRulesFormValueRefresh.d.ts +10 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
- package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
- package/es/flow/models/actions/index.d.ts +3 -0
- package/es/flow/models/base/GridModel.d.ts +3 -1
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
- package/es/index.d.ts +1 -0
- package/es/index.mjs +80 -80
- package/lib/index.js +87 -87
- package/package.json +5 -5
- package/src/__tests__/globalDeps.test.ts +5 -0
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
- package/src/flow/actions/linkageRules.tsx +8 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
- package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
- package/src/flow/index.ts +1 -0
- package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
- package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
- package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
- package/src/flow/models/actions/index.ts +3 -0
- package/src/flow/models/base/GridModel.tsx +21 -1
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +21 -5
- package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
- package/src/index.ts +1 -0
- package/src/utils/globalDeps.ts +6 -0
|
@@ -0,0 +1,90 @@
|
|
|
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 type { FlowModelContext } from '@nocobase/flow-engine';
|
|
11
|
+
|
|
12
|
+
export const getAssociationBlockResourceSettings = (ctx: FlowModelContext | any) => {
|
|
13
|
+
const blockModel = ctx?.blockModel || ctx?.model?.context?.blockModel;
|
|
14
|
+
return (
|
|
15
|
+
blockModel?.getResourceSettingsInitParams?.() ||
|
|
16
|
+
blockModel?.getStepParams?.('resourceSettings', 'init') ||
|
|
17
|
+
ctx?.model?.getStepParams?.('resourceSettings', 'init')
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const isAssociationBlockContext = (ctx: FlowModelContext | any) => {
|
|
22
|
+
return !!getAssociationBlockResourceSettings(ctx)?.associationName;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getAssociationTargetResourceSettings = (ctx: FlowModelContext | any) => {
|
|
26
|
+
const resourceSettings = getAssociationBlockResourceSettings(ctx);
|
|
27
|
+
const association = ctx?.blockModel?.association || ctx?.model?.context?.blockModel?.association;
|
|
28
|
+
const targetCollection = association?.targetCollection;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
dataSourceKey: targetCollection?.dataSourceKey || resourceSettings?.dataSourceKey,
|
|
32
|
+
collectionName: targetCollection?.name || association?.target || resourceSettings?.collectionName,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const callAssociationResourceAction = async (resource: any, action: 'add' | 'remove', values: any[]) => {
|
|
37
|
+
if (typeof resource?.[action] === 'function') {
|
|
38
|
+
return await resource[action]({ values });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await resource?.runAction?.(action, {
|
|
42
|
+
data: values,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const applyDisassociateAction = async (ctx: FlowModelContext | any) => {
|
|
47
|
+
const resource = ctx?.blockModel?.resource || ctx?.resource;
|
|
48
|
+
const collection = ctx?.blockModel?.collection || ctx?.collection;
|
|
49
|
+
|
|
50
|
+
if (!isAssociationBlockContext(ctx)) {
|
|
51
|
+
ctx.message?.error?.(ctx.t('No association block selected'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!resource) {
|
|
55
|
+
ctx.message?.error?.(ctx.t('No resource selected for disassociation'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!ctx.record) {
|
|
59
|
+
ctx.message?.error?.(ctx.t('No record selected for disassociation'));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filterByTk = collection?.getFilterByTK?.(ctx.record);
|
|
64
|
+
await callAssociationResourceAction(resource, 'remove', [filterByTk]);
|
|
65
|
+
await resource.refresh?.();
|
|
66
|
+
ctx.message?.success?.(ctx.t('Record disassociated successfully'));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const applyAssociateAction = async (ctx: FlowModelContext | any, selectedRows: any[]) => {
|
|
70
|
+
const resource = ctx?.blockModel?.resource || ctx?.resource;
|
|
71
|
+
const collection = ctx?.blockModel?.collection || ctx?.collection;
|
|
72
|
+
|
|
73
|
+
if (!isAssociationBlockContext(ctx)) {
|
|
74
|
+
ctx.message?.error?.(ctx.t('No association block selected'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!resource) {
|
|
78
|
+
ctx.message?.error?.(ctx.t('No resource selected for association'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!selectedRows?.length) {
|
|
82
|
+
ctx.message?.warning?.(ctx.t('Please select at least one record'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const values = selectedRows.map((row) => collection?.getFilterByTK?.(row) ?? row).filter((value) => value != null);
|
|
87
|
+
await callAssociationResourceAction(resource, 'add', values);
|
|
88
|
+
await resource.refresh?.();
|
|
89
|
+
ctx.message?.success?.(ctx.t('Record associated successfully'));
|
|
90
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { tExpr } from '@nocobase/flow-engine';
|
|
11
|
+
import type { ButtonProps } from 'antd';
|
|
12
|
+
import { ActionModel, ActionSceneEnum } from '../base';
|
|
13
|
+
import { applyDisassociateAction, isAssociationBlockContext } from './AssociationActionUtils';
|
|
14
|
+
|
|
15
|
+
export class DisassociateActionModel extends ActionModel {
|
|
16
|
+
static scene = ActionSceneEnum.record;
|
|
17
|
+
static capabilityActionName = 'update';
|
|
18
|
+
|
|
19
|
+
defaultProps: ButtonProps = {
|
|
20
|
+
type: 'link',
|
|
21
|
+
title: tExpr('Disassociate'),
|
|
22
|
+
icon: 'DisconnectOutlined',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
getAclActionName() {
|
|
26
|
+
return 'update';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
DisassociateActionModel.define({
|
|
31
|
+
label: tExpr('Disassociate'),
|
|
32
|
+
sort: 65,
|
|
33
|
+
hide(ctx) {
|
|
34
|
+
return !isAssociationBlockContext(ctx);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
DisassociateActionModel.registerFlow({
|
|
39
|
+
key: 'disassociateSettings',
|
|
40
|
+
title: tExpr('Disassociate settings'),
|
|
41
|
+
on: 'click',
|
|
42
|
+
steps: {
|
|
43
|
+
confirm: {
|
|
44
|
+
use: 'confirm',
|
|
45
|
+
defaultParams: {
|
|
46
|
+
enable: true,
|
|
47
|
+
title: tExpr('Disassociate record'),
|
|
48
|
+
content: tExpr('Are you sure you want to disassociate it?'),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
disassociate: {
|
|
52
|
+
async handler(ctx) {
|
|
53
|
+
await applyDisassociateAction(ctx);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
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 { BaseRecordResource, FlowEngine } from '@nocobase/flow-engine';
|
|
12
|
+
import {
|
|
13
|
+
applyDisassociateAction,
|
|
14
|
+
ActionModel,
|
|
15
|
+
applyAssociateAction,
|
|
16
|
+
AssociateActionModel,
|
|
17
|
+
CollectionActionGroupModel,
|
|
18
|
+
DisassociateActionModel,
|
|
19
|
+
getAssociationTargetResourceSettings,
|
|
20
|
+
PopupActionModel,
|
|
21
|
+
RecordActionGroupModel,
|
|
22
|
+
} from '../../..';
|
|
23
|
+
|
|
24
|
+
class TestAssociationResource<T = any> extends BaseRecordResource<T> {
|
|
25
|
+
async refresh(): Promise<void> {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const createEngine = () => {
|
|
29
|
+
const engine = new FlowEngine();
|
|
30
|
+
engine.registerModels({
|
|
31
|
+
ActionModel,
|
|
32
|
+
AssociateActionModel,
|
|
33
|
+
DisassociateActionModel,
|
|
34
|
+
PopupActionModel,
|
|
35
|
+
CollectionActionGroupModel,
|
|
36
|
+
RecordActionGroupModel,
|
|
37
|
+
});
|
|
38
|
+
return engine;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const createContext = (engine: FlowEngine, resourceSettingsInit: any) => {
|
|
42
|
+
const collection = {
|
|
43
|
+
options: {
|
|
44
|
+
availableActions: ['list', 'update'],
|
|
45
|
+
},
|
|
46
|
+
getFilterByTK: vi.fn((record) => record.id),
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
engine,
|
|
50
|
+
collection,
|
|
51
|
+
blockModel: {
|
|
52
|
+
collection,
|
|
53
|
+
getResourceSettingsInitParams: () => resourceSettingsInit,
|
|
54
|
+
},
|
|
55
|
+
} as any;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe('association action models', () => {
|
|
59
|
+
it('uses update permission for association operations', () => {
|
|
60
|
+
expect(AssociateActionModel.prototype.getAclActionName.call({})).toBe('update');
|
|
61
|
+
expect(DisassociateActionModel.prototype.getAclActionName.call({})).toBe('update');
|
|
62
|
+
expect(AssociateActionModel.capabilityActionName).toBe('update');
|
|
63
|
+
expect(DisassociateActionModel.capabilityActionName).toBe('update');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('uses association target collection for selector table blocks', () => {
|
|
67
|
+
const ctx: any = {
|
|
68
|
+
blockModel: {
|
|
69
|
+
association: {
|
|
70
|
+
target: 'transports',
|
|
71
|
+
targetCollection: {
|
|
72
|
+
name: 'transports',
|
|
73
|
+
dataSourceKey: 'main',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
getResourceSettingsInitParams: () => ({
|
|
77
|
+
dataSourceKey: 'main',
|
|
78
|
+
collectionName: 'orders',
|
|
79
|
+
associationName: 'products.transports',
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
expect(getAssociationTargetResourceSettings(ctx)).toEqual({
|
|
85
|
+
dataSourceKey: 'main',
|
|
86
|
+
collectionName: 'transports',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('shows Associate only in collection actions of association blocks', async () => {
|
|
91
|
+
const engine = createEngine();
|
|
92
|
+
|
|
93
|
+
const relationItems = await CollectionActionGroupModel.defineChildren(
|
|
94
|
+
createContext(engine, {
|
|
95
|
+
dataSourceKey: 'main',
|
|
96
|
+
collectionName: 'orders',
|
|
97
|
+
associationName: 'products.o2m_orders',
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
const normalItems = await CollectionActionGroupModel.defineChildren(
|
|
101
|
+
createContext(engine, {
|
|
102
|
+
dataSourceKey: 'main',
|
|
103
|
+
collectionName: 'orders',
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(relationItems.map((item: any) => item.useModel)).toContain('AssociateActionModel');
|
|
108
|
+
expect(relationItems.map((item: any) => item.useModel)).not.toContain('DisassociateActionModel');
|
|
109
|
+
expect(normalItems.map((item: any) => item.useModel)).not.toContain('AssociateActionModel');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('shows Disassociate only in record actions of association blocks', async () => {
|
|
113
|
+
const engine = createEngine();
|
|
114
|
+
|
|
115
|
+
const relationItems = await RecordActionGroupModel.defineChildren(
|
|
116
|
+
createContext(engine, {
|
|
117
|
+
dataSourceKey: 'main',
|
|
118
|
+
collectionName: 'orders',
|
|
119
|
+
associationName: 'products.o2m_orders',
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
const normalItems = await RecordActionGroupModel.defineChildren(
|
|
123
|
+
createContext(engine, {
|
|
124
|
+
dataSourceKey: 'main',
|
|
125
|
+
collectionName: 'orders',
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(relationItems.map((item: any) => item.useModel)).toContain('DisassociateActionModel');
|
|
130
|
+
expect(relationItems.map((item: any) => item.useModel)).not.toContain('AssociateActionModel');
|
|
131
|
+
expect(normalItems.map((item: any) => item.useModel)).not.toContain('DisassociateActionModel');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('disassociates the current record through association resource remove action', async () => {
|
|
135
|
+
const resource = {
|
|
136
|
+
runAction: vi.fn(async () => ({})),
|
|
137
|
+
refresh: vi.fn(async () => {}),
|
|
138
|
+
};
|
|
139
|
+
const collection = {
|
|
140
|
+
getFilterByTK: vi.fn(() => 12),
|
|
141
|
+
};
|
|
142
|
+
const ctx: any = {
|
|
143
|
+
blockModel: {
|
|
144
|
+
collection,
|
|
145
|
+
resource,
|
|
146
|
+
getResourceSettingsInitParams: () => ({
|
|
147
|
+
dataSourceKey: 'main',
|
|
148
|
+
collectionName: 'orders',
|
|
149
|
+
associationName: 'products.o2m_orders',
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
record: {
|
|
153
|
+
id: 12,
|
|
154
|
+
},
|
|
155
|
+
message: {
|
|
156
|
+
success: vi.fn(),
|
|
157
|
+
error: vi.fn(),
|
|
158
|
+
},
|
|
159
|
+
t: (value: string) => value,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
await applyDisassociateAction(ctx);
|
|
163
|
+
|
|
164
|
+
expect(collection.getFilterByTK).toHaveBeenCalledWith(ctx.record);
|
|
165
|
+
expect(resource.runAction).toHaveBeenCalledWith('remove', {
|
|
166
|
+
data: [12],
|
|
167
|
+
});
|
|
168
|
+
expect(resource.refresh).toHaveBeenCalled();
|
|
169
|
+
expect(ctx.message.success).toHaveBeenCalledWith('Record disassociated successfully');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('associates selected records through association resource add action', async () => {
|
|
173
|
+
const resource = {
|
|
174
|
+
runAction: vi.fn(async () => ({})),
|
|
175
|
+
refresh: vi.fn(async () => {}),
|
|
176
|
+
};
|
|
177
|
+
const collection = {
|
|
178
|
+
getFilterByTK: vi.fn((record) => record.id),
|
|
179
|
+
};
|
|
180
|
+
const ctx: any = {
|
|
181
|
+
blockModel: {
|
|
182
|
+
collection,
|
|
183
|
+
resource,
|
|
184
|
+
getResourceSettingsInitParams: () => ({
|
|
185
|
+
dataSourceKey: 'main',
|
|
186
|
+
collectionName: 'orders',
|
|
187
|
+
associationName: 'products.o2m_orders',
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
message: {
|
|
191
|
+
success: vi.fn(),
|
|
192
|
+
warning: vi.fn(),
|
|
193
|
+
error: vi.fn(),
|
|
194
|
+
},
|
|
195
|
+
t: (value: string) => value,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
await applyAssociateAction(ctx, [{ id: 11 }, { id: 12 }]);
|
|
199
|
+
|
|
200
|
+
expect(collection.getFilterByTK).toHaveBeenCalledWith({ id: 11 });
|
|
201
|
+
expect(collection.getFilterByTK).toHaveBeenCalledWith({ id: 12 });
|
|
202
|
+
expect(resource.runAction).toHaveBeenCalledWith('add', {
|
|
203
|
+
data: [11, 12],
|
|
204
|
+
});
|
|
205
|
+
expect(resource.refresh).toHaveBeenCalled();
|
|
206
|
+
expect(ctx.message.success).toHaveBeenCalledWith('Record associated successfully');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('uses nested association resource url when adding associated records', async () => {
|
|
210
|
+
const engine = createEngine();
|
|
211
|
+
const resource = engine.createResource(TestAssociationResource);
|
|
212
|
+
const api = {
|
|
213
|
+
request: vi.fn().mockResolvedValue({ data: { data: {} } }),
|
|
214
|
+
};
|
|
215
|
+
resource.setAPIClient(api as any);
|
|
216
|
+
resource.setResourceName('products.o2m_orders');
|
|
217
|
+
resource.setSourceId('362872646860800');
|
|
218
|
+
|
|
219
|
+
const ctx: any = {
|
|
220
|
+
blockModel: {
|
|
221
|
+
collection: {
|
|
222
|
+
getFilterByTK: vi.fn((record) => record.id),
|
|
223
|
+
},
|
|
224
|
+
resource,
|
|
225
|
+
getResourceSettingsInitParams: () => ({
|
|
226
|
+
dataSourceKey: 'main',
|
|
227
|
+
collectionName: 'orders',
|
|
228
|
+
associationName: 'products.o2m_orders',
|
|
229
|
+
sourceId: '362872646860800',
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
message: {
|
|
233
|
+
success: vi.fn(),
|
|
234
|
+
warning: vi.fn(),
|
|
235
|
+
error: vi.fn(),
|
|
236
|
+
},
|
|
237
|
+
t: (value: string) => value,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
await applyAssociateAction(ctx, [{ id: 11 }]);
|
|
241
|
+
|
|
242
|
+
expect(api.request).toHaveBeenCalledWith(
|
|
243
|
+
expect.objectContaining({
|
|
244
|
+
method: 'post',
|
|
245
|
+
url: 'products/362872646860800/o2m_orders:add',
|
|
246
|
+
data: [11],
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export * from './AddNewActionModel';
|
|
11
|
+
export * from './AssociateActionModel';
|
|
12
|
+
export * from './AssociationActionUtils';
|
|
11
13
|
export * from './BulkDeleteActionModel';
|
|
12
14
|
export * from './DeleteActionModel';
|
|
15
|
+
export * from './DisassociateActionModel';
|
|
13
16
|
export * from './EditActionModel';
|
|
14
17
|
export * from './FilterActionModel';
|
|
15
18
|
export * from './JSActionModel';
|
|
@@ -118,6 +118,14 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
118
118
|
private dragState?: DragState;
|
|
119
119
|
private _memoItemFlowSettings?: Exclude<FlowModelRendererProps['showFlowSettings'], boolean>;
|
|
120
120
|
|
|
121
|
+
onInit(options) {
|
|
122
|
+
super.onInit(options);
|
|
123
|
+
// 历史数据里可能残留拖拽高亮框,初始化时立即清理,避免刷新页面后常驻显示。
|
|
124
|
+
if (this.props.dragOverlayRect) {
|
|
125
|
+
this.setProps('dragOverlayRect', null);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
private updateDragPointerPosition = (event: Event) => {
|
|
122
130
|
if (!this.dragState) {
|
|
123
131
|
return;
|
|
@@ -243,6 +251,12 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
243
251
|
return this.normalizeLayoutFromSource();
|
|
244
252
|
}
|
|
245
253
|
|
|
254
|
+
serialize(): Record<string, any> {
|
|
255
|
+
const data = super.serialize();
|
|
256
|
+
data.props = _.omit(data.props, ['dragOverlayRect']);
|
|
257
|
+
return data;
|
|
258
|
+
}
|
|
259
|
+
|
|
246
260
|
syncLayoutProps(layout: GridLayoutV2) {
|
|
247
261
|
const projection = projectLayoutToLegacyRows(layout);
|
|
248
262
|
this.setProps('layout', layout);
|
|
@@ -801,11 +815,17 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
801
815
|
this.setProps('dragOverlayRect', null);
|
|
802
816
|
}
|
|
803
817
|
|
|
804
|
-
handleDragEnd(
|
|
818
|
+
handleDragEnd(event: DragEndEvent) {
|
|
805
819
|
if (!this.dragState) {
|
|
806
820
|
return;
|
|
807
821
|
}
|
|
808
822
|
|
|
823
|
+
const finalPoint = this.computePointerPosition(event);
|
|
824
|
+
if (finalPoint) {
|
|
825
|
+
const finalSlot = this.resolveDragSlot(finalPoint);
|
|
826
|
+
this.applyPreview(finalSlot);
|
|
827
|
+
}
|
|
828
|
+
|
|
809
829
|
const previewLayout = this.dragState.previewLayout;
|
|
810
830
|
if (previewLayout) {
|
|
811
831
|
if (previewLayout.layout) {
|
|
@@ -283,4 +283,102 @@ describe('GridModel drag snapshot container', () => {
|
|
|
283
283
|
type: 'column',
|
|
284
284
|
});
|
|
285
285
|
});
|
|
286
|
+
|
|
287
|
+
it('clears persisted drag overlay on init', () => {
|
|
288
|
+
const model = engine.createModel<GridModel>({
|
|
289
|
+
use: 'GridModel',
|
|
290
|
+
uid: 'grid-hidden-stale-overlay',
|
|
291
|
+
props: {
|
|
292
|
+
dragOverlayRect: {
|
|
293
|
+
top: 10,
|
|
294
|
+
left: 20,
|
|
295
|
+
width: 100,
|
|
296
|
+
height: 40,
|
|
297
|
+
type: 'column',
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
structure: {} as any,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(model.props.dragOverlayRect).toBeNull();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('omits drag overlay rect from serialized props', () => {
|
|
307
|
+
const model = engine.createModel<GridModel>({
|
|
308
|
+
use: 'GridModel',
|
|
309
|
+
uid: 'grid-serialize-overlay',
|
|
310
|
+
props: {
|
|
311
|
+
rowGap: 24,
|
|
312
|
+
dragOverlayRect: {
|
|
313
|
+
top: 10,
|
|
314
|
+
left: 20,
|
|
315
|
+
width: 100,
|
|
316
|
+
height: 40,
|
|
317
|
+
type: 'column',
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
structure: {} as any,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const serialized = model.serialize();
|
|
324
|
+
|
|
325
|
+
expect(serialized.props).toMatchObject({ rowGap: 24 });
|
|
326
|
+
expect(serialized.props).not.toHaveProperty('dragOverlayRect');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('recomputes the final slot from drag end position before saving layout', () => {
|
|
330
|
+
const model = engine.createModel<GridModel>({
|
|
331
|
+
use: 'GridModel',
|
|
332
|
+
uid: 'grid-drag-final-slot',
|
|
333
|
+
props: {},
|
|
334
|
+
structure: {} as any,
|
|
335
|
+
});
|
|
336
|
+
const applyPreview = vi.fn((slot) => {
|
|
337
|
+
(model as any).dragState.previewLayout = slot
|
|
338
|
+
? {
|
|
339
|
+
rows: { 'row-final': [['item-1']] },
|
|
340
|
+
sizes: { 'row-final': [24] },
|
|
341
|
+
}
|
|
342
|
+
: undefined;
|
|
343
|
+
});
|
|
344
|
+
const resolveDragSlot = vi.fn(() => ({
|
|
345
|
+
type: 'row-gap',
|
|
346
|
+
targetRowId: 'row-final',
|
|
347
|
+
position: 'below',
|
|
348
|
+
rect: { top: 200, left: 20, width: 440, height: 32 },
|
|
349
|
+
}));
|
|
350
|
+
const saveGridLayout = vi.fn();
|
|
351
|
+
const syncLayoutProps = vi.fn();
|
|
352
|
+
const finishDrag = vi.fn();
|
|
353
|
+
|
|
354
|
+
(model as any).applyPreview = applyPreview;
|
|
355
|
+
(model as any).resolveDragSlot = resolveDragSlot;
|
|
356
|
+
(model as any).saveGridLayout = saveGridLayout;
|
|
357
|
+
(model as any).syncLayoutProps = syncLayoutProps;
|
|
358
|
+
(model as any).finishDrag = finishDrag;
|
|
359
|
+
(model as any).dragState = {
|
|
360
|
+
sourceUid: 'item-1',
|
|
361
|
+
snapshot: { rows: {}, sizes: {} },
|
|
362
|
+
slots: [],
|
|
363
|
+
containerEl: null,
|
|
364
|
+
containerRect: { top: 0, left: 0, width: 0, height: 0 },
|
|
365
|
+
pointerOrigin: { x: 100, y: 100 },
|
|
366
|
+
activeSlotKey: null,
|
|
367
|
+
previewLayout: undefined,
|
|
368
|
+
refreshTimer: null,
|
|
369
|
+
generatedIds: new Map(),
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
model.handleDragEnd({
|
|
373
|
+
delta: { x: 30, y: 220 },
|
|
374
|
+
} as any);
|
|
375
|
+
|
|
376
|
+
expect(resolveDragSlot).toHaveBeenCalledWith({ x: 130, y: 320 });
|
|
377
|
+
expect(applyPreview).toHaveBeenCalledOnce();
|
|
378
|
+
expect(saveGridLayout).toHaveBeenCalledWith({
|
|
379
|
+
rows: { 'row-final': [['item-1']] },
|
|
380
|
+
sizes: { 'row-final': [24] },
|
|
381
|
+
});
|
|
382
|
+
expect(finishDrag).toHaveBeenCalledOnce();
|
|
383
|
+
});
|
|
286
384
|
});
|
|
@@ -38,7 +38,8 @@ import {
|
|
|
38
38
|
import { MobileLazySelect } from '../mobile-components/MobileLazySelect';
|
|
39
39
|
import { BlockSceneEnum } from '../../base/BlockModel';
|
|
40
40
|
import { ActionWithoutPermission } from '../../base/ActionModel';
|
|
41
|
-
import { EditFormModel } from '../../blocks
|
|
41
|
+
import { EditFormModel } from '../../blocks';
|
|
42
|
+
import { hasAncestorModel } from './recordSelectSettingsUtils';
|
|
42
43
|
|
|
43
44
|
function isPlainObject(val: unknown): val is Record<string, any> {
|
|
44
45
|
return !!val && typeof val === 'object' && !Array.isArray(val);
|
|
@@ -818,6 +819,9 @@ RecordSelectFieldModel.registerFlow({
|
|
|
818
819
|
if (ctx?.blockModel?.constructor?.scene === BlockSceneEnum.filter) {
|
|
819
820
|
return true;
|
|
820
821
|
}
|
|
822
|
+
if (hasAncestorModel(ctx?.model, ['SubTableColumnModel', 'SubTableFieldModel'])) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
821
825
|
return false;
|
|
822
826
|
},
|
|
823
827
|
defaultParams: {
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -194,7 +194,7 @@ function shouldCommitImmediately(value: any) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
const FieldModelRendererOptimize = React.memo((props: any) => {
|
|
197
|
-
const { model, onChange, value, ...rest } = props;
|
|
197
|
+
const { model, onChange, value, commitOnChange, ...rest } = props;
|
|
198
198
|
const pendingValueRef = React.useRef<any>(props?.value);
|
|
199
199
|
|
|
200
200
|
useEffect(() => {
|
|
@@ -204,11 +204,11 @@ const FieldModelRendererOptimize = React.memo((props: any) => {
|
|
|
204
204
|
const handleChange = React.useCallback(
|
|
205
205
|
(value: any) => {
|
|
206
206
|
pendingValueRef.current = value;
|
|
207
|
-
if (shouldCommitImmediately(value)) {
|
|
207
|
+
if (commitOnChange || shouldCommitImmediately(value)) {
|
|
208
208
|
onChange?.(value);
|
|
209
209
|
}
|
|
210
210
|
},
|
|
211
|
-
[onChange],
|
|
211
|
+
[commitOnChange, onChange],
|
|
212
212
|
);
|
|
213
213
|
|
|
214
214
|
const handleCommit = React.useCallback(() => {
|
|
@@ -241,10 +241,11 @@ interface CellProps {
|
|
|
241
241
|
rowFork?: any;
|
|
242
242
|
memoKey?: string;
|
|
243
243
|
width?: number;
|
|
244
|
+
commitOnChange?: boolean;
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
const MemoCell: React.FC<CellProps> = React.memo(
|
|
247
|
-
({ value, record, rowIdx, id, parent, parentFieldIndex, rowFork, width }) => {
|
|
248
|
+
({ value, record, rowIdx, id, parent, parentFieldIndex, rowFork, width, commitOnChange }) => {
|
|
248
249
|
const isNew = record?.__is_new__;
|
|
249
250
|
return (
|
|
250
251
|
<div
|
|
@@ -346,7 +347,11 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
346
347
|
}
|
|
347
348
|
/>
|
|
348
349
|
) : (
|
|
349
|
-
<FieldModelRendererOptimize
|
|
350
|
+
<FieldModelRendererOptimize
|
|
351
|
+
model={fork}
|
|
352
|
+
id={[(parent as any).context.fieldPath, rowIdx]}
|
|
353
|
+
commitOnChange={commitOnChange}
|
|
354
|
+
/>
|
|
350
355
|
)}
|
|
351
356
|
</FormItem>
|
|
352
357
|
);
|
|
@@ -360,6 +365,7 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
360
365
|
prev.id === next.id &&
|
|
361
366
|
prev.memoKey === next.memoKey &&
|
|
362
367
|
prev.width === next.width &&
|
|
368
|
+
prev.commitOnChange === next.commitOnChange &&
|
|
363
369
|
prev.rowIdx === next.rowIdx
|
|
364
370
|
);
|
|
365
371
|
},
|
|
@@ -428,6 +434,15 @@ export class SubTableColumnModel<
|
|
|
428
434
|
return this.parent.collection;
|
|
429
435
|
}
|
|
430
436
|
|
|
437
|
+
get hasFormulaColumn() {
|
|
438
|
+
return (
|
|
439
|
+
this.parent?.mapSubModels('columns', (column: SubTableColumnModel) => {
|
|
440
|
+
const field = column.collectionField;
|
|
441
|
+
return field?.interface === 'formula' || field?.type === 'formula';
|
|
442
|
+
}) || []
|
|
443
|
+
).some(Boolean);
|
|
444
|
+
}
|
|
445
|
+
|
|
431
446
|
onInit(options: any): void {
|
|
432
447
|
super.onInit(options);
|
|
433
448
|
this.context.defineProperty('resourceName', {
|
|
@@ -607,6 +622,7 @@ export class SubTableColumnModel<
|
|
|
607
622
|
rowFork={rowFork}
|
|
608
623
|
memoKey={rowForkKey}
|
|
609
624
|
width={this.props.width}
|
|
625
|
+
commitOnChange={this.hasFormulaColumn}
|
|
610
626
|
/>
|
|
611
627
|
);
|
|
612
628
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
export function hasAncestorModel(model: any, modelNames: string[]) {
|
|
11
|
+
let cursor = model;
|
|
12
|
+
while (cursor) {
|
|
13
|
+
const modelName = cursor?.constructor?.name;
|
|
14
|
+
if (modelName && modelNames.includes(modelName)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
cursor = cursor?.parent;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|