From 6daf805cd06bf9ea312f83cc5a0f79ce28b41f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Mon, 31 Aug 2026 15:55:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M28=20=E5=A2=9E=E5=8A=A0=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E6=B1=87=E8=81=9A=E5=8F=AF=E8=A7=86=E5=8C=96?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/base/collapse.svelte | 22 ++ .../core/NodeJoinModeSetting.svelte | 237 ++++++++++++++++++ .../core/NodeJoinModeSetting.test.ts | 82 ++++++ .../src/components/core/NodeWrapper.svelte | 137 +++++++++- .../src/components/utils/joinMode.test.ts | 157 ++++++++++++ .../src/components/utils/joinMode.ts | 163 ++++++++++++ 6 files changed, 785 insertions(+), 13 deletions(-) create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodeJoinModeSetting.svelte create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodeJoinModeSetting.test.ts create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/joinMode.test.ts create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/joinMode.ts diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/base/collapse.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/base/collapse.svelte index 5b498de2..0dc5ab2b 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/base/collapse.svelte +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/base/collapse.svelte @@ -8,6 +8,7 @@ key: string; icon?: string | Snippet; title: string | Snippet; + badge?: string | Snippet; titleHelp?: string; description?: string | Snippet; content: string | Snippet; @@ -50,6 +51,11 @@ {/if} + {#if item.badge} + + + + {/if} {#if item.titleHelp} + import { + JOIN_MODE_ALL, + JOIN_MODE_ANY, + type JoinMode, + } from '../utils/joinMode'; + + const { + value, + name, + allDisabledReason = '', + invalidMode = false, + onChange, + }: { + value: JoinMode | null; + name: string; + allDisabledReason?: string; + invalidMode?: boolean; + onChange?: (value: JoinMode) => void; + } = $props(); + + const descriptionId = $derived(`${name}-description`); + const warningId = $derived(`${name}-warning`); + + +
+ 执行时机 +

+ 多条入边时,选择节点何时开始执行 +

+ +
+ + + +
+ + {#if invalidMode || allDisabledReason} +
+ + + {invalidMode + ? '当前执行时机配置无效,请重新选择。' + : allDisabledReason} + +
+ {/if} +
+ + diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodeJoinModeSetting.test.ts b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodeJoinModeSetting.test.ts new file mode 100644 index 00000000..54d254e6 --- /dev/null +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodeJoinModeSetting.test.ts @@ -0,0 +1,82 @@ +import { flushSync, mount, unmount } from 'svelte'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import NodeJoinModeSetting from './NodeJoinModeSetting.svelte'; + +describe('NodeJoinModeSetting', () => { + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('uses native radios and reports a selected mode', async () => { + const onChange = vi.fn(); + const host = document.createElement('div'); + host.className = 'tf-theme-light'; + document.body.appendChild(host); + const app = mount(NodeJoinModeSetting, { + target: host, + props: { + name: 'join-node-a', + value: 'any', + onChange, + }, + }); + flushSync(); + + const radios = host.querySelectorAll('input[type="radio"]'); + expect(radios).toHaveLength(2); + expect(radios[0].checked).toBe(true); + radios[1].focus(); + expect(document.activeElement).toBe(radios[1]); + radios[1].click(); + expect(onChange).toHaveBeenCalledWith('all'); + + await unmount(app); + }); + + it('keeps an unsafe existing all selected while allowing any as recovery', async () => { + const onChange = vi.fn(); + const host = document.createElement('div'); + host.className = 'tf-theme-dark'; + document.body.appendChild(host); + const app = mount(NodeJoinModeSetting, { + target: host, + props: { + name: 'join-node-b', + value: 'all', + allDisabledReason: '存在条件路径,可能永久等待。', + onChange, + }, + }); + flushSync(); + + const radios = host.querySelectorAll('input[type="radio"]'); + expect(radios[1].checked).toBe(true); + expect(radios[1].disabled).toBe(true); + expect(host.querySelector('[role="alert"]')?.textContent).toContain('永久等待'); + radios[0].click(); + expect(onChange).toHaveBeenCalledWith('any'); + + await unmount(app); + }); + + it('shows an invalid configuration error without selecting a fallback', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const app = mount(NodeJoinModeSetting, { + target: host, + props: { + name: 'join-node-invalid', + value: null, + invalidMode: true, + }, + }); + flushSync(); + + const radios = host.querySelectorAll('input[type="radio"]'); + expect(Array.from(radios).every((radio) => !radio.checked)).toBe(true); + expect(host.querySelector('[role="alert"]')?.textContent).toContain('配置无效'); + + await unmount(app); + }); +}); diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodeWrapper.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodeWrapper.svelte index c73185b5..1d25d466 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodeWrapper.svelte +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodeWrapper.svelte @@ -15,6 +15,13 @@ import {getCurrentNodeId} from '#components/utils/NodeUtils'; import type {TinyflowNodeData} from '#types'; import {useTinyflowNodeSizeObserver} from '../utils/nodeSizeObserver'; + import {useTinyflowStore} from '#store/stores.svelte'; + import NodeJoinModeSetting from './NodeJoinModeSetting.svelte'; + import { + analyzeJoinMode, + getJoinModeBadge, + type JoinMode, + } from '../utils/joinMode'; const { data, @@ -52,13 +59,22 @@ const activeKeys = $derived.by(() => data.expand ? ['key'] : []); const { updateNodeData, getNode } = useSvelteFlow(); + const store = useTinyflowStore(); const updateNodeInternals = useUpdateNodeInternals(); + const joinModeAnalysis = $derived.by(() => analyzeJoinMode( + store.getNodes(), + store.getEdges(), + id, + )); + const joinModeBadge = $derived(getJoinModeBadge(data.joinMode)); + const items = $derived.by(() => { return [{ key: 'key', icon, title: data.title as string, + badge: joinModeBadge, titleHelp, description: data.description as string, content: children @@ -87,6 +103,17 @@ const MIN_LOOP_COUNT = 1; const MAX_LOOP_COUNT = 300; let loopCountHint = $state(''); + let advancedConditionOpen = $state(false); + + const updateJoinMode = (joinMode: JoinMode) => { + updateNodeData(id, {joinMode}); + }; + + $effect(() => { + if (String(data.condition || '').trim()) { + advancedConditionOpen = true; + } + }); const normalizeLoopCount = (value: unknown) => { const parsed = Number(value); @@ -210,16 +237,35 @@ }} value={data.description} /> + {#if joinModeAnalysis.incomingCount > 1} + + {/if} + {#if allowSettingOfCondition} -
- 执行条件: -