@nocobase/client-v2 2.1.8 → 2.1.9
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/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/index.mjs +102 -76
- package/lib/index.js +77 -51
- package/package.json +7 -7
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/linkageRules.tsx +75 -16
- package/src/flow/components/FieldAssignRulesEditor.tsx +62 -12
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +30 -1
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +443 -11
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/fields/InputFieldModel.tsx +14 -22
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.9",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.1.
|
|
31
|
-
"@nocobase/flow-engine": "2.1.
|
|
32
|
-
"@nocobase/sdk": "2.1.
|
|
33
|
-
"@nocobase/shared": "2.1.
|
|
34
|
-
"@nocobase/utils": "2.1.
|
|
30
|
+
"@nocobase/evaluators": "2.1.9",
|
|
31
|
+
"@nocobase/flow-engine": "2.1.9",
|
|
32
|
+
"@nocobase/sdk": "2.1.9",
|
|
33
|
+
"@nocobase/shared": "2.1.9",
|
|
34
|
+
"@nocobase/utils": "2.1.9",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"react-i18next": "^11.15.1",
|
|
47
47
|
"react-router-dom": "^6.30.1"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "2b612d81c6f6115af75ea7b279384751af869510"
|
|
50
50
|
}
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
import { css, cx } from '@emotion/css';
|
|
11
11
|
import { Input, Typography } from 'antd';
|
|
12
12
|
import type { TextAreaProps } from 'antd/es/input';
|
|
13
|
+
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
|
13
14
|
import JSON5 from 'json5';
|
|
14
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
15
|
+
import React, { useCallback, useEffect, useMemo, useState, type ChangeEvent, type FocusEvent } from 'react';
|
|
15
16
|
|
|
16
17
|
export interface JsonTextAreaProps extends Omit<TextAreaProps, 'value' | 'onChange'> {
|
|
17
18
|
value?: unknown;
|
|
@@ -36,14 +37,14 @@ function stringifyJsonValue(value: unknown, json: typeof JSON | typeof JSON5, sp
|
|
|
36
37
|
json.parse(value);
|
|
37
38
|
return value;
|
|
38
39
|
} catch {
|
|
39
|
-
return json.stringify(value, undefined, space);
|
|
40
|
+
return json.stringify(value, undefined, space) ?? '';
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
return json.stringify(value, undefined, space);
|
|
44
|
+
return json.stringify(value, undefined, space) ?? '';
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
const JsonTextAreaComponent = React.forwardRef<TextAreaRef, JsonTextAreaProps>((props, ref) => {
|
|
47
48
|
const {
|
|
48
49
|
value,
|
|
49
50
|
onChange,
|
|
@@ -74,11 +75,8 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
74
75
|
[json],
|
|
75
76
|
);
|
|
76
77
|
|
|
77
|
-
const
|
|
78
|
-
(
|
|
79
|
-
const nextText = event.target.value;
|
|
80
|
-
setText(nextText);
|
|
81
|
-
|
|
78
|
+
const validateText = useCallback(
|
|
79
|
+
(nextText: string) => {
|
|
82
80
|
try {
|
|
83
81
|
parseText(nextText);
|
|
84
82
|
setError(undefined);
|
|
@@ -89,12 +87,21 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
89
87
|
[parseText],
|
|
90
88
|
);
|
|
91
89
|
|
|
90
|
+
const handleChange = useCallback(
|
|
91
|
+
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
92
|
+
const nextText = event.target.value;
|
|
93
|
+
setText(nextText);
|
|
94
|
+
validateText(nextText);
|
|
95
|
+
},
|
|
96
|
+
[validateText],
|
|
97
|
+
);
|
|
98
|
+
|
|
92
99
|
const handleBlur = useCallback(
|
|
93
|
-
(event:
|
|
100
|
+
(event: FocusEvent<HTMLTextAreaElement>) => {
|
|
94
101
|
try {
|
|
95
102
|
const parsed = parseText(event.target.value);
|
|
96
103
|
setError(undefined);
|
|
97
|
-
setText(parsed == null ? '' : json.stringify(parsed, undefined, space));
|
|
104
|
+
setText(parsed == null ? '' : json.stringify(parsed, undefined, space) ?? '');
|
|
98
105
|
onChange?.(parsed);
|
|
99
106
|
} catch (err) {
|
|
100
107
|
setError(err instanceof Error ? err.message : String(err));
|
|
@@ -111,6 +118,7 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
111
118
|
<>
|
|
112
119
|
<Input.TextArea
|
|
113
120
|
{...textAreaProps}
|
|
121
|
+
ref={ref}
|
|
114
122
|
value={text}
|
|
115
123
|
onChange={handleChange}
|
|
116
124
|
onBlur={handleBlur}
|
|
@@ -126,4 +134,6 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
126
134
|
);
|
|
127
135
|
});
|
|
128
136
|
|
|
137
|
+
export const JsonTextArea = React.memo(JsonTextAreaComponent);
|
|
138
|
+
|
|
129
139
|
JsonTextArea.displayName = 'JsonTextArea';
|
|
@@ -0,0 +1,175 @@
|
|
|
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 {
|
|
12
|
+
FlowContextSelector,
|
|
13
|
+
formatPathToValue as formatFlowPathToValue,
|
|
14
|
+
useFlowContext,
|
|
15
|
+
type MetaTreeNode,
|
|
16
|
+
} from '@nocobase/flow-engine';
|
|
17
|
+
import { Button, theme } from 'antd';
|
|
18
|
+
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
|
19
|
+
import React, { useCallback, useMemo, useRef } from 'react';
|
|
20
|
+
import { JsonTextArea, type JsonTextAreaProps } from './JsonTextArea';
|
|
21
|
+
import { useFilteredMetaTree } from './VariableInput';
|
|
22
|
+
|
|
23
|
+
export interface VariableJsonTextAreaProps extends JsonTextAreaProps {
|
|
24
|
+
namespaces?: string[];
|
|
25
|
+
extraNodes?: MetaTreeNode[];
|
|
26
|
+
metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
27
|
+
formatPathToValue?: (meta: MetaTreeNode) => string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const VariableJsonTextArea = React.memo((props: VariableJsonTextAreaProps) => {
|
|
31
|
+
const {
|
|
32
|
+
value,
|
|
33
|
+
onChange,
|
|
34
|
+
space = 2,
|
|
35
|
+
json5 = false,
|
|
36
|
+
showError = true,
|
|
37
|
+
className,
|
|
38
|
+
status,
|
|
39
|
+
onBlur,
|
|
40
|
+
namespaces,
|
|
41
|
+
extraNodes,
|
|
42
|
+
metaTree,
|
|
43
|
+
formatPathToValue: customFormatPathToValue,
|
|
44
|
+
...textAreaProps
|
|
45
|
+
} = props;
|
|
46
|
+
const { token } = theme.useToken();
|
|
47
|
+
const flowCtx = useFlowContext();
|
|
48
|
+
const filteredMetaTree = useFilteredMetaTree({ namespaces, extraNodes });
|
|
49
|
+
const ref = useRef<TextAreaRef>(null);
|
|
50
|
+
|
|
51
|
+
const insertAtCaret = useCallback((valueToInsert: string) => {
|
|
52
|
+
const textArea = ref.current?.resizableTextArea?.textArea;
|
|
53
|
+
if (!textArea) return;
|
|
54
|
+
|
|
55
|
+
const start = textArea.selectionStart ?? textArea.value.length;
|
|
56
|
+
const end = textArea?.selectionEnd ?? start;
|
|
57
|
+
const nextText = textArea.value.slice(0, start) + valueToInsert + textArea.value.slice(end);
|
|
58
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
59
|
+
|
|
60
|
+
nativeInputValueSetter?.call(textArea, nextText);
|
|
61
|
+
textArea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
62
|
+
|
|
63
|
+
requestAnimationFrame(() => {
|
|
64
|
+
const nextPosition = start + valueToInsert.length;
|
|
65
|
+
textArea.setSelectionRange(start, nextPosition);
|
|
66
|
+
textArea.focus();
|
|
67
|
+
});
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const handleVariableSelected = useCallback(
|
|
71
|
+
(nextValue: string) => {
|
|
72
|
+
if (nextValue) {
|
|
73
|
+
insertAtCaret(nextValue);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[insertAtCaret],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const selectorMetaTree = useMemo(
|
|
80
|
+
() => metaTree ?? filteredMetaTree ?? (() => flowCtx.getPropertyMetaTree?.() ?? []),
|
|
81
|
+
[filteredMetaTree, flowCtx, metaTree],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const formatPathToValue = useMemo(() => {
|
|
85
|
+
if (!customFormatPathToValue) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
return (meta: MetaTreeNode) => customFormatPathToValue(meta) ?? formatFlowPathToValue(meta);
|
|
89
|
+
}, [customFormatPathToValue]);
|
|
90
|
+
|
|
91
|
+
const wrapperClassName = useMemo(
|
|
92
|
+
() => css`
|
|
93
|
+
position: relative;
|
|
94
|
+
width: 100%;
|
|
95
|
+
|
|
96
|
+
.ant-input {
|
|
97
|
+
width: 100%;
|
|
98
|
+
}
|
|
99
|
+
`,
|
|
100
|
+
[],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const textAreaClassName = useMemo(
|
|
104
|
+
() => css`
|
|
105
|
+
font-size: ${token.fontSizeSM}px;
|
|
106
|
+
font-family: ${token.fontFamilyCode};
|
|
107
|
+
`,
|
|
108
|
+
[token.fontFamilyCode, token.fontSizeSM],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const selectorClassName = useMemo(
|
|
112
|
+
() => css`
|
|
113
|
+
position: absolute;
|
|
114
|
+
inset-block-start: 0;
|
|
115
|
+
inset-inline-end: 0;
|
|
116
|
+
z-index: 1;
|
|
117
|
+
line-height: 0;
|
|
118
|
+
|
|
119
|
+
.ant-btn {
|
|
120
|
+
font-size: ${token.fontSizeSM}px;
|
|
121
|
+
}
|
|
122
|
+
`,
|
|
123
|
+
[token.fontSizeSM],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const buttonClassName = useMemo(
|
|
127
|
+
() => css`
|
|
128
|
+
&:not(:hover) {
|
|
129
|
+
border-inline-end-color: transparent;
|
|
130
|
+
border-block-start-color: transparent;
|
|
131
|
+
background-color: transparent;
|
|
132
|
+
}
|
|
133
|
+
`,
|
|
134
|
+
[],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className={wrapperClassName}>
|
|
139
|
+
<JsonTextArea
|
|
140
|
+
{...textAreaProps}
|
|
141
|
+
ref={ref}
|
|
142
|
+
value={value}
|
|
143
|
+
onChange={onChange}
|
|
144
|
+
space={space}
|
|
145
|
+
json5={json5}
|
|
146
|
+
showError={showError}
|
|
147
|
+
className={cx(textAreaClassName, className)}
|
|
148
|
+
status={status}
|
|
149
|
+
onBlur={onBlur}
|
|
150
|
+
disabled={textAreaProps.disabled}
|
|
151
|
+
/>
|
|
152
|
+
<div className={selectorClassName}>
|
|
153
|
+
<FlowContextSelector
|
|
154
|
+
metaTree={selectorMetaTree}
|
|
155
|
+
disabled={textAreaProps.disabled}
|
|
156
|
+
formatPathToValue={formatPathToValue}
|
|
157
|
+
onChange={handleVariableSelected}
|
|
158
|
+
>
|
|
159
|
+
<Button
|
|
160
|
+
aria-label="variable-json-switcher"
|
|
161
|
+
disabled={textAreaProps.disabled}
|
|
162
|
+
className={buttonClassName}
|
|
163
|
+
style={{ fontStyle: 'italic', fontFamily: token.fontFamilyCode }}
|
|
164
|
+
>
|
|
165
|
+
x
|
|
166
|
+
</Button>
|
|
167
|
+
</FlowContextSelector>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
VariableJsonTextArea.displayName = 'VariableJsonTextArea';
|
|
174
|
+
|
|
175
|
+
export const VariableJSON = VariableJsonTextArea;
|
|
@@ -0,0 +1,43 @@
|
|
|
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 { fireEvent, render, screen } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { JsonTextArea } from '../JsonTextArea';
|
|
14
|
+
|
|
15
|
+
describe('JsonTextArea', () => {
|
|
16
|
+
it('formats object values and emits parsed JSON on blur', async () => {
|
|
17
|
+
const handleChange = vi.fn();
|
|
18
|
+
|
|
19
|
+
render(<JsonTextArea value={{ foo: 'bar' }} onChange={handleChange} />);
|
|
20
|
+
|
|
21
|
+
const textArea = await screen.findByRole('textbox');
|
|
22
|
+
expect(textArea).toHaveValue('{\n "foo": "bar"\n}');
|
|
23
|
+
|
|
24
|
+
fireEvent.change(textArea, { target: { value: '{"foo":"baz"}' } });
|
|
25
|
+
fireEvent.blur(textArea);
|
|
26
|
+
|
|
27
|
+
expect(handleChange).toHaveBeenCalledWith({ foo: 'baz' });
|
|
28
|
+
expect(textArea).toHaveValue('{\n "foo": "baz"\n}');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('shows JSON parse errors and does not emit invalid values', async () => {
|
|
32
|
+
const handleChange = vi.fn();
|
|
33
|
+
|
|
34
|
+
render(<JsonTextArea value={{}} onChange={handleChange} />);
|
|
35
|
+
|
|
36
|
+
const textArea = await screen.findByRole('textbox');
|
|
37
|
+
fireEvent.change(textArea, { target: { value: '{' } });
|
|
38
|
+
fireEvent.blur(textArea);
|
|
39
|
+
|
|
40
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
41
|
+
expect(await screen.findByText(/JSON/)).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
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 { FlowContext, FlowContextProvider } from '@nocobase/flow-engine';
|
|
11
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { VariableJsonTextArea } from '../VariableJsonTextArea';
|
|
15
|
+
|
|
16
|
+
function createContextWithEnv() {
|
|
17
|
+
const ctx = new FlowContext();
|
|
18
|
+
(ctx as unknown as { t: (key: string) => string }).t = (key: string) => key;
|
|
19
|
+
|
|
20
|
+
ctx.defineProperty('$env', {
|
|
21
|
+
value: { API_KEY: 'secret' },
|
|
22
|
+
meta: {
|
|
23
|
+
title: 'Env',
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
API_KEY: { title: 'API Key', type: 'string' },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return ctx;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderWithCtx(ctx: FlowContext, node: React.ReactNode) {
|
|
35
|
+
return render(<FlowContextProvider context={ctx}>{node}</FlowContextProvider>);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('VariableJsonTextArea', () => {
|
|
39
|
+
it('formats object values and emits parsed JSON on blur', async () => {
|
|
40
|
+
const ctx = createContextWithEnv();
|
|
41
|
+
const handleChange = vi.fn();
|
|
42
|
+
|
|
43
|
+
renderWithCtx(ctx, <VariableJsonTextArea value={{ foo: 'bar' }} onChange={handleChange} />);
|
|
44
|
+
|
|
45
|
+
const textArea = await screen.findByRole('textbox');
|
|
46
|
+
expect(textArea).toHaveValue('{\n "foo": "bar"\n}');
|
|
47
|
+
|
|
48
|
+
fireEvent.change(textArea, { target: { value: '{"foo":"baz"}' } });
|
|
49
|
+
fireEvent.blur(textArea);
|
|
50
|
+
|
|
51
|
+
expect(handleChange).toHaveBeenCalledWith({ foo: 'baz' });
|
|
52
|
+
expect(textArea).toHaveValue('{\n "foo": "baz"\n}');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('shows JSON parse errors and does not emit invalid values', async () => {
|
|
56
|
+
const ctx = createContextWithEnv();
|
|
57
|
+
const handleChange = vi.fn();
|
|
58
|
+
|
|
59
|
+
renderWithCtx(ctx, <VariableJsonTextArea value={{}} onChange={handleChange} />);
|
|
60
|
+
|
|
61
|
+
const textArea = await screen.findByRole('textbox');
|
|
62
|
+
fireEvent.change(textArea, { target: { value: '{' } });
|
|
63
|
+
fireEvent.blur(textArea);
|
|
64
|
+
|
|
65
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
66
|
+
expect(await screen.findByText(/JSON/)).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('inserts selected variables with the client-v2 template format', async () => {
|
|
70
|
+
const ctx = createContextWithEnv();
|
|
71
|
+
|
|
72
|
+
renderWithCtx(ctx, <VariableJsonTextArea value={null} namespaces={['$env']} onChange={() => undefined} />);
|
|
73
|
+
|
|
74
|
+
fireEvent.click(await screen.findByRole('button', { name: 'variable-json-switcher' }));
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
fireEvent.click(screen.getByText('Env'));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await waitFor(() => {
|
|
81
|
+
fireEvent.click(screen.getByText('API Key'));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(await screen.findByRole('textbox')).toHaveValue('{{ ctx.$env.API_KEY }}');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
11
|
import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
12
|
-
import { fieldLinkageRules } from '../linkageRules';
|
|
12
|
+
import { fieldLinkageRules, linkageAssignField } from '../linkageRules';
|
|
13
13
|
|
|
14
14
|
describe('fieldLinkageRules action - linkage scope metadata', () => {
|
|
15
15
|
const targetCollection: any = {
|
|
@@ -868,6 +868,155 @@ describe('fieldLinkageRules action - linkage scope metadata', () => {
|
|
|
868
868
|
expect(setFormValues).toHaveBeenCalledTimes(2);
|
|
869
869
|
});
|
|
870
870
|
|
|
871
|
+
it('keeps row-scoped override patches editable after the target is user edited', async () => {
|
|
872
|
+
const store = {
|
|
873
|
+
roles: [{ uid: 'role-uid-1', name: 'existing' }],
|
|
874
|
+
};
|
|
875
|
+
const getAt = (path: Array<string | number>) => path.reduce((acc: any, seg) => acc?.[seg], store as any);
|
|
876
|
+
const setAt = (path: Array<string | number>, value: any) => {
|
|
877
|
+
let cur: any = store;
|
|
878
|
+
for (const seg of path.slice(0, -1)) {
|
|
879
|
+
cur = cur[seg];
|
|
880
|
+
}
|
|
881
|
+
cur[path[path.length - 1]] = value;
|
|
882
|
+
};
|
|
883
|
+
const editedKeys = new Set<string>();
|
|
884
|
+
const toKey = (path: Array<string | number>) => JSON.stringify(path);
|
|
885
|
+
const form = {
|
|
886
|
+
getFieldValue: (path: Array<string | number>) => getAt(path),
|
|
887
|
+
};
|
|
888
|
+
const formValueRuntime = {
|
|
889
|
+
canApplyOverrideValuePatch: vi.fn((path: Array<string | number>) => !editedKeys.has(toKey(path))),
|
|
890
|
+
};
|
|
891
|
+
const setFormValues = vi.fn(async (patches: Array<{ path: Array<string | number>; value: any }>) => {
|
|
892
|
+
for (const patch of patches) {
|
|
893
|
+
setAt(patch.path, patch.value);
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
const engine = new FlowEngine();
|
|
898
|
+
const rowFork = new FlowModel({ uid: 'role-override-row-fork', flowEngine: engine }) as any;
|
|
899
|
+
rowFork.context.defineProperty('fieldIndex', {
|
|
900
|
+
value: ['roles:0'],
|
|
901
|
+
});
|
|
902
|
+
rowFork.context.defineProperty('form', {
|
|
903
|
+
value: form,
|
|
904
|
+
});
|
|
905
|
+
rowFork.context.defineProperty('setFormValues', {
|
|
906
|
+
value: setFormValues,
|
|
907
|
+
});
|
|
908
|
+
rowFork.context.defineProperty('app', {
|
|
909
|
+
value: {
|
|
910
|
+
jsonLogic: {
|
|
911
|
+
apply: () => true,
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
});
|
|
915
|
+
rowFork.subModels = { items: [] };
|
|
916
|
+
rowFork.getAction = vi.fn((name: string) => {
|
|
917
|
+
if (name === 'linkageAssignField') {
|
|
918
|
+
return linkageAssignField;
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
const masterModel: any = {
|
|
923
|
+
uid: 'master-role-override-grid',
|
|
924
|
+
forks: new Set([rowFork]),
|
|
925
|
+
};
|
|
926
|
+
const rolesField: any = {
|
|
927
|
+
type: 'hasMany',
|
|
928
|
+
isAssociationField: () => true,
|
|
929
|
+
targetCollection: {
|
|
930
|
+
getField: (name: string) => ({ name, isAssociationField: () => false }),
|
|
931
|
+
},
|
|
932
|
+
};
|
|
933
|
+
const blockModel: any = {
|
|
934
|
+
collection: {
|
|
935
|
+
getField: (name: string) => (name === 'roles' ? rolesField : null),
|
|
936
|
+
},
|
|
937
|
+
formValueRuntime,
|
|
938
|
+
};
|
|
939
|
+
rowFork.context.defineProperty('blockModel', {
|
|
940
|
+
value: blockModel,
|
|
941
|
+
});
|
|
942
|
+
const gridModel: any = {
|
|
943
|
+
uid: 'grid-model-role-override-row',
|
|
944
|
+
context: {
|
|
945
|
+
blockModel,
|
|
946
|
+
},
|
|
947
|
+
getAction: vi.fn((name: string) => {
|
|
948
|
+
if (name === 'linkageAssignField') {
|
|
949
|
+
return linkageAssignField;
|
|
950
|
+
}
|
|
951
|
+
}),
|
|
952
|
+
__allModels: [],
|
|
953
|
+
};
|
|
954
|
+
const ctx: any = {
|
|
955
|
+
model: gridModel,
|
|
956
|
+
engine: {
|
|
957
|
+
forEachModel: (cb: (m: any) => void) => {
|
|
958
|
+
cb(masterModel);
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
flowKey: 'eventSettings',
|
|
962
|
+
inputArgs: {
|
|
963
|
+
source: 'user',
|
|
964
|
+
txId: 'tx-role-override-row',
|
|
965
|
+
changedPaths: [['roles', 0, 'uid']],
|
|
966
|
+
},
|
|
967
|
+
app: {
|
|
968
|
+
jsonLogic: {
|
|
969
|
+
apply: () => true,
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
resolveJsonTemplate: async (v: any) => v,
|
|
973
|
+
};
|
|
974
|
+
const makeParams = (value: string) => ({
|
|
975
|
+
value: [
|
|
976
|
+
{
|
|
977
|
+
key: `rule-${value}`,
|
|
978
|
+
title: `rule-${value}`,
|
|
979
|
+
enable: true,
|
|
980
|
+
condition: { logic: '$and', items: [] },
|
|
981
|
+
actions: [
|
|
982
|
+
{
|
|
983
|
+
name: 'linkageAssignField',
|
|
984
|
+
params: {
|
|
985
|
+
value: [
|
|
986
|
+
{
|
|
987
|
+
key: `r-${value}`,
|
|
988
|
+
enable: true,
|
|
989
|
+
targetPath: 'roles.name',
|
|
990
|
+
mode: 'override',
|
|
991
|
+
value,
|
|
992
|
+
condition: { logic: '$and', items: [] },
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
},
|
|
999
|
+
],
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
await fieldLinkageRules.handler(ctx, makeParams('role-uid-1'));
|
|
1003
|
+
expect(store.roles[0].name).toBe('role-uid-1');
|
|
1004
|
+
|
|
1005
|
+
store.roles[0].uid = 'role-uid-2';
|
|
1006
|
+
await fieldLinkageRules.handler(ctx, makeParams('role-uid-2'));
|
|
1007
|
+
expect(store.roles[0].name).toBe('role-uid-2');
|
|
1008
|
+
expect(setFormValues).toHaveBeenCalledTimes(2);
|
|
1009
|
+
|
|
1010
|
+
store.roles[0].name = 'manual';
|
|
1011
|
+
store.roles[0].uid = 'role-uid-3';
|
|
1012
|
+
editedKeys.add(toKey(['roles', 0, 'name']));
|
|
1013
|
+
await fieldLinkageRules.handler(ctx, makeParams('role-uid-3'));
|
|
1014
|
+
|
|
1015
|
+
expect(store.roles[0].name).toBe('manual');
|
|
1016
|
+
expect(setFormValues).toHaveBeenCalledTimes(2);
|
|
1017
|
+
expect(formValueRuntime.canApplyOverrideValuePatch).toHaveBeenCalledWith(['roles', 0, 'name']);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
871
1020
|
it('skips stale subtable row forks after the row has been removed', async () => {
|
|
872
1021
|
const setFormValues = vi.fn(async () => undefined);
|
|
873
1022
|
const defaultHandler = vi.fn((actionCtx: any, { addFormValuePatch }: any) => {
|