@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.
Files changed (76) hide show
  1. package/es/APIClient.d.ts +16 -0
  2. package/es/Application.d.ts +2 -1
  3. package/es/BaseApplication.d.ts +6 -0
  4. package/es/PluginManager.d.ts +2 -0
  5. package/es/authRedirect.d.ts +9 -16
  6. package/es/components/form/EnvVariableInput.d.ts +8 -6
  7. package/es/components/form/VariableInput.d.ts +73 -0
  8. package/es/components/form/index.d.ts +1 -0
  9. package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
  10. package/es/components/form/table/SelectionCell.d.ts +36 -0
  11. package/es/components/form/table/Table.d.ts +82 -0
  12. package/es/components/form/table/constants.d.ts +15 -0
  13. package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
  14. package/es/components/form/table/dnd/index.d.ts +9 -0
  15. package/es/components/form/table/index.d.ts +9 -0
  16. package/es/components/form/table/styles.d.ts +41 -0
  17. package/es/components/form/table/utils.d.ts +44 -0
  18. package/es/components/index.d.ts +2 -0
  19. package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
  21. package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
  22. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
  23. package/es/flow-compat/passwordUtils.d.ts +1 -1
  24. package/es/index.d.ts +1 -0
  25. package/es/index.mjs +166 -99
  26. package/es/json-logic/globalOperators.d.ts +11 -0
  27. package/es/theme/globalStyles.d.ts +9 -0
  28. package/es/theme/index.d.ts +1 -0
  29. package/es/utils/globalDeps.d.ts +7 -0
  30. package/lib/index.js +173 -106
  31. package/package.json +9 -6
  32. package/src/APIClient.ts +68 -0
  33. package/src/Application.tsx +6 -2
  34. package/src/BaseApplication.tsx +8 -0
  35. package/src/PluginManager.ts +2 -0
  36. package/src/__tests__/app.test.tsx +8 -0
  37. package/src/__tests__/authRedirect.test.ts +170 -64
  38. package/src/__tests__/globalDeps.test.ts +2 -0
  39. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
  40. package/src/__tests__/remotePlugins.test.ts +148 -0
  41. package/src/authRedirect.ts +23 -84
  42. package/src/components/form/EnvVariableInput.tsx +11 -46
  43. package/src/components/form/VariableInput.tsx +177 -0
  44. package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
  45. package/src/components/form/index.tsx +1 -0
  46. package/src/components/form/table/RowOverlayPreview.tsx +51 -0
  47. package/src/components/form/table/SelectionCell.tsx +72 -0
  48. package/src/components/form/table/Table.tsx +279 -0
  49. package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
  50. package/src/components/form/table/constants.ts +16 -0
  51. package/src/components/form/table/dnd/SortableRow.tsx +106 -0
  52. package/src/components/form/table/dnd/index.ts +10 -0
  53. package/src/components/form/table/index.tsx +13 -0
  54. package/src/components/form/table/styles.ts +110 -0
  55. package/src/components/form/table/utils.ts +75 -0
  56. package/src/components/index.ts +2 -0
  57. package/src/css-variable/CSSVariableProvider.tsx +1 -1
  58. package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
  59. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
  61. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
  62. package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
  63. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +81 -0
  64. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +34 -2
  65. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
  66. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
  67. package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
  68. package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
  69. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
  70. package/src/index.ts +1 -0
  71. package/src/json-logic/globalOperators.js +731 -0
  72. package/src/nocobase-buildin-plugin/index.tsx +4 -4
  73. package/src/theme/globalStyles.ts +21 -0
  74. package/src/theme/index.tsx +1 -0
  75. package/src/utils/globalDeps.ts +50 -30
  76. 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
- import { MenuOutlined } from '@ant-design/icons';
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 name = ctx.model.props?.name;
278
- if (name !== undefined && name !== null) {
279
- const fv = ctx.form?.getFieldValue?.(name);
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 name = ctx.model.props?.name;
288
- if (name !== undefined && name !== null) {
289
- ctx.form?.setFieldValue?.(name, v);
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.model.props?.name, cache: false });
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),