build: 清理 tinyflow-ui 构建告警

- 收口 Svelte props 响应式引用与 custom element props 声明

- 清理局部未使用样式与类型告警并保持现有业务语义
This commit is contained in:
2026-05-06 19:22:33 +08:00
parent ba70fec9a5
commit 8d07b306e5
34 changed files with 288 additions and 151 deletions

View File

@@ -1,10 +1,10 @@
<svelte:options customElement={{ <svelte:options customElement={{
tag: "tinyflow-component", tag: "tinyflow-component",
shadow: "none", shadow: "none",
// props: { props: {
// options: { reflect: true, type: 'Object', attribute: 'options' }, options: { type: 'Object', attribute: 'options' },
// onInit: { reflect: true, type: 'Object', attribute: 'onInit' } onInit: { type: 'Object', attribute: 'on-init' }
// }, },
}} /> }} />
<script lang="ts"> <script lang="ts">
@@ -14,31 +14,50 @@
import type {TinyflowData, TinyflowOptions} from '#types'; import type {TinyflowData, TinyflowOptions} from '#types';
import {setContext} from 'svelte'; import {setContext} from 'svelte';
const { options, onInit }: { const props = $props<{
options: TinyflowOptions, options: TinyflowOptions,
onInit: (svelteFlow: ReturnType<typeof useSvelteFlow>) => void, onInit: (svelteFlow: ReturnType<typeof useSvelteFlow>) => void,
} = $props(); }>();
let { data } = options; const parseData = (source: TinyflowOptions['data']) => {
let initialViewport = null; let nextData = source;
if (typeof nextData === 'string') {
if (typeof data === 'string') { try {
try { nextData = JSON.parse(nextData.trim());
data = JSON.parse(data.trim()); } catch (error) {
} catch (e) { console.error('Invalid JSON data:', nextData, error);
console.error('Invalid JSON data:', data); }
} }
} return nextData as TinyflowData | null | undefined;
initialViewport = (data as TinyflowData)?.viewport || null; };
const getOptions = () => props.options;
const contextOptions = new Proxy({} as TinyflowOptions, {
get(_target, property) {
return (getOptions() as Record<PropertyKey, unknown>)[property];
},
has(_target, property) {
return property in getOptions();
},
ownKeys() {
return Reflect.ownKeys(getOptions());
},
getOwnPropertyDescriptor(_target, property) {
const descriptor = Object.getOwnPropertyDescriptor(getOptions(), property);
return descriptor ? { ...descriptor, configurable: true } : undefined;
}
}) as TinyflowOptions;
const data = parseData(getOptions().data);
const initialViewport = data?.viewport || null;
store.init( store.init(
(data as TinyflowData)?.nodes || [], data?.nodes || [],
(data as TinyflowData)?.edges || [], data?.edges || [],
initialViewport, initialViewport,
); );
setContext('tinyflow_options', options); setContext('tinyflow_options', contextOptions);
</script> </script>
<SvelteFlowProvider> <SvelteFlowProvider>
<TinyflowCore {onInit} /> <TinyflowCore onInit={props.onInit} />
</SvelteFlowProvider> </SvelteFlowProvider>

View File

@@ -46,8 +46,6 @@
const { onInit }: { onInit: any; [key: string]: any } = $props(); const { onInit }: { onInit: any; [key: string]: any } = $props();
const svelteFlow = useSvelteFlow(); const svelteFlow = useSvelteFlow();
onInit(svelteFlow);
let showEdgePanel = $state(false); let showEdgePanel = $state(false);
let currentEdge = $state<Edge | null>(null); let currentEdge = $state<Edge | null>(null);
let nodePickerVisible = $state(false); let nodePickerVisible = $state(false);
@@ -342,7 +340,7 @@
{ duration: 180 } { duration: 180 }
); );
} else { } else {
svelteFlow.zoomTo(zoom, { duration: 180 }); (svelteFlow as any).zoomTo?.(zoom, { duration: 180 });
} }
} }
} }
@@ -726,6 +724,7 @@
} }
onMount(() => { onMount(() => {
onInit(svelteFlow);
store.updateEdges((edges) => edges.map((edge) => ensureEdgeVisualDefaults(edge))); store.updateEdges((edges) => edges.map((edge) => ensureEdgeVisualDefaults(edge)));
repairOrphanParentNodes(); repairOrphanParentNodes();
if (!readonly) { if (!readonly) {

View File

@@ -1,18 +1,30 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import type {MyHTMLButtonAttributes} from './types'; import type {MyHTMLButtonAttributes} from './types';
import type {Snippet} from 'svelte'; import type {Snippet} from 'svelte';
const { children, primary, onclick, ...rest }: MyHTMLButtonAttributes & { const {
children?: Snippet; children,
primary?: boolean; primary,
} = $props(); onclick,
class: className = '',
style = '',
...rest
}: Omit<MyHTMLButtonAttributes, 'class' | 'style'> & {
children?: Snippet;
primary?: boolean;
class?: string;
style?: string;
} = $props();
</script> </script>
<button <button
type="button" type="button"
{...rest} {...rest}
onclick={onclick} onclick={onclick}
class="tf-btn {primary?'tf-btn-primary':''} nopan nodrag {rest.class}" {style}
class="tf-btn {primary?'tf-btn-primary':''} nopan nodrag {className}"
> >
{@render children?.()} {@render children?.()}
</button> </button>

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import type {MyHTMLInputAttributes} from './types'; import type {MyHTMLInputAttributes} from './types';

View File

@@ -1,17 +1,30 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import {Button, Input} from './index'; import {Button, Input} from './index';
import type {MyHTMLAttributes} from './types'; import type {MyHTMLAttributes} from './types';
const { placeholder, label, value, buttonText = "选择...",onChosen, ...rest }: { const {
placeholder?: string; placeholder,
label?: any; label,
value?: any; value,
buttonText?:string buttonText = "选择...",
onChosen?: (value?: any, label?: any, event?: Event) => void, onChosen,
} & MyHTMLAttributes = $props(); class: className = '',
style = '',
...rest
}: Omit<MyHTMLAttributes, 'class' | 'style'> & {
placeholder?: string;
label?: any;
value?: any;
buttonText?: string;
onChosen?: (value?: any, label?: any, event?: Event) => void;
class?: string;
style?: string;
} = $props();
</script> </script>
<div {...rest} class="tf-chosen nopan nodrag {rest.class}"> <div {...rest} {style} class="tf-chosen nopan nodrag {className}">
<input type="hidden" value={value}> <input type="hidden" value={value}>
<Input value={label} {placeholder} style="flex-grow: 1;" disabled/> <Input value={label} {placeholder} style="flex-grow: 1;" disabled/>
<Button onclick={(e)=>{ <Button onclick={(e)=>{

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import type {Snippet} from 'svelte'; import type {Snippet} from 'svelte';
import {Render} from './index'; import {Render} from './index';

View File

@@ -1,6 +1,28 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import type {MyHTMLInputAttributes} from './types'; import type {MyHTMLInputAttributes} from './types';
const { ...rest }: MyHTMLInputAttributes = $props(); const {
class: className = '',
style = '',
type = 'text',
variant = 'default',
value,
...rest
}: Omit<MyHTMLInputAttributes, 'class' | 'style' | 'type' | 'value'> & {
class?: string;
style?: string;
type?: string;
value?: unknown;
variant?: 'default' | 'borderless';
} = $props();
</script> </script>
<input type="text" spellcheck="false" {...rest} class="tf-input nopan nodrag {rest.class}" /> <input
{type}
spellcheck="false"
{...rest}
{value}
{style}
class="tf-input {variant === 'borderless' ? 'tf-input-borderless' : ''} nopan nodrag {className}"
/>

View File

@@ -1,11 +1,24 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import Button from './button.svelte'; import Button from './button.svelte';
import type {MyHTMLButtonAttributes} from './types'; import type {MyHTMLButtonAttributes} from './types';
const { ...rest }: MyHTMLButtonAttributes = $props(); const {
class: className = '',
style,
...rest
}: MyHTMLButtonAttributes & {
class?: string | null;
style?: string | null;
} = $props();
</script> </script>
<Button {...rest} class="input-btn-more {rest.class}"> <Button
{...rest}
class="input-btn-more {className}"
style={style ?? undefined}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path <path
d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z"></path> d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z"></path>

View File

@@ -29,8 +29,9 @@
let floatingRef: any = $state(); let floatingRef: any = $state();
let hoveredItem: SelectItem | null = $state(null); let hoveredItem: SelectItem | null = $state(null);
let isOpen = $state(false); let isOpen = $state(false);
let selectedItem = $state<SelectItem | null>(null);
let selectedItem = $derived.by(() => { $effect(() => {
let found: SelectItem | null = null; let found: SelectItem | null = null;
const findItem = (items: SelectItem[]) => { const findItem = (items: SelectItem[]) => {
for (const it of items) { for (const it of items) {
@@ -41,8 +42,15 @@
} }
}; };
findItem(refOptions); findItem(refOptions);
return found; selectedItem = found;
}); });
let selectedNodeType = $derived(selectedItem?.nodeType);
let selectedNodeIcon = $derived(
selectedNodeType ? nodeIcons[selectedNodeType] : undefined,
);
let selectedLabel = $derived(
selectedItem?.displayLabel || selectedItem?.label || '',
);
function closeMenu() { function closeMenu() {
floatingRef?.hide(); floatingRef?.hide();
@@ -111,12 +119,12 @@
<div class="tf-mixed-box tf-mixed-ref-box"> <div class="tf-mixed-box tf-mixed-ref-box">
{#if selectedItem} {#if selectedItem}
<div class="tf-mixed-sel-val"> <div class="tf-mixed-sel-val">
{#if selectedItem.nodeType && nodeIcons[selectedItem.nodeType]} {#if selectedNodeType && selectedNodeIcon}
<span class="tf-mixed-val-icon"> <span class="tf-mixed-val-icon">
{@html nodeIcons[selectedItem.nodeType]} {@html selectedNodeIcon}
</span> </span>
{/if} {/if}
<span class="tf-mixed-val-name">{selectedItem.displayLabel || selectedItem.label}</span> <span class="tf-mixed-val-name">{selectedLabel}</span>
</div> </div>
{:else} {:else}
<div class="tf-mixed-placeholder">{placeholder}</div> <div class="tf-mixed-placeholder">{placeholder}</div>

View File

@@ -1,6 +1,10 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
let { target } = $props(); const props = $props<{ target?: string | (() => unknown) }>();
if (typeof target === 'undefined') target = "undefined"; const target = $derived(
typeof props.target === 'undefined' ? 'undefined' : props.target
);
</script> </script>
@@ -9,4 +13,3 @@
{:else } {:else }
{@html target} {@html target}
{/if} {/if}

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import {FloatingTrigger} from './index'; import {FloatingTrigger} from './index';
import type {SelectItem} from '#types'; import type {SelectItem} from '#types';
@@ -541,6 +543,7 @@
color: var(--tf-text-secondary); color: var(--tf-text-secondary);
line-height: 1.3; line-height: 1.3;
margin-top: 2px; margin-top: 2px;
line-clamp: 2;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;

View File

@@ -1,14 +1,27 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import type {MyHTMLTextareaAttributes} from './types'; import type {MyHTMLTextareaAttributes} from './types';
const { value, height, autoHeight = true, maxHeight, onHeightChange, ...rest }: MyHTMLTextareaAttributes & { const {
value?: any; value,
height?: string | number; height,
autoHeight?: boolean; autoHeight = true,
rows?: number; maxHeight,
maxHeight?: string | number; onHeightChange,
onHeightChange?: (height: string) => void; class: className = '',
} = $props(); style = '',
...rest
}: Omit<MyHTMLTextareaAttributes, 'value' | 'class' | 'style'> & {
value?: unknown;
class?: string;
style?: string;
height?: string | number;
autoHeight?: boolean;
rows?: number;
maxHeight?: string | number;
onHeightChange?: (height: string) => void;
} = $props();
let textareaEl: HTMLTextAreaElement; let textareaEl: HTMLTextAreaElement;
let defaultHeight: number; let defaultHeight: number;
@@ -69,6 +82,7 @@
bind:this={textareaEl} bind:this={textareaEl}
spellcheck="false" spellcheck="false"
{...rest} {...rest}
{style}
oninput={(e)=>{ oninput={(e)=>{
adjustHeight(); adjustHeight();
rest.oninput?.(e); rest.oninput?.(e);
@@ -77,5 +91,5 @@
adjustHeight(); adjustHeight();
rest.onchange?.(e); rest.onchange?.(e);
}} }}
class="tf-textarea nodrag nowheel {rest.class}" class="tf-textarea nodrag nowheel {className}"
>{value || ""}</textarea> >{String(value ?? '')}</textarea>

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte'; import {onMount} from 'svelte';
import {Compartment, EditorState, type Transaction} from '@codemirror/state'; import {Compartment, EditorState, type Transaction} from '@codemirror/state';

View File

@@ -79,7 +79,7 @@
}); });
triggerObject?.hide(); triggerObject?.hide();
}; };
let selectItems = useRefOptions(useChildrenOnly); let selectItems = useRefOptions(() => useChildrenOnly === true);
</script> </script>
@@ -175,4 +175,3 @@
</style> </style>

View File

@@ -275,13 +275,6 @@
align-items: center; align-items: center;
} }
.input-item-inline {
display: flex;
align-items: center;
font-size: 12px;
color: var(--tf-text-secondary);
}
.input-more-setting { .input-more-setting {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -14,6 +14,7 @@
import {useCopyNode} from '../utils/useCopyNode.svelte'; import {useCopyNode} from '../utils/useCopyNode.svelte';
import {getOptions} from '../utils/NodeUtils'; import {getOptions} from '../utils/NodeUtils';
import {getCurrentNodeId} from '#components/utils/NodeUtils'; import {getCurrentNodeId} from '#components/utils/NodeUtils';
import type {TinyflowNodeData} from '#types';
const { const {
data, data,
@@ -32,7 +33,7 @@
wrapperClass = '', wrapperClass = '',
onCollapse onCollapse
}: { }: {
data: NodeProps['data'], data: TinyflowNodeData,
id?: NodeProps['id'], id?: NodeProps['id'],
icon?: Snippet, icon?: Snippet,
handle?: Snippet, handle?: Snippet,
@@ -49,7 +50,7 @@
onCollapse?: (key: string) => void, onCollapse?: (key: string) => void,
} = $props(); } = $props();
let activeKeys = data.expand ? ['key'] : []; const activeKeys = $derived.by(() => data.expand ? ['key'] : []);
const { updateNodeData, getNode } = useSvelteFlow(); const { updateNodeData, getNode } = useSvelteFlow();
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import {defaultKeymap, history, historyKeymap} from '@codemirror/commands'; import {defaultKeymap, history, historyKeymap} from '@codemirror/commands';
import {Compartment, EditorState, type Extension} from '@codemirror/state'; import {Compartment, EditorState, type Extension} from '@codemirror/state';

View File

@@ -82,7 +82,7 @@
}); });
triggerObject?.hide(); triggerObject?.hide();
}; };
let selectItems = useRefOptions(useChildrenOnly); let selectItems = useRefOptions(() => useChildrenOnly === true);
</script> </script>

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte'; import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
@@ -7,11 +9,11 @@
import {useAddParameter} from '../utils/useAddParameter.svelte'; import {useAddParameter} from '../utils/useAddParameter.svelte';
import OutputDefList from '../core/OutputDefList.svelte'; import OutputDefList from '../core/OutputDefList.svelte';
import {onMount} from 'svelte'; import {onMount} from 'svelte';
import type {SelectItem} from '#types'; import type {SelectItem, TinyflowNodeData} from '#types';
import CodeScriptEditor from '../core/CodeScriptEditor.svelte'; import CodeScriptEditor from '../core/CodeScriptEditor.svelte';
const { data, ...rest }: { const { data, ...rest }: {
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); } = $props();

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import { import {
@@ -669,7 +671,20 @@
style="flex: 1; height: 100%; min-width: 0;" style="flex: 1; height: 100%; min-width: 0;"
/> />
{:else} {:else}
<div class="condition-branch-name" style="padding-left: 8px" ondblclick={(e) => { e.stopPropagation(); editingBranchId = branch.id; }}> <div
class="condition-branch-name"
style="padding-left: 8px"
role="button"
tabindex="0"
ondblclick={(e) => { e.stopPropagation(); editingBranchId = branch.id; }}
onkeydown={(e) => {
e.stopPropagation();
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
editingBranchId = branch.id;
}
}}
>
{branch.label || `条件分支${index + 1}`} {branch.label || `条件分支${index + 1}`}
</div> </div>
{/if} {/if}
@@ -1181,26 +1196,6 @@
height: 32px; height: 32px;
} }
.input-more-setting {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: var(--tf-bg-surface);
border: 1px solid var(--tf-border-color);
border-radius: 6px;
width: 180px;
box-shadow: var(--tf-shadow-medium);
.input-more-item {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: var(--tf-text-secondary);
}
}
.condition-expression-panel { .condition-expression-panel {
min-height: 140px; min-height: 140px;
display: flex; display: flex;

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte'; import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
@@ -7,12 +9,12 @@
import {useAddParameter} from '../utils/useAddParameter.svelte'; import {useAddParameter} from '../utils/useAddParameter.svelte';
import OutputDefList from '../core/OutputDefList.svelte'; import OutputDefList from '../core/OutputDefList.svelte';
import ConfirmParameterList from '../core/ConfirmParameterList.svelte'; import ConfirmParameterList from '../core/ConfirmParameterList.svelte';
import type {Parameter} from '#types'; import type {Parameter, TinyflowNodeData} from '#types';
import {deepEqual} from '#components/utils/deepEqual'; import {deepEqual} from '#components/utils/deepEqual';
const { data, ...rest }: { const { data, ...rest }: {
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); } = $props();
@@ -94,7 +96,7 @@
message: e.target.value message: e.target.value
} }
}) })
}} value={data.message as string||""} /> }} value={String(data.message || '')} />
</div> </div>
@@ -128,5 +130,3 @@
</style> </style>

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {type Node, type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte'; import {type Node, type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
@@ -8,17 +10,24 @@
import {getOptions} from '../utils/NodeUtils'; import {getOptions} from '../utils/NodeUtils';
import OutputDefList from '../core/OutputDefList.svelte'; import OutputDefList from '../core/OutputDefList.svelte';
import ParamTokenEditor from '../core/ParamTokenEditor.svelte'; import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
import type {TinyflowNodeData} from '#types';
import {onMount} from 'svelte';
const { data, ...rest }: { const props = $props<{
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); }>();
const data = $derived(props.data);
const currentNodeId = getCurrentNodeId(); const currentNodeId = getCurrentNodeId();
let currentNode = useNodesData(currentNodeId); let currentNode = useNodesData(currentNodeId);
const { addParameter } = useAddParameter(); const { addParameter } = useAddParameter();
const flowInstance = useSvelteFlow(); const flowInstance = useSvelteFlow();
const { updateNodeData: updateNodeDataInner } = flowInstance; const { updateNodeData: updateNodeDataInner } = flowInstance;
const getRestProps = () => {
const { data: _data, ...rest } = props;
return rest;
};
const editorParameters = $derived.by(() => { const editorParameters = $derived.by(() => {
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || []; return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
}); });
@@ -49,19 +58,22 @@
updateFormValue(form, (event.target as any)?.value); updateFormValue(form, (event.target as any)?.value);
}; };
const node = { const buildNode = (nextData = data) => ({
...rest, ...getRestProps(),
id: currentNodeId, id: currentNodeId,
data data: nextData
} as Node; } as Node);
const externalElement = document.createElement('div') as HTMLElement; const externalElement = document.createElement('div') as HTMLElement;
const options = getOptions(); const options = getOptions();
const customNode = options.customNodes![rest.type as string]; const customNode = $derived.by(() => options.customNodes![getRestProps().type as string]);
customNode.render?.(externalElement, node, flowInstance); const forms = $derived.by(() => customNode.forms);
const forms = customNode.forms;
let container: HTMLElement; onMount(() => {
customNode.render?.(externalElement, buildNode(), flowInstance);
});
let container = $state<HTMLElement | null>(null);
$effect(() => { $effect(() => {
// 注意:由于 $effect 的 state 自动追踪问题,需要 data.expand 方在 if 里的最前面 // 注意:由于 $effect 的 state 自动追踪问题,需要 data.expand 方在 if 里的最前面
if (data.expand && container) { if (data.expand && container) {
@@ -71,7 +83,7 @@
$effect(() => { $effect(() => {
if (data) { if (data) {
customNode.onUpdate?.(externalElement, { ...node, data }); customNode.onUpdate?.(externalElement, buildNode(data));
} }
}); });
@@ -94,7 +106,7 @@
</script> </script>
<NodeWrapper data={{...data, description: customNode.description}} {...rest}> <NodeWrapper data={{...data, description: customNode.description}} {...getRestProps()}>
{#snippet icon()} {#snippet icon()}
{@html customNode.icon} {@html customNode.icon}
@@ -285,6 +297,7 @@
background: var(--tf-slider-track-bg); background: var(--tf-slider-track-bg);
border-radius: 2px; border-radius: 2px;
outline: none; outline: none;
appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
} }

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps} from '@xyflow/svelte'; import {type NodeProps} from '@xyflow/svelte';
@@ -5,9 +7,10 @@
import {getCurrentNodeId} from '#components/utils/NodeUtils'; import {getCurrentNodeId} from '#components/utils/NodeUtils';
import RefParameterList from '../core/RefParameterList.svelte'; import RefParameterList from '../core/RefParameterList.svelte';
import {useAddParameter} from '../utils/useAddParameter.svelte'; import {useAddParameter} from '../utils/useAddParameter.svelte';
import type {TinyflowNodeData} from '#types';
const { data, ...rest }: { const { data, ...rest }: {
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); } = $props();
@@ -46,5 +49,3 @@
} }
</style> </style>

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte'; import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
@@ -10,9 +12,10 @@
// 添加生命周期函数 // 添加生命周期函数
import {onMount} from 'svelte'; import {onMount} from 'svelte';
import ParamTokenEditor from '../core/ParamTokenEditor.svelte'; import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
import type {TinyflowNodeData} from '#types';
const { data, ...rest }: { const { data, ...rest }: {
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); } = $props();
@@ -77,7 +80,7 @@
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || []; return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
}); });
const bodyMethods = new Set(['post', 'put', 'delete', 'patch']); const bodyMethods = new Set(['post', 'put', 'delete', 'patch']);
const showBodyConfig = $derived(bodyMethods.has((data.method || '').toLowerCase())); const showBodyConfig = $derived(bodyMethods.has(String(data.method || '').toLowerCase()));
</script> </script>
@@ -215,7 +218,7 @@
rows={5} rows={5}
style="width: 100%" style="width: 100%"
parameters={editorParameters} parameters={editorParameters}
placeholder="请输入 json 信息" value={data.bodyJson} placeholder="请输入 json 信息" value={String(data.bodyJson || '')}
oninput={(e:any)=>{ oninput={(e:any)=>{
updateNodeData(currentNodeId,{ updateNodeData(currentNodeId,{
bodyJson: e.target.value, bodyJson: e.target.value,
@@ -233,7 +236,7 @@
rows={5} rows={5}
style="width: 100%" style="width: 100%"
parameters={editorParameters} parameters={editorParameters}
placeholder="请输入请求信息" value={data.bodyRaw} placeholder="请输入请求信息" value={String(data.bodyRaw || '')}
oninput={(e:any)=>{ oninput={(e:any)=>{
updateNodeData(currentNodeId,{ updateNodeData(currentNodeId,{
bodyRaw: e.target.value, bodyRaw: e.target.value,

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useNodesData, useStore, useSvelteFlow} from '@xyflow/svelte'; import {type NodeProps, useNodesData, useStore, useSvelteFlow} from '@xyflow/svelte';
@@ -7,7 +9,7 @@
import {getOptions} from '../utils/NodeUtils'; import {getOptions} from '../utils/NodeUtils';
import {onMount} from 'svelte'; import {onMount} from 'svelte';
import OutputDefList from '../core/OutputDefList.svelte'; import OutputDefList from '../core/OutputDefList.svelte';
import type {Parameter, SelectItem} from '#types'; import type {Parameter, SelectItem, TinyflowNodeData} from '#types';
import ParamTokenEditor from '../core/ParamTokenEditor.svelte'; import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
import { import {
buildEditorReferenceParameters, buildEditorReferenceParameters,
@@ -17,7 +19,7 @@
} from '../../utils/workflowNodeFields'; } from '../../utils/workflowNodeFields';
const { data, ...rest }: { const { data, ...rest }: {
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); } = $props();

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useNodesData, useStore, useSvelteFlow} from '@xyflow/svelte'; import {type NodeProps, useNodesData, useStore, useSvelteFlow} from '@xyflow/svelte';
@@ -177,16 +179,6 @@
} }
}); });
$effect(() => {
const validIds = new Set(queryContextOptions.map((item) => String(item.value)));
const normalized = queryContextNodeIds.filter((item) => validIds.has(item));
if (normalized.length !== queryContextNodeIds.length) {
updateNodeData(currentNodeId, {
queryContextNodeIds: normalized
});
}
});
const toggleQueryContextNode = (nodeId: string) => { const toggleQueryContextNode = (nodeId: string) => {
const currentIds = [...queryContextNodeIds]; const currentIds = [...queryContextNodeIds];
const exists = currentIds.includes(nodeId); const exists = currentIds.includes(nodeId);
@@ -404,6 +396,7 @@
background: var(--tf-slider-track-bg); background: var(--tf-slider-track-bg);
border-radius: 2px; border-radius: 2px;
outline: none; outline: none;
appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
} }

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {Handle, type NodeProps, Position} from '@xyflow/svelte'; import {Handle, type NodeProps, Position} from '@xyflow/svelte';
@@ -5,9 +7,10 @@
import RefParameterList from '../core/RefParameterList.svelte'; import RefParameterList from '../core/RefParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils'; import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte'; import {useAddParameter} from '../utils/useAddParameter.svelte';
import type {TinyflowNodeData} from '#types';
const { data, ...rest }: { const { data, ...rest }: {
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); } = $props();

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte'; import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
@@ -8,11 +10,11 @@
import {getOptions} from '../utils/NodeUtils'; import {getOptions} from '../utils/NodeUtils';
import {onMount} from 'svelte'; import {onMount} from 'svelte';
import OutputDefList from '../core/OutputDefList.svelte'; import OutputDefList from '../core/OutputDefList.svelte';
import type {SelectItem} from '#types'; import type {SelectItem, TinyflowNodeData} from '#types';
import ParamTokenEditor from '../core/ParamTokenEditor.svelte'; import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
const { data, ...rest }: { const { data, ...rest }: {
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); } = $props();
@@ -109,9 +111,9 @@
placeholder="请输入关键字" placeholder="请输入关键字"
style="width: 100%" style="width: 100%"
parameters={editorParameters} parameters={editorParameters}
value={data.keyword || ''} value={String(data.keyword || '')}
oninput={(e)=>{ oninput={(e: Event)=>{
const newValue = e.target.value; const newValue = (e.target as HTMLInputElement).value;
updateNodeData(currentNodeId, ()=>{ updateNodeData(currentNodeId, ()=>{
return { return {
keyword: newValue keyword: newValue
@@ -127,10 +129,10 @@
mode="input" mode="input"
placeholder="搜索的数据条数" placeholder="搜索的数据条数"
style="width: 100%" style="width: 100%"
value={data.limit || ''} value={String(data.limit || '')}
parameters={editorParameters} parameters={editorParameters}
oninput={(e)=>{ oninput={(e: Event)=>{
const newValue = e.target.value; const newValue = (e.target as HTMLInputElement).value;
updateNodeData(currentNodeId, ()=>{ updateNodeData(currentNodeId, ()=>{
return { return {
limit: newValue limit: newValue
@@ -170,4 +172,3 @@
} }
</style> </style>

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {Heading, Input, Textarea} from '../base'; import {Heading, Input, Textarea} from '../base';
@@ -12,9 +14,10 @@
normalizeStartNodeData, normalizeStartNodeData,
normalizeStartFormMeta, normalizeStartFormMeta,
} from '../../utils/workflowNodeFields'; } from '../../utils/workflowNodeFields';
import type {TinyflowNodeData} from '#types';
const { data, ...rest }: { const { data, ...rest }: {
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); } = $props();

View File

@@ -1,3 +1,5 @@
<svelte:options customElement={{ props: {} }} />
<script lang="ts"> <script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte'; import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte'; import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
@@ -7,9 +9,10 @@
import {getCurrentNodeId} from '#components/utils/NodeUtils'; import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte'; import {useAddParameter} from '../utils/useAddParameter.svelte';
import OutputDefList from '../core/OutputDefList.svelte'; import OutputDefList from '../core/OutputDefList.svelte';
import type {TinyflowNodeData} from '#types';
const { data, ...rest }: { const { data, ...rest }: {
data: NodeProps['data'], data: TinyflowNodeData,
[key: string]: any [key: string]: any
} = $props(); } = $props();
@@ -62,7 +65,7 @@
template: e.target.value template: e.target.value
} }
}) })
}} value={data.template ||""} /> }} value={String(data.template || '')} />
</div> </div>
@@ -89,5 +92,3 @@
</style> </style>

View File

@@ -144,10 +144,16 @@ const nodeToOptions = (
} }
}; };
export const useRefOptions: any = (useChildrenOnly: boolean = false) => { export const useRefOptions: any = (
useChildrenOnly: boolean | (() => boolean) = false,
) => {
const currentNodeId = getCurrentNodeId(); const currentNodeId = getCurrentNodeId();
const currentNode = useNodesData(currentNodeId); const currentNode = useNodesData(currentNodeId);
const { nodes, edges, nodeLookup } = $derived(useStore()); const { nodes, edges, nodeLookup } = $derived(useStore());
const isChildrenOnly = () =>
typeof useChildrenOnly === 'function'
? useChildrenOnly()
: useChildrenOnly;
let selectItems = $derived.by(() => { let selectItems = $derived.by(() => {
const resultOptions = []; const resultOptions = [];
@@ -158,7 +164,7 @@ export const useRefOptions: any = (useChildrenOnly: boolean = false) => {
//通过 nodeLookup.get 才会得到有 parentId 的 node //通过 nodeLookup.get 才会得到有 parentId 的 node
const cNode = nodeLookup.get(currentNodeId)!; const cNode = nodeLookup.get(currentNodeId)!;
if (useChildrenOnly) { if (isChildrenOnly()) {
for (const node of nodes) { for (const node of nodes) {
const nodeIsChildren = node.parentId === currentNode.current.id; const nodeIsChildren = node.parentId === currentNode.current.id;
if (nodeIsChildren) { if (nodeIsChildren) {

View File

@@ -4,6 +4,7 @@ import type { Node, useSvelteFlow } from '@xyflow/svelte';
export type TinyflowData = Partial< export type TinyflowData = Partial<
ReturnType<ReturnType<typeof useSvelteFlow>['toObject']> ReturnType<ReturnType<typeof useSvelteFlow>['toObject']>
>; >;
export type TinyflowNodeData = Record<string, any>;
export type TinyflowTheme = 'light' | 'dark'; export type TinyflowTheme = 'light' | 'dark';
export type SelectItem = { export type SelectItem = {

View File

@@ -897,7 +897,10 @@ describe('workflow node fields', () => {
expect(normalizedWorkflow.nodes[0]?.data?.parameters?.[0]?.required).toBe( expect(normalizedWorkflow.nodes[0]?.data?.parameters?.[0]?.required).toBe(
true, true,
); );
expect(normalizedWorkflow.nodes[0]?.data?.startFormSchema?.[0]?.key).toBe( expect(
(normalizedWorkflow.nodes[0]?.data as Record<string, any> | undefined)
?.startFormSchema?.[0]?.key,
).toBe(
'user_input', 'user_input',
); );
expect(normalizedWorkflow.nodes[1]?.data?.parameters).toEqual([]); expect(normalizedWorkflow.nodes[1]?.data?.parameters).toEqual([]);

View File

@@ -1237,7 +1237,10 @@ function replaceStartFieldReferenceValue(
return value; return value;
} }
function collectDownstreamNodeIds(rootNodeId: string, edges: Edge[]) { export function collectDownstreamNodeIds(
rootNodeId: string,
edges: Edge[],
) {
const nodeIds = new Set<string>(); const nodeIds = new Set<string>();
const visit = (nodeId: string) => { const visit = (nodeId: string) => {
@@ -1281,7 +1284,7 @@ export function renameStartFieldReferencesInNodes(
const oldRefPath = `${normalizedStartNodeId}.${normalizedCurrentKey}`; const oldRefPath = `${normalizedStartNodeId}.${normalizedCurrentKey}`;
const newRefPath = `${normalizedStartNodeId}.${normalizedNextKey}`; const newRefPath = `${normalizedStartNodeId}.${normalizedNextKey}`;
const nextNodes = nodes.map((node) => { const nextNodes: Node[] = nodes.map((node) => {
if (node.id === normalizedStartNodeId) { if (node.id === normalizedStartNodeId) {
return node; return node;
} }
@@ -1295,7 +1298,7 @@ export function renameStartFieldReferencesInNodes(
} }
return { return {
...node, ...node,
data: nextData, data: nextData ?? {},
}; };
}); });