@nocobase/client-v2 2.1.0-beta.38 → 2.1.0-beta.41
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/Application.d.ts +6 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +51 -15
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +82 -3
- package/es/collection-manager/field-configure.d.ts +80 -0
- package/es/collection-manager/field-validation.d.ts +43 -0
- package/es/collection-manager/filter-operators/index.d.ts +46 -0
- package/es/collection-manager/filter-operators/operators.d.ts +30 -0
- package/es/collection-manager/interfaces/checkbox.d.ts +1 -41
- package/es/collection-manager/interfaces/checkboxGroup.d.ts +12 -44
- package/es/collection-manager/interfaces/collection.d.ts +12 -51
- package/es/collection-manager/interfaces/color.d.ts +1 -16
- package/es/collection-manager/interfaces/createdAt.d.ts +1 -44
- package/es/collection-manager/interfaces/createdBy.d.ts +0 -4
- package/es/collection-manager/interfaces/dateOnly.d.ts +7 -44
- package/es/collection-manager/interfaces/datetime.d.ts +1 -44
- package/es/collection-manager/interfaces/datetimeNoTz.d.ts +1 -44
- package/es/collection-manager/interfaces/email.d.ts +1 -29
- package/es/collection-manager/interfaces/id.d.ts +1 -16
- package/es/collection-manager/interfaces/index.d.ts +2 -3
- package/es/collection-manager/interfaces/input.d.ts +1 -102
- package/es/collection-manager/interfaces/integer.d.ts +1 -95
- package/es/collection-manager/interfaces/json.d.ts +16 -7
- package/es/collection-manager/interfaces/m2m.d.ts +11 -19
- package/es/collection-manager/interfaces/m2o.d.ts +11 -19
- package/es/collection-manager/interfaces/markdown.d.ts +1 -63
- package/es/collection-manager/interfaces/multipleSelect.d.ts +12 -44
- package/es/collection-manager/interfaces/nanoid.d.ts +1 -34
- package/es/collection-manager/interfaces/number.d.ts +1 -87
- package/es/collection-manager/interfaces/o2m.d.ts +12 -24
- package/es/collection-manager/interfaces/obo.d.ts +207 -0
- package/es/collection-manager/interfaces/oho.d.ts +207 -0
- package/es/collection-manager/interfaces/password.d.ts +1 -56
- package/es/collection-manager/interfaces/percent.d.ts +1 -84
- package/es/collection-manager/interfaces/phone.d.ts +1 -25
- package/es/collection-manager/interfaces/properties/index.d.ts +0 -28
- package/es/collection-manager/interfaces/radioGroup.d.ts +1 -29
- package/es/collection-manager/interfaces/richText.d.ts +1 -63
- package/es/collection-manager/interfaces/select.d.ts +12 -44
- package/es/collection-manager/interfaces/snowflake-id.d.ts +1 -34
- package/es/collection-manager/interfaces/tableoid.d.ts +1 -10
- package/es/collection-manager/interfaces/textarea.d.ts +1 -51
- package/es/collection-manager/interfaces/time.d.ts +1 -16
- package/es/collection-manager/interfaces/types.d.ts +3 -12
- package/es/collection-manager/interfaces/unixTimestamp.d.ts +1 -44
- package/es/collection-manager/interfaces/updatedAt.d.ts +1 -44
- package/es/collection-manager/interfaces/updatedBy.d.ts +0 -4
- package/es/collection-manager/interfaces/url.d.ts +1 -20
- package/es/collection-manager/interfaces/uuid.d.ts +1 -34
- package/es/collection-manager/template-fields.d.ts +53 -0
- package/es/components/form/VariableInput.d.ts +53 -2
- package/es/components/form/filter/CollectionFilter.d.ts +9 -1
- package/es/components/form/filter/CollectionFilterItem.d.ts +10 -2
- package/es/flow/components/filter/useFilterOptions.d.ts +13 -1
- package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +84 -0
- package/es/flow/models/blocks/assign-form/index.d.ts +1 -0
- package/es/flow/models/blocks/form/FormActionModel.d.ts +9 -2
- package/es/index.d.ts +4 -0
- package/es/index.mjs +105 -142
- package/lib/index.js +106 -143
- package/package.json +7 -7
- package/src/Application.tsx +24 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +71 -77
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +201 -4
- package/src/collection-manager/field-configure.ts +548 -0
- package/src/collection-manager/field-validation.ts +195 -0
- package/src/collection-manager/filter-operators/index.ts +176 -0
- package/src/collection-manager/{interfaces/properties → filter-operators}/operators.ts +24 -13
- package/src/collection-manager/interfaces/checkbox.ts +2 -9
- package/src/collection-manager/interfaces/checkboxGroup.ts +2 -10
- package/src/collection-manager/interfaces/collection.ts +2 -15
- package/src/collection-manager/interfaces/color.ts +2 -2
- package/src/collection-manager/interfaces/createdAt.ts +2 -2
- package/src/collection-manager/interfaces/createdBy.ts +1 -12
- package/src/collection-manager/interfaces/dateOnly.ts +8 -2
- package/src/collection-manager/interfaces/datetime.ts +2 -2
- package/src/collection-manager/interfaces/datetimeNoTz.ts +2 -2
- package/src/collection-manager/interfaces/email.ts +2 -9
- package/src/collection-manager/interfaces/id.ts +1 -2
- package/src/collection-manager/interfaces/index.ts +2 -3
- package/src/collection-manager/interfaces/input.ts +2 -133
- package/src/collection-manager/interfaces/integer.ts +2 -71
- package/src/collection-manager/interfaces/json.tsx +17 -11
- package/src/collection-manager/interfaces/m2m.tsx +0 -21
- package/src/collection-manager/interfaces/m2o.tsx +0 -22
- package/src/collection-manager/interfaces/markdown.ts +2 -51
- package/src/collection-manager/interfaces/multipleSelect.ts +2 -14
- package/src/collection-manager/interfaces/nanoid.ts +2 -2
- package/src/collection-manager/interfaces/number.ts +2 -85
- package/src/collection-manager/interfaces/o2m.tsx +1 -22
- package/src/collection-manager/interfaces/obo.tsx +145 -0
- package/src/collection-manager/interfaces/oho.tsx +145 -0
- package/src/collection-manager/interfaces/password.ts +2 -44
- package/src/collection-manager/interfaces/percent.ts +2 -74
- package/src/collection-manager/interfaces/phone.ts +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +0 -133
- package/src/collection-manager/interfaces/radioGroup.ts +2 -2
- package/src/collection-manager/interfaces/richText.ts +2 -51
- package/src/collection-manager/interfaces/select.ts +2 -14
- package/src/collection-manager/interfaces/snowflake-id.ts +2 -2
- package/src/collection-manager/interfaces/tableoid.ts +1 -2
- package/src/collection-manager/interfaces/textarea.ts +2 -51
- package/src/collection-manager/interfaces/time.ts +2 -2
- package/src/collection-manager/interfaces/types.ts +4 -12
- package/src/collection-manager/interfaces/unixTimestamp.tsx +2 -2
- package/src/collection-manager/interfaces/updatedAt.ts +2 -2
- package/src/collection-manager/interfaces/updatedBy.ts +1 -12
- package/src/collection-manager/interfaces/url.ts +2 -4
- package/src/collection-manager/interfaces/uuid.ts +2 -2
- package/src/collection-manager/template-fields.ts +109 -0
- package/src/components/README.md +1 -0
- package/src/components/README.zh-CN.md +1 -0
- package/src/components/form/VariableInput.tsx +101 -28
- package/src/components/form/__tests__/VariableInput.test.ts +85 -0
- package/src/components/form/filter/CollectionFilter.tsx +11 -1
- package/src/components/form/filter/CollectionFilterItem.tsx +12 -4
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +42 -0
- package/src/components/form/filter/useFilterActionProps.ts +7 -4
- package/src/flow/components/filter/useFilterOptions.ts +27 -5
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +14 -95
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +4 -7
- package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +26 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +63 -2
- package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +206 -0
- package/src/flow/models/blocks/assign-form/index.ts +1 -0
- package/src/flow/models/blocks/form/FormActionModel.tsx +30 -3
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +71 -0
- package/src/flow/models/blocks/form/submitHandler.ts +8 -1
- package/src/flow/models/fields/CollectionSelectorFieldModel.tsx +8 -2
- package/src/flow/models/fields/DisplayEnumFieldModel.tsx +8 -2
- package/src/index.ts +4 -0
- package/es/collection-manager/interfaces/linkTo.d.ts +0 -90
- package/es/collection-manager/interfaces/o2o.d.ts +0 -621
- package/es/collection-manager/interfaces/properties/operators.d.ts +0 -294
- package/es/collection-manager/interfaces/subTable.d.ts +0 -172
- package/src/collection-manager/interfaces/linkTo.ts +0 -120
- package/src/collection-manager/interfaces/o2o.tsx +0 -561
- package/src/collection-manager/interfaces/subTable.ts +0 -218
package/src/components/README.md
CHANGED
|
@@ -164,6 +164,7 @@ Key props:
|
|
|
164
164
|
- `namespaces`: restrict the picker to specific top-level namespaces. Omit to expose every registered top-level property
|
|
165
165
|
- `extraNodes`: static leaves appended after the namespace-filtered nodes. Use for variables that only make sense in the current page (e.g. `$resetLink`)
|
|
166
166
|
- `converters`: override the default path ↔ string converters. `EnvVariableInput` uses this hook to lock its output to `$env`
|
|
167
|
+
- `delimiters`: token pair wrapping the stored variable reference. Defaults to `['{{', '}}']` (Handlebars HTML-escaped). Pass `['{{{', '}}}']` for fields rendered as HTML where escaping would corrupt the variable value — e.g. the in-app message body
|
|
167
168
|
- `value` / `onChange` / `placeholder` / `disabled`: standard controlled-input props
|
|
168
169
|
|
|
169
170
|
Under the hood `VariableInput` wraps `VariableHybridInput` (inline pills), `VariableTextArea` wraps `TextAreaWithContextSelector` (textarea + variable button). Both share the same MetaTree.
|
|
@@ -164,6 +164,7 @@ import { VariableInput, VariableTextArea } from '@nocobase/client-v2';
|
|
|
164
164
|
- `namespaces`:限定可选的顶层命名空间。不传就用 `flowEngine.context` 里全部已注册的
|
|
165
165
|
- `extraNodes`:在命名空间过滤后追加几条静态变量(用于 `$resetLink` 这类只在当前页面有意义的局部变量)
|
|
166
166
|
- `converters`:覆盖默认的 path ↔ string 转换器。`EnvVariableInput` 就是用这个钩子把输出锁定到 `$env`
|
|
167
|
+
- `delimiters`:变量在存储字符串里使用的开闭分隔符,默认 `['{{', '}}']`(对应 Handlebars 的 HTML 转义形式)。若字段最终以 HTML 渲染、转义会破坏变量内容(如站内信正文),传 `['{{{', '}}}']` 走 Handlebars 的原样输出形式
|
|
167
168
|
- `value` / `onChange` / `placeholder` / `disabled`:标准受控字段属性
|
|
168
169
|
|
|
169
170
|
底层共用 `VariableHybridInput`(`VariableInput`)和 `TextAreaWithContextSelector`(`VariableTextArea`),用同一套 MetaTree 数据。
|
|
@@ -25,28 +25,75 @@ import { TextAreaWithContextSelector } from '../../flow/components/TextAreaWithC
|
|
|
25
25
|
* stored values round-trip to a labelled pill instead of falling back to a
|
|
26
26
|
* raw `{{…}}` literal.
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Variable delimiters: opening + closing tokens. Default `['{{', '}}']`
|
|
31
|
+
* matches NocoBase server template convention (Handlebars HTML-escaped
|
|
32
|
+
* output). Pass `['{{{', '}}}']` to switch to Handlebars' raw/unescaped
|
|
33
|
+
* form — required for fields whose content is rendered as HTML (e.g.
|
|
34
|
+
* in-app message body) so the variable expansion bypasses HTML escaping.
|
|
35
|
+
*
|
|
36
|
+
* Restrict to literal-token pairs, since the regex builder escapes them
|
|
37
|
+
* verbatim.
|
|
38
|
+
*/
|
|
39
|
+
export type VariableDelimiters = readonly [string, string];
|
|
40
|
+
|
|
41
|
+
const DEFAULT_DELIMITERS: VariableDelimiters = ['{{', '}}'];
|
|
42
|
+
|
|
43
|
+
function escapeForRegExp(s: string): string {
|
|
44
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Factory: returns a `parseValueToPath` bound to the given delimiters.
|
|
49
|
+
* Anchored (`^…$`) — only treats the whole input as a single variable
|
|
50
|
+
* reference, matching the v1 single-line picker behaviour.
|
|
51
|
+
*/
|
|
52
|
+
export function makeParseVariablePath(delimiters: VariableDelimiters = DEFAULT_DELIMITERS) {
|
|
53
|
+
const [open, close] = delimiters;
|
|
54
|
+
const re = new RegExp(`^${escapeForRegExp(open)}\\s*(.+?)\\s*${escapeForRegExp(close)}$`);
|
|
55
|
+
return (value?: string): string[] | undefined => {
|
|
56
|
+
if (typeof value !== 'string') return undefined;
|
|
57
|
+
const match = value.trim().match(re);
|
|
58
|
+
if (!match) return undefined;
|
|
59
|
+
let pathString = match[1];
|
|
60
|
+
// Backwards-compat: accept the legacy `ctx.` prefix so values produced by
|
|
61
|
+
// pre-fix versions of the picker still resolve to a labelled pill.
|
|
62
|
+
if (pathString === 'ctx') return [];
|
|
63
|
+
if (pathString.startsWith('ctx.')) pathString = pathString.slice(4);
|
|
64
|
+
return pathString.split('.');
|
|
65
|
+
};
|
|
48
66
|
}
|
|
49
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Factory: returns a `formatPathToValue` bound to the given delimiters.
|
|
70
|
+
* No inner spaces — matches the v1 storage shape exactly so round-trips
|
|
71
|
+
* through the API stay byte-stable.
|
|
72
|
+
*/
|
|
73
|
+
export function makeFormatVariablePath(delimiters: VariableDelimiters = DEFAULT_DELIMITERS) {
|
|
74
|
+
const [open, close] = delimiters;
|
|
75
|
+
return (meta?: MetaTreeNode): string | undefined => {
|
|
76
|
+
const paths = meta?.paths || [];
|
|
77
|
+
if (paths.length === 0) return undefined;
|
|
78
|
+
return `${open}${paths.join('.')}${close}`;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Factory: returns a global regex matching every occurrence of the
|
|
84
|
+
* variable token within a longer string. Used by `VariableHybridInput`
|
|
85
|
+
* to render embedded variables as pills.
|
|
86
|
+
*/
|
|
87
|
+
export function makeVariableRegExp(delimiters: VariableDelimiters = DEFAULT_DELIMITERS): RegExp {
|
|
88
|
+
const [open, close] = delimiters;
|
|
89
|
+
return new RegExp(`${escapeForRegExp(open)}\\s*([^{}]+?)\\s*${escapeForRegExp(close)}`, 'g');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default exports — `{{ ... }}` for the most common case. Kept named so
|
|
93
|
+
// existing callers don't need to switch to the factory.
|
|
94
|
+
export const parseVariablePath = makeParseVariablePath();
|
|
95
|
+
export const formatVariablePath = makeFormatVariablePath();
|
|
96
|
+
|
|
50
97
|
const META_TREE_CACHE_PREFIX = '@nocobase/client-v2:VariableInput:metaTree';
|
|
51
98
|
|
|
52
99
|
/**
|
|
@@ -123,8 +170,22 @@ export interface VariableInputProps {
|
|
|
123
170
|
* Override the converters used by the underlying `VariableHybridInput`.
|
|
124
171
|
* Mostly useful when the caller wants to constrain `formatPathToValue` to a
|
|
125
172
|
* specific namespace (see `EnvVariableInput` for that pattern).
|
|
173
|
+
*
|
|
174
|
+
* Takes precedence over `delimiters` when both are set on the same field
|
|
175
|
+
* (an explicit converter wins over the delimiter-derived one).
|
|
126
176
|
*/
|
|
127
177
|
converters?: VariableHybridInputConverters;
|
|
178
|
+
/**
|
|
179
|
+
* Token pair wrapping variable references in the stored string. Defaults
|
|
180
|
+
* to `['{{', '}}']` — the standard NocoBase server-template form,
|
|
181
|
+
* HTML-escaped by Handlebars. Pass `['{{{', '}}}']` for fields rendered
|
|
182
|
+
* as HTML where escaping would corrupt the variable value (e.g. the
|
|
183
|
+
* in-app message body).
|
|
184
|
+
*
|
|
185
|
+
* Ignored when `converters` is also supplied — caller-provided converters
|
|
186
|
+
* win.
|
|
187
|
+
*/
|
|
188
|
+
delimiters?: VariableDelimiters;
|
|
128
189
|
className?: string;
|
|
129
190
|
style?: React.CSSProperties;
|
|
130
191
|
}
|
|
@@ -136,16 +197,24 @@ export interface VariableInputProps {
|
|
|
136
197
|
* line of mixed literal+variable content is appropriate.
|
|
137
198
|
*/
|
|
138
199
|
export function VariableInput(props: VariableInputProps) {
|
|
139
|
-
const { namespaces, extraNodes, converters, ...rest } = props;
|
|
200
|
+
const { namespaces, extraNodes, converters, delimiters, ...rest } = props;
|
|
140
201
|
const metaTree = useFilteredMetaTree({ namespaces, extraNodes });
|
|
141
|
-
const mergedConverters = useMemo<VariableHybridInputConverters>(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
202
|
+
const mergedConverters = useMemo<VariableHybridInputConverters>(() => {
|
|
203
|
+
// Default delimiters → reuse the pre-built singletons.
|
|
204
|
+
if (!delimiters) {
|
|
205
|
+
return {
|
|
206
|
+
formatPathToValue: formatVariablePath,
|
|
207
|
+
parseValueToPath: parseVariablePath,
|
|
208
|
+
...converters,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
formatPathToValue: makeFormatVariablePath(delimiters),
|
|
213
|
+
parseValueToPath: makeParseVariablePath(delimiters),
|
|
214
|
+
variableRegExp: makeVariableRegExp(delimiters),
|
|
145
215
|
...converters,
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
);
|
|
216
|
+
};
|
|
217
|
+
}, [converters, delimiters]);
|
|
149
218
|
return <VariableHybridInput {...rest} converters={mergedConverters} metaTree={metaTree} />;
|
|
150
219
|
}
|
|
151
220
|
|
|
@@ -161,9 +230,13 @@ export interface VariableTextAreaProps extends Omit<VariableInputProps, 'convert
|
|
|
161
230
|
* is desirable (the server expands them at render time).
|
|
162
231
|
*/
|
|
163
232
|
export function VariableTextArea(props: VariableTextAreaProps) {
|
|
164
|
-
const { namespaces, extraNodes, rows, maxRows, style, ...rest } = props;
|
|
233
|
+
const { namespaces, extraNodes, rows, maxRows, style, delimiters, ...rest } = props;
|
|
165
234
|
const metaTree = useFilteredMetaTree({ namespaces, extraNodes });
|
|
166
235
|
const metaTreeGetter = useMemo(() => () => metaTree, [metaTree]);
|
|
236
|
+
const formatPathToValue = useMemo(
|
|
237
|
+
() => (delimiters ? makeFormatVariablePath(delimiters) : formatVariablePath),
|
|
238
|
+
[delimiters],
|
|
239
|
+
);
|
|
167
240
|
return (
|
|
168
241
|
<TextAreaWithContextSelector
|
|
169
242
|
{...rest}
|
|
@@ -171,7 +244,7 @@ export function VariableTextArea(props: VariableTextAreaProps) {
|
|
|
171
244
|
maxRows={maxRows}
|
|
172
245
|
style={style}
|
|
173
246
|
metaTree={metaTreeGetter}
|
|
174
|
-
formatPathToValue={(meta) =>
|
|
247
|
+
formatPathToValue={(meta) => formatPathToValue(meta) ?? ''}
|
|
175
248
|
/>
|
|
176
249
|
);
|
|
177
250
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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 {
|
|
12
|
+
formatVariablePath,
|
|
13
|
+
makeFormatVariablePath,
|
|
14
|
+
makeParseVariablePath,
|
|
15
|
+
makeVariableRegExp,
|
|
16
|
+
parseVariablePath,
|
|
17
|
+
} from '../VariableInput';
|
|
18
|
+
|
|
19
|
+
describe('VariableInput delimiter helpers', () => {
|
|
20
|
+
describe('default delimiters {{ }}', () => {
|
|
21
|
+
it('formats meta nodes into `{{x.y}}` with no inner spaces', () => {
|
|
22
|
+
expect(formatVariablePath({ paths: ['$user', 'id'] } as any)).toBe('{{$user.id}}');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('parses `{{x.y}}` into a path array', () => {
|
|
26
|
+
expect(parseVariablePath('{{ $user.id }}')).toEqual(['$user', 'id']);
|
|
27
|
+
expect(parseVariablePath('{{$user.id}}')).toEqual(['$user', 'id']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('strips a legacy `ctx.` prefix for backwards-compat', () => {
|
|
31
|
+
expect(parseVariablePath('{{ctx.$user.id}}')).toEqual(['$user', 'id']);
|
|
32
|
+
expect(parseVariablePath('{{ctx}}')).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns undefined for plain text', () => {
|
|
36
|
+
expect(parseVariablePath('hello world')).toBeUndefined();
|
|
37
|
+
expect(parseVariablePath('')).toBeUndefined();
|
|
38
|
+
expect(parseVariablePath(undefined)).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('triple-brace delimiters {{{ }}}', () => {
|
|
43
|
+
const format = makeFormatVariablePath(['{{{', '}}}']);
|
|
44
|
+
const parse = makeParseVariablePath(['{{{', '}}}']);
|
|
45
|
+
|
|
46
|
+
it('formats meta nodes into `{{{x.y}}}`', () => {
|
|
47
|
+
expect(format({ paths: ['$user', 'id'] } as any)).toBe('{{{$user.id}}}');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('parses `{{{x.y}}}` into a path array', () => {
|
|
51
|
+
expect(parse('{{{$user.id}}}')).toEqual(['$user', 'id']);
|
|
52
|
+
expect(parse('{{{ $user.id }}}')).toEqual(['$user', 'id']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('does not match the double-brace form', () => {
|
|
56
|
+
expect(parse('{{$user.id}}')).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('makeVariableRegExp', () => {
|
|
61
|
+
it('matches every occurrence in a longer string under default delimiters', () => {
|
|
62
|
+
const re = makeVariableRegExp();
|
|
63
|
+
const matches = Array.from('hello {{ $user.id }} world {{ $env.X }}'.matchAll(re)).map((m) => m[1]);
|
|
64
|
+
expect(matches).toEqual(['$user.id', '$env.X']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('matches triple-brace occurrences when configured', () => {
|
|
68
|
+
const re = makeVariableRegExp(['{{{', '}}}']);
|
|
69
|
+
const matches = Array.from('hi {{{$user.id}}} :)'.matchAll(re)).map((m) => m[1]);
|
|
70
|
+
expect(matches).toEqual(['$user.id']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('does not match double-brace occurrences under triple-brace config', () => {
|
|
74
|
+
const re = makeVariableRegExp(['{{{', '}}}']);
|
|
75
|
+
const matches = Array.from('plain {{ x }} text'.matchAll(re));
|
|
76
|
+
expect(matches).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('handles empty paths consistently', () => {
|
|
81
|
+
expect(formatVariablePath({ paths: [] } as any)).toBeUndefined();
|
|
82
|
+
expect(formatVariablePath(undefined)).toBeUndefined();
|
|
83
|
+
expect(makeFormatVariablePath(['{{{', '}}}'])({ paths: [] } as any)).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -25,7 +25,15 @@ export interface CollectionFilterProps {
|
|
|
25
25
|
t?: (key: string, options?: Record<string, any>) => string;
|
|
26
26
|
/** Whitelist of root-level field names to expose. */
|
|
27
27
|
filterableFieldNames?: string[];
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Blacklist of root-level field names to drop. Mirrors v1's `nonfilterable: [...]` on `Filter.Action`. When both `filterableFieldNames` and this prop are supplied, both apply (final = whitelist ∩ ¬blacklist).
|
|
30
|
+
*/
|
|
31
|
+
nonfilterableFieldNames?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Bypass the `filterableFieldNames` whitelist.
|
|
34
|
+
*
|
|
35
|
+
* Legacy escape hatch — prefer adjusting `filterableFieldNames` / `nonfilterableFieldNames` instead.
|
|
36
|
+
*/
|
|
29
37
|
noIgnore?: boolean;
|
|
30
38
|
/** Override the trigger button's label. Defaults to `t('Filter')`, or the v1-style `t('{{count}} filter items', { count })` when conditions are present. */
|
|
31
39
|
buttonText?: React.ReactNode;
|
|
@@ -50,6 +58,7 @@ export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
|
|
|
50
58
|
onChange,
|
|
51
59
|
t = identity,
|
|
52
60
|
filterableFieldNames,
|
|
61
|
+
nonfilterableFieldNames,
|
|
53
62
|
noIgnore,
|
|
54
63
|
buttonText,
|
|
55
64
|
showCount = true,
|
|
@@ -63,6 +72,7 @@ export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
|
|
|
63
72
|
const filterAction = useFilterActionProps({
|
|
64
73
|
collection,
|
|
65
74
|
filterableFieldNames,
|
|
75
|
+
nonfilterableFieldNames,
|
|
66
76
|
noIgnore,
|
|
67
77
|
t,
|
|
68
78
|
onApply: (filter: CompiledFilter, action: FilterApplyAction) => {
|
|
@@ -44,7 +44,15 @@ export interface CollectionFilterItemProps {
|
|
|
44
44
|
collection: Collection;
|
|
45
45
|
/** Whitelist of field names to expose; empty/undefined means all filterable fields. */
|
|
46
46
|
filterableFieldNames?: string[];
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Blacklist of field names to drop. Mirrors v1's `nonfilterable: [...]` on `Filter.Action`. When both whitelist and blacklist are supplied, both apply (final = whitelist ∩ ¬blacklist).
|
|
49
|
+
*/
|
|
50
|
+
nonfilterableFieldNames?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Bypass the `filterableFieldNames` whitelist (matches the legacy FilterItem `noIgnore`).
|
|
53
|
+
*
|
|
54
|
+
* Legacy escape hatch — prefer adjusting `filterableFieldNames` / `nonfilterableFieldNames` instead.
|
|
55
|
+
*/
|
|
48
56
|
noIgnore?: boolean;
|
|
49
57
|
/** Translator; defaults to identity so callers can omit it. */
|
|
50
58
|
t?: (key: string) => string;
|
|
@@ -84,10 +92,10 @@ const toCascaderOptions = (options: FilterOption[]): CascaderOption[] =>
|
|
|
84
92
|
*/
|
|
85
93
|
export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
86
94
|
(props) => {
|
|
87
|
-
const { collection, filterableFieldNames, noIgnore = false, t = identity } = props;
|
|
95
|
+
const { collection, filterableFieldNames, nonfilterableFieldNames, noIgnore = false, t = identity } = props;
|
|
88
96
|
const { path: leftValue, operator, value: rightValue } = props.value;
|
|
89
97
|
|
|
90
|
-
const options = useFilterOptions(collection, { filterableFieldNames, noIgnore, t });
|
|
98
|
+
const options = useFilterOptions(collection, { filterableFieldNames, nonfilterableFieldNames, noIgnore, t });
|
|
91
99
|
|
|
92
100
|
const cascaderOptions = useMemo(() => toCascaderOptions(options), [options]);
|
|
93
101
|
|
|
@@ -164,7 +172,7 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
164
172
|
*/
|
|
165
173
|
export function createCollectionFilterItem(
|
|
166
174
|
collection: Collection,
|
|
167
|
-
bound?: Pick<CollectionFilterItemProps, 'filterableFieldNames' | 'noIgnore' | 't'>,
|
|
175
|
+
bound?: Pick<CollectionFilterItemProps, 'filterableFieldNames' | 'nonfilterableFieldNames' | 'noIgnore' | 't'>,
|
|
168
176
|
) {
|
|
169
177
|
const Component: FC<{ value: CollectionFilterItemValue }> = (props) => (
|
|
170
178
|
<CollectionFilterItem {...bound} value={props.value} collection={collection} />
|
|
@@ -136,6 +136,48 @@ describe('CollectionFilterItem', () => {
|
|
|
136
136
|
expect(screen.queryByText('Lock reason')).not.toBeInTheDocument();
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
it('honours nonfilterableFieldNames blacklist', async () => {
|
|
140
|
+
const value = observable({ path: '', operator: '', value: '' });
|
|
141
|
+
const collection = buildStubCollection([
|
|
142
|
+
{ name: 'username', title: 'Username' },
|
|
143
|
+
{ name: 'lockReason', title: 'Lock reason' },
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
const { container } = render(
|
|
147
|
+
<CollectionFilterItem value={value} collection={collection} nonfilterableFieldNames={['lockReason']} />,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
openFieldCascader(container);
|
|
151
|
+
expect(await screen.findByText('Username')).toBeInTheDocument();
|
|
152
|
+
expect(screen.queryByText('Lock reason')).not.toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('applies both whitelist and blacklist (final = whitelist minus blacklist)', async () => {
|
|
156
|
+
const value = observable({ path: '', operator: '', value: '' });
|
|
157
|
+
const collection = buildStubCollection([
|
|
158
|
+
{ name: 'username', title: 'Username' },
|
|
159
|
+
{ name: 'nickname', title: 'Nickname' },
|
|
160
|
+
{ name: 'lockReason', title: 'Lock reason' },
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
// Whitelist permits username + nickname; blacklist then subtracts nickname. Result: only username should appear.
|
|
164
|
+
const { container } = render(
|
|
165
|
+
<CollectionFilterItem
|
|
166
|
+
value={value}
|
|
167
|
+
collection={collection}
|
|
168
|
+
filterableFieldNames={['username', 'nickname']}
|
|
169
|
+
nonfilterableFieldNames={['nickname']}
|
|
170
|
+
/>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
openFieldCascader(container);
|
|
174
|
+
expect(await screen.findByText('Username')).toBeInTheDocument();
|
|
175
|
+
// nickname is whitelisted but also blacklisted — blacklist wins for that field.
|
|
176
|
+
expect(screen.queryByText('Nickname')).not.toBeInTheDocument();
|
|
177
|
+
// lockReason isn't in the whitelist either, so it stays hidden.
|
|
178
|
+
expect(screen.queryByText('Lock reason')).not.toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
|
|
139
181
|
it('writes through value.value on input change', () => {
|
|
140
182
|
const value = observable({ path: 'username', operator: '$eq', value: '' });
|
|
141
183
|
const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
|
|
@@ -152,7 +152,7 @@ export interface UseFilterActionPropsResult {
|
|
|
152
152
|
* ```
|
|
153
153
|
*/
|
|
154
154
|
export function useFilterActionProps(args: UseFilterActionPropsArgs): UseFilterActionPropsResult {
|
|
155
|
-
const { collection, onApply, filterableFieldNames, noIgnore, t } = args;
|
|
155
|
+
const { collection, onApply, filterableFieldNames, nonfilterableFieldNames, noIgnore, t } = args;
|
|
156
156
|
|
|
157
157
|
// Held in a ref so the group object identity is stable for the lifetime of the host component — `<FilterContent>` mutates this object directly (push/splice on `items`, swap `logic`), and a fresh observable on every render would reset that internal state.
|
|
158
158
|
const valueRef = useRef<FilterGroupValue>();
|
|
@@ -161,11 +161,14 @@ export function useFilterActionProps(args: UseFilterActionPropsArgs): UseFilterA
|
|
|
161
161
|
}
|
|
162
162
|
const value = valueRef.current;
|
|
163
163
|
|
|
164
|
-
const options = useFilterOptions(collection, { filterableFieldNames, noIgnore, t });
|
|
164
|
+
const options = useFilterOptions(collection, { filterableFieldNames, nonfilterableFieldNames, noIgnore, t });
|
|
165
165
|
|
|
166
166
|
const FilterItem = useMemo(
|
|
167
|
-
() =>
|
|
168
|
-
|
|
167
|
+
() =>
|
|
168
|
+
collection
|
|
169
|
+
? createCollectionFilterItem(collection, { filterableFieldNames, nonfilterableFieldNames, noIgnore, t })
|
|
170
|
+
: undefined,
|
|
171
|
+
[collection, filterableFieldNames, nonfilterableFieldNames, noIgnore, t],
|
|
169
172
|
);
|
|
170
173
|
|
|
171
174
|
const onSubmit = useMemoizedFn(() => {
|
|
@@ -39,10 +39,22 @@ export type FilterOption = {
|
|
|
39
39
|
|
|
40
40
|
export interface UseFilterOptionsArgs {
|
|
41
41
|
/**
|
|
42
|
+
* **Preferred way to restrict fields**, alongside `nonfilterableFieldNames`.
|
|
43
|
+
*
|
|
42
44
|
* Whitelist of root-level field names to expose. Empty/undefined means "all filterable fields". The whitelist applies only at depth 1 — nested fields under an allowed association field are always included, matching the legacy v1 `Filter.Action` behaviour.
|
|
43
45
|
*/
|
|
44
46
|
filterableFieldNames?: string[];
|
|
45
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* **Preferred way to restrict fields**, alongside `filterableFieldNames`.
|
|
49
|
+
*
|
|
50
|
+
* Blacklist of root-level field names to drop. Mirrors v1's `nonfilterable: [...]` schema prop on `Filter.Action`. Applies at depth 1 only, same as the whitelist. When both `filterableFieldNames` and `nonfilterableFieldNames` are provided, both apply: the final field set is `(whitelist or all) minus blacklist`.
|
|
51
|
+
*/
|
|
52
|
+
nonfilterableFieldNames?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Bypass the `filterableFieldNames` whitelist (mirrors v1 `noIgnore`).
|
|
55
|
+
*
|
|
56
|
+
* Legacy escape hatch from v1 schemas — prefer adjusting `filterableFieldNames` / `nonfilterableFieldNames` instead. Kept only for parity with existing v1 schemas that already set `noIgnore`.
|
|
57
|
+
*/
|
|
46
58
|
noIgnore?: boolean;
|
|
47
59
|
/** Translator used for field/operator labels. Defaults to identity. */
|
|
48
60
|
t?: (key: string) => string;
|
|
@@ -58,14 +70,24 @@ const identity = (s: string) => s;
|
|
|
58
70
|
* - the whitelist applies at depth 1 only, so capping the root field list (e.g. to `['lockedTs', 'unlockTs', 'user']`) doesn't accidentally hide the nested `user.username` / `user.nickname` leaves.
|
|
59
71
|
*/
|
|
60
72
|
export function useFilterOptions(collection: Collection | undefined, args: UseFilterOptionsArgs = {}): FilterOption[] {
|
|
61
|
-
const { filterableFieldNames, noIgnore = false, t = identity } = args;
|
|
73
|
+
const { filterableFieldNames, nonfilterableFieldNames, noIgnore = false, t = identity } = args;
|
|
62
74
|
|
|
63
75
|
const fields = useMemo(() => collection?.getFields() || [], [collection]);
|
|
64
76
|
|
|
65
77
|
const ignoreFieldsNames = useMemo(() => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
// Whitelist contribution: every field not in `filterableFieldNames`.
|
|
79
|
+
// Skipped when `noIgnore` is set, or when no whitelist was provided.
|
|
80
|
+
const whitelistIgnored =
|
|
81
|
+
noIgnore || !filterableFieldNames?.length
|
|
82
|
+
? []
|
|
83
|
+
: fields.map((f) => f.name).filter((n) => !filterableFieldNames.includes(n));
|
|
84
|
+
// Blacklist contribution: explicit names. Always applied, even with
|
|
85
|
+
// `noIgnore` (the blacklist's whole job is to subtract specific fields).
|
|
86
|
+
const blacklistIgnored = nonfilterableFieldNames ?? [];
|
|
87
|
+
if (!blacklistIgnored.length) return whitelistIgnored;
|
|
88
|
+
// Union the two so the final ignore set is `whitelist-derived ∪ blacklist`.
|
|
89
|
+
return Array.from(new Set([...whitelistIgnored, ...blacklistIgnored]));
|
|
90
|
+
}, [fields, filterableFieldNames, nonfilterableFieldNames, noIgnore]);
|
|
69
91
|
|
|
70
92
|
return useMemo(
|
|
71
93
|
() =>
|
|
@@ -7,75 +7,22 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { tExpr,
|
|
10
|
+
import { tExpr, useFlowSettingsContext } from '@nocobase/flow-engine';
|
|
11
11
|
import { Alert, ButtonProps } from 'antd';
|
|
12
|
-
import React
|
|
12
|
+
import React from 'react';
|
|
13
13
|
import { AxiosRequestConfig } from 'axios';
|
|
14
14
|
import { ActionModel, ActionSceneEnum } from '../base/ActionModel';
|
|
15
|
-
import { CollectionActionModel } from '../base/CollectionActionModel';
|
|
16
|
-
import { RecordActionModel } from '../base/RecordActionModel';
|
|
17
15
|
import { AssignFormModel } from '../blocks/assign-form/AssignFormModel';
|
|
16
|
+
import {
|
|
17
|
+
createAssignFieldValuesStep,
|
|
18
|
+
createAssignFormSubModelOptions,
|
|
19
|
+
getAssignFieldValuesDefaultParams,
|
|
20
|
+
} from '../blocks/assign-form/assignFieldValuesFlow';
|
|
18
21
|
import { applyUpdateRecordAction } from './UpdateRecordActionUtils';
|
|
19
22
|
// import { RemoteFlowModelRenderer } from '../../FlowPage';
|
|
20
23
|
|
|
21
24
|
const SETTINGS_FLOW_KEY = 'assignSettings';
|
|
22
25
|
|
|
23
|
-
// 配置态编辑器:渲染 assignForm 子模型
|
|
24
|
-
function AssignFieldsEditor() {
|
|
25
|
-
const { model, blockModel } = useFlowSettingsContext();
|
|
26
|
-
const action: any = model;
|
|
27
|
-
const engine = useFlowEngine();
|
|
28
|
-
const initializedRef = useRef(false);
|
|
29
|
-
const [formModel, setFormModel] = React.useState<AssignFormModel | null>(null);
|
|
30
|
-
|
|
31
|
-
// 初始化回填
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
let cancelled = false;
|
|
34
|
-
(async () => {
|
|
35
|
-
const loaded = (await engine.loadOrCreateModel(
|
|
36
|
-
{
|
|
37
|
-
parentId: action.uid,
|
|
38
|
-
subKey: 'assignForm',
|
|
39
|
-
use: 'AssignFormModel',
|
|
40
|
-
},
|
|
41
|
-
{ skipSave: !model.context.flowSettingsEnabled },
|
|
42
|
-
)) as AssignFormModel;
|
|
43
|
-
if (cancelled) return;
|
|
44
|
-
setFormModel(loaded);
|
|
45
|
-
action.assignFormUid = (loaded as any)?.uid || action.assignFormUid;
|
|
46
|
-
})();
|
|
47
|
-
return () => {
|
|
48
|
-
cancelled = true;
|
|
49
|
-
};
|
|
50
|
-
}, [action, engine]);
|
|
51
|
-
|
|
52
|
-
// 初始化回填(在子模型加载完成后)
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (initializedRef.current) return;
|
|
55
|
-
if (!formModel) return;
|
|
56
|
-
const prev = action.getStepParams?.(SETTINGS_FLOW_KEY, 'assignFieldValues') || {};
|
|
57
|
-
// 注入资源上下文:与所在表格区块一致
|
|
58
|
-
const coll = blockModel?.collection || action?.context?.collection;
|
|
59
|
-
const dsKey = coll?.dataSourceKey;
|
|
60
|
-
const collName = coll?.name;
|
|
61
|
-
if (dsKey && collName) {
|
|
62
|
-
formModel.setStepParams('resourceSettings', 'init', {
|
|
63
|
-
dataSourceKey: dsKey,
|
|
64
|
-
collectionName: collName,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
formModel.setInitialAssignedValues(prev?.assignedValues || {});
|
|
68
|
-
// 批量配置态:移除 ctx.record(Action 为区块级,不具备单条记录上下文)
|
|
69
|
-
const isBulk = action instanceof CollectionActionModel && !(action instanceof RecordActionModel);
|
|
70
|
-
if (isBulk && (formModel as any)?.context?.defineProperty) {
|
|
71
|
-
formModel.context.defineProperty('record', { get: () => undefined, cache: false });
|
|
72
|
-
}
|
|
73
|
-
initializedRef.current = true;
|
|
74
|
-
}, [action, blockModel?.collection, formModel]);
|
|
75
|
-
|
|
76
|
-
return formModel ? <FlowModelRenderer model={formModel} showFlowSettings={false} /> : null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
26
|
function Info() {
|
|
80
27
|
const ctx = useFlowSettingsContext();
|
|
81
28
|
return (
|
|
@@ -124,16 +71,9 @@ UpdateRecordActionModel.define({
|
|
|
124
71
|
sort: 50,
|
|
125
72
|
// 使用函数型 createModelOptions,从父级上下文提取资源信息,直接注入到子模型的 resourceSettings.init
|
|
126
73
|
createModelOptions: (ctx) => {
|
|
127
|
-
const dsKey = ctx.collection.dataSourceKey;
|
|
128
|
-
const collName = ctx.collection.name;
|
|
129
|
-
const init = dsKey && collName ? { dataSourceKey: dsKey, collectionName: collName } : undefined;
|
|
130
74
|
return {
|
|
131
75
|
subModels: {
|
|
132
|
-
assignForm:
|
|
133
|
-
async: true,
|
|
134
|
-
use: 'AssignFormModel',
|
|
135
|
-
stepParams: { resourceSettings: { init } },
|
|
136
|
-
},
|
|
76
|
+
assignForm: createAssignFormSubModelOptions(ctx),
|
|
137
77
|
},
|
|
138
78
|
};
|
|
139
79
|
},
|
|
@@ -155,32 +95,12 @@ UpdateRecordActionModel.registerFlow({
|
|
|
155
95
|
content: tExpr('Are you sure you want to perform the Update record action?'),
|
|
156
96
|
},
|
|
157
97
|
},
|
|
158
|
-
assignFieldValues: {
|
|
98
|
+
assignFieldValues: createAssignFieldValuesStep({
|
|
99
|
+
settingsFlowKey: SETTINGS_FLOW_KEY,
|
|
159
100
|
title: tExpr('Field values'),
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
'x-decorator': 'FormItem',
|
|
164
|
-
'x-component': () => <Info />,
|
|
165
|
-
},
|
|
166
|
-
editor: {
|
|
167
|
-
'x-decorator': 'FormItem',
|
|
168
|
-
'x-component': () => <AssignFieldsEditor />,
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
},
|
|
172
|
-
async beforeParamsSave(ctx) {
|
|
173
|
-
const m = ctx.model as UpdateRecordActionModel;
|
|
174
|
-
// 跨视图栈按 uid 定位到设置面板中的真实 AssignForm 实例
|
|
175
|
-
const form: AssignFormModel = m?.assignFormUid && ctx.engine.getModel?.(m.assignFormUid, true);
|
|
176
|
-
if (!form) return;
|
|
177
|
-
await form?.form?.validateFields?.();
|
|
178
|
-
const assignedValues = form?.getAssignedValues?.() || {};
|
|
179
|
-
const grid = form?.subModels?.grid;
|
|
180
|
-
const items = grid?.subModels?.items || [];
|
|
181
|
-
ctx.model.setStepParams(SETTINGS_FLOW_KEY, 'assignFieldValues', { assignedValues });
|
|
182
|
-
},
|
|
183
|
-
},
|
|
101
|
+
tipComponent: Info,
|
|
102
|
+
validateBeforeSave: true,
|
|
103
|
+
}),
|
|
184
104
|
},
|
|
185
105
|
});
|
|
186
106
|
|
|
@@ -190,8 +110,7 @@ UpdateRecordActionModel.registerFlow({
|
|
|
190
110
|
steps: {
|
|
191
111
|
apply: {
|
|
192
112
|
async defaultParams(ctx) {
|
|
193
|
-
|
|
194
|
-
return { assignedValues: step?.assignedValues || {} };
|
|
113
|
+
return getAssignFieldValuesDefaultParams(ctx, SETTINGS_FLOW_KEY);
|
|
195
114
|
},
|
|
196
115
|
async handler(ctx, params) {
|
|
197
116
|
await applyUpdateRecordAction(ctx, params, { settingsFlowKey: SETTINGS_FLOW_KEY });
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { MultiRecordResource, SingleRecordResource
|
|
10
|
+
import { MultiRecordResource, SingleRecordResource } from '@nocobase/flow-engine';
|
|
11
11
|
import { dispatchEventDeep } from '../../utils';
|
|
12
|
+
import { resolveAssignFieldValues } from '../blocks/assign-form/assignFieldValuesFlow';
|
|
12
13
|
|
|
13
14
|
export async function refreshLinkageRulesAfterUpdate(ctx: any) {
|
|
14
15
|
const blockModel = ctx?.blockModel || ctx?.model?.context?.blockModel || ctx?.model;
|
|
@@ -59,12 +60,8 @@ export async function applyUpdateRecordAction(
|
|
|
59
60
|
const confirmParams = savedConfirm && typeof savedConfirm === 'object' ? savedConfirm : { enable: false };
|
|
60
61
|
await ctx.runAction('confirm', confirmParams);
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
assignedValues = await resolveRunJSObjectValues(ctx, params?.assignedValues);
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error('[UpdateRecordAction] RunJS execution failed', error);
|
|
67
|
-
ctx.message.error(ctx.t('RunJS execution failed'));
|
|
63
|
+
const assignedValues = await resolveAssignFieldValues(ctx, params?.assignedValues, 'UpdateRecordAction');
|
|
64
|
+
if (!assignedValues) {
|
|
68
65
|
return;
|
|
69
66
|
}
|
|
70
67
|
|