@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
@@ -11,44 +11,97 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
11
11
 
12
12
  // Mock engine api provider
13
13
  vi.mock('@nocobase/flow-engine', () => {
14
- const doc = {
15
- properties: {
16
- foo: 'foo prop',
17
- api: {
18
- description: 'api client',
19
- completion: { insertText: 'ctx.api' },
20
- properties: {
21
- request: {
22
- description: 'send request',
23
- completion: { insertText: "await ctx.api.request({ url: '', method: 'get' })" },
14
+ const baseDocProperties = {
15
+ foo: 'foo prop',
16
+ api: {
17
+ description: 'api client',
18
+ completion: { insertText: 'ctx.api' },
19
+ properties: {
20
+ request: {
21
+ description: 'send request',
22
+ completion: { insertText: "await ctx.api.request({ url: '', method: 'get' })" },
23
+ },
24
+ },
25
+ },
26
+ viewer: {
27
+ description: 'viewer helpers',
28
+ properties: {
29
+ popover: {
30
+ type: 'function',
31
+ description: 'open popover',
32
+ completion: {
33
+ insertText: 'await ctx.viewer.popover({ target: ctx.element?.__el, content: <div /> })',
34
+ requires: ['element'],
35
+ },
36
+ },
37
+ embed: {
38
+ type: 'function',
39
+ description: 'open embed',
40
+ completion: {
41
+ insertText: 'await ctx.viewer.embed({ target: ctx.element?.__el, content: <div /> })',
42
+ requires: ['element'],
24
43
  },
25
44
  },
26
45
  },
27
- popup: {
28
- description: 'popup context (async)',
29
- detail: 'Promise<any>',
30
- hidden: async (ctx: any) => !(await ctx?.popup)?.uid,
31
- properties: {
32
- uid: 'popup uid',
33
- record: 'popup record',
34
- parent: {
35
- properties: {
36
- record: 'parent record',
37
- },
46
+ },
47
+ popup: {
48
+ description: 'popup context (async)',
49
+ detail: 'Promise<any>',
50
+ hidden: async (ctx: any) => !(await ctx?.popup)?.uid,
51
+ properties: {
52
+ uid: 'popup uid',
53
+ record: 'popup record',
54
+ parent: {
55
+ properties: {
56
+ record: 'parent record',
38
57
  },
39
58
  },
40
59
  },
41
60
  },
42
- methods: {
43
- bar: {
44
- description: 'bar method',
45
- completion: { insertText: "ctx.bar('value')" },
61
+ };
62
+ const methods = {
63
+ bar: {
64
+ description: 'bar method',
65
+ completion: { insertText: "ctx.bar('value')" },
66
+ },
67
+ render: {
68
+ description: 'render react content',
69
+ completion: { insertText: 'ctx.render(<div />)', requires: ['element'] },
70
+ },
71
+ };
72
+ const baseDoc = {
73
+ properties: baseDocProperties,
74
+ methods,
75
+ };
76
+ const domDoc = {
77
+ properties: {
78
+ ...baseDocProperties,
79
+ element: {
80
+ description: 'DOM container',
81
+ detail: 'ElementProxy',
82
+ properties: {
83
+ innerHTML: 'inner html',
84
+ setAttribute: {
85
+ type: 'function',
86
+ detail: '(name: string, value: string) => void',
87
+ completion: { insertText: "ctx.element.setAttribute('data-key', 'value')" },
88
+ },
89
+ },
46
90
  },
47
91
  },
92
+ methods,
48
93
  };
94
+ const domModels = new Set([
95
+ 'JSBlockModel',
96
+ 'JSFieldModel',
97
+ 'JSEditableFieldModel',
98
+ 'JSItemModel',
99
+ 'JSColumnModel',
100
+ 'FormJSFieldItemModel',
101
+ ]);
49
102
  return {
50
- getRunJSDocFor: () => doc,
51
- FlowRunJSContext: { getDoc: () => doc },
103
+ getRunJSDocFor: (ctx: any) => (domModels.has(ctx?.model?.constructor?.name) ? domDoc : baseDoc),
104
+ FlowRunJSContext: { getDoc: () => baseDoc },
52
105
  // New cohesive APIs
53
106
  listSnippetsForContext: async () => [
54
107
  {
@@ -69,6 +122,17 @@ vi.mock('@nocobase/flow-engine', () => {
69
122
  import { buildRunJSCompletions } from '../runjsCompletions';
70
123
 
71
124
  describe('buildRunJSCompletions', () => {
125
+ const labelsOf = (completions: any[]) => new Set(completions.map((c: any) => c.label));
126
+
127
+ const appliedTextsOf = (completions: any[]) =>
128
+ completions.flatMap((completion: any) => {
129
+ const mockView = { dispatch: vi.fn() } as any;
130
+ if (typeof completion.apply === 'function') {
131
+ completion.apply(mockView, completion, 0, 0);
132
+ }
133
+ return mockView.dispatch.mock.calls.map((call: any[]) => String(call[0]?.changes?.insert || ''));
134
+ });
135
+
72
136
  it('builds ctx property/method completions and snippets', async () => {
73
137
  const hostCtx = { popup: Promise.resolve({ uid: 'p1' }) }; // not used since engine doc is mocked
74
138
  const { completions, entries } = await buildRunJSCompletions(hostCtx, 'v1', 'block');
@@ -242,4 +306,185 @@ describe('buildRunJSCompletions', () => {
242
306
  // restore
243
307
  engineDoc.properties.popup.hidden = prev;
244
308
  });
309
+
310
+ it('adds safe browser/runtime global completions and excludes blocked globals', async () => {
311
+ const { completions } = await buildRunJSCompletions({}, 'v1', 'block');
312
+ const labels = new Set(completions.map((c: any) => c.label));
313
+
314
+ expect(labels.has('window.location.reload()')).toBe(true);
315
+ expect(labels.has('document.createElement()')).toBe(true);
316
+ expect(labels.has('navigator.clipboard.writeText()')).toBe(true);
317
+ expect(labels.has('console.log()')).toBe(true);
318
+ expect(labels.has('setTimeout()')).toBe(true);
319
+ expect(labels.has('Blob')).toBe(true);
320
+ expect(labels.has('URL')).toBe(true);
321
+
322
+ expect(labels.has('navigator.userAgent')).toBe(false);
323
+ expect(labels.has('navigator.geolocation')).toBe(false);
324
+ expect(labels.has('document.cookie')).toBe(false);
325
+ expect(labels.has('document.body')).toBe(false);
326
+ expect(labels.has('document.getElementById()')).toBe(false);
327
+ expect(labels.has('window.location.href')).toBe(false);
328
+ expect(labels.has('window.location.href =')).toBe(true);
329
+ });
330
+
331
+ it('adds stable ctx runtime namespace completions', async () => {
332
+ const hostCtx = {
333
+ model: {
334
+ constructor: {
335
+ name: 'JSBlockModel',
336
+ },
337
+ },
338
+ };
339
+ const { completions } = await buildRunJSCompletions(hostCtx, 'v1', 'block');
340
+ const labels = labelsOf(completions);
341
+
342
+ expect(labels.has('ctx.runjs()')).toBe(true);
343
+ expect(labels.has('ctx.sql.run()')).toBe(true);
344
+ expect(labels.has('ctx.logger.info()')).toBe(true);
345
+ expect(labels.has('ctx.element.setAttribute()')).toBe(true);
346
+ expect(labels.has('ctx.libs.React.createElement()')).toBe(true);
347
+ expect(labels.has('ctx.libs.lodash.get()')).toBe(true);
348
+ });
349
+
350
+ it('does not expose ctx.element for generic base completions', async () => {
351
+ const { completions } = await buildRunJSCompletions({}, 'v1', 'block');
352
+ const labels = labelsOf(completions);
353
+ const appliedTexts = appliedTextsOf(completions);
354
+
355
+ expect([...labels].some((label) => String(label).startsWith('ctx.element'))).toBe(false);
356
+ expect(labels.has('ctx.render()')).toBe(false);
357
+ expect(appliedTexts.some((text) => /\bctx\.element\b/.test(text))).toBe(false);
358
+ expect(appliedTexts.some((text) => text === 'ctx.render(<div />)')).toBe(false);
359
+ });
360
+
361
+ it('keeps ctx.element completions for DOM container models', async () => {
362
+ const hostCtx = {
363
+ model: {
364
+ constructor: {
365
+ name: 'JSBlockModel',
366
+ },
367
+ },
368
+ };
369
+ const { completions } = await buildRunJSCompletions(hostCtx, 'v1', 'block');
370
+ const labels = labelsOf(completions);
371
+
372
+ expect(labels.has('ctx.element.innerHTML')).toBe(true);
373
+ expect(labels.has('ctx.element.setAttribute()')).toBe(true);
374
+ expect(labels.has('ctx.libs.ReactDOM.createRoot()')).toBe(true);
375
+ expect(labels.has('ctx.render()')).toBe(true);
376
+
377
+ const renderCompletion = completions.find((c: any) => c.label === 'ctx.render()');
378
+ const mockView = { dispatch: vi.fn() } as any;
379
+ (renderCompletion as any).apply?.(mockView, renderCompletion, 0, 0);
380
+ expect(mockView.dispatch.mock.calls[0][0]?.changes?.insert).toBe('ctx.render(<div />)');
381
+ });
382
+
383
+ it('hides ctx.element-dependent completions for eventFlow', async () => {
384
+ const hostCtx = {
385
+ model: {
386
+ constructor: {
387
+ name: 'JSBlockModel',
388
+ },
389
+ },
390
+ };
391
+ const { completions } = await buildRunJSCompletions(hostCtx, 'v1', 'eventFlow');
392
+ const labels = labelsOf(completions);
393
+ const appliedTexts = appliedTextsOf(completions);
394
+
395
+ expect([...labels].some((label) => String(label).startsWith('ctx.element'))).toBe(false);
396
+ expect(labels.has('ctx.libs.ReactDOM.createRoot()')).toBe(false);
397
+ expect(labels.has('ctx.ReactDOM.createRoot()')).toBe(false);
398
+ expect(labels.has('ctx.viewer.popover()')).toBe(false);
399
+ expect(labels.has('ctx.viewer.embed()')).toBe(false);
400
+ expect(labels.has('ctx.render()')).toBe(false);
401
+ expect(appliedTexts.some((text) => /\bctx\.element\b/.test(text))).toBe(false);
402
+ expect(appliedTexts.some((text) => text === 'ctx.render(<div />)')).toBe(false);
403
+ });
404
+
405
+ it('hides ctx.element-dependent completions for non-DOM action models', async () => {
406
+ for (const [modelName, scene] of [
407
+ ['JSRecordActionModel', 'table'],
408
+ ['JSFormActionModel', 'form'],
409
+ ['FilterFormJSActionModel', 'form'],
410
+ ]) {
411
+ const hostCtx = {
412
+ model: {
413
+ constructor: {
414
+ name: modelName,
415
+ },
416
+ },
417
+ };
418
+ const { completions } = await buildRunJSCompletions(hostCtx, 'v1', scene);
419
+ const labels = labelsOf(completions);
420
+ const appliedTexts = appliedTextsOf(completions);
421
+
422
+ expect([...labels].some((label) => String(label).startsWith('ctx.element'))).toBe(false);
423
+ expect(labels.has('ctx.libs.ReactDOM.createRoot()')).toBe(false);
424
+ expect(labels.has('ctx.ReactDOM.createRoot()')).toBe(false);
425
+ expect(labels.has('ctx.render()')).toBe(false);
426
+ expect(appliedTexts.some((text) => /\bctx\.element\b/.test(text))).toBe(false);
427
+ expect(appliedTexts.some((text) => text === 'ctx.render(<div />)')).toBe(false);
428
+ }
429
+ });
430
+
431
+ it('uses friendly insertText for ctx.loadCSS and ctx.previewRunJS', async () => {
432
+ const { completions } = await buildRunJSCompletions({}, 'v1', 'block');
433
+
434
+ for (const [label, expected] of [
435
+ ['ctx.loadCSS()', "await ctx.loadCSS('https://example.com/style.css')"],
436
+ ['ctx.previewRunJS()', "await ctx.previewRunJS('console.log(1)', 'v2')"],
437
+ ['ctx.resolveJsonTemplate()', "await ctx.resolveJsonTemplate({ value: '{{ctx.record.id}}' })"],
438
+ ]) {
439
+ const completion = completions.find((c: any) => c.label === label);
440
+ expect(completion).toBeTruthy();
441
+ expect(typeof (completion as any).apply).toBe('function');
442
+
443
+ const mockView = { dispatch: vi.fn() } as any;
444
+ (completion as any).apply?.(mockView, completion, 0, 0);
445
+ expect(mockView.dispatch.mock.calls[0][0]?.changes?.insert).toBe(expected);
446
+ expect(mockView.dispatch.mock.calls[0][0]?.changes?.insert).not.toBe(label);
447
+ }
448
+ });
449
+
450
+ it('requests completion metadata from ctx.getApiInfos() for editor completions', async () => {
451
+ const getApiInfos = vi.fn(async (options?: any) => ({
452
+ customRuntime: {
453
+ type: 'function',
454
+ description: 'custom runtime helper',
455
+ completion: {
456
+ insertText: options?.includeCompletion ? "await ctx.customRuntime('value')" : undefined,
457
+ },
458
+ },
459
+ }));
460
+
461
+ const { completions } = await buildRunJSCompletions({ getApiInfos }, 'v1', 'block');
462
+
463
+ expect(getApiInfos).toHaveBeenCalledWith({ version: 'v1', includeCompletion: true });
464
+ const completion = completions.find((c: any) => c.label === 'ctx.customRuntime()');
465
+ expect(completion).toBeTruthy();
466
+
467
+ const mockView = { dispatch: vi.fn() } as any;
468
+ (completion as any).apply?.(mockView, completion, 0, 0);
469
+ expect(mockView.dispatch.mock.calls[0][0]?.changes?.insert).toBe("await ctx.customRuntime('value')");
470
+ });
471
+
472
+ it('keeps friendly insertText for ctx.resolveJsonTemplate with Chinese docs', async () => {
473
+ const hostCtx = {
474
+ api: {
475
+ auth: {
476
+ locale: 'zh-CN',
477
+ },
478
+ },
479
+ };
480
+ const { completions } = await buildRunJSCompletions(hostCtx, 'v1', 'block');
481
+ const completion = completions.find((c: any) => c.label === 'ctx.resolveJsonTemplate()');
482
+ expect(completion).toBeTruthy();
483
+
484
+ const mockView = { dispatch: vi.fn() } as any;
485
+ (completion as any).apply?.(mockView, completion, 0, 0);
486
+ expect(mockView.dispatch.mock.calls[0][0]?.changes?.insert).toBe(
487
+ "await ctx.resolveJsonTemplate({ value: '{{ctx.record.id}}' })",
488
+ );
489
+ });
245
490
  });