From 5743b9fb3f1eb7f3b4f9b01e35c9a0f897ee7411 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 4 Oct 2026 15:51:47 +0500 Subject: [PATCH] feat: touched fields dont reset on change source image --- docs/backlog.md | 8 ++------ web/e2e/tool-flows.spec.ts | 14 ++++++++++++++ .../lib/components/schema/SchemaToolView.svelte | 13 ++++++++++++- web/src/lib/registry-schema.test.ts | 10 ++++++++++ web/src/lib/registry-schema.ts | 9 ++++++++- 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/docs/backlog.md b/docs/backlog.md index 46d770c..6f35648 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -6,13 +6,9 @@ ## UX — приоритетная очередь -Порядок согласован: A (сброс параметров) → C (контролы полей) → B (загрузка -исходника) → E (download) → D (pipeline). Делать по одной, сверху вниз. +Порядок согласован: C (контролы полей) → B (загрузка исходника) → E (download) → +D (pipeline). Делать по одной, сверху вниз. -- [ ] **A. Сброс параметров при загрузке нового изображения** — crop, resize (но - скорее всего все инструменты): при загрузке нового файла сбрасываются - размеры, настроенные пользователем. Решить, что сбрасывать (размеры под - новый исходник), а что сохранять (толерансы, цвета, форматы) - [ ] **C1. range slider field** — добавить поле для ввода цифр и кнопку сброса значения - [ ] **C2. keep Aspect field** — пересчитывать второе поле, учитывать какое diff --git a/web/e2e/tool-flows.spec.ts b/web/e2e/tool-flows.spec.ts index 79e88cf..5d24dd7 100644 --- a/web/e2e/tool-flows.spec.ts +++ b/web/e2e/tool-flows.spec.ts @@ -36,6 +36,20 @@ test("resize-png uses the source dimensions by default", async ({ page }) => { expectNoErrors(sink); }); +test("resize-png keeps user-set size when another image is uploaded", async ({ + page, +}) => { + await openTool(page, "resize-png"); + await uploadImage(page, opaquePng); + const width = page.getByRole("spinbutton", { name: "Width" }); + await width.fill("25"); + await uploadImage(page, landscapePng); + await expect(width).toHaveValue("25"); + await expect(page.getByRole("spinbutton", { name: "Height" })).toHaveValue( + "48", + ); +}); + test("crop-png starts with the full source area", async ({ page }) => { const sink = trackErrors(page); await openTool(page, "crop-png"); diff --git a/web/src/lib/components/schema/SchemaToolView.svelte b/web/src/lib/components/schema/SchemaToolView.svelte index dbad10d..e348f40 100644 --- a/web/src/lib/components/schema/SchemaToolView.svelte +++ b/web/src/lib/components/schema/SchemaToolView.svelte @@ -20,6 +20,7 @@ type ToolSchema, } from "$lib/registry-schema"; import { downloadZip } from "$lib/zip"; + import { SvelteSet } from "svelte/reactivity"; interface Props { page: Page; @@ -38,6 +39,9 @@ const resultKind = $derived(tool.result ?? "image"); let values = $state>({}); + // Поля, изменённые пользователем: при загрузке нового файла им не даём + // перезаписаться source-дефолтами. Reset снимает пометку. + const touched = new SvelteSet(); let source = $state(null); let result = $state(null); let fileResult = $state(null); @@ -67,10 +71,12 @@ } function setValue(id: string, value: unknown) { + touched.add(id); values = { ...values, [id]: value }; } function reset() { + touched.clear(); values = schema ? defaultSchemaParams(schema, { source: source ?? undefined }) : {}; @@ -87,7 +93,12 @@ const decoded = await decodeFile(file); source = decoded; if (schema) { - values = applySourceDefaults(schema, values, { source: decoded }); + values = applySourceDefaults( + schema, + values, + { source: decoded }, + touched, + ); } } catch (e) { displayError = toDisplayError(e); diff --git a/web/src/lib/registry-schema.test.ts b/web/src/lib/registry-schema.test.ts index 8d88c88..35324ec 100644 --- a/web/src/lib/registry-schema.test.ts +++ b/web/src/lib/registry-schema.test.ts @@ -89,6 +89,16 @@ describe("registry-schema: source-aware dimension defaults", () => { }); }); + it("touched-поля не перезаписываются source-дефолтами", () => { + const manual = { size: { width: 5, height: 6 } }; + expect( + applySourceDefaults(sourceSchema, manual, { source }, new Set(["size"])), + ).toEqual({ + size: { width: 5, height: 6 }, + fixed: { width: 10, height: 20 }, + }); + }); + it("uses source dimensions when sanitizing missing values", () => { expect(sanitizeSchemaParams(sourceSchema, {}, { source })).toEqual({ size: source, diff --git a/web/src/lib/registry-schema.ts b/web/src/lib/registry-schema.ts index c92be45..7f2381a 100644 --- a/web/src/lib/registry-schema.ts +++ b/web/src/lib/registry-schema.ts @@ -435,15 +435,22 @@ export function defaultSchemaParams

( return out; } +/** + * Пересчитывает дефолты от нового исходника, сохраняя значения пользователя. + * Поля из touched (изменённые вручную) не перезаписываются даже если у них + * source-дефолт — иначе загрузка нового файла сносила бы настроенные размеры. + */ export function applySourceDefaults

( schema: ToolSchema

, values: Record, context: SchemaContext, + touched?: ReadonlySet, ): Record { const defaults = defaultSchemaParams(schema, context); for (const key of Object.keys(schema.fields) as (keyof P)[]) { const spec = schema.fields[key].spec; - if (spec.kind === "dimension" && spec.defaultFromSource) continue; + const fromSource = spec.kind === "dimension" && spec.defaultFromSource; + if (fromSource && !touched?.has(key as string)) continue; if (Object.prototype.hasOwnProperty.call(values, key)) { defaults[key] = values[key as string]; }