@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.
- package/es/components/form/TypedVariableInput.d.ts +75 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +8 -0
- package/es/index.mjs +95 -95
- package/lib/index.js +105 -105
- package/package.json +7 -7
- package/src/components/README.md +48 -0
- package/src/components/README.zh-CN.md +48 -0
- package/src/components/form/TypedVariableInput.tsx +441 -0
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +152 -0
- package/src/components/form/index.tsx +1 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +100 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +23 -1
- package/src/flow/actions/linkageRules.tsx +5 -4
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +67 -0
- package/src/flow/components/code-editor/__tests__/runjsCompletions.test.ts +272 -27
- package/src/flow/components/code-editor/runjsCompletions.ts +490 -9
- package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +90 -0
- package/src/flow/models/base/GridModel.tsx +1 -1
- package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
- package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
- package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -3
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +94 -2
- package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +67 -13
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
- 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 {
|
|
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
|
|
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
|
-
|
|
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,
|