@nocobase/flow-engine 2.1.0-alpha.1 → 2.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/BlockScopedFlowEngine.js +0 -1
- package/lib/FlowDefinition.d.ts +2 -0
- package/lib/JSRunner.d.ts +6 -0
- package/lib/JSRunner.js +32 -2
- package/lib/ViewScopedFlowEngine.js +3 -0
- package/lib/acl/Acl.js +13 -3
- package/lib/components/FlowContextSelector.js +155 -10
- package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
- package/lib/components/dnd/gridDragPlanner.js +53 -1
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
- package/lib/components/variables/VariableInput.js +9 -4
- package/lib/components/variables/VariableTag.js +46 -39
- package/lib/components/variables/utils.d.ts +7 -0
- package/lib/components/variables/utils.js +42 -2
- package/lib/data-source/index.d.ts +7 -27
- package/lib/data-source/index.js +84 -51
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +163 -22
- package/lib/flowContext.d.ts +230 -7
- package/lib/flowContext.js +2267 -148
- package/lib/flowEngine.d.ts +21 -0
- package/lib/flowEngine.js +55 -13
- package/lib/flowI18n.js +6 -4
- package/lib/flowSettings.js +17 -11
- package/lib/index.d.ts +7 -1
- package/lib/index.js +21 -0
- package/lib/locale/en-US.json +9 -2
- package/lib/locale/index.d.ts +14 -0
- package/lib/locale/zh-CN.json +8 -1
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- package/lib/models/flowModel.d.ts +7 -0
- package/lib/models/flowModel.js +66 -1
- package/lib/provider.js +7 -6
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +4 -3
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/base.js +706 -41
- package/lib/runjs-context/contributions.d.ts +33 -0
- package/lib/runjs-context/contributions.js +88 -0
- package/lib/runjs-context/helpers.js +12 -1
- package/lib/runjs-context/setup.js +6 -0
- package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
- package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
- package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
- package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
- package/lib/runjs-context/snippets/index.d.ts +11 -1
- package/lib/runjs-context/snippets/index.js +61 -40
- package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
- package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
- package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
- package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
- package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
- package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
- package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
- package/lib/scheduler/ModelOperationScheduler.js +25 -21
- package/lib/types.d.ts +27 -0
- package/lib/utils/associationObjectVariable.d.ts +2 -2
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/createEphemeralContext.js +2 -2
- package/lib/utils/dateVariable.d.ts +16 -0
- package/lib/utils/dateVariable.js +380 -0
- package/lib/utils/exceptions.d.ts +7 -0
- package/lib/utils/exceptions.js +10 -0
- package/lib/utils/index.d.ts +8 -3
- package/lib/utils/index.js +45 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
- package/lib/utils/resolveRunJSObjectValues.js +61 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/runjsValue.d.ts +29 -0
- package/lib/utils/runjsValue.js +275 -0
- package/lib/utils/safeGlobals.d.ts +18 -8
- package/lib/utils/safeGlobals.js +164 -17
- package/lib/utils/schema-utils.d.ts +10 -0
- package/lib/utils/schema-utils.js +61 -0
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- package/lib/views/useDialog.js +8 -3
- package/lib/views/useDrawer.js +7 -2
- package/lib/views/usePage.d.ts +4 -0
- package/lib/views/usePage.js +43 -6
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +4 -4
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/JSRunner.ts +44 -2
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +64 -0
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flowContext.test.ts +693 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
- package/src/__tests__/flowRuntimeContext.test.ts +2 -1
- package/src/__tests__/flowSettings.open.test.tsx +123 -19
- package/src/__tests__/provider.test.tsx +0 -5
- package/src/__tests__/runjsContext.test.ts +10 -7
- package/src/__tests__/runjsContextImplementations.test.ts +34 -3
- package/src/__tests__/runjsContextRuntime.test.ts +3 -3
- package/src/__tests__/runjsContributions.test.ts +89 -0
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsLocales.test.ts +4 -1
- package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
- package/src/__tests__/runjsSnippets.test.ts +40 -3
- package/src/acl/Acl.tsx +3 -3
- package/src/components/FlowContextSelector.tsx +208 -12
- package/src/components/__tests__/gridDragPlanner.test.ts +141 -1
- package/src/components/dnd/gridDragPlanner.ts +60 -0
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
- package/src/components/variables/VariableInput.tsx +12 -4
- package/src/components/variables/VariableTag.tsx +54 -45
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
- package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
- package/src/components/variables/__tests__/utils.test.ts +81 -3
- package/src/components/variables/utils.ts +67 -6
- package/src/data-source/index.ts +88 -110
- package/src/executor/FlowExecutor.ts +200 -23
- package/src/executor/__tests__/flowExecutor.test.ts +66 -0
- package/src/flowContext.ts +2986 -211
- package/src/flowEngine.ts +58 -13
- package/src/flowI18n.ts +7 -5
- package/src/flowSettings.ts +18 -12
- package/src/index.ts +14 -1
- package/src/locale/en-US.json +9 -2
- package/src/locale/zh-CN.json +8 -1
- package/src/models/CollectionFieldModel.tsx +3 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
- package/src/models/__tests__/flowModel.clone.test.ts +416 -0
- package/src/models/__tests__/flowModel.test.ts +20 -4
- package/src/models/flowModel.tsx +94 -1
- package/src/provider.tsx +9 -7
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +11 -6
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/base.ts +715 -44
- package/src/runjs-context/contributions.ts +88 -0
- package/src/runjs-context/helpers.ts +11 -1
- package/src/runjs-context/setup.ts +6 -0
- package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
- package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
- package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
- package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
- package/src/runjs-context/snippets/index.ts +75 -41
- package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
- package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
- package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
- package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
- package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
- package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
- package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
- package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +27 -21
- package/src/types.ts +38 -1
- package/src/utils/__tests__/dateVariable.test.ts +101 -0
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/runjsValue.test.ts +44 -0
- package/src/utils/__tests__/safeGlobals.test.ts +57 -2
- package/src/utils/__tests__/utils.test.ts +95 -0
- package/src/utils/associationObjectVariable.ts +2 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/createEphemeralContext.ts +5 -4
- package/src/utils/dateVariable.ts +397 -0
- package/src/utils/exceptions.ts +11 -0
- package/src/utils/index.ts +37 -3
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/resolveRunJSObjectValues.ts +46 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/runjsValue.ts +287 -0
- package/src/utils/safeGlobals.ts +188 -17
- package/src/utils/schema-utils.ts +79 -0
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/useDialog.tsx +9 -2
- package/src/views/useDrawer.tsx +8 -1
- package/src/views/usePage.tsx +51 -5
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React, { useCallback, useRef, useMemo, useState, useEffect } from 'react';
|
|
11
|
-
import { Button, Cascader, Tooltip } from 'antd';
|
|
11
|
+
import { Button, Cascader, Input, Tooltip, theme } from 'antd';
|
|
12
12
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
13
13
|
import { cx, css } from '@emotion/css';
|
|
14
14
|
import type { ContextSelectorItem, FlowContextSelectorProps } from './variables/types';
|
|
15
15
|
import {
|
|
16
16
|
buildContextSelectorItems,
|
|
17
|
+
filterLoadedContextSelectorItems,
|
|
17
18
|
formatPathToValue,
|
|
18
19
|
parseValueToPath,
|
|
19
20
|
preloadContextSelectorPath,
|
|
@@ -34,6 +35,52 @@ const cascaderPopupAutoHeightClassName = css`
|
|
|
34
35
|
}
|
|
35
36
|
`;
|
|
36
37
|
|
|
38
|
+
type SelectedPathInfo = {
|
|
39
|
+
text: string;
|
|
40
|
+
meta?: ContextSelectorItem['meta'];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const normalizePath = (path: unknown): string[] | undefined => {
|
|
44
|
+
if (!Array.isArray(path)) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return path.map((segment) => String(segment));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getSelectedPathInfo = (path: string[] | undefined, options: ContextSelectorItem[]): SelectedPathInfo => {
|
|
52
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
53
|
+
return { text: '', meta: undefined };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const labels: string[] = [];
|
|
57
|
+
let currentOptions = options;
|
|
58
|
+
let selectedMeta: ContextSelectorItem['meta'] | undefined;
|
|
59
|
+
|
|
60
|
+
for (const segment of path) {
|
|
61
|
+
const matchedOption = currentOptions.find((item) => String(item.value) === String(segment));
|
|
62
|
+
if (!matchedOption) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const label =
|
|
67
|
+
typeof matchedOption.meta?.title === 'string'
|
|
68
|
+
? matchedOption.meta.title
|
|
69
|
+
: typeof matchedOption.label === 'string'
|
|
70
|
+
? matchedOption.label
|
|
71
|
+
: String(matchedOption.value);
|
|
72
|
+
|
|
73
|
+
labels.push(label);
|
|
74
|
+
selectedMeta = matchedOption.meta;
|
|
75
|
+
currentOptions = Array.isArray(matchedOption.children) ? matchedOption.children : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
text: labels.join(' / '),
|
|
80
|
+
meta: selectedMeta,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
37
84
|
const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
38
85
|
value,
|
|
39
86
|
onChange,
|
|
@@ -47,13 +94,15 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
47
94
|
ignoreFieldNames,
|
|
48
95
|
...cascaderProps
|
|
49
96
|
}) => {
|
|
97
|
+
const { token } = theme.useToken();
|
|
98
|
+
|
|
50
99
|
// 记录最后点击的路径,用于双击检测
|
|
51
100
|
const lastSelectedRef = useRef<{ path: string; time: number } | null>(null);
|
|
52
101
|
|
|
53
102
|
const { resolvedMetaTree, loading } = useResolvedMetaTree(metaTree);
|
|
54
103
|
|
|
55
104
|
// 获取引擎上下文中的翻译函数,若不可用则回退为原文
|
|
56
|
-
const flowCtx = useFlowContext
|
|
105
|
+
const flowCtx = useFlowContext();
|
|
57
106
|
|
|
58
107
|
const translateOptions = useCallback(
|
|
59
108
|
(items: ContextSelectorItem[] | undefined): ContextSelectorItem[] => {
|
|
@@ -63,7 +112,9 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
63
112
|
const meta = o.meta;
|
|
64
113
|
const disabled = meta ? !!(typeof meta.disabled === 'function' ? meta.disabled() : meta.disabled) : false;
|
|
65
114
|
const disabledReason = meta
|
|
66
|
-
?
|
|
115
|
+
? typeof meta.disabledReason === 'function'
|
|
116
|
+
? meta.disabledReason()
|
|
117
|
+
: meta.disabledReason
|
|
67
118
|
: undefined;
|
|
68
119
|
|
|
69
120
|
// 文本国际化:仅当 label 为字符串时进行翻译
|
|
@@ -98,7 +149,11 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
98
149
|
|
|
99
150
|
// 用于强制重新渲染的状态
|
|
100
151
|
const [updateFlag, setUpdateFlag] = useState(0);
|
|
152
|
+
const [searchText, setSearchText] = useState('');
|
|
153
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
154
|
+
const inlineFocusByPointerRef = useRef(false);
|
|
101
155
|
const triggerUpdate = useCallback(() => setUpdateFlag((prev) => prev + 1), []);
|
|
156
|
+
const isSearchEnabled = showSearch || children === null;
|
|
102
157
|
|
|
103
158
|
// 构建选项
|
|
104
159
|
// 注意:rc-cascader 内部对 options 做了基于引用的缓存(useEntities)。
|
|
@@ -106,13 +161,22 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
106
161
|
// 触发 rc-cascader 重新构建 pathKeyEntities,避免二级节点未被索引导致的报错。
|
|
107
162
|
const options = useMemo(() => {
|
|
108
163
|
if (!resolvedMetaTree) return [];
|
|
164
|
+
const refreshSeq = updateFlag;
|
|
109
165
|
const base = buildContextSelectorItems(resolvedMetaTree);
|
|
110
|
-
|
|
166
|
+
const filtered = translateOptions(base).filter((item) => {
|
|
111
167
|
if (!ignoreFieldNames || ignoreFieldNames.length === 0) return true;
|
|
112
168
|
return !ignoreFieldNames.includes(item.meta?.name || '');
|
|
113
169
|
});
|
|
170
|
+
return refreshSeq >= 0 ? filtered : [];
|
|
114
171
|
}, [resolvedMetaTree, updateFlag, translateOptions, ignoreFieldNames]);
|
|
115
172
|
|
|
173
|
+
const displayOptions = useMemo(() => {
|
|
174
|
+
if (!isSearchEnabled || !searchText.trim()) {
|
|
175
|
+
return options;
|
|
176
|
+
}
|
|
177
|
+
return filterLoadedContextSelectorItems(options, searchText);
|
|
178
|
+
}, [isSearchEnabled, options, searchText]);
|
|
179
|
+
|
|
116
180
|
// 内部展开路径:在 onlyLeafSelectable=true 时,点击父节点不会触发 onChange,
|
|
117
181
|
// 但会触发 loadData。我们在此记录路径以在懒加载后保持展开。
|
|
118
182
|
const [tempSelectedPath, setTempSelectedPath] = useState<string[]>([]);
|
|
@@ -158,23 +222,36 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
158
222
|
triggerUpdate();
|
|
159
223
|
}
|
|
160
224
|
},
|
|
161
|
-
[triggerUpdate],
|
|
225
|
+
[triggerUpdate, translateOptions],
|
|
162
226
|
);
|
|
163
227
|
|
|
164
228
|
const currentPath = useMemo(() => {
|
|
165
|
-
return customParseValueToPath(value);
|
|
229
|
+
return normalizePath(customParseValueToPath(value));
|
|
166
230
|
}, [value, customParseValueToPath]);
|
|
167
231
|
|
|
168
232
|
// 当 metaTree 为子层(如 getPropertyMetaTree('{{ ctx.collection }}') 返回的是 collection 的子节点)
|
|
169
233
|
// 而 value path 仍包含根键(如 ['collection', 'field'])时,自动丢弃不存在的首段,确保级联能正确对齐。
|
|
170
234
|
const effectivePath = useMemo(() => {
|
|
171
235
|
if (!currentPath || currentPath.length === 0) return currentPath;
|
|
236
|
+
|
|
237
|
+
if (options.length === 0) {
|
|
238
|
+
return currentPath;
|
|
239
|
+
}
|
|
240
|
+
|
|
172
241
|
const topValues = new Set(options.map((o) => String(o.value)));
|
|
173
242
|
const needTrim = !topValues.has(String(currentPath[0]));
|
|
174
243
|
const fixed = needTrim ? currentPath.slice(1) : currentPath;
|
|
175
244
|
return fixed;
|
|
176
245
|
}, [currentPath, options]);
|
|
177
246
|
|
|
247
|
+
const cascaderValue = useMemo(() => {
|
|
248
|
+
if (tempSelectedPath.length > 0) {
|
|
249
|
+
return tempSelectedPath;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return Array.isArray(effectivePath) ? effectivePath : undefined;
|
|
253
|
+
}, [effectivePath, tempSelectedPath]);
|
|
254
|
+
|
|
178
255
|
// 预加载:当存在有效路径时,按路径逐级加载 children,保证默认展开和选中路径可用
|
|
179
256
|
const pathToPreload = useMemo(() => {
|
|
180
257
|
const finalPath = effectivePath && effectivePath.length > 0 ? effectivePath : tempSelectedPath;
|
|
@@ -251,21 +328,140 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
251
328
|
return cx(cascaderPopupAutoHeightClassName, cascaderProps.popupClassName);
|
|
252
329
|
}, [cascaderProps.popupClassName]);
|
|
253
330
|
|
|
331
|
+
const cascaderSearchInputClassName = useMemo(() => {
|
|
332
|
+
return css`
|
|
333
|
+
padding: 8px;
|
|
334
|
+
border-bottom: 1px solid ${token.colorSplit};
|
|
335
|
+
`;
|
|
336
|
+
}, [token.colorSplit]);
|
|
337
|
+
|
|
338
|
+
const {
|
|
339
|
+
onDropdownVisibleChange: cascaderOnDropdownVisibleChange,
|
|
340
|
+
dropdownRender: cascaderDropdownRender,
|
|
341
|
+
...restCascaderProps
|
|
342
|
+
} = cascaderProps;
|
|
343
|
+
|
|
344
|
+
const selectedPathInfo = useMemo(() => getSelectedPathInfo(effectivePath, options), [effectivePath, options]);
|
|
345
|
+
|
|
346
|
+
const mergedOpen = open !== undefined ? open : children === null ? dropdownOpen : undefined;
|
|
347
|
+
|
|
348
|
+
const isDropdownVisible = !!mergedOpen;
|
|
349
|
+
|
|
350
|
+
const handleDropdownVisibleChange = useCallback(
|
|
351
|
+
(visible: boolean) => {
|
|
352
|
+
if (open === undefined) {
|
|
353
|
+
setDropdownOpen(visible);
|
|
354
|
+
}
|
|
355
|
+
if (!visible) {
|
|
356
|
+
setSearchText('');
|
|
357
|
+
}
|
|
358
|
+
cascaderOnDropdownVisibleChange?.(visible);
|
|
359
|
+
},
|
|
360
|
+
[cascaderOnDropdownVisibleChange, open],
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const renderDropdown = useCallback(
|
|
364
|
+
(menu: React.ReactElement) => {
|
|
365
|
+
const cascaderMenuNode = cascaderDropdownRender ? cascaderDropdownRender(menu) : menu;
|
|
366
|
+
const cascaderMenu = React.isValidElement(cascaderMenuNode) ? cascaderMenuNode : <>{cascaderMenuNode}</>;
|
|
367
|
+
if (!isSearchEnabled || children === null) {
|
|
368
|
+
return cascaderMenu;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<>
|
|
373
|
+
<div className={cascaderSearchInputClassName}>
|
|
374
|
+
<Input
|
|
375
|
+
allowClear
|
|
376
|
+
size="small"
|
|
377
|
+
value={searchText}
|
|
378
|
+
placeholder={flowCtx.t('Search')}
|
|
379
|
+
onChange={(e) => setSearchText(e.target.value)}
|
|
380
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
{cascaderMenu}
|
|
384
|
+
</>
|
|
385
|
+
);
|
|
386
|
+
},
|
|
387
|
+
[cascaderDropdownRender, cascaderSearchInputClassName, children, flowCtx, isSearchEnabled, searchText],
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const inlinePlaceholder =
|
|
391
|
+
typeof restCascaderProps.placeholder === 'string' ? restCascaderProps.placeholder : flowCtx.t('Search');
|
|
392
|
+
const hasSelectedPath = Array.isArray(effectivePath) && effectivePath.length > 0;
|
|
393
|
+
|
|
394
|
+
const handleInlineInputFocus = useCallback(() => {
|
|
395
|
+
if (open === undefined && !inlineFocusByPointerRef.current) {
|
|
396
|
+
setDropdownOpen(true);
|
|
397
|
+
}
|
|
398
|
+
}, [open]);
|
|
399
|
+
|
|
400
|
+
const markInlineFocusByPointer = useCallback(() => {
|
|
401
|
+
inlineFocusByPointerRef.current = true;
|
|
402
|
+
}, []);
|
|
403
|
+
|
|
404
|
+
const resetInlineFocusByPointer = useCallback(() => {
|
|
405
|
+
inlineFocusByPointerRef.current = false;
|
|
406
|
+
}, []);
|
|
407
|
+
|
|
408
|
+
const handleInlineInputChange = useCallback(
|
|
409
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
410
|
+
const nextValue = event.target.value;
|
|
411
|
+
|
|
412
|
+
// 下拉关闭态下点击清空:应清空真实已选值,而不是仅清空搜索词。
|
|
413
|
+
if (!isDropdownVisible && nextValue === '' && hasSelectedPath) {
|
|
414
|
+
setTempSelectedPath([]);
|
|
415
|
+
// 清空语义:传空 meta,确保上层(如 VariableInput)进入 clear 分支。
|
|
416
|
+
onChange?.('', undefined);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (open === undefined && !isDropdownVisible) {
|
|
421
|
+
setDropdownOpen(true);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
setSearchText(nextValue);
|
|
425
|
+
},
|
|
426
|
+
[hasSelectedPath, isDropdownVisible, onChange, open],
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const inlinePathText = Array.isArray(effectivePath) ? effectivePath.join(' / ') : '';
|
|
430
|
+
const inlineInputValue = isDropdownVisible ? searchText : selectedPathInfo.text || inlinePathText;
|
|
431
|
+
|
|
254
432
|
return (
|
|
255
433
|
<Cascader
|
|
256
|
-
{...
|
|
257
|
-
options={
|
|
258
|
-
value={
|
|
434
|
+
{...restCascaderProps}
|
|
435
|
+
options={displayOptions}
|
|
436
|
+
value={cascaderValue}
|
|
259
437
|
onChange={handleChange}
|
|
260
438
|
loadData={handleLoadData}
|
|
261
439
|
loading={loading}
|
|
262
440
|
changeOnSelect={!onlyLeafSelectable}
|
|
263
441
|
expandTrigger="click"
|
|
264
|
-
open={
|
|
265
|
-
showSearch={
|
|
442
|
+
open={mergedOpen}
|
|
443
|
+
showSearch={false}
|
|
266
444
|
popupClassName={mergedPopupClassName}
|
|
445
|
+
dropdownRender={renderDropdown}
|
|
446
|
+
onDropdownVisibleChange={handleDropdownVisibleChange}
|
|
267
447
|
>
|
|
268
|
-
{children === null ?
|
|
448
|
+
{children === null ? (
|
|
449
|
+
<Input
|
|
450
|
+
allowClear
|
|
451
|
+
value={inlineInputValue}
|
|
452
|
+
placeholder={inlinePlaceholder}
|
|
453
|
+
onMouseDown={markInlineFocusByPointer}
|
|
454
|
+
onMouseUp={resetInlineFocusByPointer}
|
|
455
|
+
onMouseLeave={resetInlineFocusByPointer}
|
|
456
|
+
onFocus={handleInlineInputFocus}
|
|
457
|
+
onBlur={resetInlineFocusByPointer}
|
|
458
|
+
onChange={handleInlineInputChange}
|
|
459
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
460
|
+
disabled={restCascaderProps.disabled}
|
|
461
|
+
/>
|
|
462
|
+
) : (
|
|
463
|
+
children || defaultChildren
|
|
464
|
+
)}
|
|
269
465
|
</Cascader>
|
|
270
466
|
);
|
|
271
467
|
};
|
|
@@ -19,9 +19,14 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const rect = { top: 0, left: 0, width: 100, height: 100 };
|
|
21
21
|
|
|
22
|
-
const createLayout = (
|
|
22
|
+
const createLayout = (
|
|
23
|
+
rows: Record<string, string[][]>,
|
|
24
|
+
sizes: Record<string, number[]>,
|
|
25
|
+
rowOrder?: string[],
|
|
26
|
+
): GridLayoutData => ({
|
|
23
27
|
rows,
|
|
24
28
|
sizes,
|
|
29
|
+
rowOrder,
|
|
25
30
|
});
|
|
26
31
|
|
|
27
32
|
describe('getSlotKey', () => {
|
|
@@ -275,6 +280,7 @@ describe('simulateLayoutForSlot', () => {
|
|
|
275
280
|
rowA: [24],
|
|
276
281
|
rowB: [24],
|
|
277
282
|
},
|
|
283
|
+
['rowA', 'rowB'],
|
|
278
284
|
);
|
|
279
285
|
|
|
280
286
|
const slot: LayoutSlot = {
|
|
@@ -315,6 +321,33 @@ describe('simulateLayoutForSlot', () => {
|
|
|
315
321
|
expect(result.sizes['row-new']).toEqual([24]);
|
|
316
322
|
});
|
|
317
323
|
|
|
324
|
+
it('removes empty source row when moving item into empty container slot', () => {
|
|
325
|
+
const layout = createLayout(
|
|
326
|
+
{
|
|
327
|
+
rowA: [['block-x']],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
rowA: [24],
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const slot: LayoutSlot = {
|
|
335
|
+
type: 'empty-row',
|
|
336
|
+
rect,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const result = simulateLayoutForSlot({
|
|
340
|
+
slot,
|
|
341
|
+
sourceUid: 'block-x',
|
|
342
|
+
layout,
|
|
343
|
+
generateRowId: () => 'row-new',
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(result.rows['row-new']).toEqual([['block-x']]);
|
|
347
|
+
expect(result.rows.rowA).toBeUndefined();
|
|
348
|
+
expect(result.sizes.rowA).toBeUndefined();
|
|
349
|
+
});
|
|
350
|
+
|
|
318
351
|
it('handles column slot with after position', () => {
|
|
319
352
|
const layout = createLayout(
|
|
320
353
|
{
|
|
@@ -373,6 +406,7 @@ describe('simulateLayoutForSlot', () => {
|
|
|
373
406
|
rowA: [24],
|
|
374
407
|
rowB: [24],
|
|
375
408
|
},
|
|
409
|
+
['rowA', 'rowB'],
|
|
376
410
|
);
|
|
377
411
|
|
|
378
412
|
const slot: LayoutSlot = {
|
|
@@ -392,6 +426,112 @@ describe('simulateLayoutForSlot', () => {
|
|
|
392
426
|
expect(Object.keys(result.rows)).toEqual(['rowA', 'row-inserted', 'rowB']);
|
|
393
427
|
});
|
|
394
428
|
|
|
429
|
+
it('inserts row into rowOrder when dropping below target row', () => {
|
|
430
|
+
const layout = createLayout(
|
|
431
|
+
{
|
|
432
|
+
rowA: [['a']],
|
|
433
|
+
rowB: [['b']],
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
rowA: [24],
|
|
437
|
+
rowB: [24],
|
|
438
|
+
},
|
|
439
|
+
['rowA', 'rowB'],
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
const slot: LayoutSlot = {
|
|
443
|
+
type: 'row-gap',
|
|
444
|
+
targetRowId: 'rowA',
|
|
445
|
+
position: 'below',
|
|
446
|
+
rect,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const result = simulateLayoutForSlot({
|
|
450
|
+
slot,
|
|
451
|
+
sourceUid: 'c',
|
|
452
|
+
layout,
|
|
453
|
+
generateRowId: () => 'row-new',
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(result.rowOrder).toEqual(['rowA', 'row-new', 'rowB']);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('maintains rowOrder and inserts new row before target when provided', () => {
|
|
460
|
+
const layout = createLayout(
|
|
461
|
+
{
|
|
462
|
+
rowA: [['a']],
|
|
463
|
+
rowB: [['b']],
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
rowA: [24],
|
|
467
|
+
rowB: [24],
|
|
468
|
+
},
|
|
469
|
+
['rowA', 'rowB'],
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const slot: LayoutSlot = {
|
|
473
|
+
type: 'row-gap',
|
|
474
|
+
targetRowId: 'rowB',
|
|
475
|
+
position: 'above',
|
|
476
|
+
rect,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const result = simulateLayoutForSlot({
|
|
480
|
+
slot,
|
|
481
|
+
sourceUid: 'c',
|
|
482
|
+
layout,
|
|
483
|
+
generateRowId: () => 'row-new',
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(result.rowOrder).toEqual(['rowA', 'row-new', 'rowB']);
|
|
487
|
+
expect(result.rows).toEqual({
|
|
488
|
+
rowA: [['a']],
|
|
489
|
+
'row-new': [['c']],
|
|
490
|
+
rowB: [['b']],
|
|
491
|
+
});
|
|
492
|
+
expect(result.sizes).toEqual({
|
|
493
|
+
rowA: [24],
|
|
494
|
+
'row-new': [24],
|
|
495
|
+
rowB: [24],
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('derives rowOrder from rows when missing and removes empty rows from order', () => {
|
|
500
|
+
const layout = createLayout(
|
|
501
|
+
{
|
|
502
|
+
row1: [['a']],
|
|
503
|
+
row2: [['b']],
|
|
504
|
+
row3: [['c']],
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
row1: [24],
|
|
508
|
+
row2: [24],
|
|
509
|
+
row3: [24],
|
|
510
|
+
},
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const slot: LayoutSlot = {
|
|
514
|
+
type: 'column',
|
|
515
|
+
rowId: 'row1',
|
|
516
|
+
columnIndex: 0,
|
|
517
|
+
insertIndex: 0,
|
|
518
|
+
position: 'before',
|
|
519
|
+
rect,
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const result = simulateLayoutForSlot({ slot, sourceUid: 'b', layout });
|
|
523
|
+
|
|
524
|
+
expect(result.rowOrder).toEqual(['row1', 'row3']);
|
|
525
|
+
expect(result.rows).toEqual({
|
|
526
|
+
row1: [['b', 'a']],
|
|
527
|
+
row3: [['c']],
|
|
528
|
+
});
|
|
529
|
+
expect(result.sizes).toEqual({
|
|
530
|
+
row1: [24],
|
|
531
|
+
row3: [24],
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
395
535
|
it('handles empty-column slot by replacing empty column', () => {
|
|
396
536
|
const layout = createLayout(
|
|
397
537
|
{
|
|
@@ -46,6 +46,7 @@ export interface Point {
|
|
|
46
46
|
export interface GridLayoutData {
|
|
47
47
|
rows: Record<string, string[][]>;
|
|
48
48
|
sizes: Record<string, number[]>;
|
|
49
|
+
rowOrder?: string[];
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface ColumnSlot {
|
|
@@ -142,6 +143,49 @@ export interface LayoutSnapshot {
|
|
|
142
143
|
containerRect: Rect;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
const deriveRowOrder = (rows: Record<string, string[][]>, provided?: string[]) => {
|
|
147
|
+
const order: string[] = [];
|
|
148
|
+
const used = new Set<string>();
|
|
149
|
+
|
|
150
|
+
(provided || Object.keys(rows)).forEach((rowId) => {
|
|
151
|
+
if (rows[rowId] && !used.has(rowId)) {
|
|
152
|
+
order.push(rowId);
|
|
153
|
+
used.add(rowId);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
Object.keys(rows).forEach((rowId) => {
|
|
158
|
+
if (!used.has(rowId)) {
|
|
159
|
+
order.push(rowId);
|
|
160
|
+
used.add(rowId);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return order;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const normalizeRowsWithOrder = (rows: Record<string, string[][]>, order: string[]) => {
|
|
168
|
+
const next: Record<string, string[][]> = {};
|
|
169
|
+
order.forEach((rowId) => {
|
|
170
|
+
if (rows[rowId]) {
|
|
171
|
+
next[rowId] = rows[rowId];
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
Object.keys(rows).forEach((rowId) => {
|
|
175
|
+
if (!next[rowId]) {
|
|
176
|
+
next[rowId] = rows[rowId];
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return next;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const ensureRowOrder = (layout: GridLayoutData) => {
|
|
183
|
+
const order = deriveRowOrder(layout.rows, layout.rowOrder);
|
|
184
|
+
layout.rowOrder = order;
|
|
185
|
+
layout.rows = normalizeRowsWithOrder(layout.rows, order);
|
|
186
|
+
return order;
|
|
187
|
+
};
|
|
188
|
+
|
|
145
189
|
export interface BuildLayoutSnapshotOptions {
|
|
146
190
|
container: HTMLElement | null;
|
|
147
191
|
}
|
|
@@ -465,10 +509,12 @@ const removeItemFromLayout = (layout: GridLayoutData, uidValue: string) => {
|
|
|
465
509
|
if (columns.length === 0) {
|
|
466
510
|
delete layout.rows[rowId];
|
|
467
511
|
delete layout.sizes[rowId];
|
|
512
|
+
ensureRowOrder(layout);
|
|
468
513
|
return;
|
|
469
514
|
}
|
|
470
515
|
|
|
471
516
|
normalizeRowSizes(rowId, layout);
|
|
517
|
+
ensureRowOrder(layout);
|
|
472
518
|
};
|
|
473
519
|
|
|
474
520
|
const toIntSizes = (weights: number[], count: number): number[] => {
|
|
@@ -592,8 +638,10 @@ export const simulateLayoutForSlot = ({
|
|
|
592
638
|
const cloned: GridLayoutData = {
|
|
593
639
|
rows: _.cloneDeep(layout.rows),
|
|
594
640
|
sizes: _.cloneDeep(layout.sizes),
|
|
641
|
+
rowOrder: layout.rowOrder ? [...layout.rowOrder] : undefined,
|
|
595
642
|
};
|
|
596
643
|
|
|
644
|
+
ensureRowOrder(cloned);
|
|
597
645
|
removeItemFromLayout(cloned, sourceUid);
|
|
598
646
|
|
|
599
647
|
const createRowId = generateRowId ?? uid;
|
|
@@ -638,8 +686,16 @@ export const simulateLayoutForSlot = ({
|
|
|
638
686
|
case 'row-gap': {
|
|
639
687
|
const newRowId = createRowId();
|
|
640
688
|
const rowPosition: 'before' | 'after' = slot.position === 'above' ? 'before' : 'after';
|
|
689
|
+
const currentOrder = deriveRowOrder(cloned.rows, cloned.rowOrder);
|
|
641
690
|
cloned.rows = insertRow(cloned.rows, slot.targetRowId, newRowId, rowPosition, [[sourceUid]]);
|
|
642
691
|
cloned.sizes[newRowId] = [DEFAULT_GRID_COLUMNS];
|
|
692
|
+
const targetIndex = currentOrder.indexOf(slot.targetRowId);
|
|
693
|
+
const insertIndex =
|
|
694
|
+
targetIndex === -1 ? currentOrder.length : rowPosition === 'before' ? targetIndex : targetIndex + 1;
|
|
695
|
+
const nextOrder = [...currentOrder];
|
|
696
|
+
nextOrder.splice(insertIndex, 0, newRowId);
|
|
697
|
+
cloned.rowOrder = nextOrder;
|
|
698
|
+
cloned.rows = normalizeRowsWithOrder(cloned.rows, nextOrder);
|
|
643
699
|
break;
|
|
644
700
|
}
|
|
645
701
|
case 'empty-row': {
|
|
@@ -649,11 +705,15 @@ export const simulateLayoutForSlot = ({
|
|
|
649
705
|
[newRowId]: [[sourceUid]],
|
|
650
706
|
};
|
|
651
707
|
cloned.sizes[newRowId] = [DEFAULT_GRID_COLUMNS];
|
|
708
|
+
const currentOrder = deriveRowOrder(cloned.rows, cloned.rowOrder);
|
|
709
|
+
cloned.rowOrder = [...currentOrder.filter((id) => id !== newRowId), newRowId];
|
|
710
|
+
cloned.rows = normalizeRowsWithOrder(cloned.rows, cloned.rowOrder);
|
|
652
711
|
break;
|
|
653
712
|
}
|
|
654
713
|
default:
|
|
655
714
|
break;
|
|
656
715
|
}
|
|
657
716
|
|
|
717
|
+
ensureRowOrder(cloned);
|
|
658
718
|
return cloned;
|
|
659
719
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { render, screen, fireEvent } from '@nocobase/test/client';
|
|
13
|
+
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
14
|
+
|
|
15
|
+
import { SwitchWithTitle } from '../SwitchWithTitle';
|
|
16
|
+
import { SelectWithTitle } from '../SelectWithTitle';
|
|
17
|
+
|
|
18
|
+
vi.mock('antd', async (importOriginal) => {
|
|
19
|
+
const actual = (await importOriginal()) as any;
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
Select: ({
|
|
23
|
+
popupMatchSelectWidth,
|
|
24
|
+
bordered,
|
|
25
|
+
popupClassName,
|
|
26
|
+
fieldNames,
|
|
27
|
+
labelRender,
|
|
28
|
+
optionRender,
|
|
29
|
+
dropdownRender,
|
|
30
|
+
options,
|
|
31
|
+
...props
|
|
32
|
+
}: any) => React.createElement('select', props),
|
|
33
|
+
Switch: ({ checkedChildren, unCheckedChildren, size, ...props }: any) =>
|
|
34
|
+
React.createElement('input', { ...props, type: 'checkbox', readOnly: true }),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Inline controls - stopPropagation', () => {
|
|
39
|
+
it('SwitchWithTitle click does not bubble to parent', async () => {
|
|
40
|
+
const engine = new FlowEngine();
|
|
41
|
+
const parentClick = vi.fn();
|
|
42
|
+
const onChange = vi.fn();
|
|
43
|
+
|
|
44
|
+
render(
|
|
45
|
+
<FlowEngineProvider engine={engine}>
|
|
46
|
+
<div onClick={parentClick}>
|
|
47
|
+
<SwitchWithTitle title="Enabled" itemKey="enabled" onChange={onChange} />
|
|
48
|
+
</div>
|
|
49
|
+
</FlowEngineProvider>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
fireEvent.click(screen.getByText('Enabled'));
|
|
53
|
+
|
|
54
|
+
expect(parentClick).not.toHaveBeenCalled();
|
|
55
|
+
expect(onChange).toHaveBeenCalledWith({ enabled: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('SelectWithTitle click does not bubble to parent', async () => {
|
|
59
|
+
const engine = new FlowEngine();
|
|
60
|
+
const parentClick = vi.fn();
|
|
61
|
+
|
|
62
|
+
render(
|
|
63
|
+
<FlowEngineProvider engine={engine}>
|
|
64
|
+
<div onClick={parentClick}>
|
|
65
|
+
<SelectWithTitle title="Mode" itemKey="mode" options={[{ label: 'A', value: 'a' }]} />
|
|
66
|
+
</div>
|
|
67
|
+
</FlowEngineProvider>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
fireEvent.click(screen.getByText('Mode'));
|
|
71
|
+
|
|
72
|
+
expect(parentClick).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
});
|