@nocobase/client-v2 2.1.0-beta.42 → 2.1.0-beta.44

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 (33) hide show
  1. package/es/components/form/TypedVariableInput.d.ts +75 -0
  2. package/es/components/form/index.d.ts +1 -0
  3. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  4. package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +8 -0
  5. package/es/index.mjs +95 -95
  6. package/lib/index.js +105 -105
  7. package/package.json +7 -7
  8. package/src/components/README.md +48 -0
  9. package/src/components/README.zh-CN.md +48 -0
  10. package/src/components/form/TypedVariableInput.tsx +441 -0
  11. package/src/components/form/__tests__/TypedVariableInput.test.tsx +152 -0
  12. package/src/components/form/index.tsx +1 -0
  13. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +100 -0
  14. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +23 -1
  15. package/src/flow/actions/linkageRules.tsx +5 -4
  16. package/src/flow/actions/linkageRulesRefresh.tsx +2 -3
  17. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
  18. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +67 -0
  19. package/src/flow/components/code-editor/__tests__/runjsCompletions.test.ts +272 -27
  20. package/src/flow/components/code-editor/runjsCompletions.ts +490 -9
  21. package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +90 -0
  22. package/src/flow/models/base/GridModel.tsx +1 -1
  23. package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
  24. package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
  25. package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
  26. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -3
  27. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +94 -2
  28. package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +67 -13
  29. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  30. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
  31. package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
  32. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
  33. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +70 -0
@@ -9,7 +9,12 @@
9
9
 
10
10
  import { Completion } from '@codemirror/autocomplete';
11
11
  import { EditorView } from '@codemirror/view';
12
- import { getRunJSDocFor, setupRunJSContexts, listSnippetsForContext } from '@nocobase/flow-engine';
12
+ import {
13
+ getRunJSDocFor,
14
+ setupRunJSContexts,
15
+ listSnippetsForContext,
16
+ type RunJSDocCompletionDoc,
17
+ } from '@nocobase/flow-engine';
13
18
  import { formatDocInfo } from './formatDocInfo';
14
19
 
15
20
  export type SnippetEntry = {
@@ -23,6 +28,483 @@ export type SnippetEntry = {
23
28
  scenes?: string[];
24
29
  };
25
30
 
31
+ type StaticCompletionEntry = {
32
+ label: string;
33
+ type: Completion['type'];
34
+ detail?: string;
35
+ info?: string;
36
+ insertText?: string;
37
+ boost?: number;
38
+ requires?: Array<'element'>;
39
+ };
40
+
41
+ const NON_ELEMENT_COMPLETION_SCENES = new Set(['eventFlow', 'formValue', 'linkage']);
42
+
43
+ const normalizeScenes = (scene?: string | string[]): string[] =>
44
+ (Array.isArray(scene) ? scene : scene ? [scene] : [])
45
+ .map((sceneName) => (typeof sceneName === 'string' ? sceneName.trim() : ''))
46
+ .filter(Boolean);
47
+
48
+ const hasRunJSElementContext = (doc: any, apiInfos: any, requestedScenes: string[]): boolean => {
49
+ if (requestedScenes.some((sceneName) => NON_ELEMENT_COMPLETION_SCENES.has(sceneName))) return false;
50
+ return !!((doc as any)?.properties?.element || (apiInfos as any)?.element);
51
+ };
52
+
53
+ const usesCtxElement = (text?: string): boolean => typeof text === 'string' && /\bctx\.element\b/.test(text);
54
+
55
+ const satisfiesCompletionRequirements = (
56
+ completionSpec: Pick<RunJSDocCompletionDoc, 'requires'> | undefined,
57
+ capabilities: { element: boolean },
58
+ ): boolean => {
59
+ if (!completionSpec?.requires?.length) return true;
60
+ return completionSpec.requires.every((requirement) => requirement !== 'element' || capabilities.element);
61
+ };
62
+
63
+ const createApply = (insertText?: string) => (view: EditorView, _completion: Completion, from: number, to: number) => {
64
+ if (!insertText) return;
65
+ view.dispatch({
66
+ changes: { from, to, insert: insertText },
67
+ selection: { anchor: from + insertText.length },
68
+ scrollIntoView: true,
69
+ });
70
+ };
71
+
72
+ const staticEntry = ({ label, type, detail, info, insertText, boost = 85 }: StaticCompletionEntry): Completion =>
73
+ ({
74
+ label,
75
+ type,
76
+ detail,
77
+ info,
78
+ boost,
79
+ apply: insertText ? createApply(insertText) : undefined,
80
+ }) as Completion;
81
+
82
+ const SAFE_GLOBAL_COMPLETIONS: StaticCompletionEntry[] = [
83
+ {
84
+ label: 'window.open()',
85
+ type: 'function',
86
+ detail: '(url: string, target?: string, features?: string) => Window | null',
87
+ info: 'Open a safe http/https/about:blank URL in a new tab.',
88
+ insertText: "window.open('https://example.com')",
89
+ boost: 105,
90
+ },
91
+ {
92
+ label: 'window.addEventListener()',
93
+ type: 'function',
94
+ detail: '(type: string, listener: EventListener) => void',
95
+ info: 'Attach a listener to the safe window proxy.',
96
+ insertText: "window.addEventListener('resize', () => {})",
97
+ boost: 105,
98
+ },
99
+ {
100
+ label: 'window.setTimeout()',
101
+ type: 'function',
102
+ detail: '(handler: Function, timeout?: number) => number',
103
+ insertText: 'window.setTimeout(() => {}, 0)',
104
+ boost: 100,
105
+ },
106
+ {
107
+ label: 'window.clearTimeout()',
108
+ type: 'function',
109
+ detail: '(id?: number) => void',
110
+ insertText: 'window.clearTimeout(timer)',
111
+ boost: 100,
112
+ },
113
+ {
114
+ label: 'window.setInterval()',
115
+ type: 'function',
116
+ detail: '(handler: Function, timeout?: number) => number',
117
+ insertText: 'window.setInterval(() => {}, 1000)',
118
+ boost: 100,
119
+ },
120
+ {
121
+ label: 'window.clearInterval()',
122
+ type: 'function',
123
+ detail: '(id?: number) => void',
124
+ insertText: 'window.clearInterval(timer)',
125
+ boost: 100,
126
+ },
127
+ {
128
+ label: 'window.FormData',
129
+ type: 'class',
130
+ detail: 'FormData constructor',
131
+ insertText: 'new window.FormData()',
132
+ boost: 95,
133
+ },
134
+ { label: 'window.Blob', type: 'class', detail: 'Blob constructor', insertText: 'window.Blob', boost: 95 },
135
+ { label: 'window.URL', type: 'class', detail: 'URL constructor', insertText: 'window.URL', boost: 95 },
136
+ { label: 'window.location.origin', type: 'property', detail: 'string', boost: 100 },
137
+ { label: 'window.location.protocol', type: 'property', detail: 'string', boost: 100 },
138
+ { label: 'window.location.host', type: 'property', detail: 'string', boost: 100 },
139
+ { label: 'window.location.hostname', type: 'property', detail: 'string', boost: 100 },
140
+ { label: 'window.location.port', type: 'property', detail: 'string', boost: 100 },
141
+ { label: 'window.location.pathname', type: 'property', detail: 'string', boost: 100 },
142
+ {
143
+ label: 'window.location.assign()',
144
+ type: 'function',
145
+ detail: '(url: string) => void',
146
+ insertText: "window.location.assign('/path')",
147
+ boost: 105,
148
+ },
149
+ {
150
+ label: 'window.location.replace()',
151
+ type: 'function',
152
+ detail: '(url: string) => void',
153
+ insertText: "window.location.replace('/path')",
154
+ boost: 105,
155
+ },
156
+ {
157
+ label: 'window.location.reload()',
158
+ type: 'function',
159
+ detail: '() => void',
160
+ insertText: 'window.location.reload()',
161
+ boost: 110,
162
+ },
163
+ {
164
+ label: 'window.location.href =',
165
+ type: 'snippet',
166
+ detail: 'safe assignment only',
167
+ info: 'RunJS allows assigning window.location.href, but reading href is blocked.',
168
+ insertText: "window.location.href = '/path'",
169
+ boost: 95,
170
+ },
171
+ {
172
+ label: 'document.createElement()',
173
+ type: 'function',
174
+ detail: '(tagName: string) => HTMLElement',
175
+ insertText: "document.createElement('div')",
176
+ boost: 105,
177
+ },
178
+ {
179
+ label: 'document.querySelector()',
180
+ type: 'function',
181
+ detail: '(selectors: string) => Element | null',
182
+ insertText: "document.querySelector('.selector')",
183
+ boost: 105,
184
+ },
185
+ {
186
+ label: 'document.querySelectorAll()',
187
+ type: 'function',
188
+ detail: '(selectors: string) => NodeListOf<Element>',
189
+ insertText: "document.querySelectorAll('.selector')",
190
+ boost: 105,
191
+ },
192
+ {
193
+ label: 'navigator.clipboard.writeText()',
194
+ type: 'function',
195
+ detail: '(text: string) => Promise<void>',
196
+ insertText: "await navigator.clipboard.writeText('text')",
197
+ boost: 105,
198
+ },
199
+ { label: 'navigator.onLine', type: 'property', detail: 'boolean', boost: 100 },
200
+ { label: 'navigator.language', type: 'property', detail: 'string', boost: 100 },
201
+ { label: 'navigator.languages', type: 'property', detail: 'string[]', boost: 100 },
202
+ {
203
+ label: 'console.log()',
204
+ type: 'function',
205
+ detail: '(...args: any[]) => void',
206
+ insertText: "console.log('message')",
207
+ boost: 95,
208
+ },
209
+ {
210
+ label: 'console.info()',
211
+ type: 'function',
212
+ detail: '(...args: any[]) => void',
213
+ insertText: "console.info('message')",
214
+ boost: 95,
215
+ },
216
+ {
217
+ label: 'console.warn()',
218
+ type: 'function',
219
+ detail: '(...args: any[]) => void',
220
+ insertText: "console.warn('message')",
221
+ boost: 95,
222
+ },
223
+ {
224
+ label: 'console.error()',
225
+ type: 'function',
226
+ detail: '(...args: any[]) => void',
227
+ insertText: "console.error('message')",
228
+ boost: 95,
229
+ },
230
+ {
231
+ label: 'setTimeout()',
232
+ type: 'function',
233
+ detail: '(handler: Function, timeout?: number) => number',
234
+ insertText: 'setTimeout(() => {}, 0)',
235
+ boost: 95,
236
+ },
237
+ {
238
+ label: 'clearTimeout()',
239
+ type: 'function',
240
+ detail: '(id?: number) => void',
241
+ insertText: 'clearTimeout(timer)',
242
+ boost: 95,
243
+ },
244
+ {
245
+ label: 'setInterval()',
246
+ type: 'function',
247
+ detail: '(handler: Function, timeout?: number) => number',
248
+ insertText: 'setInterval(() => {}, 1000)',
249
+ boost: 95,
250
+ },
251
+ {
252
+ label: 'clearInterval()',
253
+ type: 'function',
254
+ detail: '(id?: number) => void',
255
+ insertText: 'clearInterval(timer)',
256
+ boost: 95,
257
+ },
258
+ { label: 'Blob', type: 'class', detail: 'Blob constructor', insertText: 'Blob', boost: 90 },
259
+ { label: 'URL', type: 'class', detail: 'URL constructor', insertText: 'URL', boost: 90 },
260
+ ];
261
+
262
+ const RUNJS_RUNTIME_COMPLETIONS: StaticCompletionEntry[] = [
263
+ {
264
+ label: 'ctx.runjs()',
265
+ type: 'function',
266
+ detail: '(code: string, variables?: object, options?: object) => Promise<any>',
267
+ insertText: "await ctx.runjs('return 1')",
268
+ boost: 105,
269
+ },
270
+ {
271
+ label: 'ctx.loadCSS()',
272
+ type: 'function',
273
+ detail: '(href: string) => Promise<void>',
274
+ insertText: "await ctx.loadCSS('https://example.com/style.css')",
275
+ boost: 105,
276
+ },
277
+ {
278
+ label: 'ctx.previewRunJS()',
279
+ type: 'function',
280
+ detail: '(code: string, version?: string) => Promise<PreviewRunJSResult>',
281
+ insertText: "await ctx.previewRunJS('console.log(1)', 'v2')",
282
+ boost: 105,
283
+ },
284
+ {
285
+ label: 'ctx.requireAsync()',
286
+ type: 'function',
287
+ detail: '(url: string) => Promise<any>',
288
+ insertText: "const lib = await ctx.requireAsync('https://cdn.example.com/lib.umd.js')",
289
+ boost: 100,
290
+ },
291
+ {
292
+ label: 'ctx.importAsync()',
293
+ type: 'function',
294
+ detail: '(url: string) => Promise<any>',
295
+ insertText: "const mod = await ctx.importAsync('lodash-es')",
296
+ boost: 100,
297
+ },
298
+ {
299
+ label: 'ctx.t()',
300
+ type: 'function',
301
+ detail: '(key: string, options?: object) => string',
302
+ insertText: "ctx.t('Hello')",
303
+ boost: 100,
304
+ },
305
+ {
306
+ label: 'ctx.resolveJsonTemplate()',
307
+ type: 'function',
308
+ detail: '(template: any) => Promise<any>',
309
+ insertText: "await ctx.resolveJsonTemplate({ value: '{{ctx.record.id}}' })",
310
+ boost: 100,
311
+ },
312
+ {
313
+ label: 'ctx.sql.run()',
314
+ type: 'function',
315
+ detail: '(sql: string, options?: SQLRunOptions) => Promise<any>',
316
+ insertText: "await ctx.sql.run('SELECT 1', { type: 'selectRows' })",
317
+ boost: 100,
318
+ },
319
+ {
320
+ label: 'ctx.sql.save()',
321
+ type: 'function',
322
+ detail: '(data: { uid: string; sql: string; dataSourceKey?: string }) => Promise<void>',
323
+ insertText: "await ctx.sql.save({ uid: 'sql-uid', sql: 'SELECT 1' })",
324
+ boost: 95,
325
+ },
326
+ {
327
+ label: 'ctx.sql.runById()',
328
+ type: 'function',
329
+ detail: '(uid: string, options?: SQLRunOptions) => Promise<any>',
330
+ insertText: "await ctx.sql.runById('sql-uid', { type: 'selectRows' })",
331
+ boost: 100,
332
+ },
333
+ {
334
+ label: 'ctx.sql.destroy()',
335
+ type: 'function',
336
+ detail: '(uid: string) => Promise<void>',
337
+ insertText: "await ctx.sql.destroy('sql-uid')",
338
+ boost: 95,
339
+ },
340
+ {
341
+ label: 'ctx.logger.info()',
342
+ type: 'function',
343
+ detail: 'Pino logger info',
344
+ insertText: "ctx.logger.info({ foo: 1 }, 'message')",
345
+ boost: 95,
346
+ },
347
+ {
348
+ label: 'ctx.logger.warn()',
349
+ type: 'function',
350
+ detail: 'Pino logger warn',
351
+ insertText: "ctx.logger.warn({ foo: 1 }, 'message')",
352
+ boost: 95,
353
+ },
354
+ {
355
+ label: 'ctx.logger.error()',
356
+ type: 'function',
357
+ detail: 'Pino logger error',
358
+ insertText: "ctx.logger.error({ err }, 'message')",
359
+ boost: 95,
360
+ },
361
+ {
362
+ label: 'ctx.logger.debug()',
363
+ type: 'function',
364
+ detail: 'Pino logger debug',
365
+ insertText: "ctx.logger.debug({ foo: 1 }, 'message')",
366
+ boost: 95,
367
+ },
368
+ {
369
+ label: 'ctx.logger.child()',
370
+ type: 'function',
371
+ detail: '(bindings: object) => Logger',
372
+ insertText: "ctx.logger.child({ module: 'runjs' })",
373
+ boost: 90,
374
+ },
375
+ {
376
+ label: 'ctx.libs.React.createElement()',
377
+ type: 'function',
378
+ detail: 'React.createElement',
379
+ insertText: "ctx.libs.React.createElement('div', null, 'Hello')",
380
+ boost: 90,
381
+ },
382
+ {
383
+ label: 'ctx.libs.React.useState()',
384
+ type: 'function',
385
+ detail: 'React.useState',
386
+ insertText: 'ctx.libs.React.useState(initialValue)',
387
+ boost: 90,
388
+ },
389
+ {
390
+ label: 'ctx.libs.React.useEffect()',
391
+ type: 'function',
392
+ detail: 'React.useEffect',
393
+ insertText: 'ctx.libs.React.useEffect(() => {}, [])',
394
+ boost: 90,
395
+ },
396
+ {
397
+ label: 'ctx.libs.React.useMemo()',
398
+ type: 'function',
399
+ detail: 'React.useMemo',
400
+ insertText: 'ctx.libs.React.useMemo(() => value, [])',
401
+ boost: 85,
402
+ },
403
+ {
404
+ label: 'ctx.libs.React.useCallback()',
405
+ type: 'function',
406
+ detail: 'React.useCallback',
407
+ insertText: 'ctx.libs.React.useCallback(() => {}, [])',
408
+ boost: 85,
409
+ },
410
+ {
411
+ label: 'ctx.libs.ReactDOM.createRoot()',
412
+ type: 'function',
413
+ detail: 'ReactDOM.createRoot',
414
+ insertText: 'ctx.libs.ReactDOM.createRoot(ctx.element.__el)',
415
+ boost: 90,
416
+ requires: ['element'],
417
+ },
418
+ { label: 'ctx.libs.antd.Button', type: 'class', detail: 'Ant Design Button', boost: 90 },
419
+ { label: 'ctx.libs.antd.Table', type: 'class', detail: 'Ant Design Table', boost: 90 },
420
+ { label: 'ctx.libs.antd.Form', type: 'class', detail: 'Ant Design Form', boost: 90 },
421
+ { label: 'ctx.libs.antd.Input', type: 'class', detail: 'Ant Design Input', boost: 90 },
422
+ { label: 'ctx.libs.antd.Select', type: 'class', detail: 'Ant Design Select', boost: 90 },
423
+ { label: 'ctx.libs.antd.Modal', type: 'class', detail: 'Ant Design Modal', boost: 90 },
424
+ { label: 'ctx.libs.dayjs()', type: 'function', detail: 'dayjs', insertText: 'ctx.libs.dayjs()', boost: 90 },
425
+ {
426
+ label: 'ctx.libs.lodash.get()',
427
+ type: 'function',
428
+ detail: 'lodash.get',
429
+ insertText: "ctx.libs.lodash.get(obj, 'a.b')",
430
+ boost: 90,
431
+ },
432
+ {
433
+ label: 'ctx.libs.lodash.set()',
434
+ type: 'function',
435
+ detail: 'lodash.set',
436
+ insertText: "ctx.libs.lodash.set(obj, 'a.b', value)",
437
+ boost: 85,
438
+ },
439
+ {
440
+ label: 'ctx.libs.lodash.debounce()',
441
+ type: 'function',
442
+ detail: 'lodash.debounce',
443
+ insertText: 'ctx.libs.lodash.debounce(() => {}, 300)',
444
+ boost: 85,
445
+ },
446
+ {
447
+ label: 'ctx.libs.lodash.cloneDeep()',
448
+ type: 'function',
449
+ detail: 'lodash.cloneDeep',
450
+ insertText: 'ctx.libs.lodash.cloneDeep(value)',
451
+ boost: 85,
452
+ },
453
+ {
454
+ label: 'ctx.libs.formula.SUM()',
455
+ type: 'function',
456
+ detail: 'Formula.js SUM',
457
+ insertText: 'ctx.libs.formula.SUM(1, 2, 3)',
458
+ boost: 85,
459
+ },
460
+ {
461
+ label: 'ctx.libs.formula.AVERAGE()',
462
+ type: 'function',
463
+ detail: 'Formula.js AVERAGE',
464
+ insertText: 'ctx.libs.formula.AVERAGE(1, 2, 3)',
465
+ boost: 85,
466
+ },
467
+ {
468
+ label: 'ctx.libs.math.evaluate()',
469
+ type: 'function',
470
+ detail: 'mathjs evaluate',
471
+ insertText: "ctx.libs.math.evaluate('2 + 3')",
472
+ boost: 85,
473
+ },
474
+ {
475
+ label: 'ctx.libs.math.round()',
476
+ type: 'function',
477
+ detail: 'mathjs round',
478
+ insertText: 'ctx.libs.math.round(value, 2)',
479
+ boost: 85,
480
+ },
481
+ {
482
+ label: 'ctx.React.createElement()',
483
+ type: 'function',
484
+ detail: 'React.createElement alias',
485
+ insertText: "ctx.React.createElement('div', null, 'Hello')",
486
+ boost: 80,
487
+ },
488
+ {
489
+ label: 'ctx.ReactDOM.createRoot()',
490
+ type: 'function',
491
+ detail: 'ReactDOM.createRoot alias',
492
+ insertText: 'ctx.ReactDOM.createRoot(ctx.element.__el)',
493
+ boost: 80,
494
+ requires: ['element'],
495
+ },
496
+ { label: 'ctx.antd.Button', type: 'class', detail: 'Ant Design Button alias', boost: 80 },
497
+ { label: 'ctx.antd.Modal', type: 'class', detail: 'Ant Design Modal alias', boost: 80 },
498
+ { label: 'ctx.dayjs()', type: 'function', detail: 'dayjs alias', insertText: 'ctx.dayjs()', boost: 80 },
499
+ ];
500
+
501
+ function buildStaticRuntimeCompletions(capabilities: { element: boolean }): Completion[] {
502
+ return [...SAFE_GLOBAL_COMPLETIONS, ...RUNJS_RUNTIME_COMPLETIONS]
503
+ .filter((entry) => satisfiesCompletionRequirements(entry, capabilities))
504
+ .filter((entry) => capabilities.element || !usesCtxElement(entry.insertText))
505
+ .map(staticEntry);
506
+ }
507
+
26
508
  export async function buildRunJSCompletions(
27
509
  hostCtx: any,
28
510
  version = 'v1',
@@ -31,6 +513,7 @@ export async function buildRunJSCompletions(
31
513
  completions: Completion[];
32
514
  entries: SnippetEntry[];
33
515
  }> {
516
+ const requestedScenes = normalizeScenes(scene);
34
517
  const isFunctionLikeDocNode = (node: any, completionSpec: any): boolean => {
35
518
  if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
36
519
  const type = typeof (node as any).type === 'string' ? String((node as any).type) : '';
@@ -56,7 +539,7 @@ export async function buildRunJSCompletions(
56
539
  let apiInfos: any = null;
57
540
  try {
58
541
  if (hostCtx && typeof (hostCtx as any).getApiInfos === 'function') {
59
- apiInfos = await (hostCtx as any).getApiInfos({ version });
542
+ apiInfos = await (hostCtx as any).getApiInfos({ version, includeCompletion: true });
60
543
  }
61
544
  } catch (_) {
62
545
  apiInfos = null;
@@ -73,10 +556,12 @@ export async function buildRunJSCompletions(
73
556
  };
74
557
  const docApis = { ...((doc as any)?.properties || {}), ...normalizeMethodsRecord((doc as any)?.methods) };
75
558
  const apisSource = { ...(apiInfos && typeof apiInfos === 'object' ? apiInfos : {}), ...(docApis || {}) };
76
- const completions: Completion[] = [];
559
+ const hasElementContext = hasRunJSElementContext(doc, apiInfos, requestedScenes);
560
+ const completions: Completion[] = buildStaticRuntimeCompletions({ element: hasElementContext });
77
561
  const priorityRoots = new Set(['api', 'resource', 'viewer', 'record', 'formValues', 'popup']);
78
562
  const hiddenDecisionCache = new Map<string, { hideSelf: boolean; hideSubpaths: string[] }>();
79
563
  const hiddenPathPrefixes = new Set<string>();
564
+ if (!hasElementContext) hiddenPathPrefixes.add('ctx.element');
80
565
 
81
566
  const isHiddenByPaths = (label: string) => {
82
567
  if (!label || typeof label !== 'string') return false;
@@ -201,6 +686,8 @@ export async function buildRunJSCompletions(
201
686
  const insertText =
202
687
  completionSpec?.insertText ??
203
688
  (root === 'popup' ? `await ctx.getVar('${ctxLabel}')` : isCallable ? `${ctxLabel}()` : undefined);
689
+ if (!satisfiesCompletionRequirements(completionSpec, { element: hasElementContext })) continue;
690
+ if (!hasElementContext && usesCtxElement(insertText)) continue;
204
691
  const apply = insertText
205
692
  ? (view: EditorView, _completion: Completion, from: number, to: number) => {
206
693
  view.dispatch({
@@ -235,12 +722,6 @@ export async function buildRunJSCompletions(
235
722
  entries = [];
236
723
  }
237
724
 
238
- const requestedScenes = Array.isArray(scene)
239
- ? scene.filter((s): s is string => typeof s === 'string' && s.trim().length > 0)
240
- : scene
241
- ? [scene]
242
- : [];
243
-
244
725
  const filteredEntries = requestedScenes.length
245
726
  ? entries.filter((entry) => {
246
727
  if (entry.scenes?.length) {
@@ -53,6 +53,51 @@ describe('AssignForm value refill and save (beforeParamsSave)', () => {
53
53
  expect(saved?.assignedValues).toEqual({ nickname: 'Alice', score: 99 });
54
54
  });
55
55
 
56
+ it('UpdateRecordActionModel: saves assignedValues from assignForm subModel when assignFormUid is not ready', async () => {
57
+ const root = new FlowEngine();
58
+ root.registerModels({ UpdateRecordActionModel, AssignFormModel: TestAssignFormModel });
59
+
60
+ const action = root.createModel<UpdateRecordActionModel>({ use: 'UpdateRecordActionModel', uid: 'act-u-sub' });
61
+ const form = root.createModel<TestAssignFormModel>({
62
+ use: 'AssignFormModel',
63
+ uid: 'form-u-sub',
64
+ parentId: action.uid,
65
+ subKey: 'assignForm',
66
+ });
67
+ form.setAssignedValues({ nickname: 'Bob' });
68
+ action.setSubModel('assignForm', form);
69
+
70
+ const flow = action.getFlow('assignSettings') as any;
71
+ const step = flow?.steps?.assignFieldValues;
72
+
73
+ await step.beforeParamsSave({ engine: root, model: action }, {}, {});
74
+
75
+ const saved = action.getStepParams('assignSettings', 'assignFieldValues');
76
+ expect(saved?.assignedValues).toEqual({ nickname: 'Bob' });
77
+ });
78
+
79
+ it('UpdateRecordActionModel: keeps previous assignedValues when AssignForm is unavailable during save', async () => {
80
+ const root = new FlowEngine();
81
+ root.registerModels({ UpdateRecordActionModel, AssignFormModel: TestAssignFormModel });
82
+
83
+ const action = root.createModel<UpdateRecordActionModel>({ use: 'UpdateRecordActionModel', uid: 'act-u-missing' });
84
+ action.setStepParams('assignSettings', 'assignFieldValues', {});
85
+
86
+ const flow = action.getFlow('assignSettings') as any;
87
+ const step = flow?.steps?.assignFieldValues;
88
+
89
+ await step.beforeParamsSave(
90
+ { engine: root, model: action },
91
+ {},
92
+ {
93
+ assignedValues: { nickname: 'Previous' },
94
+ },
95
+ );
96
+
97
+ const saved = action.getStepParams('assignSettings', 'assignFieldValues');
98
+ expect(saved?.assignedValues).toEqual({ nickname: 'Previous' });
99
+ });
100
+
56
101
  it('FormSubmitActionModel: reuses assignFieldValues step and saves assignedValues from AssignForm', async () => {
57
102
  const root = new FlowEngine();
58
103
 
@@ -77,4 +122,49 @@ describe('AssignForm value refill and save (beforeParamsSave)', () => {
77
122
  const saved = action.getStepParams('submitSettings', 'assignFieldValues');
78
123
  expect(saved?.assignedValues).toEqual({ status: 'published' });
79
124
  });
125
+
126
+ it('FormSubmitActionModel: saves assignedValues from assignForm subModel when assignFormUid is not ready', async () => {
127
+ const root = new FlowEngine();
128
+ root.registerModels({ FormSubmitActionModel, AssignFormModel: TestAssignFormModel });
129
+
130
+ const action = root.createModel<FormSubmitActionModel>({ use: 'FormSubmitActionModel', uid: 'submit-u-sub' });
131
+ const form = root.createModel<TestAssignFormModel>({
132
+ use: 'AssignFormModel',
133
+ uid: 'submit-form-u-sub',
134
+ parentId: action.uid,
135
+ subKey: 'assignForm',
136
+ });
137
+ form.setAssignedValues({ status: 'draft' });
138
+ action.setSubModel('assignForm', form);
139
+
140
+ const flow = action.getFlow('submitSettings') as any;
141
+ const step = flow?.steps?.assignFieldValues;
142
+
143
+ await step.beforeParamsSave({ engine: root, model: action }, {}, {});
144
+
145
+ const saved = action.getStepParams('submitSettings', 'assignFieldValues');
146
+ expect(saved?.assignedValues).toEqual({ status: 'draft' });
147
+ });
148
+
149
+ it('FormSubmitActionModel: keeps previous assignedValues when AssignForm is unavailable during save', async () => {
150
+ const root = new FlowEngine();
151
+ root.registerModels({ FormSubmitActionModel, AssignFormModel: TestAssignFormModel });
152
+
153
+ const action = root.createModel<FormSubmitActionModel>({ use: 'FormSubmitActionModel', uid: 'submit-u-missing' });
154
+ action.setStepParams('submitSettings', 'assignFieldValues', {});
155
+
156
+ const flow = action.getFlow('submitSettings') as any;
157
+ const step = flow?.steps?.assignFieldValues;
158
+
159
+ await step.beforeParamsSave(
160
+ { engine: root, model: action },
161
+ {},
162
+ {
163
+ assignedValues: { status: 'published' },
164
+ },
165
+ );
166
+
167
+ const saved = action.getStepParams('submitSettings', 'assignFieldValues');
168
+ expect(saved?.assignedValues).toEqual({ status: 'published' });
169
+ });
80
170
  });
@@ -1006,7 +1006,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
1006
1006
  <Space
1007
1007
  ref={this.gridContainerRef}
1008
1008
  direction={'vertical'}
1009
- style={{ width: '100%', marginBottom: this.props.rowGap }}
1009
+ style={{ width: '100%', marginBottom: this.props.rowGap, display: 'flex' }}
1010
1010
  size={this.props.rowGap}
1011
1011
  >
1012
1012
  <DndProvider
@@ -49,7 +49,11 @@ export class ChildPageModel extends PageModel {
49
49
  throw new Error('Invalid drag event');
50
50
  }
51
51
 
52
- this.flowEngine.moveModel(event.active.id, event.over.id);
52
+ if (event.active.id === event.over.id) {
53
+ return;
54
+ }
55
+
56
+ await this.flowEngine.moveModel(event.active.id, event.over.id);
53
57
  }
54
58
 
55
59
  render() {
@@ -107,6 +107,18 @@ describe('ChildPageModel', () => {
107
107
  expect(mockFlowEngine.moveModel).toHaveBeenCalledWith('active-model-id', 'over-model-id');
108
108
  });
109
109
 
110
+ it('should ignore self-drop', async () => {
111
+ await childPageModel.handleDragEnd({
112
+ ...mockDragEndEvent,
113
+ over: {
114
+ ...(mockDragEndEvent.over as NonNullable<DragEndEvent['over']>),
115
+ id: 'active-model-id',
116
+ },
117
+ });
118
+
119
+ expect(mockFlowEngine.moveModel).not.toHaveBeenCalled();
120
+ });
121
+
110
122
  it('should handle case when over is null', async () => {
111
123
  const eventWithNullOver = {
112
124
  ...mockDragEndEvent,