@nocobase/flow-engine 2.1.0-beta.27 → 2.1.0-beta.30
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/variables/VariableHybridInput.d.ts +27 -0
- package/lib/components/variables/VariableHybridInput.js +499 -0
- package/lib/components/variables/index.d.ts +2 -0
- package/lib/components/variables/index.js +3 -0
- package/lib/flowContext.d.ts +1 -0
- package/lib/flowContext.js +12 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.js +1 -0
- package/lib/views/ViewNavigation.js +6 -2
- package/package.json +4 -4
- package/src/__tests__/flowContext.test.ts +17 -0
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/flowContext.ts +10 -0
- package/src/types.ts +2 -0
- package/src/views/ViewNavigation.ts +6 -2
|
@@ -0,0 +1,27 @@
|
|
|
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 React from 'react';
|
|
10
|
+
import type { MetaTreeNode } from '../../flowContext';
|
|
11
|
+
export interface VariableHybridInputConverters {
|
|
12
|
+
formatPathToValue?: (item?: MetaTreeNode) => string | undefined;
|
|
13
|
+
parseValueToPath?: (value?: string) => string[] | undefined;
|
|
14
|
+
variableRegExp?: RegExp;
|
|
15
|
+
}
|
|
16
|
+
export interface VariableHybridInputProps {
|
|
17
|
+
value?: string;
|
|
18
|
+
onChange?: (value: string) => void;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
addonBefore?: React.ReactNode;
|
|
22
|
+
metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
23
|
+
converters?: VariableHybridInputConverters;
|
|
24
|
+
style?: React.CSSProperties;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const VariableHybridInput: React.NamedExoticComponent<VariableHybridInputProps>;
|
|
@@ -0,0 +1,499 @@
|
|
|
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 VariableHybridInput_exports = {};
|
|
39
|
+
__export(VariableHybridInput_exports, {
|
|
40
|
+
VariableHybridInput: () => VariableHybridInput
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(VariableHybridInput_exports);
|
|
43
|
+
var import_css = require("@emotion/css");
|
|
44
|
+
var import_antd = require("antd");
|
|
45
|
+
var import_react = __toESM(require("react"));
|
|
46
|
+
var import_FlowContextProvider = require("../../FlowContextProvider");
|
|
47
|
+
var import_FlowContextSelector = require("../FlowContextSelector");
|
|
48
|
+
var import_useResolvedMetaTree = require("./useResolvedMetaTree");
|
|
49
|
+
var import_utils = require("./utils");
|
|
50
|
+
const DEFAULT_VARIABLE_REGEXP = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
51
|
+
const TAG_CLASS = "nb-variable-tag";
|
|
52
|
+
function reactNodeToPlainText(node) {
|
|
53
|
+
if (node == null || typeof node === "boolean") return "";
|
|
54
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
55
|
+
if (Array.isArray(node)) return node.map(reactNodeToPlainText).join("");
|
|
56
|
+
if ((0, import_react.isValidElement)(node)) return reactNodeToPlainText(node.props.children);
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
__name(reactNodeToPlainText, "reactNodeToPlainText");
|
|
60
|
+
function escapeHtml(value) {
|
|
61
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
62
|
+
}
|
|
63
|
+
__name(escapeHtml, "escapeHtml");
|
|
64
|
+
function getDomValue(element) {
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const node of Array.from(element.childNodes)) {
|
|
67
|
+
if (node instanceof HTMLElement && node.dataset.variable) {
|
|
68
|
+
out.push(node.dataset.variable);
|
|
69
|
+
} else {
|
|
70
|
+
out.push(node.textContent || "");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out.join("");
|
|
74
|
+
}
|
|
75
|
+
__name(getDomValue, "getDomValue");
|
|
76
|
+
function createTagHTML(variable, label) {
|
|
77
|
+
return `<span class="${TAG_CLASS}" contenteditable="false" data-variable="${escapeHtml(
|
|
78
|
+
variable
|
|
79
|
+
)}" title="${escapeHtml(label)}">${escapeHtml(label)}</span>`;
|
|
80
|
+
}
|
|
81
|
+
__name(createTagHTML, "createTagHTML");
|
|
82
|
+
function normalizeVariableKey(value) {
|
|
83
|
+
return value.replace(/^\{\{\s*/, "").replace(/\s*\}\}$/, "").trim();
|
|
84
|
+
}
|
|
85
|
+
__name(normalizeVariableKey, "normalizeVariableKey");
|
|
86
|
+
function renderHTML(value, labelMap, regExp) {
|
|
87
|
+
const re = new RegExp(regExp.source, regExp.flags.includes("g") ? regExp.flags : `${regExp.flags}g`);
|
|
88
|
+
return escapeHtml(value || "").replace(re, (matched) => {
|
|
89
|
+
const label = labelMap.get(normalizeVariableKey(matched)) || matched;
|
|
90
|
+
return createTagHTML(matched, label);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
__name(renderHTML, "renderHTML");
|
|
94
|
+
function buildLabelMap(nodes, ctxT, converters) {
|
|
95
|
+
const map = /* @__PURE__ */ new Map();
|
|
96
|
+
function walk(items = [], parentTitles = []) {
|
|
97
|
+
var _a;
|
|
98
|
+
for (const item of items) {
|
|
99
|
+
const titlePart = reactNodeToPlainText(item.title || item.name);
|
|
100
|
+
const titles = [...parentTitles, titlePart];
|
|
101
|
+
const value = ((_a = converters == null ? void 0 : converters.formatPathToValue) == null ? void 0 : _a.call(converters, item)) ?? (0, import_utils.formatPathToValue)(item);
|
|
102
|
+
if (value) {
|
|
103
|
+
map.set(normalizeVariableKey(value), titles.map(ctxT).join("/"));
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(item.children)) {
|
|
106
|
+
walk(item.children, titles);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
__name(walk, "walk");
|
|
111
|
+
walk(nodes);
|
|
112
|
+
return map;
|
|
113
|
+
}
|
|
114
|
+
__name(buildLabelMap, "buildLabelMap");
|
|
115
|
+
function pasteHTML(container, html, indexes) {
|
|
116
|
+
var _a;
|
|
117
|
+
const selection = (_a = window.getSelection) == null ? void 0 : _a.call(window);
|
|
118
|
+
const range = (selection == null ? void 0 : selection.rangeCount) ? selection.getRangeAt(0) : null;
|
|
119
|
+
if (!range) return;
|
|
120
|
+
if (indexes) {
|
|
121
|
+
const children = Array.from(container.childNodes);
|
|
122
|
+
if (indexes[0] === -1) {
|
|
123
|
+
if (indexes[1] && children[indexes[1] - 1]) {
|
|
124
|
+
range.setStartAfter(children[indexes[1] - 1]);
|
|
125
|
+
} else {
|
|
126
|
+
range.setStart(container, 0);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
range.setStart(children[indexes[0]], indexes[1]);
|
|
130
|
+
}
|
|
131
|
+
if (indexes[2] === -1) {
|
|
132
|
+
if (indexes[3] && children[indexes[3] - 1]) {
|
|
133
|
+
range.setEndAfter(children[indexes[3] - 1]);
|
|
134
|
+
} else {
|
|
135
|
+
range.setEnd(container, 0);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
range.setEnd(children[indexes[2]], indexes[3]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const wrapper = document.createElement("div");
|
|
142
|
+
wrapper.innerHTML = html;
|
|
143
|
+
const fragment = document.createDocumentFragment();
|
|
144
|
+
let lastNode = null;
|
|
145
|
+
while (wrapper.firstChild) {
|
|
146
|
+
lastNode = fragment.appendChild(wrapper.firstChild);
|
|
147
|
+
}
|
|
148
|
+
range.deleteContents();
|
|
149
|
+
range.insertNode(fragment);
|
|
150
|
+
if (lastNode) {
|
|
151
|
+
const next = new Range();
|
|
152
|
+
next.setStartAfter(lastNode);
|
|
153
|
+
next.collapse(true);
|
|
154
|
+
selection == null ? void 0 : selection.removeAllRanges();
|
|
155
|
+
selection == null ? void 0 : selection.addRange(next);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
__name(pasteHTML, "pasteHTML");
|
|
159
|
+
function getSingleEndRange(nodes, index, offset) {
|
|
160
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
161
|
+
if (index === -1) {
|
|
162
|
+
let realIndex2 = offset;
|
|
163
|
+
let collapseFlag = false;
|
|
164
|
+
if (realIndex2 && ((_a = nodes[realIndex2 - 1]) == null ? void 0 : _a.nodeName) === "#text" && ((_b = nodes[realIndex2]) == null ? void 0 : _b.nodeName) === "#text") {
|
|
165
|
+
collapseFlag = true;
|
|
166
|
+
}
|
|
167
|
+
let textOffset2 = 0;
|
|
168
|
+
for (let i = offset - 1; i >= 0; i -= 1) {
|
|
169
|
+
if (collapseFlag) {
|
|
170
|
+
if (((_c = nodes[i]) == null ? void 0 : _c.nodeName) === "#text") {
|
|
171
|
+
textOffset2 += ((_d = nodes[i].textContent) == null ? void 0 : _d.length) || 0;
|
|
172
|
+
} else {
|
|
173
|
+
collapseFlag = false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (((_e = nodes[i]) == null ? void 0 : _e.nodeName) === "#text" && ((_f = nodes[i + 1]) == null ? void 0 : _f.nodeName) === "#text") {
|
|
177
|
+
realIndex2 -= 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return textOffset2 ? [realIndex2, textOffset2] : [-1, realIndex2];
|
|
181
|
+
}
|
|
182
|
+
let realIndex = 0;
|
|
183
|
+
let textOffset = 0;
|
|
184
|
+
for (let i = 0; i < index + 1; i += 1) {
|
|
185
|
+
if (((_g = nodes[i]) == null ? void 0 : _g.nodeName) === "#text") {
|
|
186
|
+
if (i !== index && nodes[i + 1] && ((_h = nodes[i + 1]) == null ? void 0 : _h.nodeName) !== "#text") {
|
|
187
|
+
realIndex += 1;
|
|
188
|
+
}
|
|
189
|
+
textOffset += i === index ? offset : ((_i = nodes[i].textContent) == null ? void 0 : _i.length) || 0;
|
|
190
|
+
} else {
|
|
191
|
+
realIndex += 1;
|
|
192
|
+
textOffset = 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return [realIndex, textOffset];
|
|
196
|
+
}
|
|
197
|
+
__name(getSingleEndRange, "getSingleEndRange");
|
|
198
|
+
function getCurrentRange(element) {
|
|
199
|
+
var _a;
|
|
200
|
+
const selection = (_a = window.getSelection) == null ? void 0 : _a.call(window);
|
|
201
|
+
const range = (selection == null ? void 0 : selection.rangeCount) ? selection.getRangeAt(0) : null;
|
|
202
|
+
if (!range || !element.contains(range.commonAncestorContainer)) {
|
|
203
|
+
return [-1, 0, -1, 0];
|
|
204
|
+
}
|
|
205
|
+
const nodes = Array.from(element.childNodes);
|
|
206
|
+
if (!nodes.length) return [-1, 0, -1, 0];
|
|
207
|
+
const startIndex = range.startContainer === element ? -1 : nodes.indexOf(range.startContainer);
|
|
208
|
+
const endIndex = range.endContainer === element ? -1 : nodes.indexOf(range.endContainer);
|
|
209
|
+
return [
|
|
210
|
+
...getSingleEndRange(nodes, startIndex, range.startOffset),
|
|
211
|
+
...getSingleEndRange(nodes, endIndex, range.endOffset)
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
__name(getCurrentRange, "getCurrentRange");
|
|
215
|
+
const VariableHybridInputComponent = /* @__PURE__ */ __name((props) => {
|
|
216
|
+
const { addonBefore, className, converters, disabled, metaTree, onChange, placeholder, style } = props;
|
|
217
|
+
const { token } = import_antd.theme.useToken();
|
|
218
|
+
const ctx = (0, import_FlowContextProvider.useFlowContext)();
|
|
219
|
+
const { resolvedMetaTree } = (0, import_useResolvedMetaTree.useResolvedMetaTree)(metaTree);
|
|
220
|
+
const inputRef = (0, import_react.useRef)(null);
|
|
221
|
+
const [isComposing, setIsComposing] = (0, import_react.useState)(false);
|
|
222
|
+
const [changed, setChanged] = (0, import_react.useState)(false);
|
|
223
|
+
const [range, setRange] = (0, import_react.useState)([-1, 0, -1, 0]);
|
|
224
|
+
const value = typeof props.value === "string" ? props.value : props.value == null ? "" : String(props.value);
|
|
225
|
+
const variableRegExp = (converters == null ? void 0 : converters.variableRegExp) ?? DEFAULT_VARIABLE_REGEXP;
|
|
226
|
+
const labelMap = (0, import_react.useMemo)(
|
|
227
|
+
() => buildLabelMap(resolvedMetaTree, ctx.t, converters),
|
|
228
|
+
[resolvedMetaTree, ctx, converters]
|
|
229
|
+
);
|
|
230
|
+
const [html, setHtml] = (0, import_react.useState)(() => renderHTML(value, labelMap, variableRegExp));
|
|
231
|
+
const emitChange = (0, import_react.useCallback)(
|
|
232
|
+
(target) => {
|
|
233
|
+
onChange == null ? void 0 : onChange(getDomValue(target).trim());
|
|
234
|
+
},
|
|
235
|
+
[onChange]
|
|
236
|
+
);
|
|
237
|
+
(0, import_react.useEffect)(() => {
|
|
238
|
+
setHtml(renderHTML(value, labelMap, variableRegExp));
|
|
239
|
+
if (!changed) {
|
|
240
|
+
setRange([-1, 0, -1, 0]);
|
|
241
|
+
}
|
|
242
|
+
}, [value, labelMap]);
|
|
243
|
+
(0, import_react.useEffect)(() => {
|
|
244
|
+
var _a;
|
|
245
|
+
const element = inputRef.current;
|
|
246
|
+
if (!element) return;
|
|
247
|
+
if (document.activeElement !== element) return;
|
|
248
|
+
const nextRange = new Range();
|
|
249
|
+
if (changed) {
|
|
250
|
+
if (range.join() === "-1,0,-1,0") return;
|
|
251
|
+
const selection = (_a = window.getSelection) == null ? void 0 : _a.call(window);
|
|
252
|
+
if (!selection) return;
|
|
253
|
+
try {
|
|
254
|
+
const children = Array.from(element.childNodes);
|
|
255
|
+
if (children.length) {
|
|
256
|
+
if (range[0] === -1) {
|
|
257
|
+
if (range[1]) nextRange.setStartAfter(children[range[1] - 1]);
|
|
258
|
+
} else {
|
|
259
|
+
nextRange.setStart(children[range[0]], range[1]);
|
|
260
|
+
}
|
|
261
|
+
if (range[2] === -1) {
|
|
262
|
+
if (range[3]) nextRange.setEndAfter(children[range[3] - 1]);
|
|
263
|
+
} else {
|
|
264
|
+
nextRange.setEnd(children[range[2]], range[3]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
nextRange.collapse(true);
|
|
268
|
+
selection.removeAllRanges();
|
|
269
|
+
selection.addRange(nextRange);
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
const { lastChild } = element;
|
|
274
|
+
if (lastChild) {
|
|
275
|
+
nextRange.setStartAfter(lastChild);
|
|
276
|
+
nextRange.setEndAfter(lastChild);
|
|
277
|
+
const nodes = Array.from(element.childNodes);
|
|
278
|
+
const idx = nodes.indexOf(lastChild);
|
|
279
|
+
const startIndex = nextRange.startContainer === element ? -1 : idx;
|
|
280
|
+
const endIndex = nextRange.startContainer === element ? -1 : idx;
|
|
281
|
+
setRange([startIndex, nextRange.startOffset, endIndex, nextRange.endOffset]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}, [changed, html, range]);
|
|
285
|
+
const insertVariable = (0, import_react.useCallback)(
|
|
286
|
+
(variable, meta) => {
|
|
287
|
+
const current = inputRef.current;
|
|
288
|
+
if (!current || !variable) return;
|
|
289
|
+
const label = labelMap.get(normalizeVariableKey(variable)) || (meta ? [...meta.parentTitles || [], reactNodeToPlainText(meta.title || meta.name)].map(ctx.t).join("/") : variable);
|
|
290
|
+
current.focus();
|
|
291
|
+
pasteHTML(current, createTagHTML(variable, label), range);
|
|
292
|
+
setChanged(true);
|
|
293
|
+
setRange(getCurrentRange(current));
|
|
294
|
+
emitChange(current);
|
|
295
|
+
},
|
|
296
|
+
[labelMap, range, emitChange, ctx]
|
|
297
|
+
);
|
|
298
|
+
const handleSelectorChange = (0, import_react.useCallback)(
|
|
299
|
+
(next, meta) => {
|
|
300
|
+
if (!next) return;
|
|
301
|
+
insertVariable(next, meta);
|
|
302
|
+
},
|
|
303
|
+
[insertVariable]
|
|
304
|
+
);
|
|
305
|
+
const handleInput = (0, import_react.useCallback)(
|
|
306
|
+
({ currentTarget }) => {
|
|
307
|
+
if (isComposing) return;
|
|
308
|
+
setChanged(true);
|
|
309
|
+
setRange(getCurrentRange(currentTarget));
|
|
310
|
+
emitChange(currentTarget);
|
|
311
|
+
},
|
|
312
|
+
[emitChange, isComposing]
|
|
313
|
+
);
|
|
314
|
+
const handleBlur = (0, import_react.useCallback)(({ currentTarget }) => {
|
|
315
|
+
setRange(getCurrentRange(currentTarget));
|
|
316
|
+
}, []);
|
|
317
|
+
const handleKeyDown = (0, import_react.useCallback)((event) => {
|
|
318
|
+
if (event.key === "Enter") {
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
}
|
|
321
|
+
}, []);
|
|
322
|
+
const handlePaste = (0, import_react.useCallback)(
|
|
323
|
+
(event) => {
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
const text = event.clipboardData.getData("text/plain").replace(/\n/g, " ");
|
|
326
|
+
if (!text) return;
|
|
327
|
+
setChanged(true);
|
|
328
|
+
pasteHTML(event.currentTarget, escapeHtml(text));
|
|
329
|
+
setRange(getCurrentRange(event.currentTarget));
|
|
330
|
+
emitChange(event.currentTarget);
|
|
331
|
+
},
|
|
332
|
+
[emitChange]
|
|
333
|
+
);
|
|
334
|
+
const handleCompositionStart = (0, import_react.useCallback)(() => setIsComposing(true), []);
|
|
335
|
+
const handleCompositionEnd = (0, import_react.useCallback)(
|
|
336
|
+
({ currentTarget }) => {
|
|
337
|
+
setIsComposing(false);
|
|
338
|
+
setChanged(true);
|
|
339
|
+
setRange(getCurrentRange(currentTarget));
|
|
340
|
+
emitChange(currentTarget);
|
|
341
|
+
},
|
|
342
|
+
[emitChange]
|
|
343
|
+
);
|
|
344
|
+
const wrapperClassName = (0, import_react.useMemo)(
|
|
345
|
+
() => import_css.css`
|
|
346
|
+
display: flex;
|
|
347
|
+
width: 100%;
|
|
348
|
+
min-width: 0;
|
|
349
|
+
|
|
350
|
+
&.ant-space-compact {
|
|
351
|
+
display: flex;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* The trigger button from FlowContextSelector sits at the right end.
|
|
355
|
+
Flatten its left corners so it shares the border with the editor. */
|
|
356
|
+
> .ant-btn {
|
|
357
|
+
flex-shrink: 0;
|
|
358
|
+
border-top-left-radius: 0;
|
|
359
|
+
border-bottom-left-radius: 0;
|
|
360
|
+
margin-left: -${token.lineWidth}px;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
> .ant-btn:hover,
|
|
364
|
+
> .ant-btn:focus {
|
|
365
|
+
z-index: 2;
|
|
366
|
+
}
|
|
367
|
+
`,
|
|
368
|
+
[token.lineWidth]
|
|
369
|
+
);
|
|
370
|
+
const addonClassName = (0, import_react.useMemo)(
|
|
371
|
+
() => import_css.css`
|
|
372
|
+
display: inline-flex;
|
|
373
|
+
align-items: center;
|
|
374
|
+
padding: 0 ${token.paddingSM}px;
|
|
375
|
+
background: ${token.colorFillTertiary};
|
|
376
|
+
border: ${token.lineWidth}px ${token.lineType} ${token.colorBorder};
|
|
377
|
+
border-right: 0;
|
|
378
|
+
border-radius: ${token.borderRadius}px 0 0 ${token.borderRadius}px;
|
|
379
|
+
color: ${token.colorText};
|
|
380
|
+
font-size: ${token.fontSize}px;
|
|
381
|
+
line-height: 1;
|
|
382
|
+
white-space: nowrap;
|
|
383
|
+
`,
|
|
384
|
+
[token]
|
|
385
|
+
);
|
|
386
|
+
const editorClassName = (0, import_react.useMemo)(() => {
|
|
387
|
+
const verticalPad = Math.max(
|
|
388
|
+
0,
|
|
389
|
+
(token.controlHeight - Math.round(token.lineHeight * token.fontSize)) / 2 - token.lineWidth
|
|
390
|
+
);
|
|
391
|
+
return import_css.css`
|
|
392
|
+
flex: 1 1 auto;
|
|
393
|
+
min-width: 0;
|
|
394
|
+
min-height: ${token.controlHeight}px;
|
|
395
|
+
padding: ${verticalPad}px ${token.paddingSM}px;
|
|
396
|
+
overflow: hidden;
|
|
397
|
+
white-space: pre-wrap;
|
|
398
|
+
word-break: break-word;
|
|
399
|
+
line-height: ${token.lineHeight};
|
|
400
|
+
font-size: ${token.fontSize}px;
|
|
401
|
+
color: ${token.colorText};
|
|
402
|
+
background: ${token.colorBgContainer};
|
|
403
|
+
border: ${token.lineWidth}px ${token.lineType} ${token.colorBorder};
|
|
404
|
+
/* Right corners are always flat because the X picker button is glued to the right side. */
|
|
405
|
+
border-radius: ${addonBefore ? "0" : `${token.borderRadius}px 0 0 ${token.borderRadius}px`};
|
|
406
|
+
cursor: text;
|
|
407
|
+
transition: all ${token.motionDurationMid};
|
|
408
|
+
outline: none;
|
|
409
|
+
|
|
410
|
+
&:hover {
|
|
411
|
+
border-color: ${token.colorPrimaryHover};
|
|
412
|
+
z-index: 1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
&:focus,
|
|
416
|
+
&:focus-visible {
|
|
417
|
+
border-color: ${token.colorPrimary};
|
|
418
|
+
box-shadow: 0 0 0 ${token.controlOutlineWidth}px ${token.controlOutline};
|
|
419
|
+
z-index: 1;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
&[data-placeholder]:empty::before {
|
|
423
|
+
content: attr(data-placeholder);
|
|
424
|
+
color: ${token.colorTextPlaceholder};
|
|
425
|
+
pointer-events: none;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.${TAG_CLASS} {
|
|
429
|
+
/* inline lets long tag content wrap naturally across lines, mirroring v1. */
|
|
430
|
+
display: inline;
|
|
431
|
+
margin: 0 ${token.marginXXS}px;
|
|
432
|
+
padding: ${token.paddingXXS}px ${token.paddingXS}px;
|
|
433
|
+
font-size: ${token.fontSizeSM}px;
|
|
434
|
+
line-height: ${token.lineHeightSM};
|
|
435
|
+
color: ${token.colorPrimaryText};
|
|
436
|
+
background: ${token.colorPrimaryBg};
|
|
437
|
+
border: ${token.lineWidth}px ${token.lineType} ${token.colorPrimaryBorder};
|
|
438
|
+
border-radius: ${token.borderRadiusSM}px;
|
|
439
|
+
vertical-align: baseline;
|
|
440
|
+
user-select: none;
|
|
441
|
+
cursor: default;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
&.is-disabled {
|
|
445
|
+
background: ${token.colorBgContainerDisabled};
|
|
446
|
+
color: ${token.colorTextDisabled};
|
|
447
|
+
border-color: ${token.colorBorder};
|
|
448
|
+
cursor: not-allowed;
|
|
449
|
+
|
|
450
|
+
&:hover {
|
|
451
|
+
border-color: ${token.colorBorder};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.${TAG_CLASS} {
|
|
455
|
+
color: ${token.colorTextDisabled};
|
|
456
|
+
background: ${token.colorFillTertiary};
|
|
457
|
+
border-color: ${token.colorBorder};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
`;
|
|
461
|
+
}, [token, addonBefore]);
|
|
462
|
+
return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_antd.Space.Compact, { className: (0, import_css.cx)("nb-variable-hybrid-input", wrapperClassName, className), style }, addonBefore != null && /* @__PURE__ */ import_react.default.createElement("span", { className: addonClassName }, addonBefore), /* @__PURE__ */ import_react.default.createElement(
|
|
463
|
+
"div",
|
|
464
|
+
{
|
|
465
|
+
ref: inputRef,
|
|
466
|
+
role: "textbox",
|
|
467
|
+
"aria-label": "textbox",
|
|
468
|
+
className: (0, import_css.cx)(editorClassName, {
|
|
469
|
+
"is-disabled": disabled
|
|
470
|
+
}),
|
|
471
|
+
contentEditable: !disabled,
|
|
472
|
+
"data-placeholder": placeholder,
|
|
473
|
+
onInput: handleInput,
|
|
474
|
+
onBlur: handleBlur,
|
|
475
|
+
onKeyDown: handleKeyDown,
|
|
476
|
+
onPaste: handlePaste,
|
|
477
|
+
onCompositionStart: handleCompositionStart,
|
|
478
|
+
onCompositionEnd: handleCompositionEnd,
|
|
479
|
+
dangerouslySetInnerHTML: { __html: html }
|
|
480
|
+
}
|
|
481
|
+
), /* @__PURE__ */ import_react.default.createElement(
|
|
482
|
+
import_FlowContextSelector.FlowContextSelector,
|
|
483
|
+
{
|
|
484
|
+
metaTree,
|
|
485
|
+
disabled,
|
|
486
|
+
parseValueToPath: (converters == null ? void 0 : converters.parseValueToPath) ?? import_utils.parseValueToPath,
|
|
487
|
+
formatPathToValue: (item) => {
|
|
488
|
+
var _a;
|
|
489
|
+
return ((_a = converters == null ? void 0 : converters.formatPathToValue) == null ? void 0 : _a.call(converters, item)) || (0, import_utils.formatPathToValue)(item);
|
|
490
|
+
},
|
|
491
|
+
onChange: handleSelectorChange
|
|
492
|
+
}
|
|
493
|
+
)));
|
|
494
|
+
}, "VariableHybridInputComponent");
|
|
495
|
+
const VariableHybridInput = import_react.default.memo(VariableHybridInputComponent);
|
|
496
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
497
|
+
0 && (module.exports = {
|
|
498
|
+
VariableHybridInput
|
|
499
|
+
});
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
export { VariableInput } from './VariableInput';
|
|
10
|
+
export { VariableHybridInput } from './VariableHybridInput';
|
|
11
|
+
export type { VariableHybridInputProps, VariableHybridInputConverters } from './VariableHybridInput';
|
|
10
12
|
export { SlateVariableEditor } from './SlateVariableEditor';
|
|
11
13
|
export { VariableTag } from './VariableTag';
|
|
12
14
|
export { InlineVariableTag } from './InlineVariableTag';
|
|
@@ -29,12 +29,14 @@ var variables_exports = {};
|
|
|
29
29
|
__export(variables_exports, {
|
|
30
30
|
InlineVariableTag: () => import_InlineVariableTag.InlineVariableTag,
|
|
31
31
|
SlateVariableEditor: () => import_SlateVariableEditor.SlateVariableEditor,
|
|
32
|
+
VariableHybridInput: () => import_VariableHybridInput.VariableHybridInput,
|
|
32
33
|
VariableInput: () => import_VariableInput.VariableInput,
|
|
33
34
|
VariableTag: () => import_VariableTag.VariableTag,
|
|
34
35
|
useResolvedMetaTree: () => import_useResolvedMetaTree.useResolvedMetaTree
|
|
35
36
|
});
|
|
36
37
|
module.exports = __toCommonJS(variables_exports);
|
|
37
38
|
var import_VariableInput = require("./VariableInput");
|
|
39
|
+
var import_VariableHybridInput = require("./VariableHybridInput");
|
|
38
40
|
var import_SlateVariableEditor = require("./SlateVariableEditor");
|
|
39
41
|
var import_VariableTag = require("./VariableTag");
|
|
40
42
|
var import_InlineVariableTag = require("./InlineVariableTag");
|
|
@@ -45,6 +47,7 @@ __reExport(variables_exports, require("./utils"), module.exports);
|
|
|
45
47
|
0 && (module.exports = {
|
|
46
48
|
InlineVariableTag,
|
|
47
49
|
SlateVariableEditor,
|
|
50
|
+
VariableHybridInput,
|
|
48
51
|
VariableInput,
|
|
49
52
|
VariableTag,
|
|
50
53
|
useResolvedMetaTree,
|
package/lib/flowContext.d.ts
CHANGED
|
@@ -349,6 +349,7 @@ declare class BaseFlowEngineContext extends FlowContext {
|
|
|
349
349
|
runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any;
|
|
350
350
|
engine: FlowEngine;
|
|
351
351
|
api: APIClient;
|
|
352
|
+
locale: string;
|
|
352
353
|
viewer: FlowViewer;
|
|
353
354
|
view: FlowView;
|
|
354
355
|
modal: HookAPI;
|
package/lib/flowContext.js
CHANGED
|
@@ -2307,6 +2307,18 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
|
|
|
2307
2307
|
this.defineMethod("t", (keyOrTemplate, options) => {
|
|
2308
2308
|
return i18n.translate(keyOrTemplate, options);
|
|
2309
2309
|
});
|
|
2310
|
+
this.defineProperty("locale", {
|
|
2311
|
+
get: /* @__PURE__ */ __name(() => {
|
|
2312
|
+
var _a, _b, _c;
|
|
2313
|
+
return ((_b = (_a = this.api) == null ? void 0 : _a.auth) == null ? void 0 : _b.locale) || ((_c = this.i18n) == null ? void 0 : _c.language);
|
|
2314
|
+
}, "get"),
|
|
2315
|
+
cache: false,
|
|
2316
|
+
meta: Object.assign(() => ({ type: "string", title: this.t("Current language"), sort: 970 }), {
|
|
2317
|
+
title: (0, import_utils.escapeT)("Current language"),
|
|
2318
|
+
sort: 970,
|
|
2319
|
+
hasChildren: false
|
|
2320
|
+
})
|
|
2321
|
+
});
|
|
2310
2322
|
this.defineMethod("renderJson", function(template) {
|
|
2311
2323
|
return this.resolveJsonTemplate(template);
|
|
2312
2324
|
});
|
package/lib/types.d.ts
CHANGED
|
@@ -120,7 +120,9 @@ export declare enum ActionScene {
|
|
|
120
120
|
/** 按钮级联动规则可用 */
|
|
121
121
|
ACTION_LINKAGE_RULES = 5,
|
|
122
122
|
/** 动态事件流可用 */
|
|
123
|
-
DYNAMIC_EVENT_FLOW = 6
|
|
123
|
+
DYNAMIC_EVENT_FLOW = 6,
|
|
124
|
+
/** 菜单项联动规则可用 */
|
|
125
|
+
MENU_LINKAGE_RULES = 7
|
|
124
126
|
}
|
|
125
127
|
/**
|
|
126
128
|
* Defines a reusable action with generic model type support.
|
package/lib/types.js
CHANGED
|
@@ -36,6 +36,7 @@ var ActionScene = /* @__PURE__ */ ((ActionScene2) => {
|
|
|
36
36
|
ActionScene2[ActionScene2["DETAILS_FIELD_LINKAGE_RULES"] = 4] = "DETAILS_FIELD_LINKAGE_RULES";
|
|
37
37
|
ActionScene2[ActionScene2["ACTION_LINKAGE_RULES"] = 5] = "ACTION_LINKAGE_RULES";
|
|
38
38
|
ActionScene2[ActionScene2["DYNAMIC_EVENT_FLOW"] = 6] = "DYNAMIC_EVENT_FLOW";
|
|
39
|
+
ActionScene2[ActionScene2["MENU_LINKAGE_RULES"] = 7] = "MENU_LINKAGE_RULES";
|
|
39
40
|
return ActionScene2;
|
|
40
41
|
})(ActionScene || {});
|
|
41
42
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -43,6 +43,10 @@ function encodeFilterByTk(val) {
|
|
|
43
43
|
return encodeURIComponent(String(val));
|
|
44
44
|
}
|
|
45
45
|
__name(encodeFilterByTk, "encodeFilterByTk");
|
|
46
|
+
function hasUsableSourceId(sourceId) {
|
|
47
|
+
return sourceId !== void 0 && sourceId !== null && String(sourceId) !== "";
|
|
48
|
+
}
|
|
49
|
+
__name(hasUsableSourceId, "hasUsableSourceId");
|
|
46
50
|
function generatePathnameFromViewParams(viewParams) {
|
|
47
51
|
if (!viewParams || viewParams.length === 0) {
|
|
48
52
|
return "/admin";
|
|
@@ -62,8 +66,8 @@ function generatePathnameFromViewParams(viewParams) {
|
|
|
62
66
|
segments.push("filterbytk", encoded);
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
|
-
if (viewParam.sourceId) {
|
|
66
|
-
segments.push("sourceid", viewParam.sourceId);
|
|
69
|
+
if (hasUsableSourceId(viewParam.sourceId)) {
|
|
70
|
+
segments.push("sourceid", String(viewParam.sourceId));
|
|
67
71
|
}
|
|
68
72
|
});
|
|
69
73
|
return "/" + segments.join("/");
|
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.30",
|
|
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.30",
|
|
12
|
+
"@nocobase/shared": "2.1.0-beta.30",
|
|
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": "1a493069cd0d8a4f403668bfe592879678c894b2"
|
|
41
41
|
}
|
|
@@ -160,6 +160,23 @@ describe('FlowContext properties and methods', () => {
|
|
|
160
160
|
expect(ctx.shared).toBe('from delegate');
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
it('should expose current language as a top-level variable', async () => {
|
|
164
|
+
const engine = new FlowEngine();
|
|
165
|
+
const ctx = engine.context;
|
|
166
|
+
ctx.defineProperty('api', { value: { auth: { locale: 'zh-CN' } } });
|
|
167
|
+
ctx.defineProperty('i18n', { value: { language: 'en-US' } });
|
|
168
|
+
|
|
169
|
+
expect(ctx.locale).toBe('zh-CN');
|
|
170
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.locale }}')).resolves.toBe('zh-CN');
|
|
171
|
+
|
|
172
|
+
const localeNode = ctx.getPropertyMetaTree().find((node) => node.name === 'locale');
|
|
173
|
+
expect(localeNode).toMatchObject({
|
|
174
|
+
name: 'locale',
|
|
175
|
+
title: '{{t("Current language")}}',
|
|
176
|
+
paths: ['locale'],
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
163
180
|
it('should throw sync error in get', () => {
|
|
164
181
|
const ctx = new FlowContext();
|
|
165
182
|
ctx.defineProperty('error', {
|
|
@@ -0,0 +1,531 @@
|
|
|
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 { css, cx } from '@emotion/css';
|
|
11
|
+
import { Space, theme } from 'antd';
|
|
12
|
+
import React, { isValidElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
+
import type { MetaTreeNode } from '../../flowContext';
|
|
14
|
+
import { useFlowContext } from '../../FlowContextProvider';
|
|
15
|
+
import { FlowContextSelector } from '../FlowContextSelector';
|
|
16
|
+
import { useResolvedMetaTree } from './useResolvedMetaTree';
|
|
17
|
+
import { formatPathToValue as defaultFormatPathToValue, parseValueToPath as defaultParseValueToPath } from './utils';
|
|
18
|
+
|
|
19
|
+
type RangeIndexes = [number, number, number, number];
|
|
20
|
+
|
|
21
|
+
const DEFAULT_VARIABLE_REGEXP = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
22
|
+
const TAG_CLASS = 'nb-variable-tag';
|
|
23
|
+
|
|
24
|
+
export interface VariableHybridInputConverters {
|
|
25
|
+
formatPathToValue?: (item?: MetaTreeNode) => string | undefined;
|
|
26
|
+
parseValueToPath?: (value?: string) => string[] | undefined;
|
|
27
|
+
variableRegExp?: RegExp;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface VariableHybridInputProps {
|
|
31
|
+
value?: string;
|
|
32
|
+
onChange?: (value: string) => void;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
placeholder?: string;
|
|
35
|
+
addonBefore?: React.ReactNode;
|
|
36
|
+
metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
37
|
+
converters?: VariableHybridInputConverters;
|
|
38
|
+
style?: React.CSSProperties;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function reactNodeToPlainText(node: React.ReactNode): string {
|
|
43
|
+
if (node == null || typeof node === 'boolean') return '';
|
|
44
|
+
if (typeof node === 'string' || typeof node === 'number') return String(node);
|
|
45
|
+
if (Array.isArray(node)) return node.map(reactNodeToPlainText).join('');
|
|
46
|
+
if (isValidElement(node)) return reactNodeToPlainText(node.props.children);
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function escapeHtml(value: string) {
|
|
51
|
+
return value
|
|
52
|
+
.replace(/&/g, '&')
|
|
53
|
+
.replace(/</g, '<')
|
|
54
|
+
.replace(/>/g, '>')
|
|
55
|
+
.replace(/"/g, '"')
|
|
56
|
+
.replace(/'/g, ''');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getDomValue(element: HTMLElement) {
|
|
60
|
+
const out: string[] = [];
|
|
61
|
+
for (const node of Array.from(element.childNodes)) {
|
|
62
|
+
if (node instanceof HTMLElement && node.dataset.variable) {
|
|
63
|
+
out.push(node.dataset.variable);
|
|
64
|
+
} else {
|
|
65
|
+
out.push(node.textContent || '');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out.join('');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createTagHTML(variable: string, label: string) {
|
|
72
|
+
return `<span class="${TAG_CLASS}" contenteditable="false" data-variable="${escapeHtml(
|
|
73
|
+
variable,
|
|
74
|
+
)}" title="${escapeHtml(label)}">${escapeHtml(label)}</span>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Strip outer `{{ }}` and trim, so values like `{{ $env.x }}` and `{{$env.x}}`
|
|
78
|
+
// share the same lookup key.
|
|
79
|
+
function normalizeVariableKey(value: string): string {
|
|
80
|
+
return value
|
|
81
|
+
.replace(/^\{\{\s*/, '')
|
|
82
|
+
.replace(/\s*\}\}$/, '')
|
|
83
|
+
.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderHTML(value: string, labelMap: Map<string, string>, regExp: RegExp) {
|
|
87
|
+
const re = new RegExp(regExp.source, regExp.flags.includes('g') ? regExp.flags : `${regExp.flags}g`);
|
|
88
|
+
return escapeHtml(value || '').replace(re, (matched) => {
|
|
89
|
+
const label = labelMap.get(normalizeVariableKey(matched)) || matched;
|
|
90
|
+
return createTagHTML(matched, label);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildLabelMap(
|
|
95
|
+
nodes: MetaTreeNode[] | undefined,
|
|
96
|
+
ctxT: (text: string) => string,
|
|
97
|
+
converters?: VariableHybridInputConverters,
|
|
98
|
+
) {
|
|
99
|
+
const map = new Map<string, string>();
|
|
100
|
+
|
|
101
|
+
function walk(items: MetaTreeNode[] = [], parentTitles: string[] = []) {
|
|
102
|
+
for (const item of items) {
|
|
103
|
+
const titlePart = reactNodeToPlainText(item.title || item.name);
|
|
104
|
+
const titles = [...parentTitles, titlePart];
|
|
105
|
+
const value = converters?.formatPathToValue?.(item) ?? defaultFormatPathToValue(item);
|
|
106
|
+
if (value) {
|
|
107
|
+
map.set(normalizeVariableKey(value), titles.map(ctxT).join('/'));
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(item.children)) {
|
|
110
|
+
walk(item.children as MetaTreeNode[], titles);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
walk(nodes);
|
|
116
|
+
return map;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pasteHTML(container: HTMLElement, html: string, indexes?: RangeIndexes) {
|
|
120
|
+
const selection = window.getSelection?.();
|
|
121
|
+
const range = selection?.rangeCount ? selection.getRangeAt(0) : null;
|
|
122
|
+
if (!range) return;
|
|
123
|
+
|
|
124
|
+
if (indexes) {
|
|
125
|
+
const children = Array.from(container.childNodes);
|
|
126
|
+
if (indexes[0] === -1) {
|
|
127
|
+
if (indexes[1] && children[indexes[1] - 1]) {
|
|
128
|
+
range.setStartAfter(children[indexes[1] - 1]);
|
|
129
|
+
} else {
|
|
130
|
+
range.setStart(container, 0);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
range.setStart(children[indexes[0]], indexes[1]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (indexes[2] === -1) {
|
|
137
|
+
if (indexes[3] && children[indexes[3] - 1]) {
|
|
138
|
+
range.setEndAfter(children[indexes[3] - 1]);
|
|
139
|
+
} else {
|
|
140
|
+
range.setEnd(container, 0);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
range.setEnd(children[indexes[2]], indexes[3]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const wrapper = document.createElement('div');
|
|
148
|
+
wrapper.innerHTML = html;
|
|
149
|
+
const fragment = document.createDocumentFragment();
|
|
150
|
+
let lastNode: ChildNode | null = null;
|
|
151
|
+
while (wrapper.firstChild) {
|
|
152
|
+
lastNode = fragment.appendChild(wrapper.firstChild);
|
|
153
|
+
}
|
|
154
|
+
range.deleteContents();
|
|
155
|
+
range.insertNode(fragment);
|
|
156
|
+
|
|
157
|
+
if (lastNode) {
|
|
158
|
+
const next = new Range();
|
|
159
|
+
next.setStartAfter(lastNode);
|
|
160
|
+
next.collapse(true);
|
|
161
|
+
selection?.removeAllRanges();
|
|
162
|
+
selection?.addRange(next);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getSingleEndRange(nodes: ChildNode[], index: number, offset: number): [number, number] {
|
|
167
|
+
if (index === -1) {
|
|
168
|
+
let realIndex = offset;
|
|
169
|
+
let collapseFlag = false;
|
|
170
|
+
if (realIndex && nodes[realIndex - 1]?.nodeName === '#text' && nodes[realIndex]?.nodeName === '#text') {
|
|
171
|
+
collapseFlag = true;
|
|
172
|
+
}
|
|
173
|
+
let textOffset = 0;
|
|
174
|
+
for (let i = offset - 1; i >= 0; i -= 1) {
|
|
175
|
+
if (collapseFlag) {
|
|
176
|
+
if (nodes[i]?.nodeName === '#text') {
|
|
177
|
+
textOffset += nodes[i].textContent?.length || 0;
|
|
178
|
+
} else {
|
|
179
|
+
collapseFlag = false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (nodes[i]?.nodeName === '#text' && nodes[i + 1]?.nodeName === '#text') {
|
|
183
|
+
realIndex -= 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return textOffset ? [realIndex, textOffset] : [-1, realIndex];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let realIndex = 0;
|
|
190
|
+
let textOffset = 0;
|
|
191
|
+
for (let i = 0; i < index + 1; i += 1) {
|
|
192
|
+
if (nodes[i]?.nodeName === '#text') {
|
|
193
|
+
if (i !== index && nodes[i + 1] && nodes[i + 1]?.nodeName !== '#text') {
|
|
194
|
+
realIndex += 1;
|
|
195
|
+
}
|
|
196
|
+
textOffset += i === index ? offset : nodes[i].textContent?.length || 0;
|
|
197
|
+
} else {
|
|
198
|
+
realIndex += 1;
|
|
199
|
+
textOffset = 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [realIndex, textOffset];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getCurrentRange(element: HTMLElement): RangeIndexes {
|
|
206
|
+
const selection = window.getSelection?.();
|
|
207
|
+
const range = selection?.rangeCount ? selection.getRangeAt(0) : null;
|
|
208
|
+
if (!range || !element.contains(range.commonAncestorContainer)) {
|
|
209
|
+
return [-1, 0, -1, 0];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const nodes = Array.from(element.childNodes);
|
|
213
|
+
if (!nodes.length) return [-1, 0, -1, 0];
|
|
214
|
+
|
|
215
|
+
const startIndex = range.startContainer === element ? -1 : nodes.indexOf(range.startContainer as HTMLElement);
|
|
216
|
+
const endIndex = range.endContainer === element ? -1 : nodes.indexOf(range.endContainer as HTMLElement);
|
|
217
|
+
|
|
218
|
+
return [
|
|
219
|
+
...getSingleEndRange(nodes, startIndex, range.startOffset),
|
|
220
|
+
...getSingleEndRange(nodes, endIndex, range.endOffset),
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const VariableHybridInputComponent: React.FC<VariableHybridInputProps> = (props) => {
|
|
225
|
+
const { addonBefore, className, converters, disabled, metaTree, onChange, placeholder, style } = props;
|
|
226
|
+
const { token } = theme.useToken();
|
|
227
|
+
const ctx = useFlowContext();
|
|
228
|
+
const { resolvedMetaTree } = useResolvedMetaTree(metaTree);
|
|
229
|
+
const inputRef = useRef<HTMLDivElement>(null);
|
|
230
|
+
const [isComposing, setIsComposing] = useState(false);
|
|
231
|
+
const [changed, setChanged] = useState(false);
|
|
232
|
+
const [range, setRange] = useState<RangeIndexes>([-1, 0, -1, 0]);
|
|
233
|
+
|
|
234
|
+
const value = typeof props.value === 'string' ? props.value : props.value == null ? '' : String(props.value);
|
|
235
|
+
const variableRegExp = converters?.variableRegExp ?? DEFAULT_VARIABLE_REGEXP;
|
|
236
|
+
|
|
237
|
+
const labelMap = useMemo(
|
|
238
|
+
() => buildLabelMap(resolvedMetaTree as MetaTreeNode[] | undefined, ctx.t, converters),
|
|
239
|
+
[resolvedMetaTree, ctx, converters],
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const [html, setHtml] = useState(() => renderHTML(value, labelMap, variableRegExp));
|
|
243
|
+
|
|
244
|
+
const emitChange = useCallback(
|
|
245
|
+
(target: HTMLElement) => {
|
|
246
|
+
onChange?.(getDomValue(target).trim());
|
|
247
|
+
},
|
|
248
|
+
[onChange],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
setHtml(renderHTML(value, labelMap, variableRegExp));
|
|
253
|
+
if (!changed) {
|
|
254
|
+
setRange([-1, 0, -1, 0]);
|
|
255
|
+
}
|
|
256
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
257
|
+
}, [value, labelMap]);
|
|
258
|
+
|
|
259
|
+
// Restore caret position after html update
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
const element = inputRef.current;
|
|
262
|
+
if (!element) return;
|
|
263
|
+
if (document.activeElement !== element) return;
|
|
264
|
+
|
|
265
|
+
const nextRange = new Range();
|
|
266
|
+
if (changed) {
|
|
267
|
+
if (range.join() === '-1,0,-1,0') return;
|
|
268
|
+
const selection = window.getSelection?.();
|
|
269
|
+
if (!selection) return;
|
|
270
|
+
try {
|
|
271
|
+
const children = Array.from(element.childNodes) as HTMLElement[];
|
|
272
|
+
if (children.length) {
|
|
273
|
+
if (range[0] === -1) {
|
|
274
|
+
if (range[1]) nextRange.setStartAfter(children[range[1] - 1]);
|
|
275
|
+
} else {
|
|
276
|
+
nextRange.setStart(children[range[0]], range[1]);
|
|
277
|
+
}
|
|
278
|
+
if (range[2] === -1) {
|
|
279
|
+
if (range[3]) nextRange.setEndAfter(children[range[3] - 1]);
|
|
280
|
+
} else {
|
|
281
|
+
nextRange.setEnd(children[range[2]], range[3]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
nextRange.collapse(true);
|
|
285
|
+
selection.removeAllRanges();
|
|
286
|
+
selection.addRange(nextRange);
|
|
287
|
+
} catch {
|
|
288
|
+
/* ignore */
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
const { lastChild } = element;
|
|
292
|
+
if (lastChild) {
|
|
293
|
+
nextRange.setStartAfter(lastChild);
|
|
294
|
+
nextRange.setEndAfter(lastChild);
|
|
295
|
+
const nodes = Array.from(element.childNodes);
|
|
296
|
+
const idx = nodes.indexOf(lastChild);
|
|
297
|
+
const startIndex = nextRange.startContainer === element ? -1 : idx;
|
|
298
|
+
const endIndex = nextRange.startContainer === element ? -1 : idx;
|
|
299
|
+
setRange([startIndex, nextRange.startOffset, endIndex, nextRange.endOffset]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}, [changed, html, range]);
|
|
303
|
+
|
|
304
|
+
const insertVariable = useCallback(
|
|
305
|
+
(variable: string, meta?: MetaTreeNode) => {
|
|
306
|
+
const current = inputRef.current;
|
|
307
|
+
if (!current || !variable) return;
|
|
308
|
+
|
|
309
|
+
const label =
|
|
310
|
+
labelMap.get(normalizeVariableKey(variable)) ||
|
|
311
|
+
(meta
|
|
312
|
+
? [...(meta.parentTitles || []), reactNodeToPlainText(meta.title || meta.name)].map(ctx.t).join('/')
|
|
313
|
+
: variable);
|
|
314
|
+
|
|
315
|
+
current.focus();
|
|
316
|
+
pasteHTML(current, createTagHTML(variable, label), range);
|
|
317
|
+
setChanged(true);
|
|
318
|
+
setRange(getCurrentRange(current));
|
|
319
|
+
emitChange(current);
|
|
320
|
+
},
|
|
321
|
+
[labelMap, range, emitChange, ctx],
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const handleSelectorChange = useCallback(
|
|
325
|
+
(next: string, meta?: MetaTreeNode) => {
|
|
326
|
+
if (!next) return;
|
|
327
|
+
insertVariable(next, meta);
|
|
328
|
+
},
|
|
329
|
+
[insertVariable],
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const handleInput = useCallback(
|
|
333
|
+
({ currentTarget }: React.FormEvent<HTMLDivElement>) => {
|
|
334
|
+
if (isComposing) return;
|
|
335
|
+
setChanged(true);
|
|
336
|
+
setRange(getCurrentRange(currentTarget));
|
|
337
|
+
emitChange(currentTarget);
|
|
338
|
+
},
|
|
339
|
+
[emitChange, isComposing],
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const handleBlur = useCallback(({ currentTarget }: React.FocusEvent<HTMLDivElement>) => {
|
|
343
|
+
setRange(getCurrentRange(currentTarget));
|
|
344
|
+
}, []);
|
|
345
|
+
|
|
346
|
+
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
347
|
+
if (event.key === 'Enter') {
|
|
348
|
+
event.preventDefault();
|
|
349
|
+
}
|
|
350
|
+
}, []);
|
|
351
|
+
|
|
352
|
+
const handlePaste = useCallback(
|
|
353
|
+
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
// Paste as plain text only; variable tags must be inserted via the picker.
|
|
356
|
+
const text = event.clipboardData.getData('text/plain').replace(/\n/g, ' ');
|
|
357
|
+
if (!text) return;
|
|
358
|
+
setChanged(true);
|
|
359
|
+
pasteHTML(event.currentTarget, escapeHtml(text));
|
|
360
|
+
setRange(getCurrentRange(event.currentTarget));
|
|
361
|
+
emitChange(event.currentTarget);
|
|
362
|
+
},
|
|
363
|
+
[emitChange],
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const handleCompositionStart = useCallback(() => setIsComposing(true), []);
|
|
367
|
+
const handleCompositionEnd = useCallback(
|
|
368
|
+
({ currentTarget }: React.CompositionEvent<HTMLDivElement>) => {
|
|
369
|
+
setIsComposing(false);
|
|
370
|
+
setChanged(true);
|
|
371
|
+
setRange(getCurrentRange(currentTarget));
|
|
372
|
+
emitChange(currentTarget);
|
|
373
|
+
},
|
|
374
|
+
[emitChange],
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const wrapperClassName = useMemo(
|
|
378
|
+
() => css`
|
|
379
|
+
display: flex;
|
|
380
|
+
width: 100%;
|
|
381
|
+
min-width: 0;
|
|
382
|
+
|
|
383
|
+
&.ant-space-compact {
|
|
384
|
+
display: flex;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* The trigger button from FlowContextSelector sits at the right end.
|
|
388
|
+
Flatten its left corners so it shares the border with the editor. */
|
|
389
|
+
> .ant-btn {
|
|
390
|
+
flex-shrink: 0;
|
|
391
|
+
border-top-left-radius: 0;
|
|
392
|
+
border-bottom-left-radius: 0;
|
|
393
|
+
margin-left: -${token.lineWidth}px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
> .ant-btn:hover,
|
|
397
|
+
> .ant-btn:focus {
|
|
398
|
+
z-index: 2;
|
|
399
|
+
}
|
|
400
|
+
`,
|
|
401
|
+
[token.lineWidth],
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const addonClassName = useMemo(
|
|
405
|
+
() => css`
|
|
406
|
+
display: inline-flex;
|
|
407
|
+
align-items: center;
|
|
408
|
+
padding: 0 ${token.paddingSM}px;
|
|
409
|
+
background: ${token.colorFillTertiary};
|
|
410
|
+
border: ${token.lineWidth}px ${token.lineType} ${token.colorBorder};
|
|
411
|
+
border-right: 0;
|
|
412
|
+
border-radius: ${token.borderRadius}px 0 0 ${token.borderRadius}px;
|
|
413
|
+
color: ${token.colorText};
|
|
414
|
+
font-size: ${token.fontSize}px;
|
|
415
|
+
line-height: 1;
|
|
416
|
+
white-space: nowrap;
|
|
417
|
+
`,
|
|
418
|
+
[token],
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const editorClassName = useMemo(() => {
|
|
422
|
+
const verticalPad = Math.max(
|
|
423
|
+
0,
|
|
424
|
+
(token.controlHeight - Math.round(token.lineHeight * token.fontSize)) / 2 - token.lineWidth,
|
|
425
|
+
);
|
|
426
|
+
return css`
|
|
427
|
+
flex: 1 1 auto;
|
|
428
|
+
min-width: 0;
|
|
429
|
+
min-height: ${token.controlHeight}px;
|
|
430
|
+
padding: ${verticalPad}px ${token.paddingSM}px;
|
|
431
|
+
overflow: hidden;
|
|
432
|
+
white-space: pre-wrap;
|
|
433
|
+
word-break: break-word;
|
|
434
|
+
line-height: ${token.lineHeight};
|
|
435
|
+
font-size: ${token.fontSize}px;
|
|
436
|
+
color: ${token.colorText};
|
|
437
|
+
background: ${token.colorBgContainer};
|
|
438
|
+
border: ${token.lineWidth}px ${token.lineType} ${token.colorBorder};
|
|
439
|
+
/* Right corners are always flat because the X picker button is glued to the right side. */
|
|
440
|
+
border-radius: ${addonBefore ? '0' : `${token.borderRadius}px 0 0 ${token.borderRadius}px`};
|
|
441
|
+
cursor: text;
|
|
442
|
+
transition: all ${token.motionDurationMid};
|
|
443
|
+
outline: none;
|
|
444
|
+
|
|
445
|
+
&:hover {
|
|
446
|
+
border-color: ${token.colorPrimaryHover};
|
|
447
|
+
z-index: 1;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
&:focus,
|
|
451
|
+
&:focus-visible {
|
|
452
|
+
border-color: ${token.colorPrimary};
|
|
453
|
+
box-shadow: 0 0 0 ${token.controlOutlineWidth}px ${token.controlOutline};
|
|
454
|
+
z-index: 1;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
&[data-placeholder]:empty::before {
|
|
458
|
+
content: attr(data-placeholder);
|
|
459
|
+
color: ${token.colorTextPlaceholder};
|
|
460
|
+
pointer-events: none;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.${TAG_CLASS} {
|
|
464
|
+
/* inline lets long tag content wrap naturally across lines, mirroring v1. */
|
|
465
|
+
display: inline;
|
|
466
|
+
margin: 0 ${token.marginXXS}px;
|
|
467
|
+
padding: ${token.paddingXXS}px ${token.paddingXS}px;
|
|
468
|
+
font-size: ${token.fontSizeSM}px;
|
|
469
|
+
line-height: ${token.lineHeightSM};
|
|
470
|
+
color: ${token.colorPrimaryText};
|
|
471
|
+
background: ${token.colorPrimaryBg};
|
|
472
|
+
border: ${token.lineWidth}px ${token.lineType} ${token.colorPrimaryBorder};
|
|
473
|
+
border-radius: ${token.borderRadiusSM}px;
|
|
474
|
+
vertical-align: baseline;
|
|
475
|
+
user-select: none;
|
|
476
|
+
cursor: default;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
&.is-disabled {
|
|
480
|
+
background: ${token.colorBgContainerDisabled};
|
|
481
|
+
color: ${token.colorTextDisabled};
|
|
482
|
+
border-color: ${token.colorBorder};
|
|
483
|
+
cursor: not-allowed;
|
|
484
|
+
|
|
485
|
+
&:hover {
|
|
486
|
+
border-color: ${token.colorBorder};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.${TAG_CLASS} {
|
|
490
|
+
color: ${token.colorTextDisabled};
|
|
491
|
+
background: ${token.colorFillTertiary};
|
|
492
|
+
border-color: ${token.colorBorder};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
`;
|
|
496
|
+
}, [token, addonBefore]);
|
|
497
|
+
|
|
498
|
+
return (
|
|
499
|
+
<>
|
|
500
|
+
<Space.Compact className={cx('nb-variable-hybrid-input', wrapperClassName, className)} style={style}>
|
|
501
|
+
{addonBefore != null && <span className={addonClassName}>{addonBefore}</span>}
|
|
502
|
+
<div
|
|
503
|
+
ref={inputRef}
|
|
504
|
+
role="textbox"
|
|
505
|
+
aria-label="textbox"
|
|
506
|
+
className={cx(editorClassName, {
|
|
507
|
+
'is-disabled': disabled,
|
|
508
|
+
})}
|
|
509
|
+
contentEditable={!disabled}
|
|
510
|
+
data-placeholder={placeholder}
|
|
511
|
+
onInput={handleInput}
|
|
512
|
+
onBlur={handleBlur}
|
|
513
|
+
onKeyDown={handleKeyDown}
|
|
514
|
+
onPaste={handlePaste}
|
|
515
|
+
onCompositionStart={handleCompositionStart}
|
|
516
|
+
onCompositionEnd={handleCompositionEnd}
|
|
517
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
518
|
+
/>
|
|
519
|
+
<FlowContextSelector
|
|
520
|
+
metaTree={metaTree}
|
|
521
|
+
disabled={disabled}
|
|
522
|
+
parseValueToPath={converters?.parseValueToPath ?? defaultParseValueToPath}
|
|
523
|
+
formatPathToValue={(item) => converters?.formatPathToValue?.(item) || defaultFormatPathToValue(item)}
|
|
524
|
+
onChange={handleSelectorChange}
|
|
525
|
+
/>
|
|
526
|
+
</Space.Compact>
|
|
527
|
+
</>
|
|
528
|
+
);
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
export const VariableHybridInput = React.memo(VariableHybridInputComponent);
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export { VariableInput } from './VariableInput';
|
|
11
|
+
export { VariableHybridInput } from './VariableHybridInput';
|
|
12
|
+
export type { VariableHybridInputProps, VariableHybridInputConverters } from './VariableHybridInput';
|
|
11
13
|
export { SlateVariableEditor } from './SlateVariableEditor';
|
|
12
14
|
export { VariableTag } from './VariableTag';
|
|
13
15
|
export { InlineVariableTag } from './InlineVariableTag';
|
package/src/flowContext.ts
CHANGED
|
@@ -3036,6 +3036,7 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
3036
3036
|
declare runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any;
|
|
3037
3037
|
declare engine: FlowEngine;
|
|
3038
3038
|
declare api: APIClient;
|
|
3039
|
+
declare locale: string;
|
|
3039
3040
|
declare viewer: FlowViewer;
|
|
3040
3041
|
declare view: FlowView;
|
|
3041
3042
|
declare modal: HookAPI;
|
|
@@ -3146,6 +3147,15 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3146
3147
|
this.defineMethod('t', (keyOrTemplate: string, options?: any) => {
|
|
3147
3148
|
return i18n.translate(keyOrTemplate, options);
|
|
3148
3149
|
});
|
|
3150
|
+
this.defineProperty('locale', {
|
|
3151
|
+
get: () => this.api?.auth?.locale || this.i18n?.language,
|
|
3152
|
+
cache: false,
|
|
3153
|
+
meta: Object.assign(() => ({ type: 'string', title: this.t('Current language'), sort: 970 }), {
|
|
3154
|
+
title: escapeT('Current language'),
|
|
3155
|
+
sort: 970,
|
|
3156
|
+
hasChildren: false,
|
|
3157
|
+
}),
|
|
3158
|
+
});
|
|
3149
3159
|
this.defineMethod('renderJson', function (template: any) {
|
|
3150
3160
|
return this.resolveJsonTemplate(template);
|
|
3151
3161
|
});
|
package/src/types.ts
CHANGED
|
@@ -26,6 +26,10 @@ function encodeFilterByTk(val: SharedViewParam['filterByTk']): string {
|
|
|
26
26
|
return encodeURIComponent(String(val));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function hasUsableSourceId(sourceId: unknown): sourceId is string | number {
|
|
30
|
+
return sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
/**
|
|
30
34
|
* 将 ViewParam 数组转换为 pathname
|
|
31
35
|
*
|
|
@@ -65,8 +69,8 @@ export function generatePathnameFromViewParams(viewParams: ViewParams[]): string
|
|
|
65
69
|
segments.push('filterbytk', encoded);
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
|
-
if (viewParam.sourceId) {
|
|
69
|
-
segments.push('sourceid', viewParam.sourceId);
|
|
72
|
+
if (hasUsableSourceId(viewParam.sourceId)) {
|
|
73
|
+
segments.push('sourceid', String(viewParam.sourceId));
|
|
70
74
|
}
|
|
71
75
|
});
|
|
72
76
|
|