@nocobase/client-v2 2.2.0-beta.9 → 2.3.0-alpha.1
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/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -1
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +4 -0
- package/es/index.mjs +153 -118
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +162 -127
- package/lib/locale/languageCodes.js +2 -1
- package/package.json +8 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/app.test.tsx +73 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +23 -6
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +180 -3
- package/src/components/form/ScanInput/useCodeScanner.ts +170 -5
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +1 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +2 -2
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +86 -11
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +91 -20
- package/src/flow/components/FlowRoute.tsx +7 -3
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +1 -0
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +184 -3
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +572 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +4 -0
- package/src/locale/languageCodes.ts +2 -1
- package/src/nocobase-buildin-plugin/index.tsx +12 -0
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -45,6 +45,7 @@ interface FilterGroupProps {
|
|
|
45
45
|
value: Record<string, any>;
|
|
46
46
|
/** 自定义筛选项组件 */
|
|
47
47
|
FilterItem?: React.FC<FilterItemProps>;
|
|
48
|
+
disabled?: boolean;
|
|
48
49
|
closeIcon?: ReactNode;
|
|
49
50
|
/** 是否显示边框 */
|
|
50
51
|
showBorder?: boolean;
|
|
@@ -89,6 +90,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
89
90
|
const {
|
|
90
91
|
value = { logic: '$and', items: [] },
|
|
91
92
|
FilterItem,
|
|
93
|
+
disabled = false,
|
|
92
94
|
showBorder = false,
|
|
93
95
|
onRemove,
|
|
94
96
|
onChange,
|
|
@@ -120,11 +122,17 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
120
122
|
};
|
|
121
123
|
|
|
122
124
|
const handleLogicChange = (newLogic: '$and' | '$or') => {
|
|
125
|
+
if (disabled) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
123
128
|
value.logic = newLogic;
|
|
124
129
|
onChange?.(value);
|
|
125
130
|
};
|
|
126
131
|
|
|
127
132
|
const handleAddCondition = () => {
|
|
133
|
+
if (disabled) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
128
136
|
items.push({
|
|
129
137
|
path: '',
|
|
130
138
|
operator: '',
|
|
@@ -134,6 +142,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
134
142
|
};
|
|
135
143
|
|
|
136
144
|
const handleAddConditionGroup = () => {
|
|
145
|
+
if (disabled) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
137
148
|
items.push({
|
|
138
149
|
logic: '$and',
|
|
139
150
|
items: [],
|
|
@@ -142,6 +153,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
142
153
|
};
|
|
143
154
|
|
|
144
155
|
const handleRemoveItem = (index: number) => {
|
|
156
|
+
if (disabled) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
145
159
|
items.splice(index, 1);
|
|
146
160
|
onChange?.(value);
|
|
147
161
|
};
|
|
@@ -160,11 +174,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
160
174
|
<button
|
|
161
175
|
type="button"
|
|
162
176
|
aria-label="icon-close"
|
|
177
|
+
disabled={disabled}
|
|
163
178
|
style={{
|
|
164
179
|
position: 'absolute',
|
|
165
180
|
right: 10,
|
|
166
181
|
top: 10,
|
|
167
|
-
cursor: 'pointer',
|
|
182
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
168
183
|
background: 'transparent',
|
|
169
184
|
border: 0,
|
|
170
185
|
padding: 0,
|
|
@@ -182,6 +197,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
182
197
|
data-testid="filter-select-all-or-any"
|
|
183
198
|
style={{ width: 'auto' }}
|
|
184
199
|
value={logic}
|
|
200
|
+
disabled={disabled}
|
|
185
201
|
onChange={handleLogicChange}
|
|
186
202
|
>
|
|
187
203
|
<Select.Option value="$and">{t('All')}</Select.Option>
|
|
@@ -200,6 +216,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
200
216
|
key={getFilterItemKey(item)}
|
|
201
217
|
value={item}
|
|
202
218
|
FilterItem={FilterItem}
|
|
219
|
+
disabled={disabled}
|
|
203
220
|
showBorder={true}
|
|
204
221
|
onRemove={() => handleRemoveItem(index)}
|
|
205
222
|
onChange={(v) => {
|
|
@@ -221,11 +238,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
221
238
|
<button
|
|
222
239
|
type="button"
|
|
223
240
|
aria-label="icon-close"
|
|
241
|
+
disabled={disabled}
|
|
224
242
|
style={{
|
|
225
243
|
marginLeft: 8,
|
|
226
244
|
marginRight: 8,
|
|
227
245
|
flex: '0 0 auto',
|
|
228
|
-
cursor: 'pointer',
|
|
246
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
229
247
|
background: 'transparent',
|
|
230
248
|
border: 0,
|
|
231
249
|
padding: 0,
|
|
@@ -251,11 +269,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
251
269
|
<button
|
|
252
270
|
type="button"
|
|
253
271
|
aria-label="icon-close"
|
|
272
|
+
disabled={disabled}
|
|
254
273
|
style={{
|
|
255
274
|
marginLeft: 8,
|
|
256
275
|
marginRight: 8,
|
|
257
276
|
flex: '0 0 auto',
|
|
258
|
-
cursor: 'pointer',
|
|
277
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
259
278
|
background: 'transparent',
|
|
260
279
|
border: 0,
|
|
261
280
|
padding: 0,
|
|
@@ -277,11 +296,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
277
296
|
<button
|
|
278
297
|
type="button"
|
|
279
298
|
aria-label="icon-close"
|
|
299
|
+
disabled={disabled}
|
|
280
300
|
style={{
|
|
281
301
|
marginLeft: 8,
|
|
282
302
|
marginRight: 8,
|
|
283
303
|
flex: '0 0 auto',
|
|
284
|
-
cursor: 'pointer',
|
|
304
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
285
305
|
background: 'transparent',
|
|
286
306
|
border: 0,
|
|
287
307
|
padding: 0,
|
|
@@ -298,7 +318,14 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
298
318
|
</div>
|
|
299
319
|
|
|
300
320
|
<Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
|
|
301
|
-
<Button
|
|
321
|
+
<Button
|
|
322
|
+
style={{ padding: 0 }}
|
|
323
|
+
type="link"
|
|
324
|
+
size="small"
|
|
325
|
+
icon={<PlusOutlined />}
|
|
326
|
+
onClick={handleAddCondition}
|
|
327
|
+
disabled={disabled}
|
|
328
|
+
>
|
|
302
329
|
{t('Add condition')}
|
|
303
330
|
</Button>
|
|
304
331
|
<Button
|
|
@@ -307,6 +334,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
307
334
|
size="small"
|
|
308
335
|
icon={<PlusOutlined />}
|
|
309
336
|
onClick={handleAddConditionGroup}
|
|
337
|
+
disabled={disabled}
|
|
310
338
|
>
|
|
311
339
|
{t('Add condition group')}
|
|
312
340
|
</Button>
|
|
@@ -62,6 +62,51 @@ async function buildCollectionLeftMetaTreeLocal(ctx: any): Promise<MetaTreeNode[
|
|
|
62
62
|
return await resolve(subTree);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function buildCollectionFieldMetaNode(collection: any, path: string[]): MetaTreeNode | null {
|
|
66
|
+
if (!collection || !path.length) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normalizedPath = path[0] === 'collection' ? path.slice(1) : path;
|
|
71
|
+
if (!normalizedPath.length) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parentTitles: string[] = [];
|
|
76
|
+
let currentCollection = collection;
|
|
77
|
+
let currentNode: MetaTreeNode | null = null;
|
|
78
|
+
|
|
79
|
+
for (const segment of normalizedPath) {
|
|
80
|
+
const field =
|
|
81
|
+
currentCollection?.getField?.(segment) ??
|
|
82
|
+
currentCollection?.getFields?.()?.find?.((candidate: any) => candidate?.name === segment);
|
|
83
|
+
if (!field) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const title = field.title || field.uiSchema?.title || field.name || segment;
|
|
88
|
+
currentNode = {
|
|
89
|
+
name: field.name || segment,
|
|
90
|
+
title,
|
|
91
|
+
type: field.type,
|
|
92
|
+
interface: field.interface,
|
|
93
|
+
uiSchema: field.uiSchema,
|
|
94
|
+
options: field.options,
|
|
95
|
+
paths: ['collection', ...normalizedPath.slice(0, parentTitles.length + 1)],
|
|
96
|
+
parentTitles: [...parentTitles],
|
|
97
|
+
} as MetaTreeNode;
|
|
98
|
+
|
|
99
|
+
if (field.targetCollection) {
|
|
100
|
+
parentTitles.push(title);
|
|
101
|
+
currentCollection = field.targetCollection;
|
|
102
|
+
} else {
|
|
103
|
+
currentCollection = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return currentNode;
|
|
108
|
+
}
|
|
109
|
+
|
|
65
110
|
export interface VariableFilterItemValue {
|
|
66
111
|
path: string;
|
|
67
112
|
operator: string;
|
|
@@ -75,6 +120,7 @@ export interface VariableFilterItemProps {
|
|
|
75
120
|
/** 筛选条件值对象 */
|
|
76
121
|
value: VariableFilterItemValue;
|
|
77
122
|
model: FlowModel;
|
|
123
|
+
disabled?: boolean;
|
|
78
124
|
/**
|
|
79
125
|
* 是否启用右侧 VariableInput(变量或静态值二合一)。
|
|
80
126
|
* 默认 false:保持原有行为,右侧仅静态输入组件。
|
|
@@ -85,6 +131,10 @@ export interface VariableFilterItemProps {
|
|
|
85
131
|
* 默认使用整棵 ctx 的 metaTree:model.context.getPropertyMetaTree()
|
|
86
132
|
*/
|
|
87
133
|
rightMetaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
134
|
+
/**
|
|
135
|
+
* 右侧变量的领域转换器。常量和空值仍由 VariableFilterItem 处理,未匹配的值回退到 FlowEngine 默认转换器。
|
|
136
|
+
*/
|
|
137
|
+
rightVariableConverters?: Pick<Converters, 'resolvePathFromValue' | 'resolveValueFromPath'>;
|
|
88
138
|
ignoreFieldNames?: string[];
|
|
89
139
|
maxAssociationFieldDepth?: number;
|
|
90
140
|
}
|
|
@@ -265,11 +315,57 @@ function normalizeRightValueInput(input: unknown) {
|
|
|
265
315
|
return input;
|
|
266
316
|
}
|
|
267
317
|
|
|
318
|
+
function findMetaTreeNodeByPath(metaTree: MetaTreeNode[], targetPath: string[]): MetaTreeNode | null {
|
|
319
|
+
if (!Array.isArray(metaTree) || !targetPath.length) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const traverseByName = (nodes: MetaTreeNode[], path: string[]): MetaTreeNode | null => {
|
|
324
|
+
if (!path.length) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const [head, ...rest] = path;
|
|
329
|
+
const matched = nodes.find((node) => String(node.name) === String(head));
|
|
330
|
+
if (!matched) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
if (!rest.length) {
|
|
334
|
+
return matched;
|
|
335
|
+
}
|
|
336
|
+
if (!Array.isArray(matched.children)) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return traverseByName(matched.children as MetaTreeNode[], rest);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const byNames = traverseByName(metaTree, targetPath);
|
|
343
|
+
if (byNames) {
|
|
344
|
+
return byNames;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const topNames = new Set(metaTree.map((node) => String(node.name)));
|
|
348
|
+
if (!topNames.has(String(targetPath[0]))) {
|
|
349
|
+
return traverseByName(metaTree, targetPath.slice(1));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
268
355
|
/**
|
|
269
356
|
* 上下文筛选项组件
|
|
270
357
|
*/
|
|
271
358
|
export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
272
|
-
({
|
|
359
|
+
({
|
|
360
|
+
value,
|
|
361
|
+
model,
|
|
362
|
+
disabled = false,
|
|
363
|
+
rightAsVariable,
|
|
364
|
+
rightMetaTree,
|
|
365
|
+
rightVariableConverters,
|
|
366
|
+
ignoreFieldNames,
|
|
367
|
+
maxAssociationFieldDepth,
|
|
368
|
+
}) => {
|
|
273
369
|
// 使用 View 上下文,确保可访问 ctx.view 的异步子树
|
|
274
370
|
const ctx = useFlowViewContext();
|
|
275
371
|
const t = model.translate;
|
|
@@ -334,6 +430,9 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
334
430
|
// 处理左侧值变化(值由 converters 决定如何解析)
|
|
335
431
|
const handleLeftChange = useCallback(
|
|
336
432
|
(variableValue: string, meta?: MetaTreeNode) => {
|
|
433
|
+
if (disabled) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
337
436
|
const prevPath = value.path || '';
|
|
338
437
|
const nextPath = variableValue || '';
|
|
339
438
|
const changed = nextPath !== prevPath;
|
|
@@ -348,7 +447,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
348
447
|
setLeftMeta(meta);
|
|
349
448
|
}
|
|
350
449
|
},
|
|
351
|
-
[value],
|
|
450
|
+
[disabled, value],
|
|
352
451
|
);
|
|
353
452
|
|
|
354
453
|
// 自定义转换器来捕获 MetaTreeNode,并在选择左值时设置默认操作符
|
|
@@ -376,6 +475,9 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
376
475
|
// 处理操作符变化
|
|
377
476
|
const handleOperatorChange = useCallback(
|
|
378
477
|
(operatorValue: string) => {
|
|
478
|
+
if (disabled) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
379
481
|
value.operator = operatorValue;
|
|
380
482
|
const cur = operatorMetaList.find((op) => op.value === operatorValue);
|
|
381
483
|
if (cur?.noValue) {
|
|
@@ -386,7 +488,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
386
488
|
value.noValue = false;
|
|
387
489
|
}
|
|
388
490
|
},
|
|
389
|
-
[operatorMetaList, value],
|
|
491
|
+
[disabled, operatorMetaList, value],
|
|
390
492
|
);
|
|
391
493
|
|
|
392
494
|
// 使用公共静态输入渲染器(抽取到 utils)
|
|
@@ -464,9 +566,12 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
464
566
|
|
|
465
567
|
const setRightValue = useCallback(
|
|
466
568
|
(next: unknown) => {
|
|
569
|
+
if (disabled) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
467
572
|
value.value = normalizeRightValueInput(next) as VariableFilterItemValue['value'];
|
|
468
573
|
},
|
|
469
|
-
[value],
|
|
574
|
+
[disabled, value],
|
|
470
575
|
);
|
|
471
576
|
|
|
472
577
|
// 右侧静态输入(无变量模式)与右侧 VariableInput 的静态渲染组件,统一复用
|
|
@@ -520,6 +625,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
520
625
|
{...nextProps}
|
|
521
626
|
style={{ width: '100%', ...pickStyle(nextProps.style) }}
|
|
522
627
|
value={normalized}
|
|
628
|
+
disabled={disabled}
|
|
523
629
|
onChange={(vals: any) => setRightValue(vals)}
|
|
524
630
|
/>
|
|
525
631
|
</div>
|
|
@@ -529,10 +635,19 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
529
635
|
const Comp = staticInputRenderer;
|
|
530
636
|
return (
|
|
531
637
|
<div style={{ flex: '1 1 40%', minWidth: 160, maxWidth: '100%' }}>
|
|
532
|
-
<Comp value={rightValue} onChange={(val) => setRightValue(val)} />
|
|
638
|
+
<Comp value={rightValue} onChange={(val) => setRightValue(val)} disabled={disabled} />
|
|
533
639
|
</div>
|
|
534
640
|
);
|
|
535
|
-
}, [
|
|
641
|
+
}, [
|
|
642
|
+
disabled,
|
|
643
|
+
operator,
|
|
644
|
+
operatorMetaList,
|
|
645
|
+
rightValue,
|
|
646
|
+
staticInputRenderer,
|
|
647
|
+
model.context.app,
|
|
648
|
+
setRightValue,
|
|
649
|
+
enumOptions,
|
|
650
|
+
]);
|
|
536
651
|
|
|
537
652
|
// Null 占位组件(仿照 DefaultValue.tsx 的实现)
|
|
538
653
|
const NullComponent = useMemo(() => {
|
|
@@ -582,17 +697,19 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
582
697
|
const first = meta?.paths?.[0];
|
|
583
698
|
if (first === 'constant') return '';
|
|
584
699
|
if (first === 'null') return null;
|
|
585
|
-
return
|
|
700
|
+
return rightVariableConverters?.resolveValueFromPath?.(meta);
|
|
586
701
|
},
|
|
587
702
|
resolvePathFromValue: (val) => {
|
|
588
703
|
if (val === null) return ['null'];
|
|
589
|
-
//
|
|
590
|
-
const parsed =
|
|
704
|
+
// 变量表达式:优先使用领域转换器,再回退内置解析;其他静态值走 constant
|
|
705
|
+
const parsed =
|
|
706
|
+
rightVariableConverters?.resolvePathFromValue?.(val) ??
|
|
707
|
+
(typeof val === 'string' ? parseValueToPath(val) : undefined);
|
|
591
708
|
if (parsed) return parsed;
|
|
592
709
|
return ['constant'];
|
|
593
710
|
},
|
|
594
711
|
};
|
|
595
|
-
}, [NullComponent, staticInputRenderer]);
|
|
712
|
+
}, [NullComponent, rightVariableConverters, staticInputRenderer]);
|
|
596
713
|
|
|
597
714
|
// 为 2.0 左侧字段选择器追加“接口 filterable.children”定义(如 chinaRegion 的“省市区名称”子项),
|
|
598
715
|
// 以恢复 1.0 左侧子菜单的能力。
|
|
@@ -652,6 +769,34 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
652
769
|
};
|
|
653
770
|
}, [getFieldInterface, model, maxAssociationFieldDepth]);
|
|
654
771
|
|
|
772
|
+
useEffect(() => {
|
|
773
|
+
if (leftMeta || !value.path) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
let cancelled = false;
|
|
778
|
+
const restore = async () => {
|
|
779
|
+
const nodes = await enhancedMetaTree();
|
|
780
|
+
if (cancelled) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const path = customConverters.resolvePathFromValue(value.path);
|
|
784
|
+
if (!path?.length) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const matched =
|
|
788
|
+
findMetaTreeNodeByPath(nodes, path) || buildCollectionFieldMetaNode(model.context.collection, path);
|
|
789
|
+
if (matched) {
|
|
790
|
+
setLeftMeta(matched);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
restore();
|
|
795
|
+
return () => {
|
|
796
|
+
cancelled = true;
|
|
797
|
+
};
|
|
798
|
+
}, [customConverters, enhancedMetaTree, leftMeta, model.context.collection, value.path]);
|
|
799
|
+
|
|
655
800
|
return (
|
|
656
801
|
<Space wrap style={{ width: '100%' }}>
|
|
657
802
|
<VariableInput
|
|
@@ -659,6 +804,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
659
804
|
metaTree={enhancedMetaTree}
|
|
660
805
|
onChange={handleLeftChange}
|
|
661
806
|
converters={customConverters}
|
|
807
|
+
disabled={disabled}
|
|
662
808
|
showValueComponent={false}
|
|
663
809
|
style={{ flex: '1 1 40%', minWidth: 160, maxWidth: '100%' }}
|
|
664
810
|
onlyLeafSelectable={true}
|
|
@@ -670,6 +816,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
670
816
|
style={{ flex: '0 0 140px', minWidth: 120, maxWidth: '100%' }}
|
|
671
817
|
placeholder={t('Comparison')}
|
|
672
818
|
value={operator || undefined}
|
|
819
|
+
disabled={disabled}
|
|
673
820
|
onChange={handleOperatorChange}
|
|
674
821
|
>
|
|
675
822
|
{operatorOptions.map((op) => (
|
|
@@ -686,6 +833,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
686
833
|
onChange={(v) => setRightValue(v)}
|
|
687
834
|
metaTree={mergedRightMetaTree}
|
|
688
835
|
converters={rightConverters}
|
|
836
|
+
disabled={disabled}
|
|
689
837
|
showValueComponent
|
|
690
838
|
style={{ flex: '1 1 40%', minWidth: 160, maxWidth: '100%' }}
|
|
691
839
|
placeholder={t('Enter value')}
|
|
@@ -140,4 +140,49 @@ describe('FilterGroup closeIcon', () => {
|
|
|
140
140
|
expect(onChange).toHaveBeenCalledTimes(1);
|
|
141
141
|
expect(value.items[0].items).toHaveLength(1);
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
it('disables close and add actions when the filter group is disabled', () => {
|
|
145
|
+
const value = {
|
|
146
|
+
logic: '$and',
|
|
147
|
+
items: [
|
|
148
|
+
{
|
|
149
|
+
path: 'name',
|
|
150
|
+
operator: 'eq',
|
|
151
|
+
value: 'test',
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
const onRemove = vi.fn();
|
|
156
|
+
const onChange = vi.fn();
|
|
157
|
+
|
|
158
|
+
renderWithProviders(
|
|
159
|
+
<FilterGroup
|
|
160
|
+
value={value}
|
|
161
|
+
FilterItem={DummyFilterItem}
|
|
162
|
+
showBorder
|
|
163
|
+
onRemove={onRemove}
|
|
164
|
+
onChange={onChange}
|
|
165
|
+
disabled
|
|
166
|
+
/>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const closeButtons = screen
|
|
170
|
+
.getAllByLabelText('icon-close')
|
|
171
|
+
.map((element) => element.closest('button'))
|
|
172
|
+
.filter(Boolean) as HTMLButtonElement[];
|
|
173
|
+
expect(closeButtons).toHaveLength(2);
|
|
174
|
+
expect(closeButtons[0]).toBeDisabled();
|
|
175
|
+
expect(closeButtons[1]).toBeDisabled();
|
|
176
|
+
expect(screen.getByText('Add condition').closest('button')).toBeDisabled();
|
|
177
|
+
expect(screen.getByText('Add condition group').closest('button')).toBeDisabled();
|
|
178
|
+
|
|
179
|
+
fireEvent.click(closeButtons[0]);
|
|
180
|
+
fireEvent.click(closeButtons[1]);
|
|
181
|
+
fireEvent.click(screen.getByText('Add condition').closest('button') as HTMLButtonElement);
|
|
182
|
+
fireEvent.click(screen.getByText('Add condition group').closest('button') as HTMLButtonElement);
|
|
183
|
+
|
|
184
|
+
expect(onRemove).not.toHaveBeenCalled();
|
|
185
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
186
|
+
expect(value.items).toHaveLength(1);
|
|
187
|
+
});
|
|
143
188
|
});
|
|
@@ -193,4 +193,13 @@ describe('VariableFilterItem with leftMetaTree', () => {
|
|
|
193
193
|
fireEvent.click(screen.getByTestId('variable-input'));
|
|
194
194
|
expect(value.path).toBe('title');
|
|
195
195
|
});
|
|
196
|
+
|
|
197
|
+
it('restores the saved left meta so the operator select shows its label on reopen', async () => {
|
|
198
|
+
const value = { path: 'title', operator: '$eq', value: 'x' } as any;
|
|
199
|
+
const model = createModelWithCollection();
|
|
200
|
+
|
|
201
|
+
render(<VariableFilterItem value={value} model={model} rightAsVariable={false} />);
|
|
202
|
+
|
|
203
|
+
expect(await screen.findByText('Equals')).toBeInTheDocument();
|
|
204
|
+
});
|
|
196
205
|
});
|
|
@@ -11,7 +11,7 @@ import React from 'react';
|
|
|
11
11
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
12
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
13
13
|
import { VariableFilterItem, VariableFilterItemValue } from '../VariableFilterItem';
|
|
14
|
-
import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
14
|
+
import { FlowEngine, FlowModel, type Converters, type MetaTreeNode } from '@nocobase/flow-engine';
|
|
15
15
|
import { observable } from '@formily/reactive';
|
|
16
16
|
import { createMockFlowApp, TestCollectionFieldInterface } from '../../../__tests__/helpers/mockFlowApp';
|
|
17
17
|
|
|
@@ -158,6 +158,75 @@ describe('VariableFilterItem', () => {
|
|
|
158
158
|
expect(value.value).toBe('abc');
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
it('passes disabled through to the left selector, operator select, and right variable input', async () => {
|
|
162
|
+
const value: VariableFilterItemValue = { path: '', operator: '', value: '' };
|
|
163
|
+
const model = CreateModel();
|
|
164
|
+
|
|
165
|
+
render(<VariableFilterItem value={value} model={model} rightAsVariable disabled />);
|
|
166
|
+
|
|
167
|
+
const leftVariableInputProps = (globalThis as any).__LAST_VARIABLE_INPUT_PROPS__;
|
|
168
|
+
expect(leftVariableInputProps.disabled).toBe(true);
|
|
169
|
+
|
|
170
|
+
fireEvent.click(screen.getAllByTestId('variable-input')[0]);
|
|
171
|
+
|
|
172
|
+
const variableInputs = screen.getAllByTestId('variable-input');
|
|
173
|
+
expect(variableInputs.length).toBeGreaterThanOrEqual(2);
|
|
174
|
+
const rightVariableInputProps = (globalThis as any).__LAST_VARIABLE_INPUT_PROPS__;
|
|
175
|
+
expect(rightVariableInputProps.disabled).toBe(true);
|
|
176
|
+
|
|
177
|
+
const operatorSelect = document.body.querySelector('.ant-select') as HTMLDivElement | null;
|
|
178
|
+
expect(operatorSelect).not.toBeNull();
|
|
179
|
+
expect(operatorSelect).toHaveClass('ant-select-disabled');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('composes custom right variable converters with constant, null, and core fallbacks', () => {
|
|
183
|
+
const value: VariableFilterItemValue = {
|
|
184
|
+
path: 'name',
|
|
185
|
+
operator: '$eq',
|
|
186
|
+
value: '{{$context.data.id}}',
|
|
187
|
+
};
|
|
188
|
+
const model = CreateModel();
|
|
189
|
+
const rightVariableConverters: Pick<Converters, 'resolvePathFromValue' | 'resolveValueFromPath'> = {
|
|
190
|
+
resolvePathFromValue: (input) => (input === '{{$context.data.id}}' ? ['$context', 'data', 'id'] : undefined),
|
|
191
|
+
resolveValueFromPath: (metaTreeNode) =>
|
|
192
|
+
metaTreeNode.paths?.[0] === '$context' ? `{{${metaTreeNode.paths.join('.')}}}` : undefined,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
render(
|
|
196
|
+
<VariableFilterItem
|
|
197
|
+
value={value}
|
|
198
|
+
model={model}
|
|
199
|
+
rightAsVariable
|
|
200
|
+
rightVariableConverters={rightVariableConverters}
|
|
201
|
+
/>,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const variableInputProps = (
|
|
205
|
+
globalThis as {
|
|
206
|
+
__LAST_VARIABLE_INPUT_PROPS__?: {
|
|
207
|
+
converters?: Converters;
|
|
208
|
+
value?: unknown;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
).__LAST_VARIABLE_INPUT_PROPS__;
|
|
212
|
+
const converters = variableInputProps?.converters;
|
|
213
|
+
const constantNode: MetaTreeNode = { name: 'constant', title: 'Constant', type: 'string', paths: ['constant'] };
|
|
214
|
+
const nullNode: MetaTreeNode = { name: 'null', title: 'Null', type: 'object', paths: ['null'] };
|
|
215
|
+
const workflowNode: MetaTreeNode = {
|
|
216
|
+
name: 'id',
|
|
217
|
+
title: 'ID',
|
|
218
|
+
type: 'number',
|
|
219
|
+
paths: ['$context', 'data', 'id'],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
expect(variableInputProps?.value).toBe('{{$context.data.id}}');
|
|
223
|
+
expect(converters?.resolvePathFromValue?.('{{$context.data.id}}')).toEqual(['$context', 'data', 'id']);
|
|
224
|
+
expect(converters?.resolvePathFromValue?.('{{ ctx.$context.data.id }}')).toEqual(['$context', 'data', 'id']);
|
|
225
|
+
expect(converters?.resolveValueFromPath?.(constantNode)).toBe('');
|
|
226
|
+
expect(converters?.resolveValueFromPath?.(nullNode)).toBeNull();
|
|
227
|
+
expect(converters?.resolveValueFromPath?.(workflowNode)).toBe('{{$context.data.id}}');
|
|
228
|
+
});
|
|
229
|
+
|
|
161
230
|
it('uses scoped context dataSourceManager when app dataSourceManager has no field interface manager', async () => {
|
|
162
231
|
const value = observable({ path: '', operator: '', value: '' }) as any;
|
|
163
232
|
const model = CreateModel();
|
package/src/flow/index.ts
CHANGED
|
@@ -98,6 +98,7 @@ export * from './FlowPage';
|
|
|
98
98
|
export * from './models';
|
|
99
99
|
export * from './utils';
|
|
100
100
|
export * from './actions';
|
|
101
|
+
export { FieldAssignValueInput } from './components/FieldAssignValueInput';
|
|
101
102
|
export * from './system-settings';
|
|
102
103
|
export * from './admin-shell/admin-layout';
|
|
103
104
|
export * from './admin-shell/BaseLayoutModel';
|