@nocobase/client-v2 2.2.0-beta.8 → 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 +3 -0
- package/es/RouteRepository.d.ts +8 -0
- package/es/RouterManager.d.ts +15 -0
- package/es/authRedirect.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/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
- 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/ActionModelCore.d.ts +2 -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/routeTransientInputArgs.d.ts +14 -0
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +6 -0
- package/es/index.mjs +279 -158
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +280 -159
- 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 +13 -6
- package/src/RouteRepository.ts +25 -0
- package/src/RouterManager.tsx +142 -1
- package/src/__tests__/RouteRepository.test.ts +23 -0
- package/src/__tests__/RouterManager.test.ts +125 -0
- package/src/__tests__/app.test.tsx +73 -0
- package/src/__tests__/authRedirect.test.ts +17 -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/authRedirect.ts +38 -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/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/FlowPage.tsx +38 -3
- package/src/flow/__tests__/FlowPage.test.tsx +201 -27
- package/src/flow/__tests__/FlowRoute.test.tsx +549 -4
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- 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/__tests__/openView.defineProps.route.test.tsx +80 -20
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/dateTimeFormat.tsx +2 -2
- 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/actions/openView.tsx +21 -1
- package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -5
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
- package/src/flow/common/Markdown/Display.tsx +14 -3
- package/src/flow/common/Markdown/Edit.tsx +19 -7
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +91 -20
- package/src/flow/components/FlowRoute.tsx +134 -18
- 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/internal/components/Markdown/util.ts +2 -1
- package/src/flow/models/base/ActionModel.tsx +10 -8
- package/src/flow/models/base/ActionModelCore.tsx +5 -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/blocks/table/TableColumnModel.tsx +6 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
- package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
- 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/models/topbar/TopbarActionModel.tsx +78 -3
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +6 -0
- package/src/locale/languageCodes.ts +2 -1
- package/src/nocobase-buildin-plugin/index.tsx +19 -1
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
- package/src/utils/markdownSanitize.ts +88 -0
|
@@ -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';
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
import _ from 'lodash';
|
|
11
11
|
import { useEffect, useState } from 'react';
|
|
12
|
+
import { stripMarkdownIframes } from '../../../../utils/markdownSanitize';
|
|
12
13
|
|
|
13
14
|
export const parseMarkdown = _.memoize(async (text: string) => {
|
|
14
15
|
if (!text) {
|
|
15
16
|
return text;
|
|
16
17
|
}
|
|
17
18
|
const m = await import('./md');
|
|
18
|
-
return m.default.render(text);
|
|
19
|
+
return stripMarkdownIframes(m.default.render(text));
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
export function useParseMarkdown(text: string) {
|
|
@@ -20,7 +20,7 @@ import { ActionModel } from './ActionModelCore';
|
|
|
20
20
|
|
|
21
21
|
function getMobileDefaultProps(ctx) {
|
|
22
22
|
const defaultProps = ctx.model.defaultProps || {};
|
|
23
|
-
if (ctx.isMobileLayout && defaultProps.icon) {
|
|
23
|
+
if (ctx.isMobileLayout && defaultProps.icon && ctx.model.enableEditIconOnly !== false) {
|
|
24
24
|
return {
|
|
25
25
|
...defaultProps,
|
|
26
26
|
iconOnly: true,
|
|
@@ -43,6 +43,7 @@ ActionModel.registerFlow({
|
|
|
43
43
|
'x-decorator': 'FormItem',
|
|
44
44
|
'x-component': 'Input',
|
|
45
45
|
title: tExpr('Button title'),
|
|
46
|
+
description: ctx.model.getTitleFieldDescription?.(),
|
|
46
47
|
}
|
|
47
48
|
: undefined,
|
|
48
49
|
tooltip: ctx.model.enableEditTooltip
|
|
@@ -59,13 +60,14 @@ ActionModel.registerFlow({
|
|
|
59
60
|
title: tExpr('Button icon'),
|
|
60
61
|
}
|
|
61
62
|
: undefined,
|
|
62
|
-
iconOnly:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
iconOnly:
|
|
64
|
+
ctx.model.enableEditIcon && ctx.model.enableEditIconOnly !== false
|
|
65
|
+
? {
|
|
66
|
+
'x-decorator': 'FormItem',
|
|
67
|
+
'x-component': 'Switch',
|
|
68
|
+
title: tExpr('Icon only'),
|
|
69
|
+
}
|
|
70
|
+
: undefined,
|
|
69
71
|
type: ctx.model.enableEditType
|
|
70
72
|
? {
|
|
71
73
|
'x-decorator': 'FormItem',
|
|
@@ -62,6 +62,7 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
62
62
|
enableEditTooltip = true;
|
|
63
63
|
enableEditTitle = true;
|
|
64
64
|
enableEditIcon = true;
|
|
65
|
+
enableEditIconOnly = true;
|
|
65
66
|
enableEditType = true;
|
|
66
67
|
enableEditDanger = true;
|
|
67
68
|
enableEditColor = false;
|
|
@@ -136,6 +137,10 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
136
137
|
return this.props.title;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
getTitleFieldDescription() {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
getIcon() {
|
|
140
145
|
return this.props.icon;
|
|
141
146
|
}
|