@nocobase/client-v2 2.1.0-beta.33 → 2.1.0-beta.35
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/APIClient.d.ts +16 -0
- package/es/Application.d.ts +2 -1
- package/es/BaseApplication.d.ts +6 -0
- package/es/PluginManager.d.ts +2 -0
- package/es/authRedirect.d.ts +9 -16
- package/es/components/form/EnvVariableInput.d.ts +8 -6
- package/es/components/form/VariableInput.d.ts +73 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
- package/es/components/form/table/SelectionCell.d.ts +36 -0
- package/es/components/form/table/Table.d.ts +82 -0
- package/es/components/form/table/constants.d.ts +15 -0
- package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
- package/es/components/form/table/dnd/index.d.ts +9 -0
- package/es/components/form/table/index.d.ts +9 -0
- package/es/components/form/table/styles.d.ts +41 -0
- package/es/components/form/table/utils.d.ts +44 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
- package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +166 -99
- package/es/json-logic/globalOperators.d.ts +11 -0
- package/es/theme/globalStyles.d.ts +9 -0
- package/es/theme/index.d.ts +1 -0
- package/es/utils/globalDeps.d.ts +7 -0
- package/lib/index.js +173 -106
- package/package.json +9 -6
- package/src/APIClient.ts +68 -0
- package/src/Application.tsx +6 -2
- package/src/BaseApplication.tsx +8 -0
- package/src/PluginManager.ts +2 -0
- package/src/__tests__/app.test.tsx +8 -0
- package/src/__tests__/authRedirect.test.ts +170 -64
- package/src/__tests__/globalDeps.test.ts +2 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
- package/src/__tests__/remotePlugins.test.ts +148 -0
- package/src/authRedirect.ts +23 -84
- package/src/components/form/EnvVariableInput.tsx +11 -46
- package/src/components/form/VariableInput.tsx +177 -0
- package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/form/table/RowOverlayPreview.tsx +51 -0
- package/src/components/form/table/SelectionCell.tsx +72 -0
- package/src/components/form/table/Table.tsx +279 -0
- package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
- package/src/components/form/table/constants.ts +16 -0
- package/src/components/form/table/dnd/SortableRow.tsx +106 -0
- package/src/components/form/table/dnd/index.ts +10 -0
- package/src/components/form/table/index.tsx +13 -0
- package/src/components/form/table/styles.ts +110 -0
- package/src/components/form/table/utils.ts +75 -0
- package/src/components/index.ts +2 -0
- package/src/css-variable/CSSVariableProvider.tsx +1 -1
- package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
- package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +81 -0
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +34 -2
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
- package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
- package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
- package/src/index.ts +1 -0
- package/src/json-logic/globalOperators.js +731 -0
- package/src/nocobase-buildin-plugin/index.tsx +4 -4
- package/src/theme/globalStyles.ts +21 -0
- package/src/theme/index.tsx +1 -0
- package/src/utils/globalDeps.ts +50 -30
- package/src/utils/remotePlugins.ts +107 -6
|
@@ -7,10 +7,97 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { render, waitFor } from '@testing-library/react';
|
|
12
|
+
import { FlowEngine } from '@nocobase/flow-engine';
|
|
10
13
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
14
|
import { filterFormDefaultValues } from '../../../../actions/filterFormDefaultValues';
|
|
12
15
|
import { FilterFormBlockModel } from '../FilterFormBlockModel';
|
|
13
16
|
|
|
17
|
+
function resolveTemplateValue(raw: any, values: Record<string, any>): any {
|
|
18
|
+
if (typeof raw === 'string') {
|
|
19
|
+
const matched = raw.match(/^\{\{\s*ctx\.formValues\.([^}]+?)\s*\}\}$/);
|
|
20
|
+
return matched ? values[matched[1]] : raw;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(raw)) {
|
|
23
|
+
return raw.map((item) => resolveTemplateValue(item, values));
|
|
24
|
+
}
|
|
25
|
+
if (raw && typeof raw === 'object') {
|
|
26
|
+
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, resolveTemplateValue(value, values)]));
|
|
27
|
+
}
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<string, any> = {}) {
|
|
32
|
+
const values = { ...initialValues };
|
|
33
|
+
const createItem = (fieldPath: string, uid: string) => ({
|
|
34
|
+
uid,
|
|
35
|
+
fieldPath,
|
|
36
|
+
props: { name: `${fieldPath}_${uid}` },
|
|
37
|
+
getProps() {
|
|
38
|
+
return this.props;
|
|
39
|
+
},
|
|
40
|
+
getStepParams(flowKey: string, stepKey: string) {
|
|
41
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') {
|
|
42
|
+
return { fieldPath };
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
},
|
|
46
|
+
subModels: {
|
|
47
|
+
field: {},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const model = {
|
|
51
|
+
defaultValuesRefreshSeq: 0,
|
|
52
|
+
lastDefaultValueByFieldName: new Map<string, any>(),
|
|
53
|
+
form: {
|
|
54
|
+
getFieldsValue: () => ({ ...values }),
|
|
55
|
+
getFieldValue: (name: string) => values[name],
|
|
56
|
+
setFieldValue: (name: string, value: any) => {
|
|
57
|
+
values[name] = value;
|
|
58
|
+
},
|
|
59
|
+
setFieldsValue: (next: Record<string, any>) => {
|
|
60
|
+
Object.assign(values, next);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
context: {
|
|
64
|
+
resolveJsonTemplate: vi.fn((raw) => resolveTemplateValue(raw, values)),
|
|
65
|
+
app: {
|
|
66
|
+
jsonLogic: {
|
|
67
|
+
apply: vi.fn((logic: Record<string, any[]>) => {
|
|
68
|
+
const [[operator, args]] = Object.entries(logic);
|
|
69
|
+
if (operator === '$eq') return args[0] === args[1];
|
|
70
|
+
return true;
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
subModels: {
|
|
76
|
+
grid: {
|
|
77
|
+
subModels: {
|
|
78
|
+
items: [createItem('nickname', 'nick'), createItem('username', 'user')],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
getStepParams: vi.fn((flowKey: string, stepKey: string) => {
|
|
83
|
+
if (flowKey === 'formFilterBlockModelSettings' && stepKey === 'defaultValues') {
|
|
84
|
+
return { value: rules };
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}),
|
|
88
|
+
canApplyFormDefaultValue: (FilterFormBlockModel.prototype as any).canApplyFormDefaultValue,
|
|
89
|
+
matchDefaultValueCondition: (FilterFormBlockModel.prototype as any).matchDefaultValueCondition,
|
|
90
|
+
applyFormDefaultValues: FilterFormBlockModel.prototype.applyFormDefaultValues,
|
|
91
|
+
handleFilterFormValuesChange: (FilterFormBlockModel.prototype as any).handleFilterFormValuesChange,
|
|
92
|
+
dispatchEvent: vi.fn(),
|
|
93
|
+
emitter: {
|
|
94
|
+
emit: vi.fn(),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return { model, values };
|
|
99
|
+
}
|
|
100
|
+
|
|
14
101
|
describe('filter-form defaultValues wiring', () => {
|
|
15
102
|
it('loads action and model modules', () => {
|
|
16
103
|
expect(filterFormDefaultValues).toBeTruthy();
|
|
@@ -48,4 +135,254 @@ describe('filter-form defaultValues wiring', () => {
|
|
|
48
135
|
expect(model.initialDefaultsPromise).toBeUndefined();
|
|
49
136
|
expect(model.applyFormDefaultValues).not.toHaveBeenCalled();
|
|
50
137
|
});
|
|
138
|
+
|
|
139
|
+
it('exposes current form values in the filter form variable meta tree', async () => {
|
|
140
|
+
const engine = new FlowEngine();
|
|
141
|
+
|
|
142
|
+
const dataSource = engine.context.dataSourceManager.getDataSource('main');
|
|
143
|
+
dataSource.addCollection({
|
|
144
|
+
name: 'users',
|
|
145
|
+
filterTargetKey: ['id', 'tenantId'],
|
|
146
|
+
fields: [
|
|
147
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
148
|
+
{ name: 'tenantId', type: 'string', interface: 'text' },
|
|
149
|
+
{ name: 'name', type: 'string', interface: 'text' },
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
dataSource.addCollection({
|
|
153
|
+
name: 'departments',
|
|
154
|
+
filterTargetKey: 'id',
|
|
155
|
+
fields: [
|
|
156
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
157
|
+
{ name: 'name', type: 'string', interface: 'text' },
|
|
158
|
+
{ name: 'owner', type: 'belongsTo', target: 'users', interface: 'm2o' },
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
dataSource.addCollection({
|
|
162
|
+
name: 'tasks',
|
|
163
|
+
filterTargetKey: 'id',
|
|
164
|
+
fields: [
|
|
165
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
166
|
+
{ name: 'title', type: 'string', interface: 'text' },
|
|
167
|
+
{ name: 'department', type: 'belongsTo', target: 'departments', interface: 'm2o' },
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
engine.registerModels({ FilterFormBlockModel });
|
|
172
|
+
const model = engine.createModel<FilterFormBlockModel>({
|
|
173
|
+
use: 'FilterFormBlockModel',
|
|
174
|
+
uid: 'filter-form-current-form',
|
|
175
|
+
subModels: {
|
|
176
|
+
grid: {
|
|
177
|
+
subModels: {
|
|
178
|
+
items: [],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
} as any);
|
|
183
|
+
|
|
184
|
+
function HookCaller() {
|
|
185
|
+
model.useHooksBeforeRender();
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
render(React.createElement(HookCaller));
|
|
190
|
+
|
|
191
|
+
const store = {
|
|
192
|
+
title: 'bug',
|
|
193
|
+
'department_department-filter': 1,
|
|
194
|
+
'department.owner_owner-filter': { id: 7, tenantId: 'tenant-a' },
|
|
195
|
+
};
|
|
196
|
+
const fakeForm = {
|
|
197
|
+
getFieldsValue: () => ({ ...store }),
|
|
198
|
+
};
|
|
199
|
+
model.context.defineProperty('form', { value: fakeForm });
|
|
200
|
+
model.subModels.grid.subModels.items = [
|
|
201
|
+
{
|
|
202
|
+
uid: 'department-filter',
|
|
203
|
+
fieldPath: 'department',
|
|
204
|
+
props: { name: 'department_department-filter' },
|
|
205
|
+
subModels: {
|
|
206
|
+
field: {
|
|
207
|
+
context: {
|
|
208
|
+
collectionField: dataSource.getCollection('tasks').getField('department'),
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
uid: 'owner-filter',
|
|
215
|
+
fieldPath: 'department.owner',
|
|
216
|
+
props: { name: 'department.owner_owner-filter' },
|
|
217
|
+
subModels: {
|
|
218
|
+
field: {
|
|
219
|
+
context: {
|
|
220
|
+
collectionField: dataSource.getCollection('departments').getField('owner'),
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
expect((model.context as any).formValues).toMatchObject({
|
|
228
|
+
...store,
|
|
229
|
+
department: {
|
|
230
|
+
'owner_owner-filter': { id: 7, tenantId: 'tenant-a' },
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const options = (model.context as any).getPropertyOptions('formValues');
|
|
235
|
+
const meta = await options.meta();
|
|
236
|
+
const properties = await meta.properties();
|
|
237
|
+
const metaTree = await (model.context as any).getPropertyMetaTree();
|
|
238
|
+
|
|
239
|
+
expect(options.resolveOnServer('department_department-filter')).toBe(false);
|
|
240
|
+
expect(options.resolveOnServer('department_department-filter.name')).toBe(true);
|
|
241
|
+
expect(options.resolveOnServer('department_department-filter[0].name')).toBe(true);
|
|
242
|
+
expect(options.resolveOnServer('department.owner_owner-filter.name')).toBe(true);
|
|
243
|
+
expect(options.serverOnlyWhenContextParams).toBe(true);
|
|
244
|
+
expect(meta.title).toBe('Current form');
|
|
245
|
+
expect(properties.department.properties['owner_owner-filter'].title).toBe('owner');
|
|
246
|
+
expect(metaTree).toEqual(
|
|
247
|
+
expect.arrayContaining([
|
|
248
|
+
expect.objectContaining({
|
|
249
|
+
name: 'formValues',
|
|
250
|
+
title: 'Current form',
|
|
251
|
+
}),
|
|
252
|
+
]),
|
|
253
|
+
);
|
|
254
|
+
expect(await meta.buildVariablesParams(model.context)).toMatchObject({
|
|
255
|
+
'department_department-filter': { collection: 'departments', dataSourceKey: 'main', filterByTk: 1 },
|
|
256
|
+
department: {
|
|
257
|
+
'owner_owner-filter': {
|
|
258
|
+
collection: 'users',
|
|
259
|
+
dataSourceKey: 'main',
|
|
260
|
+
filterByTk: { id: 7, tenantId: 'tenant-a' },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('refreshes default values that depend on current filter form values', async () => {
|
|
267
|
+
const { model, values } = createFilterFormDefaultValuesModel(
|
|
268
|
+
[
|
|
269
|
+
{
|
|
270
|
+
key: 'username-default',
|
|
271
|
+
enable: true,
|
|
272
|
+
targetPath: 'username',
|
|
273
|
+
mode: 'default',
|
|
274
|
+
value: '{{ ctx.formValues.nickname_nick }}',
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
{ nickname_nick: 'Alice' },
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
281
|
+
expect(values.username_user).toBe('Alice');
|
|
282
|
+
|
|
283
|
+
values.nickname_nick = 'Bob';
|
|
284
|
+
model.defaultValuesRefreshSeq += 1;
|
|
285
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
|
|
286
|
+
refreshSeq: model.defaultValuesRefreshSeq,
|
|
287
|
+
});
|
|
288
|
+
expect(values.username_user).toBe('Bob');
|
|
289
|
+
|
|
290
|
+
values.username_user = 'Manual';
|
|
291
|
+
values.nickname_nick = 'Carol';
|
|
292
|
+
model.defaultValuesRefreshSeq += 1;
|
|
293
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
|
|
294
|
+
refreshSeq: model.defaultValuesRefreshSeq,
|
|
295
|
+
});
|
|
296
|
+
expect(values.username_user).toBe('Manual');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('applies fixed values even when the target filter field already has a value', async () => {
|
|
300
|
+
const { model, values } = createFilterFormDefaultValuesModel(
|
|
301
|
+
[
|
|
302
|
+
{
|
|
303
|
+
key: 'username-fixed',
|
|
304
|
+
enable: true,
|
|
305
|
+
targetPath: 'username',
|
|
306
|
+
mode: 'assign',
|
|
307
|
+
value: '{{ ctx.formValues.nickname_nick }}',
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
{ nickname_nick: 'Bob', username_user: 'Manual' },
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
314
|
+
|
|
315
|
+
expect(values.username_user).toBe('Bob');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('skips filter form field values when the rule condition does not match', async () => {
|
|
319
|
+
const { model, values } = createFilterFormDefaultValuesModel(
|
|
320
|
+
[
|
|
321
|
+
{
|
|
322
|
+
key: 'username-condition',
|
|
323
|
+
enable: true,
|
|
324
|
+
targetPath: 'username',
|
|
325
|
+
mode: 'assign',
|
|
326
|
+
condition: {
|
|
327
|
+
logic: '$and',
|
|
328
|
+
items: [{ path: '{{ ctx.formValues.nickname_nick }}', operator: '$eq', value: 'allow' }],
|
|
329
|
+
},
|
|
330
|
+
value: 'Matched',
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
{ nickname_nick: 'deny' },
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
337
|
+
expect(values.username_user).toBeUndefined();
|
|
338
|
+
|
|
339
|
+
values.nickname_nick = 'allow';
|
|
340
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
341
|
+
expect(values.username_user).toBe('Matched');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('emits formValuesChange with final values after applying dependent field values', async () => {
|
|
345
|
+
const { model, values } = createFilterFormDefaultValuesModel(
|
|
346
|
+
[
|
|
347
|
+
{
|
|
348
|
+
key: 'username-fixed',
|
|
349
|
+
enable: true,
|
|
350
|
+
targetPath: 'username',
|
|
351
|
+
mode: 'assign',
|
|
352
|
+
value: '{{ ctx.formValues.nickname_nick }}',
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
{ nickname_nick: 'Bob' },
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
(model as any).handleFilterFormValuesChange({ nickname_nick: 'Bob' }, { nickname_nick: 'Bob' });
|
|
359
|
+
|
|
360
|
+
await waitFor(() => {
|
|
361
|
+
expect(values.username_user).toBe('Bob');
|
|
362
|
+
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
363
|
+
'formValuesChange',
|
|
364
|
+
{
|
|
365
|
+
changedValues: {
|
|
366
|
+
nickname_nick: 'Bob',
|
|
367
|
+
username_user: 'Bob',
|
|
368
|
+
},
|
|
369
|
+
allValues: {
|
|
370
|
+
nickname_nick: 'Bob',
|
|
371
|
+
username_user: 'Bob',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{ debounce: true },
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
expect(model.emitter.emit).toHaveBeenCalledWith('formValuesChange', {
|
|
378
|
+
changedValues: {
|
|
379
|
+
nickname_nick: 'Bob',
|
|
380
|
+
username_user: 'Bob',
|
|
381
|
+
},
|
|
382
|
+
allValues: {
|
|
383
|
+
nickname_nick: 'Bob',
|
|
384
|
+
username_user: 'Bob',
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
});
|
|
51
388
|
});
|
|
@@ -7,84 +7,4 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
import { TinyColor } from '@ctrl/tinycolor';
|
|
12
|
-
import { useSortable } from '@dnd-kit/sortable';
|
|
13
|
-
import { css } from '@emotion/css';
|
|
14
|
-
import { theme } from 'antd';
|
|
15
|
-
import classNames from 'classnames';
|
|
16
|
-
import React, { useMemo } from 'react';
|
|
17
|
-
|
|
18
|
-
type DragSortRowContextValue = {
|
|
19
|
-
attributes?: Record<string, unknown>;
|
|
20
|
-
listeners?: Record<string, unknown>;
|
|
21
|
-
setActivatorNodeRef?: (node: HTMLElement | null) => void;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const DragSortRowContext = React.createContext<DragSortRowContextValue | null>(null);
|
|
25
|
-
|
|
26
|
-
const sortHandleClass = css`
|
|
27
|
-
display: inline-flex;
|
|
28
|
-
align-items: center;
|
|
29
|
-
justify-content: center;
|
|
30
|
-
cursor: grab;
|
|
31
|
-
`;
|
|
32
|
-
|
|
33
|
-
export const SortHandle: React.FC<{ id: string | number; style?: React.CSSProperties }> = (props) => {
|
|
34
|
-
const { id: _id, ...otherProps } = props;
|
|
35
|
-
const dragSortContext = React.useContext(DragSortRowContext);
|
|
36
|
-
// return <MenuOutlined ref={setNodeRef} {...otherProps} {...listeners} style={{ cursor: 'grab' }} />;
|
|
37
|
-
return (
|
|
38
|
-
<span
|
|
39
|
-
ref={dragSortContext?.setActivatorNodeRef}
|
|
40
|
-
{...dragSortContext?.attributes}
|
|
41
|
-
{...dragSortContext?.listeners}
|
|
42
|
-
{...otherProps}
|
|
43
|
-
className={classNames(sortHandleClass)}
|
|
44
|
-
>
|
|
45
|
-
<MenuOutlined />
|
|
46
|
-
</span>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const SortableRow = (props) => {
|
|
51
|
-
const { token }: any = theme.useToken();
|
|
52
|
-
const id = props['data-row-key']?.toString();
|
|
53
|
-
const { setNodeRef, setActivatorNodeRef, attributes, listeners, active, over } = useSortable({
|
|
54
|
-
id,
|
|
55
|
-
});
|
|
56
|
-
const { rowIndex, ...others } = props;
|
|
57
|
-
const isOver = over?.id === id;
|
|
58
|
-
const classObj = useMemo(() => {
|
|
59
|
-
const borderColor = new TinyColor(token.colorPrimary).setAlpha(0.6).toHex8String();
|
|
60
|
-
return {
|
|
61
|
-
topActiveClass: css`
|
|
62
|
-
& > td {
|
|
63
|
-
border-top: 2px solid ${borderColor} !important;
|
|
64
|
-
}
|
|
65
|
-
`,
|
|
66
|
-
bottomActiveClass: css`
|
|
67
|
-
& > td {
|
|
68
|
-
border-bottom: 2px solid ${borderColor} !important;
|
|
69
|
-
}
|
|
70
|
-
`,
|
|
71
|
-
};
|
|
72
|
-
}, [token.colorPrimary]);
|
|
73
|
-
|
|
74
|
-
const className =
|
|
75
|
-
(active?.data.current?.sortable.index ?? -1) > rowIndex ? classObj.topActiveClass : classObj.bottomActiveClass;
|
|
76
|
-
|
|
77
|
-
const row = (
|
|
78
|
-
<DragSortRowContext.Provider value={{ listeners, setActivatorNodeRef }}>
|
|
79
|
-
<tr
|
|
80
|
-
ref={(node) => {
|
|
81
|
-
setNodeRef(node);
|
|
82
|
-
}}
|
|
83
|
-
{...others}
|
|
84
|
-
className={classNames(props.className, { [className]: active && isOver })}
|
|
85
|
-
/>
|
|
86
|
-
</DragSortRowContext.Provider>
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
return row;
|
|
90
|
-
};
|
|
10
|
+
export { DragSortRowContext, SortHandle, SortableRow } from '../../../../../components/form/table/dnd/SortableRow';
|
|
@@ -69,6 +69,106 @@ function resolveScriptCode(codeParam?: string) {
|
|
|
69
69
|
return typeof raw === 'string' ? raw.trim() : '';
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
type NamePathPart = string | number;
|
|
73
|
+
|
|
74
|
+
function toNamePath(input: unknown): NamePathPart[] | null {
|
|
75
|
+
if (Array.isArray(input)) {
|
|
76
|
+
return input.filter((item): item is NamePathPart => typeof item === 'string' || typeof item === 'number');
|
|
77
|
+
}
|
|
78
|
+
if (typeof input === 'number') {
|
|
79
|
+
return [input];
|
|
80
|
+
}
|
|
81
|
+
if (typeof input === 'string') {
|
|
82
|
+
return input
|
|
83
|
+
.split('.')
|
|
84
|
+
.map((item) => item.trim())
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function startsWithNamePath(namePath: NamePathPart[], prefix: NamePathPart[]) {
|
|
91
|
+
return prefix.length <= namePath.length && prefix.every((item, index) => String(namePath[index]) === String(item));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getFieldSettingsNamePath(model: any): NamePathPart[] | null {
|
|
95
|
+
const init =
|
|
96
|
+
model?.getStepParams?.('fieldSettings', 'init') || model?.parent?.getStepParams?.('fieldSettings', 'init');
|
|
97
|
+
const fieldPath = toNamePath(init?.fieldPath);
|
|
98
|
+
const associationPath = toNamePath(init?.associationPathName);
|
|
99
|
+
|
|
100
|
+
if (!fieldPath?.length) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!associationPath?.length || startsWithNamePath(fieldPath, associationPath)) {
|
|
105
|
+
return fieldPath;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [...associationPath, ...fieldPath];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function applyFieldIndex(namePath: NamePathPart[] | null, fieldIndex: unknown): NamePathPart[] | null {
|
|
112
|
+
if (!namePath?.length) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (namePath.some((item) => typeof item === 'number') || !Array.isArray(fieldIndex) || fieldIndex.length === 0) {
|
|
116
|
+
return namePath;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const indexQueues = new Map<string, number[]>();
|
|
120
|
+
for (const item of fieldIndex) {
|
|
121
|
+
if (typeof item !== 'string') continue;
|
|
122
|
+
const [fieldName, indexStr] = item.split(':');
|
|
123
|
+
const index = Number(indexStr);
|
|
124
|
+
if (!fieldName || !Number.isFinite(index)) continue;
|
|
125
|
+
const queue = indexQueues.get(fieldName) || [];
|
|
126
|
+
queue.push(index);
|
|
127
|
+
indexQueues.set(fieldName, queue);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!indexQueues.size) {
|
|
131
|
+
return namePath;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result: NamePathPart[] = [];
|
|
135
|
+
for (const item of namePath) {
|
|
136
|
+
result.push(item);
|
|
137
|
+
const queue = indexQueues.get(String(item));
|
|
138
|
+
if (queue?.length) {
|
|
139
|
+
result.push(queue.shift() as number);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveEffectiveNamePath(ctx: any): NamePathPart[] | null {
|
|
146
|
+
const namePath =
|
|
147
|
+
getFieldSettingsNamePath(ctx.model) || toNamePath(ctx.fieldPathArray) || toNamePath(ctx.model?.props?.name);
|
|
148
|
+
return applyFieldIndex(namePath, ctx.fieldIndex);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function setFormValue(form: any, namePath: NamePathPart[], value: any) {
|
|
152
|
+
if (typeof form?.setFieldValue === 'function') {
|
|
153
|
+
form.setFieldValue(namePath, value);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (typeof form?.setFieldsValue === 'function') {
|
|
158
|
+
const patch: any = {};
|
|
159
|
+
let cursor = patch;
|
|
160
|
+
namePath.forEach((item, index) => {
|
|
161
|
+
if (index === namePath.length - 1) {
|
|
162
|
+
cursor[item] = value;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
cursor[item] = typeof namePath[index + 1] === 'number' ? [] : {};
|
|
166
|
+
cursor = cursor[item];
|
|
167
|
+
});
|
|
168
|
+
form.setFieldsValue(patch);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
72
172
|
const JSFormRuntime: React.FC<{
|
|
73
173
|
model: JSEditableFieldModel;
|
|
74
174
|
value?: any;
|
|
@@ -274,9 +374,9 @@ JSEditableFieldModel.registerFlow({
|
|
|
274
374
|
cache: false,
|
|
275
375
|
});
|
|
276
376
|
ctx.defineMethod('getValue', () => {
|
|
277
|
-
const
|
|
278
|
-
if (
|
|
279
|
-
const fv = ctx.form?.getFieldValue?.(
|
|
377
|
+
const namePath = resolveEffectiveNamePath(ctx);
|
|
378
|
+
if (namePath?.length) {
|
|
379
|
+
const fv = ctx.form?.getFieldValue?.(namePath);
|
|
280
380
|
return fv !== undefined ? fv : ctx.model.props?.value;
|
|
281
381
|
}
|
|
282
382
|
return ctx.model.props?.value;
|
|
@@ -284,15 +384,15 @@ JSEditableFieldModel.registerFlow({
|
|
|
284
384
|
ctx.defineMethod('setValue', (v) => {
|
|
285
385
|
try {
|
|
286
386
|
ctx.model.setProps('value', v);
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
289
|
-
ctx.form
|
|
387
|
+
const namePath = resolveEffectiveNamePath(ctx);
|
|
388
|
+
if (namePath?.length) {
|
|
389
|
+
setFormValue(ctx.form, namePath, v);
|
|
290
390
|
}
|
|
291
391
|
} catch (_) {
|
|
292
392
|
// ignore
|
|
293
393
|
}
|
|
294
394
|
});
|
|
295
|
-
ctx.defineProperty('namePath', { get: () => ctx
|
|
395
|
+
ctx.defineProperty('namePath', { get: () => resolveEffectiveNamePath(ctx), cache: false });
|
|
296
396
|
ctx.defineProperty('disabled', { get: () => !!ctx.model.props?.disabled, cache: false });
|
|
297
397
|
ctx.defineProperty('readOnly', {
|
|
298
398
|
get: () => isReadOnlyMode(ctx.model),
|