@nocobase/flow-engine 2.2.0-beta.14 → 2.2.0-beta.16
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/acl/Acl.d.ts +2 -1
- package/lib/acl/Acl.js +28 -0
- package/lib/components/FlowContextSelector.js +55 -12
- package/lib/components/FormItem.js +11 -7
- package/lib/components/MobilePopup.js +14 -3
- package/lib/components/subModel/LazyDropdown.js +41 -26
- package/lib/components/variables/VariableHybridInput.d.ts +9 -0
- package/lib/components/variables/VariableHybridInput.js +146 -17
- package/lib/components/variables/VariableInput.js +19 -7
- package/lib/components/variables/VariableTag.js +48 -36
- package/lib/components/variables/types.d.ts +21 -0
- package/lib/flowI18n.js +3 -3
- package/lib/locale/en-US.json +2 -0
- package/lib/locale/index.d.ts +4 -0
- package/lib/locale/zh-CN.json +2 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.js +1 -0
- package/package.json +4 -4
- package/src/__tests__/flowI18n.test.ts +11 -0
- package/src/acl/Acl.tsx +36 -1
- package/src/acl/__tests__/Acl.test.tsx +70 -0
- package/src/components/FlowContextSelector.tsx +66 -11
- package/src/components/FormItem.tsx +12 -7
- package/src/components/MobilePopup.tsx +16 -4
- package/src/components/__tests__/FormItem.test.tsx +17 -2
- package/src/components/__tests__/MobilePopup.test.tsx +42 -1
- package/src/components/subModel/LazyDropdown.tsx +44 -26
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +85 -2
- package/src/components/variables/VariableHybridInput.tsx +185 -14
- package/src/components/variables/VariableInput.tsx +32 -7
- package/src/components/variables/VariableTag.tsx +51 -37
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +60 -3
- package/src/components/variables/__tests__/VariableHybridInput.test.tsx +212 -0
- package/src/components/variables/__tests__/VariableInput.test.tsx +202 -6
- package/src/components/variables/__tests__/VariableTag.test.tsx +80 -0
- package/src/components/variables/types.ts +21 -0
- package/src/flowI18n.ts +8 -3
- package/src/locale/__tests__/index.test.ts +21 -0
- package/src/locale/en-US.json +2 -0
- package/src/locale/zh-CN.json +2 -0
- package/src/types.ts +2 -0
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import { Input } from 'antd';
|
|
11
12
|
import React from 'react';
|
|
12
13
|
import { describe, expect, it, vi } from 'vitest';
|
|
13
14
|
import type { ContextSelectorItem } from '../types';
|
|
@@ -100,6 +101,127 @@ describe('VariableInput', () => {
|
|
|
100
101
|
);
|
|
101
102
|
});
|
|
102
103
|
|
|
104
|
+
it('renders a variable tag when custom converters resolve a non-ctx workflow variable format', async () => {
|
|
105
|
+
const flowContext = createTestFlowContext();
|
|
106
|
+
const workflowMetaTree = [
|
|
107
|
+
{
|
|
108
|
+
name: '$context',
|
|
109
|
+
title: 'Trigger variables',
|
|
110
|
+
type: 'object',
|
|
111
|
+
paths: ['$context'],
|
|
112
|
+
children: [
|
|
113
|
+
{
|
|
114
|
+
name: 'data',
|
|
115
|
+
title: 'Trigger data',
|
|
116
|
+
type: 'object',
|
|
117
|
+
paths: ['$context', 'data'],
|
|
118
|
+
children: [
|
|
119
|
+
{
|
|
120
|
+
name: 'updatedAt',
|
|
121
|
+
title: 'Last updated at',
|
|
122
|
+
type: 'string',
|
|
123
|
+
paths: ['$context', 'data', 'updatedAt'],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
render(
|
|
132
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
133
|
+
<VariableInput
|
|
134
|
+
value="{{$context.data.updatedAt}}"
|
|
135
|
+
metaTree={workflowMetaTree}
|
|
136
|
+
converters={{
|
|
137
|
+
resolvePathFromValue: (currentValue) =>
|
|
138
|
+
currentValue === '{{$context.data.updatedAt}}' ? ['$context', 'data', 'updatedAt'] : undefined,
|
|
139
|
+
resolveValueFromPath: () => undefined,
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
</TestFlowContextWrapper>,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await waitFor(
|
|
146
|
+
() => {
|
|
147
|
+
const variableTag = screen.getByText('Trigger variables/Trigger data/Last updated at');
|
|
148
|
+
expect(variableTag).toBeInTheDocument();
|
|
149
|
+
expect(variableTag.closest('.ant-tag')).toBeInTheDocument();
|
|
150
|
+
},
|
|
151
|
+
{ timeout: 3000 },
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const selectorButton = screen.getByRole('button');
|
|
155
|
+
expect(selectorButton.className).toContain('ant-btn-primary');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('renders a parsing failure tag when a custom variable path is missing from the meta tree', async () => {
|
|
159
|
+
const flowContext = createTestFlowContext();
|
|
160
|
+
const workflowMetaTree = [
|
|
161
|
+
{
|
|
162
|
+
name: '$context',
|
|
163
|
+
title: 'Trigger variables',
|
|
164
|
+
type: 'object',
|
|
165
|
+
paths: ['$context'],
|
|
166
|
+
children: [
|
|
167
|
+
{
|
|
168
|
+
name: 'data',
|
|
169
|
+
title: 'Trigger data',
|
|
170
|
+
type: 'object',
|
|
171
|
+
paths: ['$context', 'data'],
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
render(
|
|
178
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
179
|
+
<VariableInput
|
|
180
|
+
value="{{$context.data.id}}"
|
|
181
|
+
metaTree={workflowMetaTree}
|
|
182
|
+
converters={{
|
|
183
|
+
resolvePathFromValue: (currentValue) =>
|
|
184
|
+
currentValue === '{{$context.data.id}}' ? ['$context', 'data', 'id'] : undefined,
|
|
185
|
+
resolveValueFromPath: () => undefined,
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
</TestFlowContextWrapper>,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
await waitFor(
|
|
192
|
+
() => {
|
|
193
|
+
const variableTag = screen.getByText('Variable parsing failed');
|
|
194
|
+
expect(variableTag).toBeInTheDocument();
|
|
195
|
+
expect(variableTag.closest('.ant-tag')).toHaveClass('ant-tag-error');
|
|
196
|
+
},
|
|
197
|
+
{ timeout: 3000 },
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(screen.queryByDisplayValue('{{$context.data.id}}')).not.toBeInTheDocument();
|
|
201
|
+
expect(screen.getByRole('button').className).toContain('ant-btn-primary');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('disables custom tag input when rendering a selected variable', async () => {
|
|
205
|
+
const flowContext = createTestFlowContext();
|
|
206
|
+
const { container } = render(
|
|
207
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
208
|
+
<VariableInput value="{{ ctx.user.name }}" metaTree={() => flowContext.getPropertyMetaTree()} />
|
|
209
|
+
</TestFlowContextWrapper>,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
await waitFor(
|
|
213
|
+
() => {
|
|
214
|
+
expect(screen.getByText('User/Name')).toBeInTheDocument();
|
|
215
|
+
},
|
|
216
|
+
{ timeout: 3000 },
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const selectElement = container.querySelector('.ant-select.variable');
|
|
220
|
+
expect(selectElement).toBeInTheDocument();
|
|
221
|
+
expect(selectElement).not.toHaveClass('ant-select-show-search');
|
|
222
|
+
expect(container.querySelector('.ant-select-selection-search-input')).toHaveAttribute('readonly');
|
|
223
|
+
});
|
|
224
|
+
|
|
103
225
|
it('should render FlowContextSelector button', async () => {
|
|
104
226
|
const flowContext = createTestFlowContext();
|
|
105
227
|
render(
|
|
@@ -112,6 +234,62 @@ describe('VariableInput', () => {
|
|
|
112
234
|
expect(selectorButton).toBeInTheDocument();
|
|
113
235
|
});
|
|
114
236
|
|
|
237
|
+
it('disables the FlowContextSelector button when disabled', async () => {
|
|
238
|
+
const flowContext = createTestFlowContext();
|
|
239
|
+
render(
|
|
240
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
241
|
+
<VariableInput value="test" metaTree={() => flowContext.getPropertyMetaTree()} disabled />
|
|
242
|
+
</TestFlowContextWrapper>,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const selectorButton = await screen.findByRole('button');
|
|
246
|
+
expect(selectorButton).toBeDisabled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should not highlight the selector button for synthetic constant/null paths', async () => {
|
|
250
|
+
const flowContext = createTestFlowContext();
|
|
251
|
+
|
|
252
|
+
render(
|
|
253
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
254
|
+
<VariableInput
|
|
255
|
+
value=""
|
|
256
|
+
metaTree={[
|
|
257
|
+
{ name: 'constant', title: 'Constant', type: 'string', paths: ['constant'] },
|
|
258
|
+
{ name: 'null', title: 'Null', type: 'object', paths: ['null'] },
|
|
259
|
+
...flowContext.getPropertyMetaTree(),
|
|
260
|
+
]}
|
|
261
|
+
converters={{
|
|
262
|
+
renderInputComponent: (meta) => {
|
|
263
|
+
const first = meta?.paths?.[0];
|
|
264
|
+
if (first === 'constant') {
|
|
265
|
+
return (props: any) => <input aria-label="constant-value" {...props} />;
|
|
266
|
+
}
|
|
267
|
+
if (first === 'null') {
|
|
268
|
+
return () => <Input placeholder="<Null>" readOnly />;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
},
|
|
272
|
+
resolveValueFromPath: (meta) => {
|
|
273
|
+
const first = meta?.paths?.[0];
|
|
274
|
+
if (first === 'constant') return '';
|
|
275
|
+
if (first === 'null') return null;
|
|
276
|
+
return undefined;
|
|
277
|
+
},
|
|
278
|
+
resolvePathFromValue: (currentValue) => {
|
|
279
|
+
if (currentValue === null) return ['null'];
|
|
280
|
+
const trimmed = typeof currentValue === 'string' ? currentValue.trim() : currentValue;
|
|
281
|
+
if (trimmed === '') return ['constant'];
|
|
282
|
+
return undefined;
|
|
283
|
+
},
|
|
284
|
+
}}
|
|
285
|
+
/>
|
|
286
|
+
</TestFlowContextWrapper>,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const selectorButton = await screen.findByRole('button');
|
|
290
|
+
expect(selectorButton.className).not.toContain('ant-btn-primary');
|
|
291
|
+
});
|
|
292
|
+
|
|
115
293
|
it('should handle onChange from Input', async () => {
|
|
116
294
|
const onChange = vi.fn();
|
|
117
295
|
const flowContext = createTestFlowContext();
|
|
@@ -195,20 +373,21 @@ describe('VariableInput', () => {
|
|
|
195
373
|
|
|
196
374
|
const selectElement = container.querySelector('.ant-select');
|
|
197
375
|
expect(selectElement).toBeInTheDocument();
|
|
376
|
+
if (!selectElement) {
|
|
377
|
+
throw new Error('Expected variable tag select wrapper to be present');
|
|
378
|
+
}
|
|
198
379
|
|
|
199
380
|
// 触发鼠标悬停以显示清除按钮
|
|
200
|
-
fireEvent.mouseEnter(selectElement
|
|
381
|
+
fireEvent.mouseEnter(selectElement);
|
|
201
382
|
|
|
202
383
|
// 尝试触发清除功能
|
|
203
384
|
// 方法1: 直接触发 Select 组件的 onClear 事件
|
|
204
|
-
const selectInstance = selectElement as any;
|
|
205
|
-
|
|
206
385
|
// 尝试通过键盘事件触发清除
|
|
207
|
-
fireEvent.keyDown(selectElement
|
|
386
|
+
fireEvent.keyDown(selectElement, { key: 'Backspace', code: 'Backspace' });
|
|
208
387
|
|
|
209
388
|
// 或者尝试触发自定义的清除逻辑
|
|
210
389
|
const clearEvents = new CustomEvent('clear');
|
|
211
|
-
selectElement
|
|
390
|
+
selectElement.dispatchEvent(clearEvents);
|
|
212
391
|
|
|
213
392
|
// 检查是否调用了清除功能(可能需要调整期望)
|
|
214
393
|
// 如果清除按钮不能直接测试,我们验证组件支持清除功能
|
|
@@ -276,7 +455,24 @@ describe('VariableInput', () => {
|
|
|
276
455
|
expect(input).toHaveClass('custom-class');
|
|
277
456
|
|
|
278
457
|
// Note: The disabled prop might not be correctly passed through in the current implementation
|
|
279
|
-
|
|
458
|
+
expect(input).toBeDisabled();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('disables the rendered variable tag when the input is disabled', async () => {
|
|
462
|
+
const flowContext = createTestFlowContext();
|
|
463
|
+
const { container } = render(
|
|
464
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
465
|
+
<VariableInput value="{{ ctx.user.name }}" metaTree={() => flowContext.getPropertyMetaTree()} disabled />
|
|
466
|
+
</TestFlowContextWrapper>,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
await waitFor(() => {
|
|
470
|
+
expect(screen.getByText('User/Name')).toBeInTheDocument();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const selectElement = container.querySelector('.ant-select.variable');
|
|
474
|
+
expect(selectElement).toHaveClass('ant-select-disabled');
|
|
475
|
+
expect(container.querySelector('.ant-select-clear')).not.toBeInTheDocument();
|
|
280
476
|
});
|
|
281
477
|
|
|
282
478
|
it('should handle empty metaTree', async () => {
|
|
@@ -54,6 +54,44 @@ describe('VariableTag', () => {
|
|
|
54
54
|
expect(onClear).toBeInstanceOf(Function);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
it('keeps custom tag input enabled by default', async () => {
|
|
58
|
+
const onClear = vi.fn();
|
|
59
|
+
const { container } = renderWithCtx(<VariableTag value="{{ ctx.User.Email }}" onClear={onClear} />);
|
|
60
|
+
|
|
61
|
+
await waitFor(
|
|
62
|
+
() => {
|
|
63
|
+
expect(screen.getByText('User/Email')).toBeInTheDocument();
|
|
64
|
+
},
|
|
65
|
+
{ timeout: 3000 },
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const selectElement = container.querySelector('.ant-select.variable');
|
|
69
|
+
expect(selectElement).toBeInTheDocument();
|
|
70
|
+
expect(selectElement).toHaveClass('ant-select-show-search');
|
|
71
|
+
expect(container.querySelector('.ant-select-selection-search-input')).not.toHaveAttribute('readonly');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('can disable custom tag input while keeping clear enabled', async () => {
|
|
75
|
+
const onClear = vi.fn();
|
|
76
|
+
const { container } = renderWithCtx(
|
|
77
|
+
<VariableTag value="{{ ctx.User.Email }}" onClear={onClear} allowCustomTagInput={false} />,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await waitFor(
|
|
81
|
+
() => {
|
|
82
|
+
expect(screen.getByText('User/Email')).toBeInTheDocument();
|
|
83
|
+
},
|
|
84
|
+
{ timeout: 3000 },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const selectElement = container.querySelector('.ant-select.variable');
|
|
88
|
+
expect(selectElement).toBeInTheDocument();
|
|
89
|
+
expect(selectElement).not.toHaveClass('ant-select-disabled');
|
|
90
|
+
expect(selectElement).not.toHaveClass('ant-select-show-search');
|
|
91
|
+
expect(container.querySelector('.ant-select-clear')).toBeInTheDocument();
|
|
92
|
+
expect(container.querySelector('.ant-select-selection-search-input')).toHaveAttribute('readonly');
|
|
93
|
+
});
|
|
94
|
+
|
|
57
95
|
it('should not show close button when onClear is not provided', async () => {
|
|
58
96
|
const { container } = renderWithCtx(<VariableTag value="{{ ctx.User.Name }}" />);
|
|
59
97
|
|
|
@@ -166,6 +204,32 @@ describe('VariableTag', () => {
|
|
|
166
204
|
}
|
|
167
205
|
});
|
|
168
206
|
|
|
207
|
+
it('renders a red failure pill when the variable path cannot be resolved', async () => {
|
|
208
|
+
renderWithCtx(
|
|
209
|
+
<VariableTag
|
|
210
|
+
value="{{ ctx.missing.field }}"
|
|
211
|
+
metaTree={[
|
|
212
|
+
{
|
|
213
|
+
name: 'user',
|
|
214
|
+
title: 'User',
|
|
215
|
+
type: 'object',
|
|
216
|
+
paths: ['user'],
|
|
217
|
+
children: [{ name: 'name', title: 'Name', type: 'string', paths: ['user', 'name'] }],
|
|
218
|
+
},
|
|
219
|
+
]}
|
|
220
|
+
/>,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await waitFor(
|
|
224
|
+
() => {
|
|
225
|
+
const tag = screen.getByText('Variable parsing failed');
|
|
226
|
+
expect(tag).toBeInTheDocument();
|
|
227
|
+
expect(tag.closest('.ant-tag')).toHaveClass('ant-tag-error');
|
|
228
|
+
},
|
|
229
|
+
{ timeout: 3000 },
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
169
233
|
it('should render Select component with proper structure', async () => {
|
|
170
234
|
const { container } = render(<VariableTag value="{{ ctx.Test }}" />);
|
|
171
235
|
|
|
@@ -233,6 +297,22 @@ describe('VariableTag', () => {
|
|
|
233
297
|
expect(selectElement).not.toHaveClass('ant-select-disabled');
|
|
234
298
|
});
|
|
235
299
|
|
|
300
|
+
it('does not show clear affordance when disabled even if onClear is provided', async () => {
|
|
301
|
+
const onClear = vi.fn();
|
|
302
|
+
const { container } = renderWithCtx(<VariableTag value="{{ ctx.Test }}" onClear={onClear} disabled />);
|
|
303
|
+
|
|
304
|
+
await waitFor(
|
|
305
|
+
() => {
|
|
306
|
+
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
307
|
+
},
|
|
308
|
+
{ timeout: 3000 },
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const selectElement = container.querySelector('.ant-select.variable');
|
|
312
|
+
expect(selectElement).toHaveClass('ant-select-disabled');
|
|
313
|
+
expect(container.querySelector('.ant-select-clear')).not.toBeInTheDocument();
|
|
314
|
+
});
|
|
315
|
+
|
|
236
316
|
it('should have proper accessibility attributes for Select component', async () => {
|
|
237
317
|
const { container } = renderWithCtx(<VariableTag value="{{ ctx.Test }}" />);
|
|
238
318
|
|
|
@@ -16,12 +16,30 @@ export interface FlowContextSelectorProps
|
|
|
16
16
|
value?: string;
|
|
17
17
|
onChange?: (value: string, metaTreeNode?: MetaTreeNode) => void;
|
|
18
18
|
children?: CascaderProps<ContextSelectorItem>['children'];
|
|
19
|
+
/**
|
|
20
|
+
* Controls whether the default `x` trigger button is rendered as active
|
|
21
|
+
* (`type="primary"`). When omitted, the selector falls back to its parsed
|
|
22
|
+
* `value` path (`true` iff a valid variable path is currently selected).
|
|
23
|
+
*
|
|
24
|
+
* Use this when callers intentionally feed synthetic paths such as
|
|
25
|
+
* `['constant']` / `['null']` into the cascader to keep menu state aligned,
|
|
26
|
+
* but only want real variable references to show the blue active button.
|
|
27
|
+
*/
|
|
28
|
+
active?: boolean;
|
|
19
29
|
metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
20
30
|
parseValueToPath?: (value: string) => string[] | undefined;
|
|
21
31
|
formatPathToValue?: (item: MetaTreeNode) => string;
|
|
22
32
|
open?: boolean;
|
|
23
33
|
onlyLeafSelectable?: boolean;
|
|
24
34
|
ignoreFieldNames?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Footer rendered at the bottom of the dropdown. Defaults to a muted
|
|
37
|
+
* "Double click to choose entire object" hint when non-leaf selection is
|
|
38
|
+
* allowed (`onlyLeafSelectable` is false) — since double-clicking a non-leaf
|
|
39
|
+
* node selects the whole object. Pass an explicit node to override, or `null`
|
|
40
|
+
* to hide it.
|
|
41
|
+
*/
|
|
42
|
+
dropdownFooter?: React.ReactNode;
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
export interface ContextSelectorItem {
|
|
@@ -76,7 +94,10 @@ export interface VariableInputProps {
|
|
|
76
94
|
|
|
77
95
|
export interface VariableTagProps {
|
|
78
96
|
value?: string;
|
|
97
|
+
resolvedPath?: Array<string | number>;
|
|
79
98
|
onClear?: () => void;
|
|
99
|
+
disabled?: boolean;
|
|
100
|
+
allowCustomTagInput?: boolean;
|
|
80
101
|
className?: string;
|
|
81
102
|
style?: React.CSSProperties;
|
|
82
103
|
metaTreeNode?: MetaTreeNode | null;
|
package/src/flowI18n.ts
CHANGED
|
@@ -64,7 +64,9 @@ export class FlowI18n {
|
|
|
64
64
|
* @private
|
|
65
65
|
*/
|
|
66
66
|
private isTemplate(str: string): boolean {
|
|
67
|
-
|
|
67
|
+
// The closing quote is a backreference to the opening one (group 1) so an embedded quote of a different type — e.g.
|
|
68
|
+
// {{t('… "Post-action event" …')}} — does not terminate the key early.
|
|
69
|
+
return /\{\{\s*t\s*\(\s*(["'`])(?:\\.|(?!\1).)*?\1\s*(?:,\s*.*?)?\s*\)\s*\}\}/.test(str);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -72,9 +74,12 @@ export class FlowI18n {
|
|
|
72
74
|
* @private
|
|
73
75
|
*/
|
|
74
76
|
private compileTemplate(template: string): string {
|
|
77
|
+
// `(["'`])` captures the opening quote; the key allows escaped chars (`\\.`) and any char that is not that same
|
|
78
|
+
// quote (`(?!\1).`), and `\1` closes on the matching quote. This keeps embedded quotes of a different type inside
|
|
79
|
+
// the key instead of truncating it at the first quote of any kind.
|
|
75
80
|
return template.replace(
|
|
76
|
-
/\{\{\s*t\s*\(\s*["'`](
|
|
77
|
-
(match, key, optionsStr) => {
|
|
81
|
+
/\{\{\s*t\s*\(\s*(["'`])((?:\\.|(?!\1).)*?)\1\s*(?:,\s*((?:[^{}]|\{[^}]*\})*?))?\s*\)\s*\}\}/g,
|
|
82
|
+
(match, _quote, key, optionsStr) => {
|
|
78
83
|
try {
|
|
79
84
|
let templateOptions = {};
|
|
80
85
|
if (optionsStr) {
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
|
|
12
|
+
import { getFlowEngineTranslation } from '../index';
|
|
13
|
+
|
|
14
|
+
describe('flow engine locale', () => {
|
|
15
|
+
it('translates the default secondary confirmation text into Chinese', () => {
|
|
16
|
+
expect(getFlowEngineTranslation('Please Confirm', 'zh-CN')).toBe('请确认');
|
|
17
|
+
expect(getFlowEngineTranslation('Are you sure you want to perform the action?', 'zh-CN')).toBe(
|
|
18
|
+
'确定要执行此操作吗?',
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
});
|
package/src/locale/en-US.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"Add": "Add",
|
|
3
|
+
"Are you sure you want to perform the action?": "Are you sure you want to perform the action?",
|
|
3
4
|
"Are you sure you want to delete this item? This action cannot be undone.": "Are you sure you want to delete this item? This action cannot be undone.",
|
|
4
5
|
"Are you sure to convert this template block to copy mode?": "Are you sure you want to convert this template block to copy mode?",
|
|
5
6
|
"Array index out of bounds": "Array index {{index}} out of bounds for '{{subKey}}'",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"Other blocks": "Other blocks",
|
|
54
55
|
"Parent not found, cannot replace block": "Parent not found, cannot replace block",
|
|
55
56
|
"Previous step": "Previous step",
|
|
57
|
+
"Please Confirm": "Please Confirm",
|
|
56
58
|
"Replace current block with template?": "Replace current block with template?",
|
|
57
59
|
"Replaced with template block": "Replaced with template block",
|
|
58
60
|
"Render failed": "Render failed",
|
package/src/locale/zh-CN.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"Add": "添加",
|
|
3
|
+
"Are you sure you want to perform the action?": "确定要执行此操作吗?",
|
|
3
4
|
"Are you sure you want to delete this item? This action cannot be undone.": "确定要删除此项吗?此操作不可撤销。",
|
|
4
5
|
"Are you sure to convert this template block to copy mode?": "确定将该模板区块转换为复制模式吗?",
|
|
5
6
|
"Array index out of bounds": "数组索引 {{index}} 超出 '{{subKey}}' 的边界",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"OK": "确定",
|
|
60
61
|
"Other blocks": "其他区块",
|
|
61
62
|
"Previous step": "上一步",
|
|
63
|
+
"Please Confirm": "请确认",
|
|
62
64
|
"Render failed": "渲染失败",
|
|
63
65
|
"Response record": "响应结果记录",
|
|
64
66
|
"Step configuration": "步骤配置",
|