@nocobase/flow-engine 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 (45) hide show
  1. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -31
  2. package/lib/components/subModel/LazyDropdown.js +17 -9
  3. package/lib/executor/FlowExecutor.js +0 -3
  4. package/lib/flowContext.d.ts +6 -1
  5. package/lib/flowContext.js +35 -6
  6. package/lib/flowEngine.d.ts +4 -3
  7. package/lib/flowEngine.js +69 -37
  8. package/lib/models/flowModel.js +45 -13
  9. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  10. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  11. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  12. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  13. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  14. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  15. package/lib/runjs-context/contexts/base.js +464 -29
  16. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  17. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  18. package/lib/utils/loadedPageCache.d.ts +24 -0
  19. package/lib/utils/loadedPageCache.js +139 -0
  20. package/package.json +4 -4
  21. package/src/__tests__/flowContext.test.ts +23 -0
  22. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  23. package/src/__tests__/runjsContext.test.ts +18 -0
  24. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  25. package/src/__tests__/runjsLocales.test.ts +6 -5
  26. package/src/__tests__/viewScopedFlowEngine.test.ts +133 -0
  27. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +79 -37
  28. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +150 -3
  29. package/src/components/subModel/LazyDropdown.tsx +16 -7
  30. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +51 -0
  31. package/src/executor/FlowExecutor.ts +0 -3
  32. package/src/executor/__tests__/flowExecutor.test.ts +2 -4
  33. package/src/flowContext.ts +40 -6
  34. package/src/flowEngine.ts +71 -35
  35. package/src/models/__tests__/flowModel.test.ts +13 -28
  36. package/src/models/flowModel.tsx +62 -29
  37. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  38. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  39. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  40. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  41. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  42. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  43. package/src/runjs-context/contexts/base.ts +467 -31
  44. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  45. 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.42",
3
+ "version": "2.1.0-beta.44",
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.42",
12
- "@nocobase/shared": "2.1.0-beta.42",
11
+ "@nocobase/sdk": "2.1.0-beta.44",
12
+ "@nocobase/shared": "2.1.0-beta.44",
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": "619b4d06c934bf3266ba7f388438db018217c87d"
40
+ "gitHead": "540d4897b1696cc24c0754521b0b766978b4933c"
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', {
@@ -8,9 +8,30 @@
8
8
  */
9
9
 
10
10
  import { reaction } from '@nocobase/flow-engine';
11
- import { beforeEach, describe, expect, it } from 'vitest';
11
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
12
12
  import { FlowEngine } from '../flowEngine';
13
13
  import { FlowModel } from '../models';
14
+ import type { IFlowModelRepository } from '../types';
15
+
16
+ class MoveRepository implements IFlowModelRepository {
17
+ move = vi.fn(async (_sourceId: string, _targetId: string, _position: 'before' | 'after'): Promise<void> => {});
18
+
19
+ async findOne(): Promise<Record<string, unknown> | null> {
20
+ return null;
21
+ }
22
+
23
+ async save(): Promise<Record<string, unknown>> {
24
+ return {};
25
+ }
26
+
27
+ async destroy(): Promise<boolean> {
28
+ return true;
29
+ }
30
+
31
+ async duplicate(): Promise<Record<string, unknown> | null> {
32
+ return null;
33
+ }
34
+ }
14
35
 
15
36
  describe('FlowEngine moveModel', () => {
16
37
  let engine: FlowEngine;
@@ -20,6 +41,14 @@ describe('FlowEngine moveModel', () => {
20
41
  engine.registerModels({ FlowModel });
21
42
  });
22
43
 
44
+ const createParentWithChildren = () => {
45
+ const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
46
+ parent.addSubModel('items', { uid: 'child-a', use: 'FlowModel' });
47
+ parent.addSubModel('items', { uid: 'child-b', use: 'FlowModel' });
48
+ parent.addSubModel('items', { uid: 'child-c', use: 'FlowModel' });
49
+ return parent;
50
+ };
51
+
23
52
  it('keeps subModels array reactive after move so later additions trigger reactions', async () => {
24
53
  const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
25
54
  parent.addSubModel('items', { uid: 'child-a', use: 'FlowModel' });
@@ -40,4 +69,55 @@ describe('FlowEngine moveModel', () => {
40
69
  dispose();
41
70
  expect(seen).toEqual([3]);
42
71
  });
72
+
73
+ it('persists an after move when dragging forward', async () => {
74
+ const repository = new MoveRepository();
75
+ engine.setModelRepository(repository);
76
+ const parent = createParentWithChildren();
77
+
78
+ await engine.moveModel('child-a', 'child-c');
79
+
80
+ expect(repository.move).toHaveBeenCalledWith('child-a', 'child-c', 'after');
81
+ expect((parent.subModels.items as FlowModel[]).map((item) => item.uid)).toEqual(['child-b', 'child-c', 'child-a']);
82
+ });
83
+
84
+ it('persists a before move when dragging backward', async () => {
85
+ const repository = new MoveRepository();
86
+ engine.setModelRepository(repository);
87
+ const parent = createParentWithChildren();
88
+
89
+ await engine.moveModel('child-c', 'child-a');
90
+
91
+ expect(repository.move).toHaveBeenCalledWith('child-c', 'child-a', 'before');
92
+ expect((parent.subModels.items as FlowModel[]).map((item) => item.uid)).toEqual(['child-c', 'child-a', 'child-b']);
93
+ });
94
+
95
+ it('does not persist self-drop', async () => {
96
+ const repository = new MoveRepository();
97
+ engine.setModelRepository(repository);
98
+ const parent = createParentWithChildren();
99
+
100
+ await engine.moveModel('child-a', 'child-a');
101
+
102
+ expect(repository.move).not.toHaveBeenCalled();
103
+ expect((parent.subModels.items as FlowModel[]).map((item) => item.uid)).toEqual(['child-a', 'child-b', 'child-c']);
104
+ });
105
+
106
+ it('keeps null sortIndex subModels in stable order', () => {
107
+ const parent = engine.createModel({
108
+ uid: 'parent',
109
+ use: 'FlowModel',
110
+ subModels: {
111
+ items: [
112
+ { uid: 'child-a', use: 'FlowModel', sortIndex: null as unknown as number },
113
+ { uid: 'child-b', use: 'FlowModel', sortIndex: null as unknown as number },
114
+ ],
115
+ },
116
+ });
117
+
118
+ parent.addSubModel('items', { uid: 'child-c', use: 'FlowModel' });
119
+
120
+ expect((parent.subModels.items as FlowModel[]).map((item) => item.uid)).toEqual(['child-a', 'child-b', 'child-c']);
121
+ expect((parent.subModels.items as FlowModel[]).map((item) => item.sortIndex)).toEqual([1, 2, 3]);
122
+ });
43
123
  });
@@ -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
- expect(doc?.properties?.element).toContain('ElementProxy');
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
- expect(doc?.properties?.element).toContain('表格单元格');
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
- const message = doc?.properties?.message;
34
- const messageText =
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
  });