@nocobase/flow-engine 2.1.0-beta.41 → 2.1.0-beta.43
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/lib/components/subModel/LazyDropdown.js +17 -9
- package/lib/flowContext.d.ts +6 -1
- package/lib/flowContext.js +35 -6
- package/lib/flowEngine.d.ts +1 -0
- package/lib/flowEngine.js +53 -30
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
- package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
- package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/base.js +464 -29
- package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
- package/lib/runjs-context/contexts/elementDoc.js +152 -0
- package/lib/utils/loadedPageCache.d.ts +24 -0
- package/lib/utils/loadedPageCache.js +139 -0
- package/package.json +4 -4
- package/src/__tests__/flowContext.test.ts +23 -0
- package/src/__tests__/runjsContext.test.ts +18 -0
- package/src/__tests__/runjsContextImplementations.test.ts +9 -2
- package/src/__tests__/runjsLocales.test.ts +6 -5
- package/src/__tests__/viewScopedFlowEngine.test.ts +133 -0
- package/src/components/subModel/LazyDropdown.tsx +16 -7
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +51 -0
- package/src/flowContext.ts +40 -6
- package/src/flowEngine.ts +51 -27
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
- package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
- package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/base.ts +467 -31
- package/src/runjs-context/contexts/elementDoc.ts +130 -0
- package/src/utils/loadedPageCache.ts +147 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RunJSDocPropertyDoc } from '../../flowContext';
|
|
11
|
+
|
|
12
|
+
const elementProperties: Record<string, RunJSDocPropertyDoc> = {
|
|
13
|
+
innerHTML: 'Sanitized inner HTML string.',
|
|
14
|
+
outerHTML: 'Sanitized outer HTML string.',
|
|
15
|
+
textContent: 'Text content.',
|
|
16
|
+
style: 'Inline style declaration.',
|
|
17
|
+
classList: 'DOMTokenList for CSS classes.',
|
|
18
|
+
appendChild: {
|
|
19
|
+
type: 'function',
|
|
20
|
+
description: 'Append a DOM node or text.',
|
|
21
|
+
detail: '(child: Node | string) => void',
|
|
22
|
+
completion: { insertText: `ctx.element.appendChild('text')` },
|
|
23
|
+
},
|
|
24
|
+
setAttribute: {
|
|
25
|
+
type: 'function',
|
|
26
|
+
description: 'Set an HTML attribute.',
|
|
27
|
+
detail: '(name: string, value: string) => void',
|
|
28
|
+
completion: { insertText: `ctx.element.setAttribute('data-key', 'value')` },
|
|
29
|
+
},
|
|
30
|
+
getAttribute: {
|
|
31
|
+
type: 'function',
|
|
32
|
+
description: 'Read an HTML attribute.',
|
|
33
|
+
detail: '(name: string) => string | null',
|
|
34
|
+
completion: { insertText: `ctx.element.getAttribute('data-key')` },
|
|
35
|
+
},
|
|
36
|
+
querySelector: {
|
|
37
|
+
type: 'function',
|
|
38
|
+
description: 'Find the first matching descendant.',
|
|
39
|
+
detail: '(selectors: string) => Element | null',
|
|
40
|
+
completion: { insertText: `ctx.element.querySelector('.selector')` },
|
|
41
|
+
},
|
|
42
|
+
querySelectorAll: {
|
|
43
|
+
type: 'function',
|
|
44
|
+
description: 'Find all matching descendants.',
|
|
45
|
+
detail: '(selectors: string) => NodeListOf<Element>',
|
|
46
|
+
completion: { insertText: `ctx.element.querySelectorAll('.selector')` },
|
|
47
|
+
},
|
|
48
|
+
addEventListener: {
|
|
49
|
+
type: 'function',
|
|
50
|
+
description: 'Attach an event listener to the element.',
|
|
51
|
+
detail: '(type: string, listener: EventListener) => void',
|
|
52
|
+
completion: { insertText: `ctx.element.addEventListener('click', (event) => {})` },
|
|
53
|
+
},
|
|
54
|
+
removeEventListener: {
|
|
55
|
+
type: 'function',
|
|
56
|
+
description: 'Remove an event listener from the element.',
|
|
57
|
+
detail: '(type: string, listener: EventListener) => void',
|
|
58
|
+
completion: { insertText: `ctx.element.removeEventListener('click', handler)` },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const zhCNElementProperties: Record<string, RunJSDocPropertyDoc> = {
|
|
63
|
+
innerHTML: '已消毒的 innerHTML 字符串。',
|
|
64
|
+
outerHTML: '已消毒的 outerHTML 字符串。',
|
|
65
|
+
textContent: '文本内容。',
|
|
66
|
+
style: '内联样式声明。',
|
|
67
|
+
classList: 'CSS class 列表。',
|
|
68
|
+
appendChild: {
|
|
69
|
+
type: 'function',
|
|
70
|
+
description: '追加 DOM 节点或文本。',
|
|
71
|
+
detail: '(child: Node | string) => void',
|
|
72
|
+
completion: { insertText: `ctx.element.appendChild('text')` },
|
|
73
|
+
},
|
|
74
|
+
setAttribute: {
|
|
75
|
+
type: 'function',
|
|
76
|
+
description: '设置 HTML 属性。',
|
|
77
|
+
detail: '(name: string, value: string) => void',
|
|
78
|
+
completion: { insertText: `ctx.element.setAttribute('data-key', 'value')` },
|
|
79
|
+
},
|
|
80
|
+
getAttribute: {
|
|
81
|
+
type: 'function',
|
|
82
|
+
description: '读取 HTML 属性。',
|
|
83
|
+
detail: '(name: string) => string | null',
|
|
84
|
+
completion: { insertText: `ctx.element.getAttribute('data-key')` },
|
|
85
|
+
},
|
|
86
|
+
querySelector: {
|
|
87
|
+
type: 'function',
|
|
88
|
+
description: '查询第一个匹配的后代元素。',
|
|
89
|
+
detail: '(selectors: string) => Element | null',
|
|
90
|
+
completion: { insertText: `ctx.element.querySelector('.selector')` },
|
|
91
|
+
},
|
|
92
|
+
querySelectorAll: {
|
|
93
|
+
type: 'function',
|
|
94
|
+
description: '查询所有匹配的后代元素。',
|
|
95
|
+
detail: '(selectors: string) => NodeListOf<Element>',
|
|
96
|
+
completion: { insertText: `ctx.element.querySelectorAll('.selector')` },
|
|
97
|
+
},
|
|
98
|
+
addEventListener: {
|
|
99
|
+
type: 'function',
|
|
100
|
+
description: '给元素添加事件监听。',
|
|
101
|
+
detail: '(type: string, listener: EventListener) => void',
|
|
102
|
+
completion: { insertText: `ctx.element.addEventListener('click', (event) => {})` },
|
|
103
|
+
},
|
|
104
|
+
removeEventListener: {
|
|
105
|
+
type: 'function',
|
|
106
|
+
description: '移除元素事件监听。',
|
|
107
|
+
detail: '(type: string, listener: EventListener) => void',
|
|
108
|
+
completion: { insertText: `ctx.element.removeEventListener('click', handler)` },
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export function createElementPropertyDoc(
|
|
113
|
+
description = 'Current DOM container for this RunJS context. Usually an ElementProxy.',
|
|
114
|
+
): RunJSDocPropertyDoc {
|
|
115
|
+
return {
|
|
116
|
+
description,
|
|
117
|
+
detail: 'HTMLElement | ElementProxy',
|
|
118
|
+
properties: elementProperties,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createZhCNElementPropertyDoc(
|
|
123
|
+
description = '当前 RunJS 上下文的 DOM 容器,通常为 ElementProxy。',
|
|
124
|
+
): RunJSDocPropertyDoc {
|
|
125
|
+
return {
|
|
126
|
+
description,
|
|
127
|
+
detail: 'HTMLElement | ElementProxy',
|
|
128
|
+
properties: zhCNElementProperties,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FlowModel } from '../models';
|
|
11
|
+
|
|
12
|
+
type LoadedPageOptions = {
|
|
13
|
+
parentId?: string;
|
|
14
|
+
subKey?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type DirtyKeyOptions = {
|
|
18
|
+
force?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type FlowSettingsContextLike = {
|
|
22
|
+
flowSettingsEnabled?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type FlowEngineLike = {
|
|
26
|
+
context?: FlowSettingsContextLike;
|
|
27
|
+
previousEngine?: FlowEngineLike;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getLoadedPageKey = (options?: LoadedPageOptions): string | undefined => {
|
|
31
|
+
const parentId = options?.parentId;
|
|
32
|
+
const subKey = options?.subKey;
|
|
33
|
+
if (!parentId || subKey !== 'page') {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return `${parentId}::${subKey}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getLoadedPageKeyFromModel = (model?: FlowModel | null): string | undefined => {
|
|
40
|
+
let current = model;
|
|
41
|
+
while (current) {
|
|
42
|
+
if (current.subKey === 'page' && current.parent?.uid) {
|
|
43
|
+
return getLoadedPageKey({ parentId: current.parent.uid, subKey: current.subKey });
|
|
44
|
+
}
|
|
45
|
+
current = current.parent as FlowModel | undefined;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isFlowSettingsEnabledForContext = (context?: FlowSettingsContextLike): boolean => {
|
|
51
|
+
try {
|
|
52
|
+
return !!context?.flowSettingsEnabled;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isFlowSettingsEnabledForModel = (model?: FlowModel | null): boolean => {
|
|
59
|
+
if (isFlowSettingsEnabledForContext(model?.context as FlowSettingsContextLike | undefined)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const visited = new Set<FlowEngineLike>();
|
|
64
|
+
let engine = model?.flowEngine as FlowEngineLike | undefined;
|
|
65
|
+
while (engine && !visited.has(engine)) {
|
|
66
|
+
visited.add(engine);
|
|
67
|
+
if (isFlowSettingsEnabledForContext(engine.context)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
engine = engine.previousEngine;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const removeLoadedModelTree = (model?: FlowModel | null): void => {
|
|
76
|
+
if (!model?.uid || !model.flowEngine) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (model.flowEngine.getModel(model.uid) === model) {
|
|
80
|
+
model.flowEngine.removeModelWithSubModels(model.uid);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const mountLoadedModelToParent = <T extends FlowModel = FlowModel>(model: T | null, forceReplace = false): T | null => {
|
|
85
|
+
if (!model?.parent || !model.subKey) {
|
|
86
|
+
return model;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const mounted = (model.parent.subModels as any)?.[model.subKey];
|
|
90
|
+
const existing =
|
|
91
|
+
forceReplace && model.subType !== 'array' && mounted && !Array.isArray(mounted)
|
|
92
|
+
? (mounted as FlowModel)
|
|
93
|
+
: model.parent.findSubModel(model.subKey, (m) => m.uid === model.uid);
|
|
94
|
+
if (existing) {
|
|
95
|
+
if (!forceReplace || existing === model) {
|
|
96
|
+
return model;
|
|
97
|
+
}
|
|
98
|
+
removeLoadedModelTree(existing);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (model.subType === 'array') {
|
|
102
|
+
model.parent.addSubModel(model.subKey, model);
|
|
103
|
+
} else {
|
|
104
|
+
model.parent.setSubModel(model.subKey, model);
|
|
105
|
+
}
|
|
106
|
+
return model;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const createLoadedPageCache = () => {
|
|
110
|
+
const dirtyKeys = new Set<string>();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
getDirtyKeyForModel(model?: FlowModel | null, options?: DirtyKeyOptions): string | undefined {
|
|
114
|
+
if (!options?.force && !isFlowSettingsEnabledForModel(model)) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
return getLoadedPageKeyFromModel(model);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
markDirty(key?: string): void {
|
|
121
|
+
if (key) {
|
|
122
|
+
dirtyKeys.add(key);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
shouldBypass(options?: LoadedPageOptions, isFlowSettingsEnabled?: () => boolean): boolean {
|
|
127
|
+
const key = getLoadedPageKey(options);
|
|
128
|
+
if (!key || !dirtyKeys.has(key)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return !isFlowSettingsEnabled?.();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
clear(options?: LoadedPageOptions): void {
|
|
139
|
+
const key = getLoadedPageKey(options);
|
|
140
|
+
if (key) {
|
|
141
|
+
dirtyKeys.delete(key);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
mountModelToParent: mountLoadedModelToParent,
|
|
146
|
+
};
|
|
147
|
+
};
|