@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,152 @@
|
|
|
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
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (let key of __getOwnPropNames(from))
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var elementDoc_exports = {};
|
|
29
|
+
__export(elementDoc_exports, {
|
|
30
|
+
createElementPropertyDoc: () => createElementPropertyDoc,
|
|
31
|
+
createZhCNElementPropertyDoc: () => createZhCNElementPropertyDoc
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(elementDoc_exports);
|
|
34
|
+
const elementProperties = {
|
|
35
|
+
innerHTML: "Sanitized inner HTML string.",
|
|
36
|
+
outerHTML: "Sanitized outer HTML string.",
|
|
37
|
+
textContent: "Text content.",
|
|
38
|
+
style: "Inline style declaration.",
|
|
39
|
+
classList: "DOMTokenList for CSS classes.",
|
|
40
|
+
appendChild: {
|
|
41
|
+
type: "function",
|
|
42
|
+
description: "Append a DOM node or text.",
|
|
43
|
+
detail: "(child: Node | string) => void",
|
|
44
|
+
completion: { insertText: `ctx.element.appendChild('text')` }
|
|
45
|
+
},
|
|
46
|
+
setAttribute: {
|
|
47
|
+
type: "function",
|
|
48
|
+
description: "Set an HTML attribute.",
|
|
49
|
+
detail: "(name: string, value: string) => void",
|
|
50
|
+
completion: { insertText: `ctx.element.setAttribute('data-key', 'value')` }
|
|
51
|
+
},
|
|
52
|
+
getAttribute: {
|
|
53
|
+
type: "function",
|
|
54
|
+
description: "Read an HTML attribute.",
|
|
55
|
+
detail: "(name: string) => string | null",
|
|
56
|
+
completion: { insertText: `ctx.element.getAttribute('data-key')` }
|
|
57
|
+
},
|
|
58
|
+
querySelector: {
|
|
59
|
+
type: "function",
|
|
60
|
+
description: "Find the first matching descendant.",
|
|
61
|
+
detail: "(selectors: string) => Element | null",
|
|
62
|
+
completion: { insertText: `ctx.element.querySelector('.selector')` }
|
|
63
|
+
},
|
|
64
|
+
querySelectorAll: {
|
|
65
|
+
type: "function",
|
|
66
|
+
description: "Find all matching descendants.",
|
|
67
|
+
detail: "(selectors: string) => NodeListOf<Element>",
|
|
68
|
+
completion: { insertText: `ctx.element.querySelectorAll('.selector')` }
|
|
69
|
+
},
|
|
70
|
+
addEventListener: {
|
|
71
|
+
type: "function",
|
|
72
|
+
description: "Attach an event listener to the element.",
|
|
73
|
+
detail: "(type: string, listener: EventListener) => void",
|
|
74
|
+
completion: { insertText: `ctx.element.addEventListener('click', (event) => {})` }
|
|
75
|
+
},
|
|
76
|
+
removeEventListener: {
|
|
77
|
+
type: "function",
|
|
78
|
+
description: "Remove an event listener from the element.",
|
|
79
|
+
detail: "(type: string, listener: EventListener) => void",
|
|
80
|
+
completion: { insertText: `ctx.element.removeEventListener('click', handler)` }
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const zhCNElementProperties = {
|
|
84
|
+
innerHTML: "\u5DF2\u6D88\u6BD2\u7684 innerHTML \u5B57\u7B26\u4E32\u3002",
|
|
85
|
+
outerHTML: "\u5DF2\u6D88\u6BD2\u7684 outerHTML \u5B57\u7B26\u4E32\u3002",
|
|
86
|
+
textContent: "\u6587\u672C\u5185\u5BB9\u3002",
|
|
87
|
+
style: "\u5185\u8054\u6837\u5F0F\u58F0\u660E\u3002",
|
|
88
|
+
classList: "CSS class \u5217\u8868\u3002",
|
|
89
|
+
appendChild: {
|
|
90
|
+
type: "function",
|
|
91
|
+
description: "\u8FFD\u52A0 DOM \u8282\u70B9\u6216\u6587\u672C\u3002",
|
|
92
|
+
detail: "(child: Node | string) => void",
|
|
93
|
+
completion: { insertText: `ctx.element.appendChild('text')` }
|
|
94
|
+
},
|
|
95
|
+
setAttribute: {
|
|
96
|
+
type: "function",
|
|
97
|
+
description: "\u8BBE\u7F6E HTML \u5C5E\u6027\u3002",
|
|
98
|
+
detail: "(name: string, value: string) => void",
|
|
99
|
+
completion: { insertText: `ctx.element.setAttribute('data-key', 'value')` }
|
|
100
|
+
},
|
|
101
|
+
getAttribute: {
|
|
102
|
+
type: "function",
|
|
103
|
+
description: "\u8BFB\u53D6 HTML \u5C5E\u6027\u3002",
|
|
104
|
+
detail: "(name: string) => string | null",
|
|
105
|
+
completion: { insertText: `ctx.element.getAttribute('data-key')` }
|
|
106
|
+
},
|
|
107
|
+
querySelector: {
|
|
108
|
+
type: "function",
|
|
109
|
+
description: "\u67E5\u8BE2\u7B2C\u4E00\u4E2A\u5339\u914D\u7684\u540E\u4EE3\u5143\u7D20\u3002",
|
|
110
|
+
detail: "(selectors: string) => Element | null",
|
|
111
|
+
completion: { insertText: `ctx.element.querySelector('.selector')` }
|
|
112
|
+
},
|
|
113
|
+
querySelectorAll: {
|
|
114
|
+
type: "function",
|
|
115
|
+
description: "\u67E5\u8BE2\u6240\u6709\u5339\u914D\u7684\u540E\u4EE3\u5143\u7D20\u3002",
|
|
116
|
+
detail: "(selectors: string) => NodeListOf<Element>",
|
|
117
|
+
completion: { insertText: `ctx.element.querySelectorAll('.selector')` }
|
|
118
|
+
},
|
|
119
|
+
addEventListener: {
|
|
120
|
+
type: "function",
|
|
121
|
+
description: "\u7ED9\u5143\u7D20\u6DFB\u52A0\u4E8B\u4EF6\u76D1\u542C\u3002",
|
|
122
|
+
detail: "(type: string, listener: EventListener) => void",
|
|
123
|
+
completion: { insertText: `ctx.element.addEventListener('click', (event) => {})` }
|
|
124
|
+
},
|
|
125
|
+
removeEventListener: {
|
|
126
|
+
type: "function",
|
|
127
|
+
description: "\u79FB\u9664\u5143\u7D20\u4E8B\u4EF6\u76D1\u542C\u3002",
|
|
128
|
+
detail: "(type: string, listener: EventListener) => void",
|
|
129
|
+
completion: { insertText: `ctx.element.removeEventListener('click', handler)` }
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
function createElementPropertyDoc(description = "Current DOM container for this RunJS context. Usually an ElementProxy.") {
|
|
133
|
+
return {
|
|
134
|
+
description,
|
|
135
|
+
detail: "HTMLElement | ElementProxy",
|
|
136
|
+
properties: elementProperties
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
__name(createElementPropertyDoc, "createElementPropertyDoc");
|
|
140
|
+
function createZhCNElementPropertyDoc(description = "\u5F53\u524D RunJS \u4E0A\u4E0B\u6587\u7684 DOM \u5BB9\u5668\uFF0C\u901A\u5E38\u4E3A ElementProxy\u3002") {
|
|
141
|
+
return {
|
|
142
|
+
description,
|
|
143
|
+
detail: "HTMLElement | ElementProxy",
|
|
144
|
+
properties: zhCNElementProperties
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
__name(createZhCNElementPropertyDoc, "createZhCNElementPropertyDoc");
|
|
148
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
149
|
+
0 && (module.exports = {
|
|
150
|
+
createElementPropertyDoc,
|
|
151
|
+
createZhCNElementPropertyDoc
|
|
152
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
import type { FlowModel } from '../models';
|
|
10
|
+
type LoadedPageOptions = {
|
|
11
|
+
parentId?: string;
|
|
12
|
+
subKey?: string;
|
|
13
|
+
};
|
|
14
|
+
type DirtyKeyOptions = {
|
|
15
|
+
force?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare const createLoadedPageCache: () => {
|
|
18
|
+
getDirtyKeyForModel(model?: FlowModel | null, options?: DirtyKeyOptions): string | undefined;
|
|
19
|
+
markDirty(key?: string): void;
|
|
20
|
+
shouldBypass(options?: LoadedPageOptions, isFlowSettingsEnabled?: () => boolean): boolean;
|
|
21
|
+
clear(options?: LoadedPageOptions): void;
|
|
22
|
+
mountModelToParent: <T extends FlowModel<import("..").DefaultStructure> = FlowModel<import("..").DefaultStructure>>(model: T, forceReplace?: boolean) => T;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (let key of __getOwnPropNames(from))
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var loadedPageCache_exports = {};
|
|
29
|
+
__export(loadedPageCache_exports, {
|
|
30
|
+
createLoadedPageCache: () => createLoadedPageCache
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(loadedPageCache_exports);
|
|
33
|
+
const getLoadedPageKey = /* @__PURE__ */ __name((options) => {
|
|
34
|
+
const parentId = options == null ? void 0 : options.parentId;
|
|
35
|
+
const subKey = options == null ? void 0 : options.subKey;
|
|
36
|
+
if (!parentId || subKey !== "page") {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
return `${parentId}::${subKey}`;
|
|
40
|
+
}, "getLoadedPageKey");
|
|
41
|
+
const getLoadedPageKeyFromModel = /* @__PURE__ */ __name((model) => {
|
|
42
|
+
var _a;
|
|
43
|
+
let current = model;
|
|
44
|
+
while (current) {
|
|
45
|
+
if (current.subKey === "page" && ((_a = current.parent) == null ? void 0 : _a.uid)) {
|
|
46
|
+
return getLoadedPageKey({ parentId: current.parent.uid, subKey: current.subKey });
|
|
47
|
+
}
|
|
48
|
+
current = current.parent;
|
|
49
|
+
}
|
|
50
|
+
return void 0;
|
|
51
|
+
}, "getLoadedPageKeyFromModel");
|
|
52
|
+
const isFlowSettingsEnabledForContext = /* @__PURE__ */ __name((context) => {
|
|
53
|
+
try {
|
|
54
|
+
return !!(context == null ? void 0 : context.flowSettingsEnabled);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}, "isFlowSettingsEnabledForContext");
|
|
59
|
+
const isFlowSettingsEnabledForModel = /* @__PURE__ */ __name((model) => {
|
|
60
|
+
if (isFlowSettingsEnabledForContext(model == null ? void 0 : model.context)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const visited = /* @__PURE__ */ new Set();
|
|
64
|
+
let engine = model == null ? void 0 : model.flowEngine;
|
|
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
|
+
}, "isFlowSettingsEnabledForModel");
|
|
74
|
+
const removeLoadedModelTree = /* @__PURE__ */ __name((model) => {
|
|
75
|
+
if (!(model == null ? void 0 : model.uid) || !model.flowEngine) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (model.flowEngine.getModel(model.uid) === model) {
|
|
79
|
+
model.flowEngine.removeModelWithSubModels(model.uid);
|
|
80
|
+
}
|
|
81
|
+
}, "removeLoadedModelTree");
|
|
82
|
+
const mountLoadedModelToParent = /* @__PURE__ */ __name((model, forceReplace = false) => {
|
|
83
|
+
var _a;
|
|
84
|
+
if (!(model == null ? void 0 : model.parent) || !model.subKey) {
|
|
85
|
+
return model;
|
|
86
|
+
}
|
|
87
|
+
const mounted = (_a = model.parent.subModels) == null ? void 0 : _a[model.subKey];
|
|
88
|
+
const existing = forceReplace && model.subType !== "array" && mounted && !Array.isArray(mounted) ? mounted : model.parent.findSubModel(model.subKey, (m) => m.uid === model.uid);
|
|
89
|
+
if (existing) {
|
|
90
|
+
if (!forceReplace || existing === model) {
|
|
91
|
+
return model;
|
|
92
|
+
}
|
|
93
|
+
removeLoadedModelTree(existing);
|
|
94
|
+
}
|
|
95
|
+
if (model.subType === "array") {
|
|
96
|
+
model.parent.addSubModel(model.subKey, model);
|
|
97
|
+
} else {
|
|
98
|
+
model.parent.setSubModel(model.subKey, model);
|
|
99
|
+
}
|
|
100
|
+
return model;
|
|
101
|
+
}, "mountLoadedModelToParent");
|
|
102
|
+
const createLoadedPageCache = /* @__PURE__ */ __name(() => {
|
|
103
|
+
const dirtyKeys = /* @__PURE__ */ new Set();
|
|
104
|
+
return {
|
|
105
|
+
getDirtyKeyForModel(model, options) {
|
|
106
|
+
if (!(options == null ? void 0 : options.force) && !isFlowSettingsEnabledForModel(model)) {
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
return getLoadedPageKeyFromModel(model);
|
|
110
|
+
},
|
|
111
|
+
markDirty(key) {
|
|
112
|
+
if (key) {
|
|
113
|
+
dirtyKeys.add(key);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
shouldBypass(options, isFlowSettingsEnabled) {
|
|
117
|
+
const key = getLoadedPageKey(options);
|
|
118
|
+
if (!key || !dirtyKeys.has(key)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return !(isFlowSettingsEnabled == null ? void 0 : isFlowSettingsEnabled());
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
clear(options) {
|
|
128
|
+
const key = getLoadedPageKey(options);
|
|
129
|
+
if (key) {
|
|
130
|
+
dirtyKeys.delete(key);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
mountModelToParent: mountLoadedModelToParent
|
|
134
|
+
};
|
|
135
|
+
}, "createLoadedPageCache");
|
|
136
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
137
|
+
0 && (module.exports = {
|
|
138
|
+
createLoadedPageCache
|
|
139
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.43",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.1.0-beta.
|
|
12
|
-
"@nocobase/shared": "2.1.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.1.0-beta.43",
|
|
12
|
+
"@nocobase/shared": "2.1.0-beta.43",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "6d7750e2373bf2451d246de88cc1f62491685e18"
|
|
41
41
|
}
|
|
@@ -796,6 +796,29 @@ describe('FlowContext.getApiInfos', () => {
|
|
|
796
796
|
expect((infos.bar?.ref as any)?.url).toBe('https://example.com');
|
|
797
797
|
});
|
|
798
798
|
|
|
799
|
+
it('should include completion only when requested by getApiInfos()', async () => {
|
|
800
|
+
const ctx = new FlowContext();
|
|
801
|
+
ctx.defineMethod('bar', () => 2, {
|
|
802
|
+
description: 'Bar',
|
|
803
|
+
completion: { insertText: 'ctx.bar()' },
|
|
804
|
+
});
|
|
805
|
+
ctx.defineProperty('token', {
|
|
806
|
+
value: 't',
|
|
807
|
+
info: {
|
|
808
|
+
description: 'Token string',
|
|
809
|
+
completion: { insertText: 'ctx.token' },
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const compact = await ctx.getApiInfos();
|
|
814
|
+
expect((compact.bar as any)?.completion).toBeUndefined();
|
|
815
|
+
expect((compact.token as any)?.completion).toBeUndefined();
|
|
816
|
+
|
|
817
|
+
const editorInfos = await ctx.getApiInfos({ includeCompletion: true });
|
|
818
|
+
expect((editorInfos.bar as any)?.completion?.insertText).toBe('ctx.bar()');
|
|
819
|
+
expect((editorInfos.token as any)?.completion?.insertText).toBe('ctx.token');
|
|
820
|
+
});
|
|
821
|
+
|
|
799
822
|
it('should return property infos with completion/ref/examples', async () => {
|
|
800
823
|
const ctx = new FlowContext();
|
|
801
824
|
ctx.defineProperty('token', {
|
|
@@ -88,6 +88,19 @@ describe('flowRunJSContext registry and doc', () => {
|
|
|
88
88
|
const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
|
|
89
89
|
expect(doc).toBeTruthy();
|
|
90
90
|
expect(doc?.label).toMatch(/RunJS base/);
|
|
91
|
+
expect(doc?.properties?.element).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should mark element-dependent base completions with element requirement', () => {
|
|
95
|
+
const ctx: any = { model: { constructor: { name: 'UnknownModel' } } };
|
|
96
|
+
const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
|
|
97
|
+
|
|
98
|
+
expect((doc?.methods?.render as any)?.completion?.requires).toContain('element');
|
|
99
|
+
expect((doc?.properties?.viewer as any)?.properties?.popover?.completion?.requires).toContain('element');
|
|
100
|
+
expect((doc?.properties?.viewer as any)?.properties?.embed?.completion?.requires).toContain('element');
|
|
101
|
+
expect(
|
|
102
|
+
(doc?.properties?.libs as any)?.properties?.ReactDOM?.properties?.createRoot?.completion?.requires,
|
|
103
|
+
).toContain('element');
|
|
91
104
|
});
|
|
92
105
|
|
|
93
106
|
it('should support locale-specific doc', () => {
|
|
@@ -99,6 +112,11 @@ describe('flowRunJSContext registry and doc', () => {
|
|
|
99
112
|
const messageText =
|
|
100
113
|
typeof message === 'string' ? message : (message as any)?.description ?? (message as any)?.detail ?? '';
|
|
101
114
|
expect(String(messageText)).toMatch(/Ant Design 全局消息/);
|
|
115
|
+
expect((doc?.methods?.render as any)?.completion?.requires).toContain('element');
|
|
116
|
+
expect((doc?.properties?.viewer as any)?.properties?.popover?.completion?.requires).toContain('element');
|
|
117
|
+
expect(
|
|
118
|
+
(doc?.properties?.libs as any)?.properties?.ReactDOM?.properties?.createRoot?.completion?.requires,
|
|
119
|
+
).toContain('element');
|
|
102
120
|
});
|
|
103
121
|
|
|
104
122
|
it('should fallback to English when locale is not found', () => {
|
|
@@ -27,7 +27,10 @@ describe('Specific RunJSContext implementations', () => {
|
|
|
27
27
|
const ctx: any = { model: { constructor: { name: 'JSColumnModel' } } };
|
|
28
28
|
const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
|
|
29
29
|
expect(doc?.properties?.element).toBeTruthy();
|
|
30
|
-
|
|
30
|
+
const elementDoc: any = doc?.properties?.element;
|
|
31
|
+
expect(elementDoc?.detail).toContain('ElementProxy');
|
|
32
|
+
expect(elementDoc?.properties?.setAttribute).toBeTruthy();
|
|
33
|
+
expect(elementDoc?.properties?.querySelector).toBeTruthy();
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
it('should have record property in doc', () => {
|
|
@@ -68,7 +71,9 @@ describe('Specific RunJSContext implementations', () => {
|
|
|
68
71
|
(ctx as any).defineProperty('api', { value: { auth: { locale: 'zh-CN' } } });
|
|
69
72
|
const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
|
|
70
73
|
expect(doc?.label).toMatch(/JS 列/);
|
|
71
|
-
|
|
74
|
+
const elementDoc: any = doc?.properties?.element;
|
|
75
|
+
expect(elementDoc?.description).toContain('表格单元格');
|
|
76
|
+
expect(elementDoc?.properties?.addEventListener).toBeTruthy();
|
|
72
77
|
});
|
|
73
78
|
|
|
74
79
|
it('should create instance successfully', () => {
|
|
@@ -162,6 +167,7 @@ describe('Specific RunJSContext implementations', () => {
|
|
|
162
167
|
const ctx: any = { model: { constructor: { name: 'JSRecordActionModel' } } };
|
|
163
168
|
const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
|
|
164
169
|
expect(doc?.properties?.record).toBeTruthy();
|
|
170
|
+
expect(doc?.properties?.element).toBeUndefined();
|
|
165
171
|
});
|
|
166
172
|
|
|
167
173
|
it('should have filterByTk property', () => {
|
|
@@ -184,6 +190,7 @@ describe('Specific RunJSContext implementations', () => {
|
|
|
184
190
|
const ctx: any = { model: { constructor: { name: 'JSCollectionActionModel' } } };
|
|
185
191
|
const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
|
|
186
192
|
expect(doc?.properties?.resource).toBeTruthy();
|
|
193
|
+
expect(doc?.properties?.element).toBeUndefined();
|
|
187
194
|
});
|
|
188
195
|
|
|
189
196
|
it('should support zh-CN locale', () => {
|
|
@@ -12,6 +12,10 @@ import { getRunJSDocFor } from '..';
|
|
|
12
12
|
import { setupRunJSContexts } from '../runjs-context/setup';
|
|
13
13
|
import { FlowContext } from '../flowContext';
|
|
14
14
|
|
|
15
|
+
function getRunJSDocText(doc: unknown) {
|
|
16
|
+
return typeof doc === 'string' ? doc : (doc as any)?.description ?? (doc as any)?.detail ?? '';
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
describe('RunJS locales patch (engine doc)', () => {
|
|
16
20
|
beforeAll(async () => {
|
|
17
21
|
await setupRunJSContexts();
|
|
@@ -30,10 +34,7 @@ describe('RunJS locales patch (engine doc)', () => {
|
|
|
30
34
|
(ctx as any).defineProperty('model', { value: { constructor: { name: 'JSBlockModel' } } });
|
|
31
35
|
(ctx as any).defineProperty('api', { value: { auth: { locale: 'zh-CN' } } });
|
|
32
36
|
const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
typeof message === 'string' ? message : (message as any)?.description ?? (message as any)?.detail ?? '';
|
|
36
|
-
expect(String(messageText)).toMatch(/Ant Design 全局消息 API/);
|
|
37
|
-
expect(String(doc?.methods?.t || '')).toMatch(/国际化函数/);
|
|
37
|
+
expect(String(getRunJSDocText(doc?.properties?.message))).toMatch(/Ant Design 全局消息 API/);
|
|
38
|
+
expect(String(getRunJSDocText(doc?.methods?.t))).toMatch(/国际化函数/);
|
|
38
39
|
});
|
|
39
40
|
});
|
|
@@ -18,6 +18,33 @@ import { APIClient as SDKApiClient } from '@nocobase/sdk';
|
|
|
18
18
|
import { FlowEngine } from '../flowEngine';
|
|
19
19
|
import { createViewScopedEngine } from '../ViewScopedFlowEngine';
|
|
20
20
|
import { FlowModel } from '../models';
|
|
21
|
+
import type { IFlowModelRepository } from '../types';
|
|
22
|
+
|
|
23
|
+
const clone = <T>(value: T): T => (value == null ? value : JSON.parse(JSON.stringify(value)));
|
|
24
|
+
|
|
25
|
+
class DirtyPageRepository implements IFlowModelRepository<FlowModel> {
|
|
26
|
+
public findOneCalls = 0;
|
|
27
|
+
public data: Record<string, any> | null = null;
|
|
28
|
+
|
|
29
|
+
async findOne(): Promise<Record<string, any> | null> {
|
|
30
|
+
this.findOneCalls += 1;
|
|
31
|
+
return clone(this.data);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async save(model: FlowModel): Promise<Record<string, any>> {
|
|
35
|
+
return model.serialize();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async destroy(): Promise<boolean> {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async move(): Promise<void> {}
|
|
43
|
+
|
|
44
|
+
async duplicate(): Promise<Record<string, any> | null> {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
21
48
|
|
|
22
49
|
describe('ViewScopedFlowEngine', () => {
|
|
23
50
|
it('shares global actions/events and model classes with parent', async () => {
|
|
@@ -307,4 +334,110 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
307
334
|
expect(result).not.toBeNull();
|
|
308
335
|
expect(result?.uid).toBe('child-normal');
|
|
309
336
|
});
|
|
337
|
+
|
|
338
|
+
it('reloads a dirty loaded page from repository and replaces stale parent reference', async () => {
|
|
339
|
+
const root = new FlowEngine();
|
|
340
|
+
const repository = new DirtyPageRepository();
|
|
341
|
+
root.setModelRepository(repository);
|
|
342
|
+
|
|
343
|
+
class ParentModel extends FlowModel {}
|
|
344
|
+
class PageModel extends FlowModel {}
|
|
345
|
+
class BlockModel extends FlowModel {}
|
|
346
|
+
root.registerModels({ ParentModel, PageModel, BlockModel });
|
|
347
|
+
|
|
348
|
+
const parent = root.createModel<ParentModel>({ use: 'ParentModel', uid: 'popup-action' });
|
|
349
|
+
const oldScoped = createViewScopedEngine(root);
|
|
350
|
+
const stalePage = oldScoped.createModel<PageModel>({
|
|
351
|
+
use: 'PageModel',
|
|
352
|
+
uid: 'popup-page',
|
|
353
|
+
parentId: parent.uid,
|
|
354
|
+
subKey: 'page',
|
|
355
|
+
subType: 'object',
|
|
356
|
+
subModels: {
|
|
357
|
+
items: [{ use: 'BlockModel', uid: 'stale-block' }],
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
const staleBlock = stalePage.findSubModel('items' as any, (item) => item.uid === 'stale-block') as FlowModel;
|
|
361
|
+
parent.setSubModel('page', stalePage);
|
|
362
|
+
oldScoped.unlinkFromStack();
|
|
363
|
+
|
|
364
|
+
repository.data = {
|
|
365
|
+
use: 'PageModel',
|
|
366
|
+
uid: 'popup-page',
|
|
367
|
+
parentId: parent.uid,
|
|
368
|
+
subKey: 'page',
|
|
369
|
+
subType: 'object',
|
|
370
|
+
subModels: {
|
|
371
|
+
items: [{ use: 'BlockModel', uid: 'fresh-block' }],
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
root.flowSettings.enable();
|
|
376
|
+
await staleBlock.saveStepParams();
|
|
377
|
+
root.flowSettings.disable();
|
|
378
|
+
repository.findOneCalls = 0;
|
|
379
|
+
|
|
380
|
+
const runtimeScoped = createViewScopedEngine(root);
|
|
381
|
+
const loaded = await runtimeScoped.loadOrCreateModel<PageModel>({
|
|
382
|
+
async: true,
|
|
383
|
+
parentId: parent.uid,
|
|
384
|
+
subKey: 'page',
|
|
385
|
+
subType: 'object',
|
|
386
|
+
use: 'PageModel',
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
expect(repository.findOneCalls).toBe(1);
|
|
390
|
+
expect(loaded).not.toBe(stalePage);
|
|
391
|
+
expect((parent.subModels as any).page).toBe(loaded);
|
|
392
|
+
expect(loaded?.mapSubModels('items' as any, (item) => item.uid)).toEqual(['fresh-block']);
|
|
393
|
+
|
|
394
|
+
repository.findOneCalls = 0;
|
|
395
|
+
const nextRuntimeScoped = createViewScopedEngine(root);
|
|
396
|
+
const loadedAgain = await nextRuntimeScoped.loadOrCreateModel<PageModel>({
|
|
397
|
+
async: true,
|
|
398
|
+
parentId: parent.uid,
|
|
399
|
+
subKey: 'page',
|
|
400
|
+
subType: 'object',
|
|
401
|
+
use: 'PageModel',
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(repository.findOneCalls).toBe(0);
|
|
405
|
+
expect(loadedAgain?.uid).toBe('popup-page');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('does not bypass loaded page cache after a non-config save', async () => {
|
|
409
|
+
const root = new FlowEngine();
|
|
410
|
+
const repository = new DirtyPageRepository();
|
|
411
|
+
root.setModelRepository(repository);
|
|
412
|
+
|
|
413
|
+
class ParentModel extends FlowModel {}
|
|
414
|
+
class PageModel extends FlowModel {}
|
|
415
|
+
root.registerModels({ ParentModel, PageModel });
|
|
416
|
+
|
|
417
|
+
const parent = root.createModel<ParentModel>({ use: 'ParentModel', uid: 'normal-parent' });
|
|
418
|
+
const stalePage = root.createModel<PageModel>({
|
|
419
|
+
use: 'PageModel',
|
|
420
|
+
uid: 'normal-page',
|
|
421
|
+
parentId: parent.uid,
|
|
422
|
+
subKey: 'page',
|
|
423
|
+
subType: 'object',
|
|
424
|
+
});
|
|
425
|
+
parent.setSubModel('page', stalePage);
|
|
426
|
+
|
|
427
|
+
root.flowSettings.disable();
|
|
428
|
+
await root.saveModel(stalePage);
|
|
429
|
+
repository.findOneCalls = 0;
|
|
430
|
+
|
|
431
|
+
const runtimeScoped = createViewScopedEngine(root);
|
|
432
|
+
const loaded = await runtimeScoped.loadOrCreateModel<PageModel>({
|
|
433
|
+
async: true,
|
|
434
|
+
parentId: parent.uid,
|
|
435
|
+
subKey: 'page',
|
|
436
|
+
subType: 'object',
|
|
437
|
+
use: 'PageModel',
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(repository.findOneCalls).toBe(0);
|
|
441
|
+
expect(loaded?.uid).toBe('normal-page');
|
|
442
|
+
});
|
|
310
443
|
});
|
|
@@ -463,6 +463,7 @@ const createSearchItem = (
|
|
|
463
463
|
},
|
|
464
464
|
activateSearchSubmenu: (key: string) => void,
|
|
465
465
|
deactivateSearchSubmenu: (key: string) => void,
|
|
466
|
+
shouldActivateSearchSubmenu: boolean,
|
|
466
467
|
) => ({
|
|
467
468
|
key: `${searchKey}-search`,
|
|
468
469
|
type: 'group' as const,
|
|
@@ -480,28 +481,34 @@ const createSearchItem = (
|
|
|
480
481
|
onChange={(e) => {
|
|
481
482
|
e.stopPropagation();
|
|
482
483
|
const value = e.target.value;
|
|
483
|
-
|
|
484
|
+
if (shouldActivateSearchSubmenu) {
|
|
485
|
+
activateSearchSubmenu(searchKey);
|
|
486
|
+
}
|
|
484
487
|
if ((e.nativeEvent as any)?.isComposing || searchHandlers.isComposing(searchKey)) {
|
|
485
488
|
searchHandlers.updateInputValue(searchKey, value);
|
|
486
489
|
return;
|
|
487
490
|
}
|
|
488
|
-
if (!value) {
|
|
491
|
+
if (!value && shouldActivateSearchSubmenu) {
|
|
489
492
|
deactivateSearchSubmenu(searchKey);
|
|
490
493
|
}
|
|
491
494
|
searchHandlers.updateSearchValue(searchKey, value);
|
|
492
495
|
}}
|
|
493
496
|
onCompositionStart={(e) => {
|
|
494
497
|
e.stopPropagation();
|
|
495
|
-
|
|
498
|
+
if (shouldActivateSearchSubmenu) {
|
|
499
|
+
activateSearchSubmenu(searchKey);
|
|
500
|
+
}
|
|
496
501
|
searchHandlers.startComposition(searchKey);
|
|
497
502
|
}}
|
|
498
503
|
onCompositionEnd={(e) => {
|
|
499
504
|
e.stopPropagation();
|
|
500
505
|
const value = e.currentTarget.value;
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
506
|
+
if (shouldActivateSearchSubmenu) {
|
|
507
|
+
if (value) {
|
|
508
|
+
activateSearchSubmenu(searchKey);
|
|
509
|
+
} else {
|
|
510
|
+
deactivateSearchSubmenu(searchKey);
|
|
511
|
+
}
|
|
505
512
|
}
|
|
506
513
|
searchHandlers.endComposition(searchKey, value);
|
|
507
514
|
}}
|
|
@@ -737,6 +744,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
737
744
|
const searchKey = keyPath;
|
|
738
745
|
const currentSearchValue = searchValues[searchKey] || '';
|
|
739
746
|
const currentInputValue = inputValues[searchKey] ?? currentSearchValue;
|
|
747
|
+
const shouldActivateSearchSubmenu = !(item.type === 'group' && path.length === 0);
|
|
740
748
|
|
|
741
749
|
// 递归过滤:当 child 为分组时,会继续向下过滤其 children;
|
|
742
750
|
// 仅保留自身匹配或存在匹配子项的分组。
|
|
@@ -770,6 +778,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
770
778
|
searchHandlers,
|
|
771
779
|
activateSearchSubmenu,
|
|
772
780
|
deactivateSearchSubmenu,
|
|
781
|
+
shouldActivateSearchSubmenu,
|
|
773
782
|
);
|
|
774
783
|
const dividerItem = { key: `${keyPath}-search-divider`, type: 'divider' as const };
|
|
775
784
|
|