@nocobase/flow-engine 2.1.0-beta.26 → 2.1.0-beta.29
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/FormItem.d.ts +6 -0
- package/lib/components/FormItem.js +11 -3
- 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/data-source/index.d.ts +2 -0
- package/lib/data-source/index.js +35 -3
- package/lib/views/ViewNavigation.js +6 -2
- package/package.json +4 -4
- package/src/components/FormItem.tsx +7 -1
- package/src/components/__tests__/FormItem.test.tsx +25 -0
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/data-source/__tests__/collection.test.ts +41 -2
- package/src/data-source/__tests__/index.test.ts +34 -0
- package/src/data-source/index.ts +45 -3
- package/src/views/ViewNavigation.ts +6 -2
package/lib/data-source/index.js
CHANGED
|
@@ -42,6 +42,7 @@ __export(data_source_exports, {
|
|
|
42
42
|
CollectionManager: () => CollectionManager,
|
|
43
43
|
DataSource: () => DataSource,
|
|
44
44
|
DataSourceManager: () => DataSourceManager,
|
|
45
|
+
getCollectionFieldInterface: () => getCollectionFieldInterface,
|
|
45
46
|
isFieldInterfaceMatch: () => isFieldInterfaceMatch,
|
|
46
47
|
jioToJoiSchema: () => import_jioToJoiSchema.jioToJoiSchema
|
|
47
48
|
});
|
|
@@ -279,6 +280,20 @@ const _DataSourceManager = class _DataSourceManager {
|
|
|
279
280
|
};
|
|
280
281
|
__name(_DataSourceManager, "DataSourceManager");
|
|
281
282
|
let DataSourceManager = _DataSourceManager;
|
|
283
|
+
function getCollectionFieldInterface(interfaceName, ...dataSourceManagers) {
|
|
284
|
+
if (!interfaceName) {
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
for (const dataSourceManager of dataSourceManagers) {
|
|
288
|
+
const collectionFieldInterfaceManager = dataSourceManager == null ? void 0 : dataSourceManager.collectionFieldInterfaceManager;
|
|
289
|
+
const getFieldInterface = collectionFieldInterfaceManager == null ? void 0 : collectionFieldInterfaceManager.getFieldInterface;
|
|
290
|
+
if (typeof getFieldInterface === "function") {
|
|
291
|
+
return getFieldInterface.call(collectionFieldInterfaceManager, interfaceName);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return void 0;
|
|
295
|
+
}
|
|
296
|
+
__name(getCollectionFieldInterface, "getCollectionFieldInterface");
|
|
282
297
|
const _DataSource = class _DataSource {
|
|
283
298
|
dataSourceManager;
|
|
284
299
|
collectionManager;
|
|
@@ -982,7 +997,17 @@ const _CollectionField = class _CollectionField {
|
|
|
982
997
|
abortEarly: false
|
|
983
998
|
});
|
|
984
999
|
if (error) {
|
|
985
|
-
const message = error.details.map((d) =>
|
|
1000
|
+
const message = error.details.map((d) => {
|
|
1001
|
+
const translated = this.flowEngine.translate(d.type, {
|
|
1002
|
+
...d.context,
|
|
1003
|
+
ns: "data-source-main",
|
|
1004
|
+
label
|
|
1005
|
+
});
|
|
1006
|
+
if (translated && translated !== d.type) {
|
|
1007
|
+
return translated;
|
|
1008
|
+
}
|
|
1009
|
+
return d.message.replace(/"value"/g, `"${label}"`);
|
|
1010
|
+
}).join(", ");
|
|
986
1011
|
return Promise.reject(message);
|
|
987
1012
|
}
|
|
988
1013
|
return Promise.resolve();
|
|
@@ -1002,8 +1027,14 @@ const _CollectionField = class _CollectionField {
|
|
|
1002
1027
|
return this.targetCollection.getFields();
|
|
1003
1028
|
}
|
|
1004
1029
|
getInterfaceOptions() {
|
|
1005
|
-
|
|
1006
|
-
|
|
1030
|
+
var _a, _b, _c;
|
|
1031
|
+
const ctx = this.flowEngine.context;
|
|
1032
|
+
return getCollectionFieldInterface(
|
|
1033
|
+
this.interface,
|
|
1034
|
+
(_b = (_a = this.collection) == null ? void 0 : _a.dataSource) == null ? void 0 : _b.dataSourceManager,
|
|
1035
|
+
ctx.dataSourceManager,
|
|
1036
|
+
(_c = ctx.app) == null ? void 0 : _c.dataSourceManager
|
|
1037
|
+
);
|
|
1007
1038
|
}
|
|
1008
1039
|
getFilterOperators() {
|
|
1009
1040
|
var _a;
|
|
@@ -1068,6 +1099,7 @@ __name(isFieldInterfaceMatch, "isFieldInterfaceMatch");
|
|
|
1068
1099
|
CollectionManager,
|
|
1069
1100
|
DataSource,
|
|
1070
1101
|
DataSourceManager,
|
|
1102
|
+
getCollectionFieldInterface,
|
|
1071
1103
|
isFieldInterfaceMatch,
|
|
1072
1104
|
jioToJoiSchema
|
|
1073
1105
|
});
|
|
@@ -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.29",
|
|
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.29",
|
|
12
|
+
"@nocobase/shared": "2.1.0-beta.29",
|
|
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": "86c41be29dcbcac6fd6aa46b4a137ef07a27c1d0"
|
|
41
41
|
}
|
|
@@ -19,6 +19,9 @@ interface ExtendedFormItemProps extends FormItemProps {
|
|
|
19
19
|
showLabel?: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export const verticalFormItemLabelStyle = { paddingBottom: 0 };
|
|
23
|
+
export const formItemStyle = { marginBottom: 12 };
|
|
24
|
+
|
|
22
25
|
const formItemPropKeys: (keyof ExtendedFormItemProps)[] = [
|
|
23
26
|
'colon',
|
|
24
27
|
'dependencies',
|
|
@@ -73,6 +76,8 @@ export const FormItem = ({
|
|
|
73
76
|
});
|
|
74
77
|
const { label, labelWrap, colon = true, layout } = rest;
|
|
75
78
|
const effectiveLabelWrap = !layout || layout === 'vertical' ? true : labelWrap;
|
|
79
|
+
const labelColStyle =
|
|
80
|
+
layout === 'vertical' ? { width: labelWidth, ...verticalFormItemLabelStyle } : { width: labelWidth };
|
|
76
81
|
const renderLabel = () => {
|
|
77
82
|
if (!showLabel) return null;
|
|
78
83
|
if (effectiveLabelWrap) {
|
|
@@ -118,7 +123,8 @@ export const FormItem = ({
|
|
|
118
123
|
return (
|
|
119
124
|
<Form.Item
|
|
120
125
|
{...rest}
|
|
121
|
-
|
|
126
|
+
style={{ ...formItemStyle, ...rest.style }}
|
|
127
|
+
labelCol={{ style: labelColStyle }}
|
|
122
128
|
layout={layout}
|
|
123
129
|
label={renderLabel()}
|
|
124
130
|
colon={false}
|
|
@@ -0,0 +1,25 @@
|
|
|
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, expect, it } from 'vitest';
|
|
11
|
+
import { formItemStyle, verticalFormItemLabelStyle } from '../FormItem';
|
|
12
|
+
|
|
13
|
+
describe('FormItem', () => {
|
|
14
|
+
it('keeps vertical label-to-value spacing consistent with v1', () => {
|
|
15
|
+
expect(verticalFormItemLabelStyle).toEqual({
|
|
16
|
+
paddingBottom: 0,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('keeps spacing between form items consistent with v1', () => {
|
|
21
|
+
expect(formItemStyle).toEqual({
|
|
22
|
+
marginBottom: 12,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -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);
|