@nocobase/client-v2 2.1.0-alpha.37 → 2.1.0-alpha.38
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/index.mjs +14 -14
- package/lib/index.js +15 -15
- package/package.json +6 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
- 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/fields/JSEditableFieldModel.tsx +107 -7
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.0-alpha.
|
|
3
|
+
"version": "2.1.0-alpha.38",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"@formily/antd-v5": "1.2.3",
|
|
25
25
|
"@formily/react": "^2.2.27",
|
|
26
26
|
"@formily/shared": "^2.2.27",
|
|
27
|
-
"@nocobase/evaluators": "2.1.0-alpha.
|
|
28
|
-
"@nocobase/flow-engine": "2.1.0-alpha.
|
|
29
|
-
"@nocobase/sdk": "2.1.0-alpha.
|
|
30
|
-
"@nocobase/shared": "2.1.0-alpha.
|
|
27
|
+
"@nocobase/evaluators": "2.1.0-alpha.38",
|
|
28
|
+
"@nocobase/flow-engine": "2.1.0-alpha.38",
|
|
29
|
+
"@nocobase/sdk": "2.1.0-alpha.38",
|
|
30
|
+
"@nocobase/shared": "2.1.0-alpha.38",
|
|
31
31
|
"ahooks": "^3.7.2",
|
|
32
32
|
"antd": "5.24.2",
|
|
33
33
|
"antd-style": "3.7.1",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"react-i18next": "^11.15.1",
|
|
42
42
|
"react-router-dom": "^6.30.1"
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "cc99815fc9ae0612d6db0db5ebbc87a774fff8ed"
|
|
45
45
|
}
|
|
@@ -681,6 +681,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
681
681
|
AdminLayoutMenuItemModel.registerFlow({
|
|
682
682
|
key: 'menuCreation',
|
|
683
683
|
title: 'Add menu item',
|
|
684
|
+
manual: true,
|
|
684
685
|
steps: {
|
|
685
686
|
basic: {
|
|
686
687
|
title: 'Add menu item',
|
|
@@ -697,6 +698,7 @@ AdminLayoutMenuItemModel.registerFlow({
|
|
|
697
698
|
AdminLayoutMenuItemModel.registerFlow({
|
|
698
699
|
key: 'menuSettings',
|
|
699
700
|
title: 'Menu settings',
|
|
701
|
+
manual: true,
|
|
700
702
|
steps: {
|
|
701
703
|
edit: {
|
|
702
704
|
title: 'Edit',
|
|
@@ -14,6 +14,7 @@ import { render, waitFor } from '@testing-library/react';
|
|
|
14
14
|
import { App, ConfigProvider } from 'antd';
|
|
15
15
|
import { useCodeRunner } from '../hooks/useCodeRunner';
|
|
16
16
|
import {
|
|
17
|
+
FlowContext,
|
|
17
18
|
FlowEngine,
|
|
18
19
|
FlowModel,
|
|
19
20
|
FlowEngineProvider,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
ElementProxy,
|
|
22
23
|
createSafeWindow,
|
|
23
24
|
createSafeDocument,
|
|
25
|
+
createViewScopedEngine,
|
|
24
26
|
} from '@nocobase/flow-engine';
|
|
25
27
|
import { JSEditableFieldModel } from '../../../models/fields/JSEditableFieldModel';
|
|
26
28
|
|
|
@@ -31,6 +33,20 @@ class DummyJsAutoModel extends FlowModel {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function registerRunJsPreviewFlow(model: FlowModel) {
|
|
37
|
+
model.registerFlow('jsSettings', {
|
|
38
|
+
steps: {
|
|
39
|
+
runJs: {
|
|
40
|
+
useRawParams: true,
|
|
41
|
+
async handler(ctx) {
|
|
42
|
+
const code = ctx?.inputArgs?.preview?.code || '';
|
|
43
|
+
return ctx.runjs(code, undefined, { preprocessTemplates: true });
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
describe('useCodeRunner (beforeRender)', () => {
|
|
35
51
|
it('logs success and captures console output', async () => {
|
|
36
52
|
const engine = new FlowEngine();
|
|
@@ -192,6 +208,71 @@ describe('useCodeRunner (beforeRender)', () => {
|
|
|
192
208
|
});
|
|
193
209
|
});
|
|
194
210
|
|
|
211
|
+
it('keeps popup context when a top scoped engine has another model with the same uid', async () => {
|
|
212
|
+
const engine = new FlowEngine();
|
|
213
|
+
engine.registerModels({ DummyJsAutoModel });
|
|
214
|
+
const model = engine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'same-popup-uid' });
|
|
215
|
+
model.context.defineProperty('popup', {
|
|
216
|
+
value: {
|
|
217
|
+
uid: 'popup-view',
|
|
218
|
+
record: { username: 'alice' },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
registerRunJsPreviewFlow(model);
|
|
222
|
+
|
|
223
|
+
const scopedEngine = createViewScopedEngine(engine);
|
|
224
|
+
const topModel = scopedEngine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'same-popup-uid' });
|
|
225
|
+
registerRunJsPreviewFlow(topModel);
|
|
226
|
+
|
|
227
|
+
const { result } = renderHook(() => useCodeRunner(model.context, 'v1'));
|
|
228
|
+
let runResult: any;
|
|
229
|
+
await act(async () => {
|
|
230
|
+
runResult = await result.current.run(`
|
|
231
|
+
const currentUsername = await ctx.getVar('ctx.popup.record.username');
|
|
232
|
+
console.log(currentUsername);
|
|
233
|
+
return currentUsername;
|
|
234
|
+
`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(runResult?.success).toBe(true);
|
|
238
|
+
expect(runResult?.value).toBe('alice');
|
|
239
|
+
expect(result.current.logs.some((l) => l.level === 'log' && l.msg.includes('alice'))).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('runs direct event-flow previews against the popup-bound model context when the settings view has no popup', async () => {
|
|
243
|
+
const engine = new FlowEngine();
|
|
244
|
+
engine.registerModels({ DummyJsAutoModel });
|
|
245
|
+
const model = engine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'direct-popup-uid' });
|
|
246
|
+
model.context.defineProperty('popup', {
|
|
247
|
+
value: {
|
|
248
|
+
uid: 'popup-view',
|
|
249
|
+
record: { username: 'alice' },
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const scopedEngine = createViewScopedEngine(engine);
|
|
254
|
+
scopedEngine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'direct-popup-uid' });
|
|
255
|
+
|
|
256
|
+
const settingsCtx = new FlowContext();
|
|
257
|
+
settingsCtx.defineProperty('engine', { value: scopedEngine });
|
|
258
|
+
settingsCtx.defineProperty('popup', { value: undefined });
|
|
259
|
+
settingsCtx.addDelegate(model.context);
|
|
260
|
+
|
|
261
|
+
const { result } = renderHook(() => useCodeRunner(settingsCtx as any, 'v1'));
|
|
262
|
+
let runResult: any;
|
|
263
|
+
await act(async () => {
|
|
264
|
+
runResult = await result.current.run(`
|
|
265
|
+
const currentUsername = await ctx.getVar('ctx.popup.record.username');
|
|
266
|
+
console.log(currentUsername);
|
|
267
|
+
return currentUsername;
|
|
268
|
+
`);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(runResult?.success).toBe(true);
|
|
272
|
+
expect(runResult?.value).toBe('alice');
|
|
273
|
+
expect(result.current.logs.some((l) => l.level === 'log' && l.msg.includes('alice'))).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
195
276
|
it('compiles JSX in preview and renders antd Input without syntax error', async () => {
|
|
196
277
|
const engine = new FlowEngine();
|
|
197
278
|
engine.registerModels({ DummyJsAutoModel });
|
|
@@ -112,6 +112,31 @@ function createLoggerWrapperFactory(append: (level: RunLog['level'], args: any[]
|
|
|
112
112
|
return wrap;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function hasPopupViewMarkers(view: any): boolean {
|
|
116
|
+
const inputArgs = view?.inputArgs || {};
|
|
117
|
+
const openerUids = inputArgs?.openerUids;
|
|
118
|
+
const viewStack = view?.navigation?.viewStack;
|
|
119
|
+
|
|
120
|
+
return (Array.isArray(openerUids) && openerUids.length > 0) || (Array.isArray(viewStack) && viewStack.length >= 2);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function hasPreviewPopupContext(ctx: any): Promise<boolean> {
|
|
124
|
+
if (!ctx) return false;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const popup = await ctx.popup;
|
|
128
|
+
if (popup) return true;
|
|
129
|
+
} catch (_) {
|
|
130
|
+
// ignore unavailable popup getters
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
return hasPopupViewMarkers(await ctx.view);
|
|
135
|
+
} catch (_) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
116
141
|
const [logs, setLogs] = useState<RunLog[]>([]);
|
|
117
142
|
const [running, setRunning] = useState(false);
|
|
@@ -131,7 +156,14 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
|
131
156
|
const model = hostCtx?.model;
|
|
132
157
|
if (!model) throw new Error('No model in FlowContext');
|
|
133
158
|
const engine = hostCtx.engine;
|
|
134
|
-
const
|
|
159
|
+
const globalRuntimeModel = engine.getModel(model.uid, true) || model;
|
|
160
|
+
const [hostHasPopupContext, modelHasPopupContext] = await Promise.all([
|
|
161
|
+
hasPreviewPopupContext(hostCtx),
|
|
162
|
+
hasPreviewPopupContext(model.context),
|
|
163
|
+
]);
|
|
164
|
+
const shouldPreservePopupModel = hostHasPopupContext || modelHasPopupContext;
|
|
165
|
+
const runtimeModel = shouldPreservePopupModel ? model : globalRuntimeModel;
|
|
166
|
+
const directRunCtx = hostHasPopupContext ? hostCtx : modelHasPopupContext ? model.context : hostCtx;
|
|
135
167
|
|
|
136
168
|
const nativeConsole: Record<RunLog['level'], (...args: any[]) => void> = {
|
|
137
169
|
log: (...args) => console.log(...args),
|
|
@@ -255,7 +287,7 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
|
255
287
|
if (!flow) {
|
|
256
288
|
// 无可用流程(典型场景:联动规则里的 RunJS 预览),直接在当前上下文执行代码
|
|
257
289
|
const navigator = createSafeNavigator();
|
|
258
|
-
await
|
|
290
|
+
await directRunCtx.runjs(
|
|
259
291
|
code,
|
|
260
292
|
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
261
293
|
{ version },
|
|
@@ -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),
|
|
@@ -11,6 +11,7 @@ import React from 'react';
|
|
|
11
11
|
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
12
12
|
import { describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import { FlowEngine, FlowEngineProvider, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
|
14
|
+
import { get as lodashGet, set as lodashSet } from 'lodash';
|
|
14
15
|
import { JSEditableFieldModel } from '../JSEditableFieldModel';
|
|
15
16
|
|
|
16
17
|
function createField(props?: Record<string, any>, code = '') {
|
|
@@ -73,6 +74,21 @@ const React = ctx.React;
|
|
|
73
74
|
ctx.render(<span data-testid="js-readonly-state">{String(ctx.readOnly)}</span>);
|
|
74
75
|
`;
|
|
75
76
|
|
|
77
|
+
const SET_VALUE_AND_RENDER_NAME_PATH_CODE = `
|
|
78
|
+
const React = ctx.React;
|
|
79
|
+
ctx.setValue?.('44');
|
|
80
|
+
ctx.render(<span data-testid="js-name-path">{JSON.stringify(ctx.namePath)}</span>);
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
function createFormStub(initialValues: any = {}) {
|
|
84
|
+
const store = JSON.parse(JSON.stringify(initialValues));
|
|
85
|
+
return {
|
|
86
|
+
getFieldValue: (namePath: any) => lodashGet(store, namePath),
|
|
87
|
+
setFieldValue: (namePath: any, value: any) => lodashSet(store, namePath, value),
|
|
88
|
+
getFieldsValue: () => store,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
class ParentModel extends FlowModel<any> {
|
|
77
93
|
render() {
|
|
78
94
|
return <FlowModelRenderer model={this.subModels.field} />;
|
|
@@ -83,6 +99,11 @@ function renderParentFieldWithFlowRenderer(
|
|
|
83
99
|
fieldProps?: Record<string, any>,
|
|
84
100
|
parentProps?: Record<string, any>,
|
|
85
101
|
code = EDITABLE_CODE,
|
|
102
|
+
options?: {
|
|
103
|
+
fieldIndex?: string[];
|
|
104
|
+
fieldStepParams?: Record<string, any>;
|
|
105
|
+
form?: any;
|
|
106
|
+
},
|
|
86
107
|
) {
|
|
87
108
|
const engine = new FlowEngine();
|
|
88
109
|
engine.registerModels({ JSEditableFieldModel, ParentModel });
|
|
@@ -96,6 +117,7 @@ function renderParentFieldWithFlowRenderer(
|
|
|
96
117
|
uid: 'js-field-with-parent',
|
|
97
118
|
props: fieldProps,
|
|
98
119
|
stepParams: {
|
|
120
|
+
...(options?.fieldStepParams || {}),
|
|
99
121
|
jsSettings: {
|
|
100
122
|
runJs: {
|
|
101
123
|
code,
|
|
@@ -106,6 +128,13 @@ function renderParentFieldWithFlowRenderer(
|
|
|
106
128
|
},
|
|
107
129
|
});
|
|
108
130
|
|
|
131
|
+
if (options?.form) {
|
|
132
|
+
parent.context.defineProperty('form', { value: options.form });
|
|
133
|
+
}
|
|
134
|
+
if (options?.fieldIndex) {
|
|
135
|
+
parent.subModels.field.context.defineProperty('fieldIndex', { value: options.fieldIndex });
|
|
136
|
+
}
|
|
137
|
+
|
|
109
138
|
render(
|
|
110
139
|
<FlowEngineProvider engine={engine}>
|
|
111
140
|
<FlowModelRenderer model={parent} />
|
|
@@ -207,4 +236,72 @@ describe('JSEditableFieldModel', () => {
|
|
|
207
236
|
applyFlowSpy.mockRestore();
|
|
208
237
|
}
|
|
209
238
|
});
|
|
239
|
+
|
|
240
|
+
it('writes top-level form values through the effective name path', async () => {
|
|
241
|
+
const form = createFormStub({});
|
|
242
|
+
|
|
243
|
+
renderParentFieldWithFlowRenderer({ name: 'staffname' }, undefined, SET_VALUE_AND_RENDER_NAME_PATH_CODE, {
|
|
244
|
+
form,
|
|
245
|
+
fieldStepParams: {
|
|
246
|
+
fieldSettings: {
|
|
247
|
+
init: {
|
|
248
|
+
fieldPath: 'staffname',
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await waitFor(() => {
|
|
255
|
+
expect(form.getFieldValue(['staffname'])).toBe('44');
|
|
256
|
+
expect(screen.getByTestId('js-name-path')).toHaveTextContent(JSON.stringify(['staffname']));
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('writes subform list values under the association path instead of the form root', async () => {
|
|
261
|
+
const form = createFormStub({ org_o2m: [{}] });
|
|
262
|
+
|
|
263
|
+
renderParentFieldWithFlowRenderer({ name: 'orgname' }, undefined, SET_VALUE_AND_RENDER_NAME_PATH_CODE, {
|
|
264
|
+
form,
|
|
265
|
+
fieldIndex: ['org_o2m:0'],
|
|
266
|
+
fieldStepParams: {
|
|
267
|
+
fieldSettings: {
|
|
268
|
+
init: {
|
|
269
|
+
fieldPath: 'orgname',
|
|
270
|
+
associationPathName: 'org_o2m',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await waitFor(() => {
|
|
277
|
+
expect(form.getFieldValue(['org_o2m', 0, 'orgname'])).toBe('44');
|
|
278
|
+
expect(form.getFieldValue(['orgname'])).toBeUndefined();
|
|
279
|
+
expect(screen.getByTestId('js-name-path')).toHaveTextContent(JSON.stringify(['org_o2m', 0, 'orgname']));
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('writes nested subform list values with the full field index chain', async () => {
|
|
284
|
+
const form = createFormStub({ users: [{ roles: [{}, {}] }] });
|
|
285
|
+
|
|
286
|
+
renderParentFieldWithFlowRenderer({ name: 'roleName' }, undefined, SET_VALUE_AND_RENDER_NAME_PATH_CODE, {
|
|
287
|
+
form,
|
|
288
|
+
fieldIndex: ['users:0', 'roles:1'],
|
|
289
|
+
fieldStepParams: {
|
|
290
|
+
fieldSettings: {
|
|
291
|
+
init: {
|
|
292
|
+
fieldPath: 'roleName',
|
|
293
|
+
associationPathName: 'users.roles',
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await waitFor(() => {
|
|
300
|
+
expect(form.getFieldValue(['users', 0, 'roles', 1, 'roleName'])).toBe('44');
|
|
301
|
+
expect(form.getFieldValue(['roleName'])).toBeUndefined();
|
|
302
|
+
expect(screen.getByTestId('js-name-path')).toHaveTextContent(
|
|
303
|
+
JSON.stringify(['users', 0, 'roles', 1, 'roleName']),
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
210
307
|
});
|