@nocobase/flow-engine 2.0.0-alpha.25 → 2.0.0-alpha.26
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/flowContext.js +45 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -0
- package/lib/models/flowModel.js +3 -3
- package/lib/runjs-context/contexts/base.js +14 -0
- package/lib/runjs-context/snippets/index.js +1 -0
- package/lib/runjs-context/snippets/scene/block/render-button-handler.snippet.js +6 -4
- package/lib/runjs-context/snippets/scene/block/render-info-card.snippet.js +15 -16
- package/lib/runjs-context/snippets/scene/block/render-react-jsx.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/block/render-react-jsx.snippet.js +58 -0
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +7 -7
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +23 -28
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +19 -20
- package/lib/utils/jsxTransform.d.ts +15 -0
- package/lib/utils/jsxTransform.js +68 -0
- package/package.json +5 -4
- package/src/flowContext.ts +63 -0
- package/src/index.ts +1 -0
- package/src/models/flowModel.tsx +2 -2
- package/src/runjs-context/contexts/base.ts +16 -0
- package/src/runjs-context/snippets/index.ts +1 -0
- package/src/runjs-context/snippets/scene/block/render-button-handler.snippet.ts +6 -4
- package/src/runjs-context/snippets/scene/block/render-info-card.snippet.ts +15 -16
- package/src/runjs-context/snippets/scene/block/render-react-jsx.snippet.ts +39 -0
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +7 -7
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +23 -28
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +19 -20
- package/src/utils/__tests__/jsxTransform.test.ts +38 -0
- package/src/utils/jsxTransform.ts +39 -0
package/lib/flowContext.js
CHANGED
|
@@ -61,6 +61,7 @@ var import_lodash = __toESM(require("lodash"));
|
|
|
61
61
|
var import_qs = __toESM(require("qs"));
|
|
62
62
|
var import_react = __toESM(require("react"));
|
|
63
63
|
var ReactDOMClient = __toESM(require("react-dom/client"));
|
|
64
|
+
var import_ElementProxy = require("./ElementProxy");
|
|
64
65
|
var import_Acl = require("./acl/Acl");
|
|
65
66
|
var import_ContextPathProxy = require("./ContextPathProxy");
|
|
66
67
|
var import_data_source = require("./data-source");
|
|
@@ -1280,6 +1281,50 @@ const _FlowRunJSContext = class _FlowRunJSContext extends FlowContext {
|
|
|
1280
1281
|
}, "createRoot")
|
|
1281
1282
|
};
|
|
1282
1283
|
this.defineProperty("ReactDOM", { value: ReactDOMShim });
|
|
1284
|
+
this.defineMethod(
|
|
1285
|
+
"render",
|
|
1286
|
+
function(vnode, container) {
|
|
1287
|
+
const el = container || this.element;
|
|
1288
|
+
if (!el) throw new Error("ctx.render: container not provided and ctx.element is not available");
|
|
1289
|
+
const containerEl = (el == null ? void 0 : el.__el) || el;
|
|
1290
|
+
const globalRef = globalThis;
|
|
1291
|
+
globalRef.__nbRunjsRoots = globalRef.__nbRunjsRoots || /* @__PURE__ */ new WeakMap();
|
|
1292
|
+
const rootMap = globalRef.__nbRunjsRoots;
|
|
1293
|
+
if (typeof vnode === "string") {
|
|
1294
|
+
const existingRoot = rootMap.get(containerEl);
|
|
1295
|
+
if (existingRoot && typeof existingRoot.unmount === "function") {
|
|
1296
|
+
try {
|
|
1297
|
+
existingRoot.unmount();
|
|
1298
|
+
} finally {
|
|
1299
|
+
rootMap.delete(containerEl);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const proxy = new import_ElementProxy.ElementProxy(containerEl);
|
|
1303
|
+
proxy.innerHTML = String(vnode ?? "");
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
if (vnode && vnode.nodeType && (vnode.nodeType === 1 || vnode.nodeType === 3 || vnode.nodeType === 11)) {
|
|
1307
|
+
const existingRoot = rootMap.get(containerEl);
|
|
1308
|
+
if (existingRoot && typeof existingRoot.unmount === "function") {
|
|
1309
|
+
try {
|
|
1310
|
+
existingRoot.unmount();
|
|
1311
|
+
} finally {
|
|
1312
|
+
rootMap.delete(containerEl);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
while (containerEl.firstChild) containerEl.removeChild(containerEl.firstChild);
|
|
1316
|
+
containerEl.appendChild(vnode);
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
let root = rootMap.get(containerEl);
|
|
1320
|
+
if (!root) {
|
|
1321
|
+
root = this.ReactDOM.createRoot(containerEl);
|
|
1322
|
+
rootMap.set(containerEl, root);
|
|
1323
|
+
}
|
|
1324
|
+
root.render(vnode);
|
|
1325
|
+
return root;
|
|
1326
|
+
}
|
|
1327
|
+
);
|
|
1283
1328
|
}
|
|
1284
1329
|
static define(meta, options) {
|
|
1285
1330
|
const locale = options == null ? void 0 : options.locale;
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -28,6 +28,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
28
28
|
var src_exports = {};
|
|
29
29
|
__export(src_exports, {
|
|
30
30
|
RunJSContextRegistry: () => import_registry.RunJSContextRegistry,
|
|
31
|
+
compileRunJs: () => import_jsxTransform.compileRunJs,
|
|
31
32
|
createBlockScopedEngine: () => import_BlockScopedFlowEngine.createBlockScopedEngine,
|
|
32
33
|
createJSRunnerWithVersion: () => import_helpers.createJSRunnerWithVersion,
|
|
33
34
|
createViewScopedEngine: () => import_ViewScopedFlowEngine.createViewScopedEngine,
|
|
@@ -43,6 +44,7 @@ __export(src_exports, {
|
|
|
43
44
|
module.exports = __toCommonJS(src_exports);
|
|
44
45
|
__reExport(src_exports, require("./types"), module.exports);
|
|
45
46
|
__reExport(src_exports, require("./utils"), module.exports);
|
|
47
|
+
var import_jsxTransform = require("./utils/jsxTransform");
|
|
46
48
|
__reExport(src_exports, require("./resources"), module.exports);
|
|
47
49
|
__reExport(src_exports, require("./flowEngine"), module.exports);
|
|
48
50
|
__reExport(src_exports, require("./hooks"), module.exports);
|
|
@@ -68,6 +70,7 @@ var import_BlockScopedFlowEngine = require("./BlockScopedFlowEngine");
|
|
|
68
70
|
// Annotate the CommonJS export names for ESM import in node:
|
|
69
71
|
0 && (module.exports = {
|
|
70
72
|
RunJSContextRegistry,
|
|
73
|
+
compileRunJs,
|
|
71
74
|
createBlockScopedEngine,
|
|
72
75
|
createJSRunnerWithVersion,
|
|
73
76
|
createViewScopedEngine,
|
package/lib/models/flowModel.js
CHANGED
|
@@ -1054,7 +1054,8 @@ const _FlowModel = class _FlowModel {
|
|
|
1054
1054
|
uid: this.uid,
|
|
1055
1055
|
...import_lodash.default.omit(this._options, ["flowEngine"]),
|
|
1056
1056
|
stepParams: this.stepParams,
|
|
1057
|
-
sortIndex: this.sortIndex
|
|
1057
|
+
sortIndex: this.sortIndex,
|
|
1058
|
+
flowRegistry: {}
|
|
1058
1059
|
};
|
|
1059
1060
|
const subModels = this.subModels;
|
|
1060
1061
|
for (const subModelKey in subModels) {
|
|
@@ -1069,8 +1070,7 @@ const _FlowModel = class _FlowModel {
|
|
|
1069
1070
|
}
|
|
1070
1071
|
}
|
|
1071
1072
|
for (const [key, flow] of this.flowRegistry.getFlows()) {
|
|
1072
|
-
data[
|
|
1073
|
-
data["flowRegistry"][key] = flow.toData();
|
|
1073
|
+
data.flowRegistry[key] = flow.toData();
|
|
1074
1074
|
}
|
|
1075
1075
|
return data;
|
|
1076
1076
|
}
|
|
@@ -69,6 +69,13 @@ function defineBaseContextMeta() {
|
|
|
69
69
|
},
|
|
70
70
|
methods: {
|
|
71
71
|
t: 'Internationalization function for translating text. Parameters: (key: string, variables?: object) => string. Example: `ctx.t("Hello {name}", { name: "World" })`',
|
|
72
|
+
render: {
|
|
73
|
+
description: 'Render into container. Accepts ReactElement, DOM Node/Fragment, or HTML string. Parameters: (vnode: ReactElement | Node | DocumentFragment | string, container?: HTMLElement|ElementProxy) => Root|null. Example: `ctx.render(<div>Hello</div>)` or `ctx.render("<b>hi</b>")`',
|
|
74
|
+
detail: "ReactDOM Root",
|
|
75
|
+
completion: {
|
|
76
|
+
insertText: `ctx.render(<div />)`
|
|
77
|
+
}
|
|
78
|
+
},
|
|
72
79
|
requireAsync: 'Asynchronously load external libraries from URL. Parameters: (url: string) => Promise<any>. Example: `const lodash = await ctx.requireAsync("https://cdn.jsdelivr.net/npm/lodash")`',
|
|
73
80
|
importAsync: 'Dynamically import ESM module by URL. Parameters: (url: string) => Promise<Module>. Example: `const mod = await ctx.importAsync("https://cdn.jsdelivr.net/npm/lit-html@2/+esm")`',
|
|
74
81
|
resolveJsonTemplate: "Resolve JSON templates containing variable expressions with {{ }} syntax. Parameters: (template: any, context?: object) => any",
|
|
@@ -140,6 +147,13 @@ function defineBaseContextMeta() {
|
|
|
140
147
|
},
|
|
141
148
|
methods: {
|
|
142
149
|
t: '\u56FD\u9645\u5316\u51FD\u6570\uFF0C\u7528\u4E8E\u7FFB\u8BD1\u6587\u6848\u3002\u53C2\u6570\uFF1A(key: string, variables?: object) => string\u3002\u793A\u4F8B\uFF1A`ctx.t("\u4F60\u597D {name}", { name: "\u4E16\u754C" })`',
|
|
150
|
+
render: {
|
|
151
|
+
description: '\u6E32\u67D3\u5230\u5BB9\u5668\u3002vnode \u652F\u6301 ReactElement\u3001DOM \u8282\u70B9/\u7247\u6BB5\u3001\u6216 HTML \u5B57\u7B26\u4E32\u3002\u53C2\u6570\uFF1A(vnode: ReactElement | Node | DocumentFragment | string, container?: HTMLElement|ElementProxy) => Root|null\u3002\u793A\u4F8B\uFF1A`ctx.render(<div />)` \u6216 `ctx.render("<b>hi</b>")`',
|
|
152
|
+
detail: "ReactDOM Root",
|
|
153
|
+
completion: {
|
|
154
|
+
insertText: `ctx.render(<div />)`
|
|
155
|
+
}
|
|
156
|
+
},
|
|
143
157
|
requireAsync: '\u6309 URL \u5F02\u6B65\u52A0\u8F7D\u5916\u90E8\u5E93\u3002\u53C2\u6570\uFF1A(url: string) => Promise<any>\u3002\u793A\u4F8B\uFF1A`const lodash = await ctx.requireAsync("https://cdn.jsdelivr.net/npm/lodash")`',
|
|
144
158
|
importAsync: '\u6309 URL \u52A8\u6001\u5BFC\u5165 ESM \u6A21\u5757\uFF08\u5F00\u53D1/\u751F\u4EA7\u5747\u53EF\u7528\uFF09\u3002\u53C2\u6570\uFF1A(url: string) => Promise<Module>\u3002\u793A\u4F8B\uFF1A`const mod = await ctx.importAsync("https://cdn.jsdelivr.net/npm/lit-html@2/+esm")`',
|
|
145
159
|
resolveJsonTemplate: "\u89E3\u6790\u542B {{ }} \u53D8\u91CF\u8868\u8FBE\u5F0F\u7684 JSON \u6A21\u677F\u3002\u53C2\u6570\uFF1A(template: any, context?: object) => any",
|
|
@@ -60,6 +60,7 @@ const snippets = {
|
|
|
60
60
|
"scene/block/echarts-init": /* @__PURE__ */ __name(() => import("./scene/block/echarts-init.snippet"), "scene/block/echarts-init"),
|
|
61
61
|
// scene/block
|
|
62
62
|
"scene/block/render-react": /* @__PURE__ */ __name(() => import("./scene/block/render-react.snippet"), "scene/block/render-react"),
|
|
63
|
+
"scene/block/render-react-jsx": /* @__PURE__ */ __name(() => import("./scene/block/render-react-jsx.snippet"), "scene/block/render-react-jsx"),
|
|
63
64
|
"scene/block/render-button-handler": /* @__PURE__ */ __name(() => import("./scene/block/render-button-handler.snippet"), "scene/block/render-button-handler"),
|
|
64
65
|
"scene/block/add-event-listener": /* @__PURE__ */ __name(() => import("./scene/block/add-event-listener.snippet"), "scene/block/add-event-listener"),
|
|
65
66
|
"scene/block/chartjs-bar": /* @__PURE__ */ __name(() => import("./scene/block/chartjs-bar.snippet"), "scene/block/chartjs-bar"),
|
|
@@ -42,11 +42,13 @@ const snippet = {
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
content: `
|
|
45
|
-
const {
|
|
46
|
-
const { Button } = antd;
|
|
45
|
+
const { Button } = ctx.antd;
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
ctx.render(
|
|
48
|
+
<Button type="primary" onClick={() => ctx.message.success(ctx.t('Clicked!'))}>
|
|
49
|
+
{ctx.t('Button')}
|
|
50
|
+
</Button>
|
|
51
|
+
);
|
|
50
52
|
`
|
|
51
53
|
};
|
|
52
54
|
var render_button_handler_snippet_default = snippet;
|
|
@@ -42,29 +42,28 @@ const snippet = {
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
content: `
|
|
45
|
-
const { Card, Descriptions, Tag
|
|
46
|
-
const { createElement: h } = ctx.React;
|
|
45
|
+
const { Card, Descriptions, Tag } = ctx.antd;
|
|
47
46
|
|
|
48
47
|
if (!ctx.record) {
|
|
49
|
-
ctx.
|
|
48
|
+
ctx.render('<div style="padding:16px;color:#999;">' + ctx.t('No record data') + '</div>');
|
|
50
49
|
return;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
const record = ctx.record;
|
|
54
53
|
|
|
55
|
-
ctx.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
record.createdAt ? new Date(record.createdAt).toLocaleString() : '-'
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
54
|
+
ctx.render(
|
|
55
|
+
<Card title={ctx.t('Record Details')} bordered style={{ margin: 0 }}>
|
|
56
|
+
<Descriptions column={2} size="small">
|
|
57
|
+
<Descriptions.Item label={ctx.t('ID')}>{record.id || '-'}</Descriptions.Item>
|
|
58
|
+
<Descriptions.Item label={ctx.t('Status')}>
|
|
59
|
+
<Tag color={record.status === 'active' ? 'green' : 'default'}>{record.status || '-'}</Tag>
|
|
60
|
+
</Descriptions.Item>
|
|
61
|
+
<Descriptions.Item label={ctx.t('Title')}>{record.title || '-'}</Descriptions.Item>
|
|
62
|
+
<Descriptions.Item label={ctx.t('Created At')}>
|
|
63
|
+
{record.createdAt ? new Date(record.createdAt).toLocaleString() : '-'}
|
|
64
|
+
</Descriptions.Item>
|
|
65
|
+
</Descriptions>
|
|
66
|
+
</Card>
|
|
68
67
|
);
|
|
69
68
|
|
|
70
69
|
`
|
|
@@ -0,0 +1,11 @@
|
|
|
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 { SnippetModule } from '../../types';
|
|
10
|
+
declare const snippet: SnippetModule;
|
|
11
|
+
export default snippet;
|
|
@@ -0,0 +1,58 @@
|
|
|
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 __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var render_react_jsx_snippet_exports = {};
|
|
28
|
+
__export(render_react_jsx_snippet_exports, {
|
|
29
|
+
default: () => render_react_jsx_snippet_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(render_react_jsx_snippet_exports);
|
|
32
|
+
var import_JSBlockRunJSContext = require("../../../contexts/JSBlockRunJSContext");
|
|
33
|
+
const snippet = {
|
|
34
|
+
contexts: [import_JSBlockRunJSContext.JSBlockRunJSContext],
|
|
35
|
+
prefix: "sn-react-jsx",
|
|
36
|
+
label: "Render React (JSX)",
|
|
37
|
+
description: "Render a simple React component using JSX syntax",
|
|
38
|
+
locales: {
|
|
39
|
+
"zh-CN": {
|
|
40
|
+
label: "\u6E32\u67D3 React\uFF08JSX\uFF09",
|
|
41
|
+
description: "\u4F7F\u7528 JSX \u8BED\u6CD5\u6E32\u67D3\u4E00\u4E2A\u7B80\u5355\u7684 React \u7EC4\u4EF6"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
content: `
|
|
45
|
+
// Render a React component with JSX
|
|
46
|
+
const { React } = ctx;
|
|
47
|
+
|
|
48
|
+
const App = () => (
|
|
49
|
+
<div style={{ padding: 12 }}>
|
|
50
|
+
<h3 style={{ margin: 0, color: '#1890ff' }}>Hello JSX</h3>
|
|
51
|
+
<div style={{ color: '#555' }}>This block is rendered by JSX.</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
ctx.render(<App />);
|
|
56
|
+
`
|
|
57
|
+
};
|
|
58
|
+
var render_react_jsx_snippet_default = snippet;
|
|
@@ -43,15 +43,15 @@ const snippet = {
|
|
|
43
43
|
},
|
|
44
44
|
content: `
|
|
45
45
|
// Render a React element into ctx.element via ReactDOM
|
|
46
|
-
const {
|
|
47
|
-
const { Button } = antd;
|
|
46
|
+
const { Button } = ctx.antd;
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
ctx.render(
|
|
49
|
+
<div style={{ padding: 12 }}>
|
|
50
|
+
<Button type="primary" onClick={() => ctx.message.success(ctx.t('Clicked!'))}>
|
|
51
|
+
{ctx.t('Click')}
|
|
52
|
+
</Button>
|
|
53
|
+
</div>
|
|
53
54
|
);
|
|
54
|
-
ReactDOM.createRoot(ctx.element).render(node);
|
|
55
55
|
`
|
|
56
56
|
};
|
|
57
57
|
var render_react_snippet_default = snippet;
|
|
@@ -43,7 +43,6 @@ const snippet = {
|
|
|
43
43
|
},
|
|
44
44
|
content: `
|
|
45
45
|
const { Card, Statistic, Row, Col } = ctx.antd;
|
|
46
|
-
const { createElement: h } = ctx.React;
|
|
47
46
|
|
|
48
47
|
const res = await ctx.api.request({
|
|
49
48
|
url: 'users:list',
|
|
@@ -67,33 +66,29 @@ const distinctRoles = new Set(
|
|
|
67
66
|
.filter(Boolean),
|
|
68
67
|
).size;
|
|
69
68
|
|
|
70
|
-
ctx.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
})
|
|
94
|
-
)
|
|
95
|
-
)
|
|
96
|
-
)
|
|
69
|
+
ctx.render(
|
|
70
|
+
<Row gutter={16}>
|
|
71
|
+
<Col span={6}>
|
|
72
|
+
<Card>
|
|
73
|
+
<Statistic title={ctx.t('Total users')} value={total} valueStyle={{ color: '#3f8600' }} />
|
|
74
|
+
</Card>
|
|
75
|
+
</Col>
|
|
76
|
+
<Col span={6}>
|
|
77
|
+
<Card>
|
|
78
|
+
<Statistic title={ctx.t('Administrators')} value={adminCount} valueStyle={{ color: '#1890ff' }} />
|
|
79
|
+
</Card>
|
|
80
|
+
</Col>
|
|
81
|
+
<Col span={6}>
|
|
82
|
+
<Card>
|
|
83
|
+
<Statistic title={ctx.t('Users with email')} value={withEmail} valueStyle={{ color: '#faad14' }} />
|
|
84
|
+
</Card>
|
|
85
|
+
</Col>
|
|
86
|
+
<Col span={6}>
|
|
87
|
+
<Card>
|
|
88
|
+
<Statistic title={ctx.t('Distinct roles')} value={distinctRoles} valueStyle={{ color: '#cf1322' }} />
|
|
89
|
+
</Card>
|
|
90
|
+
</Col>
|
|
91
|
+
</Row>
|
|
97
92
|
);
|
|
98
93
|
`
|
|
99
94
|
};
|
|
@@ -43,7 +43,6 @@ const snippet = {
|
|
|
43
43
|
},
|
|
44
44
|
content: `
|
|
45
45
|
const { Timeline, Card } = ctx.antd;
|
|
46
|
-
const { createElement: h } = ctx.React;
|
|
47
46
|
|
|
48
47
|
const res = await ctx.api.request({
|
|
49
48
|
url: 'users:list',
|
|
@@ -57,28 +56,28 @@ const res = await ctx.api.request({
|
|
|
57
56
|
const records = res?.data?.data || [];
|
|
58
57
|
|
|
59
58
|
if (!records.length) {
|
|
60
|
-
ctx.
|
|
59
|
+
ctx.render('<div style="padding:16px;color:#999;">' + ctx.t('No data') + '</div>');
|
|
61
60
|
return;
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
ctx.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
key
|
|
70
|
-
label
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
record.email
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
63
|
+
ctx.render(
|
|
64
|
+
<Card title={ctx.t('Activity Timeline')} bordered>
|
|
65
|
+
<Timeline mode="left">
|
|
66
|
+
{records.map((record) => (
|
|
67
|
+
<Timeline.Item
|
|
68
|
+
key={record.id}
|
|
69
|
+
label={record.createdAt ? new Date(record.createdAt).toLocaleString() : ''}
|
|
70
|
+
>
|
|
71
|
+
<div>
|
|
72
|
+
<strong>{record.nickname || record.username || ctx.t('Unnamed user')}</strong>
|
|
73
|
+
{record.email ? (
|
|
74
|
+
<div style={{ color: '#999', fontSize: '12px', marginTop: '4px' }}>{record.email}</div>
|
|
75
|
+
) : null}
|
|
76
|
+
</div>
|
|
77
|
+
</Timeline.Item>
|
|
78
|
+
))}
|
|
79
|
+
</Timeline>
|
|
80
|
+
</Card>
|
|
82
81
|
);
|
|
83
82
|
`
|
|
84
83
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
* Lightweight JSX -> JS compiler for RunJS user code.
|
|
11
|
+
* - Uses sucrase via dynamic import (lazy; avoids static cycles and cost when not needed)
|
|
12
|
+
* - Maps JSX to ctx.React.createElement / ctx.React.Fragment so no global React is required
|
|
13
|
+
* - If sucrase is unavailable or transform throws, returns original code as graceful fallback
|
|
14
|
+
*/
|
|
15
|
+
export declare function compileRunJs(code: string): Promise<string>;
|
|
@@ -0,0 +1,68 @@
|
|
|
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 __create = Object.create;
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
20
|
+
};
|
|
21
|
+
var __copyProps = (to, from, except, desc) => {
|
|
22
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
23
|
+
for (let key of __getOwnPropNames(from))
|
|
24
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
25
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
26
|
+
}
|
|
27
|
+
return to;
|
|
28
|
+
};
|
|
29
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
30
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
31
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
32
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
33
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
34
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
35
|
+
mod
|
|
36
|
+
));
|
|
37
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
38
|
+
var jsxTransform_exports = {};
|
|
39
|
+
__export(jsxTransform_exports, {
|
|
40
|
+
compileRunJs: () => compileRunJs
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(jsxTransform_exports);
|
|
43
|
+
async function compileRunJs(code) {
|
|
44
|
+
var _a;
|
|
45
|
+
const maybeJSX = /<[A-Za-z]|<\//.test(code);
|
|
46
|
+
if (!maybeJSX) return code;
|
|
47
|
+
try {
|
|
48
|
+
const mod = await import("sucrase");
|
|
49
|
+
const transform = (mod == null ? void 0 : mod.transform) || ((_a = mod == null ? void 0 : mod.default) == null ? void 0 : _a.transform);
|
|
50
|
+
if (typeof transform !== "function") return code;
|
|
51
|
+
const res = transform(code, {
|
|
52
|
+
transforms: ["jsx"],
|
|
53
|
+
jsxPragma: "ctx.React.createElement",
|
|
54
|
+
jsxFragmentPragma: "ctx.React.Fragment",
|
|
55
|
+
// Keep ES syntax as-is; JSRunner runs in modern engines
|
|
56
|
+
disableESTransforms: true,
|
|
57
|
+
production: true
|
|
58
|
+
});
|
|
59
|
+
return (res && (res.code || (res == null ? void 0 : res.output))) ?? code;
|
|
60
|
+
} catch (_e) {
|
|
61
|
+
return code;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
__name(compileRunJs, "compileRunJs");
|
|
65
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
66
|
+
0 && (module.exports = {
|
|
67
|
+
compileRunJs
|
|
68
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.26",
|
|
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.0.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.0.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.0.0-alpha.26",
|
|
12
|
+
"@nocobase/shared": "2.0.0-alpha.26",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"dompurify": "^3.0.2",
|
|
15
15
|
"lodash": "^4.x",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"slate": "^0.103.0",
|
|
19
19
|
"slate-history": "^0.109.0",
|
|
20
20
|
"slate-react": "^0.110.3",
|
|
21
|
+
"sucrase": "^3.35.0",
|
|
21
22
|
"uid": "^2.0.2"
|
|
22
23
|
},
|
|
23
24
|
"peerDependencies": {
|
|
@@ -34,5 +35,5 @@
|
|
|
34
35
|
],
|
|
35
36
|
"author": "NocoBase Team",
|
|
36
37
|
"license": "AGPL-3.0",
|
|
37
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "3f9e6825dfefabec9640dc46783955174275df04"
|
|
38
39
|
}
|
package/src/flowContext.ts
CHANGED
|
@@ -20,6 +20,7 @@ import pino from 'pino';
|
|
|
20
20
|
import qs from 'qs';
|
|
21
21
|
import React, { createRef } from 'react';
|
|
22
22
|
import * as ReactDOMClient from 'react-dom/client';
|
|
23
|
+
import { ElementProxy } from './ElementProxy';
|
|
23
24
|
import type { Location } from 'react-router-dom';
|
|
24
25
|
import { ACL } from './acl/Acl';
|
|
25
26
|
import { ContextPathProxy } from './ContextPathProxy';
|
|
@@ -1698,6 +1699,68 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1698
1699
|
},
|
|
1699
1700
|
};
|
|
1700
1701
|
this.defineProperty('ReactDOM', { value: ReactDOMShim });
|
|
1702
|
+
|
|
1703
|
+
// Convenience: ctx.render(<App />[, container])
|
|
1704
|
+
// - container defaults to ctx.element if available
|
|
1705
|
+
// - internally uses engine.reactView.createRoot to inherit app context
|
|
1706
|
+
// - caches root per container via global WeakMap
|
|
1707
|
+
this.defineMethod(
|
|
1708
|
+
'render',
|
|
1709
|
+
function (
|
|
1710
|
+
this: any,
|
|
1711
|
+
vnode: React.ReactElement | Node | DocumentFragment | string,
|
|
1712
|
+
container?: Element | DocumentFragment,
|
|
1713
|
+
) {
|
|
1714
|
+
const el = (container as any) || (this.element as any);
|
|
1715
|
+
if (!el) throw new Error('ctx.render: container not provided and ctx.element is not available');
|
|
1716
|
+
const containerEl: any = (el as any)?.__el || el; // unwrap ElementProxy
|
|
1717
|
+
const globalRef: any = globalThis as any;
|
|
1718
|
+
globalRef.__nbRunjsRoots = globalRef.__nbRunjsRoots || new WeakMap<any, any>();
|
|
1719
|
+
const rootMap: WeakMap<any, any> = globalRef.__nbRunjsRoots;
|
|
1720
|
+
|
|
1721
|
+
// If vnode is string (HTML), unmount react root and set sanitized HTML
|
|
1722
|
+
if (typeof vnode === 'string') {
|
|
1723
|
+
const existingRoot = rootMap.get(containerEl);
|
|
1724
|
+
if (existingRoot && typeof existingRoot.unmount === 'function') {
|
|
1725
|
+
try {
|
|
1726
|
+
existingRoot.unmount();
|
|
1727
|
+
} finally {
|
|
1728
|
+
rootMap.delete(containerEl);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const proxy: any = new ElementProxy(containerEl);
|
|
1732
|
+
proxy.innerHTML = String(vnode ?? '');
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// If vnode is a DOM Node or DocumentFragment, unmount and replace content
|
|
1737
|
+
if (
|
|
1738
|
+
vnode &&
|
|
1739
|
+
(vnode as any).nodeType &&
|
|
1740
|
+
((vnode as any).nodeType === 1 || (vnode as any).nodeType === 3 || (vnode as any).nodeType === 11)
|
|
1741
|
+
) {
|
|
1742
|
+
const existingRoot = rootMap.get(containerEl);
|
|
1743
|
+
if (existingRoot && typeof existingRoot.unmount === 'function') {
|
|
1744
|
+
try {
|
|
1745
|
+
existingRoot.unmount();
|
|
1746
|
+
} finally {
|
|
1747
|
+
rootMap.delete(containerEl);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
while (containerEl.firstChild) containerEl.removeChild(containerEl.firstChild);
|
|
1751
|
+
containerEl.appendChild(vnode as any);
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
let root = rootMap.get(containerEl);
|
|
1756
|
+
if (!root) {
|
|
1757
|
+
root = this.ReactDOM.createRoot(containerEl);
|
|
1758
|
+
rootMap.set(containerEl, root);
|
|
1759
|
+
}
|
|
1760
|
+
root.render(vnode as any);
|
|
1761
|
+
return root;
|
|
1762
|
+
},
|
|
1763
|
+
);
|
|
1701
1764
|
}
|
|
1702
1765
|
static define(meta: RunJSDocMeta, options?: { locale?: string }) {
|
|
1703
1766
|
const locale = options?.locale;
|
package/src/index.ts
CHANGED
package/src/models/flowModel.tsx
CHANGED
|
@@ -1331,6 +1331,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1331
1331
|
..._.omit(this._options, ['flowEngine']),
|
|
1332
1332
|
stepParams: this.stepParams,
|
|
1333
1333
|
sortIndex: this.sortIndex,
|
|
1334
|
+
flowRegistry: {},
|
|
1334
1335
|
};
|
|
1335
1336
|
const subModels = this.subModels as {
|
|
1336
1337
|
[key: string]: FlowModel | FlowModel[];
|
|
@@ -1347,8 +1348,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1347
1348
|
}
|
|
1348
1349
|
}
|
|
1349
1350
|
for (const [key, flow] of this.flowRegistry.getFlows()) {
|
|
1350
|
-
data[
|
|
1351
|
-
data['flowRegistry'][key] = flow.toData();
|
|
1351
|
+
data.flowRegistry[key] = flow.toData();
|
|
1352
1352
|
}
|
|
1353
1353
|
return data;
|
|
1354
1354
|
}
|
|
@@ -50,6 +50,14 @@ export function defineBaseContextMeta() {
|
|
|
50
50
|
},
|
|
51
51
|
methods: {
|
|
52
52
|
t: 'Internationalization function for translating text. Parameters: (key: string, variables?: object) => string. Example: `ctx.t("Hello {name}", { name: "World" })`',
|
|
53
|
+
render: {
|
|
54
|
+
description:
|
|
55
|
+
'Render into container. Accepts ReactElement, DOM Node/Fragment, or HTML string. Parameters: (vnode: ReactElement | Node | DocumentFragment | string, container?: HTMLElement|ElementProxy) => Root|null. Example: `ctx.render(<div>Hello</div>)` or `ctx.render("<b>hi</b>")`',
|
|
56
|
+
detail: 'ReactDOM Root',
|
|
57
|
+
completion: {
|
|
58
|
+
insertText: `ctx.render(<div />)`,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
53
61
|
requireAsync:
|
|
54
62
|
'Asynchronously load external libraries from URL. Parameters: (url: string) => Promise<any>. Example: `const lodash = await ctx.requireAsync("https://cdn.jsdelivr.net/npm/lodash")`',
|
|
55
63
|
importAsync:
|
|
@@ -127,6 +135,14 @@ export function defineBaseContextMeta() {
|
|
|
127
135
|
},
|
|
128
136
|
methods: {
|
|
129
137
|
t: '国际化函数,用于翻译文案。参数:(key: string, variables?: object) => string。示例:`ctx.t("你好 {name}", { name: "世界" })`',
|
|
138
|
+
render: {
|
|
139
|
+
description:
|
|
140
|
+
'渲染到容器。vnode 支持 ReactElement、DOM 节点/片段、或 HTML 字符串。参数:(vnode: ReactElement | Node | DocumentFragment | string, container?: HTMLElement|ElementProxy) => Root|null。示例:`ctx.render(<div />)` 或 `ctx.render("<b>hi</b>")`',
|
|
141
|
+
detail: 'ReactDOM Root',
|
|
142
|
+
completion: {
|
|
143
|
+
insertText: `ctx.render(<div />)`,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
130
146
|
requireAsync:
|
|
131
147
|
'按 URL 异步加载外部库。参数:(url: string) => Promise<any>。示例:`const lodash = await ctx.requireAsync("https://cdn.jsdelivr.net/npm/lodash")`',
|
|
132
148
|
importAsync:
|
|
@@ -27,6 +27,7 @@ const snippets: Record<string, () => Promise<any>> = {
|
|
|
27
27
|
'scene/block/echarts-init': () => import('./scene/block/echarts-init.snippet'),
|
|
28
28
|
// scene/block
|
|
29
29
|
'scene/block/render-react': () => import('./scene/block/render-react.snippet'),
|
|
30
|
+
'scene/block/render-react-jsx': () => import('./scene/block/render-react-jsx.snippet'),
|
|
30
31
|
'scene/block/render-button-handler': () => import('./scene/block/render-button-handler.snippet'),
|
|
31
32
|
'scene/block/add-event-listener': () => import('./scene/block/add-event-listener.snippet'),
|
|
32
33
|
'scene/block/chartjs-bar': () => import('./scene/block/chartjs-bar.snippet'),
|
|
@@ -22,11 +22,13 @@ const snippet: SnippetModule = {
|
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
content: `
|
|
25
|
-
const {
|
|
26
|
-
const { Button } = antd;
|
|
25
|
+
const { Button } = ctx.antd;
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
ctx.render(
|
|
28
|
+
<Button type="primary" onClick={() => ctx.message.success(ctx.t('Clicked!'))}>
|
|
29
|
+
{ctx.t('Button')}
|
|
30
|
+
</Button>
|
|
31
|
+
);
|
|
30
32
|
`,
|
|
31
33
|
};
|
|
32
34
|
|
|
@@ -22,29 +22,28 @@ const snippet: SnippetModule = {
|
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
content: `
|
|
25
|
-
const { Card, Descriptions, Tag
|
|
26
|
-
const { createElement: h } = ctx.React;
|
|
25
|
+
const { Card, Descriptions, Tag } = ctx.antd;
|
|
27
26
|
|
|
28
27
|
if (!ctx.record) {
|
|
29
|
-
ctx.
|
|
28
|
+
ctx.render('<div style="padding:16px;color:#999;">' + ctx.t('No record data') + '</div>');
|
|
30
29
|
return;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
const record = ctx.record;
|
|
34
33
|
|
|
35
|
-
ctx.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
record.createdAt ? new Date(record.createdAt).toLocaleString() : '-'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
ctx.render(
|
|
35
|
+
<Card title={ctx.t('Record Details')} bordered style={{ margin: 0 }}>
|
|
36
|
+
<Descriptions column={2} size="small">
|
|
37
|
+
<Descriptions.Item label={ctx.t('ID')}>{record.id || '-'}</Descriptions.Item>
|
|
38
|
+
<Descriptions.Item label={ctx.t('Status')}>
|
|
39
|
+
<Tag color={record.status === 'active' ? 'green' : 'default'}>{record.status || '-'}</Tag>
|
|
40
|
+
</Descriptions.Item>
|
|
41
|
+
<Descriptions.Item label={ctx.t('Title')}>{record.title || '-'}</Descriptions.Item>
|
|
42
|
+
<Descriptions.Item label={ctx.t('Created At')}>
|
|
43
|
+
{record.createdAt ? new Date(record.createdAt).toLocaleString() : '-'}
|
|
44
|
+
</Descriptions.Item>
|
|
45
|
+
</Descriptions>
|
|
46
|
+
</Card>
|
|
48
47
|
);
|
|
49
48
|
|
|
50
49
|
`,
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { SnippetModule } from '../../types';
|
|
11
|
+
import { JSBlockRunJSContext } from '../../../contexts/JSBlockRunJSContext';
|
|
12
|
+
|
|
13
|
+
const snippet: SnippetModule = {
|
|
14
|
+
contexts: [JSBlockRunJSContext],
|
|
15
|
+
prefix: 'sn-react-jsx',
|
|
16
|
+
label: 'Render React (JSX)',
|
|
17
|
+
description: 'Render a simple React component using JSX syntax',
|
|
18
|
+
locales: {
|
|
19
|
+
'zh-CN': {
|
|
20
|
+
label: '渲染 React(JSX)',
|
|
21
|
+
description: '使用 JSX 语法渲染一个简单的 React 组件',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
content: `
|
|
25
|
+
// Render a React component with JSX
|
|
26
|
+
const { React } = ctx;
|
|
27
|
+
|
|
28
|
+
const App = () => (
|
|
29
|
+
<div style={{ padding: 12 }}>
|
|
30
|
+
<h3 style={{ margin: 0, color: '#1890ff' }}>Hello JSX</h3>
|
|
31
|
+
<div style={{ color: '#555' }}>This block is rendered by JSX.</div>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
ctx.render(<App />);
|
|
36
|
+
`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default snippet;
|
|
@@ -23,15 +23,15 @@ const snippet: SnippetModule = {
|
|
|
23
23
|
},
|
|
24
24
|
content: `
|
|
25
25
|
// Render a React element into ctx.element via ReactDOM
|
|
26
|
-
const {
|
|
27
|
-
const { Button } = antd;
|
|
26
|
+
const { Button } = ctx.antd;
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
ctx.render(
|
|
29
|
+
<div style={{ padding: 12 }}>
|
|
30
|
+
<Button type="primary" onClick={() => ctx.message.success(ctx.t('Clicked!'))}>
|
|
31
|
+
{ctx.t('Click')}
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
33
34
|
);
|
|
34
|
-
ReactDOM.createRoot(ctx.element).render(node);
|
|
35
35
|
`,
|
|
36
36
|
};
|
|
37
37
|
|
|
@@ -23,7 +23,6 @@ const snippet: SnippetModule = {
|
|
|
23
23
|
},
|
|
24
24
|
content: `
|
|
25
25
|
const { Card, Statistic, Row, Col } = ctx.antd;
|
|
26
|
-
const { createElement: h } = ctx.React;
|
|
27
26
|
|
|
28
27
|
const res = await ctx.api.request({
|
|
29
28
|
url: 'users:list',
|
|
@@ -47,33 +46,29 @@ const distinctRoles = new Set(
|
|
|
47
46
|
.filter(Boolean),
|
|
48
47
|
).size;
|
|
49
48
|
|
|
50
|
-
ctx.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
})
|
|
74
|
-
)
|
|
75
|
-
)
|
|
76
|
-
)
|
|
49
|
+
ctx.render(
|
|
50
|
+
<Row gutter={16}>
|
|
51
|
+
<Col span={6}>
|
|
52
|
+
<Card>
|
|
53
|
+
<Statistic title={ctx.t('Total users')} value={total} valueStyle={{ color: '#3f8600' }} />
|
|
54
|
+
</Card>
|
|
55
|
+
</Col>
|
|
56
|
+
<Col span={6}>
|
|
57
|
+
<Card>
|
|
58
|
+
<Statistic title={ctx.t('Administrators')} value={adminCount} valueStyle={{ color: '#1890ff' }} />
|
|
59
|
+
</Card>
|
|
60
|
+
</Col>
|
|
61
|
+
<Col span={6}>
|
|
62
|
+
<Card>
|
|
63
|
+
<Statistic title={ctx.t('Users with email')} value={withEmail} valueStyle={{ color: '#faad14' }} />
|
|
64
|
+
</Card>
|
|
65
|
+
</Col>
|
|
66
|
+
<Col span={6}>
|
|
67
|
+
<Card>
|
|
68
|
+
<Statistic title={ctx.t('Distinct roles')} value={distinctRoles} valueStyle={{ color: '#cf1322' }} />
|
|
69
|
+
</Card>
|
|
70
|
+
</Col>
|
|
71
|
+
</Row>
|
|
77
72
|
);
|
|
78
73
|
`,
|
|
79
74
|
};
|
|
@@ -23,7 +23,6 @@ const snippet: SnippetModule = {
|
|
|
23
23
|
},
|
|
24
24
|
content: `
|
|
25
25
|
const { Timeline, Card } = ctx.antd;
|
|
26
|
-
const { createElement: h } = ctx.React;
|
|
27
26
|
|
|
28
27
|
const res = await ctx.api.request({
|
|
29
28
|
url: 'users:list',
|
|
@@ -37,28 +36,28 @@ const res = await ctx.api.request({
|
|
|
37
36
|
const records = res?.data?.data || [];
|
|
38
37
|
|
|
39
38
|
if (!records.length) {
|
|
40
|
-
ctx.
|
|
39
|
+
ctx.render('<div style="padding:16px;color:#999;">' + ctx.t('No data') + '</div>');
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
ctx.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
key
|
|
50
|
-
label
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
record.email
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
ctx.render(
|
|
44
|
+
<Card title={ctx.t('Activity Timeline')} bordered>
|
|
45
|
+
<Timeline mode="left">
|
|
46
|
+
{records.map((record) => (
|
|
47
|
+
<Timeline.Item
|
|
48
|
+
key={record.id}
|
|
49
|
+
label={record.createdAt ? new Date(record.createdAt).toLocaleString() : ''}
|
|
50
|
+
>
|
|
51
|
+
<div>
|
|
52
|
+
<strong>{record.nickname || record.username || ctx.t('Unnamed user')}</strong>
|
|
53
|
+
{record.email ? (
|
|
54
|
+
<div style={{ color: '#999', fontSize: '12px', marginTop: '4px' }}>{record.email}</div>
|
|
55
|
+
) : null}
|
|
56
|
+
</div>
|
|
57
|
+
</Timeline.Item>
|
|
58
|
+
))}
|
|
59
|
+
</Timeline>
|
|
60
|
+
</Card>
|
|
62
61
|
);
|
|
63
62
|
`,
|
|
64
63
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
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 { describe, it, expect } from 'vitest';
|
|
11
|
+
import { compileRunJs } from '../../utils/jsxTransform';
|
|
12
|
+
|
|
13
|
+
describe('compileRunJs', () => {
|
|
14
|
+
it('returns original code when no JSX is present', async () => {
|
|
15
|
+
const src = `const a = 1; const b = a + 1;`;
|
|
16
|
+
const out = await compileRunJs(src);
|
|
17
|
+
expect(out).toBe(src);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('transforms JSX when sucrase is available (skip if missing)', async () => {
|
|
21
|
+
const src = `ctx.render(<div className="x">hi</div>);`;
|
|
22
|
+
|
|
23
|
+
// Try to import sucrase to decide if this environment has it installed
|
|
24
|
+
const hasSucrase = await import('sucrase').then(() => true).catch(() => false);
|
|
25
|
+
|
|
26
|
+
const out = await compileRunJs(src);
|
|
27
|
+
|
|
28
|
+
if (!hasSucrase) {
|
|
29
|
+
// Environment without sucrase: current implementation falls back to original code
|
|
30
|
+
expect(out).toBe(src);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// sucrase available: output should contain React.createElement mapping
|
|
35
|
+
expect(out).not.toBe(src);
|
|
36
|
+
expect(out).toMatch(/ctx\.React\.createElement/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* Lightweight JSX -> JS compiler for RunJS user code.
|
|
12
|
+
* - Uses sucrase via dynamic import (lazy; avoids static cycles and cost when not needed)
|
|
13
|
+
* - Maps JSX to ctx.React.createElement / ctx.React.Fragment so no global React is required
|
|
14
|
+
* - If sucrase is unavailable or transform throws, returns original code as graceful fallback
|
|
15
|
+
*/
|
|
16
|
+
export async function compileRunJs(code: string): Promise<string> {
|
|
17
|
+
// Fast path: cheap heuristic to skip import if no JSX likely present
|
|
18
|
+
// (not strict; still safe because we fall back to transform if dynamic import is cheap)
|
|
19
|
+
const maybeJSX = /<[A-Za-z]|<\//.test(code);
|
|
20
|
+
if (!maybeJSX) return code;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const mod: any = await import('sucrase');
|
|
24
|
+
const transform = mod?.transform || mod?.default?.transform;
|
|
25
|
+
if (typeof transform !== 'function') return code;
|
|
26
|
+
const res = transform(code, {
|
|
27
|
+
transforms: ['jsx'],
|
|
28
|
+
jsxPragma: 'ctx.React.createElement',
|
|
29
|
+
jsxFragmentPragma: 'ctx.React.Fragment',
|
|
30
|
+
// Keep ES syntax as-is; JSRunner runs in modern engines
|
|
31
|
+
disableESTransforms: true,
|
|
32
|
+
production: true,
|
|
33
|
+
});
|
|
34
|
+
return (res && (res.code || res?.output)) ?? code;
|
|
35
|
+
} catch (_e) {
|
|
36
|
+
// Fallback: return original code if transform fails
|
|
37
|
+
return code;
|
|
38
|
+
}
|
|
39
|
+
}
|