feat: collapse\expand steps in pipeline

This commit is contained in:
2026-10-06 19:08:47 +05:00
parent e7d3925eb2
commit 5c58451f2f
7 changed files with 47 additions and 7 deletions
-3
View File
@@ -20,9 +20,6 @@
(любой шаг, не только первый), остальные шаги и их параметры сохраняются, (любой шаг, не только первый), остальные шаги и их параметры сохраняются,
вход перечитывается. Открытый вопрос: что делать с несовместимыми входами вход перечитывается. Открытый вопрос: что делать с несовместимыми входами
(text-source ↔ file). (text-source ↔ file).
- [ ] **Идея 2. Сворачивать инструменты в chain** — оценка S. Тоггл сворачивания
звена до заголовка «Шаг n: название» (превью скрываются). Состояние
свёрнутости помнить в workspace-pipeline.
- [ ] **Идея 3. Несколько цепочек: сохранение и загрузка** — оценка M. - [ ] **Идея 3. Несколько цепочек: сохранение и загрузка** — оценка M.
Именованные цепочки в localStorage (список, создать/переименовать/ Именованные цепочки в localStorage (список, создать/переименовать/
удалить), быстрое переключение. Формат экспортного JSON расширить удалить), быстрое переключение. Формат экспортного JSON расширить
+15 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { GripVertical, X } from "@lucide/svelte"; import { ChevronDown, ChevronRight, GripVertical, X } from "@lucide/svelte";
import IconButton from "$lib/components/ui/IconButton.svelte"; import IconButton from "$lib/components/ui/IconButton.svelte";
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
@@ -9,9 +9,11 @@
title?: string; title?: string;
type?: string; type?: string;
draggable?: boolean; draggable?: boolean;
collapsed?: boolean;
tools?: Snippet; tools?: Snippet;
children?: Snippet; children?: Snippet;
onremove?: () => void; onremove?: () => void;
ontoggle?: () => void;
ondragstart?: (e: DragEvent) => void; ondragstart?: (e: DragEvent) => void;
ondragover?: (e: DragEvent) => void; ondragover?: (e: DragEvent) => void;
ondrop?: (e: DragEvent) => void; ondrop?: (e: DragEvent) => void;
@@ -23,9 +25,11 @@
title, title,
type, type,
draggable = false, draggable = false,
collapsed = false,
tools, tools,
children, children,
onremove, onremove,
ontoggle,
ondragstart, ondragstart,
ondragover, ondragover,
ondrop, ondrop,
@@ -54,6 +58,15 @@
</div> </div>
<div class="step-tools"> <div class="step-tools">
{#if tools}{@render tools()}{/if} {#if tools}{@render tools()}{/if}
{#if ontoggle}
<IconButton
icon={collapsed ? ChevronRight : ChevronDown}
label={collapsed ? t("chain.expandStep") : t("chain.collapseStep")}
variant="clear"
size="s"
onclick={() => ontoggle()}
/>
{/if}
{#if onremove} {#if onremove}
<IconButton <IconButton
icon={X} icon={X}
@@ -65,7 +78,7 @@
{/if} {/if}
</div> </div>
</header> </header>
{#if children} {#if children && !collapsed}
<div class="step-body"> <div class="step-body">
{@render children()} {@render children()}
</div> </div>
@@ -240,6 +240,12 @@
steps = removeStep(steps, key); steps = removeStep(steps, key);
} }
function toggleStep(index: number) {
const step = steps[index];
if (!step) return;
steps = steps.with(index, { ...step, collapsed: !step.collapsed });
}
function onStepDrop(e: DragEvent, to: number) { function onStepDrop(e: DragEvent, to: number) {
e.preventDefault(); e.preventDefault();
if (dragFrom !== null) steps = moveStep(steps, dragFrom, to); if (dragFrom !== null) steps = moveStep(steps, dragFrom, to);
@@ -458,6 +464,8 @@
index={i + 1} index={i + 1}
title={stepTitle(step.id)} title={stepTitle(step.id)}
draggable draggable
collapsed={step.collapsed ?? false}
ontoggle={() => toggleStep(i)}
onremove={steps.length > 1 onremove={steps.length > 1
? () => removeStepAt(step.key) ? () => removeStepAt(step.key)
: undefined} : undefined}
+2
View File
@@ -49,6 +49,8 @@ export const en: Dict = {
}, },
chain: { chain: {
addStep: "Add step", addStep: "Add step",
collapseStep: "Collapse step",
expandStep: "Expand step",
inputLegend: "Input", inputLegend: "Input",
resultLegend: "Result", resultLegend: "Result",
paramsLegend: "Parameters", paramsLegend: "Parameters",
+2
View File
@@ -50,6 +50,8 @@ export const ru: Dict = {
}, },
chain: { chain: {
addStep: "Добавить шаг", addStep: "Добавить шаг",
collapseStep: "Свернуть шаг",
expandStep: "Развернуть шаг",
inputLegend: "Вход", inputLegend: "Вход",
resultLegend: "Результат", resultLegend: "Результат",
paramsLegend: "Параметры", paramsLegend: "Параметры",
+8 -2
View File
@@ -2,10 +2,10 @@ import { getTool, PAGES, type Page } from "./registry";
import { defaultSchemaParams, sanitizeSchemaParams } from "./registry-schema"; import { defaultSchemaParams, sanitizeSchemaParams } from "./registry-schema";
export interface ChainStep { export interface ChainStep {
/** Стабильный ключ для keyed each и drag-n-drop (переживает reorder). */
key: string; key: string;
id: string; id: string;
params: Record<string, unknown>; params: Record<string, unknown>;
collapsed?: boolean;
} }
export const CHAIN_VERSION = 1; export const CHAIN_VERSION = 1;
@@ -77,6 +77,8 @@ export function parseChain(data: unknown): ChainStep[] | null {
out.push({ out.push({
key: uid(), key: uid(),
id, id,
collapsed:
(raw as { collapsed?: unknown }).collapsed === true ? true : undefined,
params: sanitizeSchemaParams( params: sanitizeSchemaParams(
tool.schema, tool.schema,
(params ?? {}) as Record<string, unknown>, (params ?? {}) as Record<string, unknown>,
@@ -101,7 +103,11 @@ export function saveChain(slug: string, steps: ChainStep[]): void {
storageKey(slug), storageKey(slug),
JSON.stringify({ JSON.stringify({
version: CHAIN_VERSION, version: CHAIN_VERSION,
steps: steps.map((s) => ({ id: s.id, params: s.params })), steps: steps.map((s) => ({
id: s.id,
params: s.params,
...(s.collapsed ? { collapsed: true } : {}),
})),
}), }),
); );
} }
+12
View File
@@ -79,6 +79,18 @@ describe("pipeline: parseChain (localStorage/импорт)", () => {
expect(chain[0].params).toMatchObject({ x: 5, y: 0 }); expect(chain[0].params).toMatchObject({ x: 5, y: 0 });
}); });
it("parseChain сохраняет collapsed, по умолчанию шаг развёрнут", () => {
const chain = parseChain({
version: CHAIN_VERSION,
steps: [
{ id: "crop", params: {}, collapsed: true },
{ id: "resize", params: {} },
],
})!;
expect(chain[0].collapsed).toBe(true);
expect(chain[1].collapsed).toBeUndefined();
});
it.each([ it.each([
["нет версии", { steps: [{ id: "crop" }] }], ["нет версии", { steps: [{ id: "crop" }] }],
["другая версия", { version: 99, steps: [{ id: "crop" }] }], ["другая версия", { version: 99, steps: [{ id: "crop" }] }],