From 3c9175f8a39fa156009733a775efefb85ee0208d Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 11 Oct 2026 10:58:54 +0500 Subject: [PATCH] fix: update error\warning flow --- docs/backlog.md | 10 +++ .../lib/components/schema/PreviewTile.svelte | 12 ++++ .../components/schema/SchemaActions.svelte | 17 +++++ .../components/schema/SchemaResultTile.svelte | 3 + .../components/schema/SchemaSourceTile.svelte | 12 +++- .../components/schema/SchemaToolView.svelte | 71 +++++++++++++------ .../schema/schema-tool-runner.svelte.ts | 50 ++++++++----- web/src/lib/i18n/en.ts | 2 + web/src/lib/i18n/ru.ts | 2 + 9 files changed, 138 insertions(+), 41 deletions(-) diff --git a/docs/backlog.md b/docs/backlog.md index 9040549..518cfd0 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -114,6 +114,16 @@ ## UI и качество +- [ ] **обдумать агрегацию предупреждений на тайле** — сейчас разрешено два + бейджа рядом (derived `resultNote` + runtime-warnings); после + использования решить, нужен ли один сводный бейдж со счётчиком и списком +- [ ] **лимит КБ/качество как авто-шаг пайплайна** — вынести size limit/quality + из меню Download в автоматический инструмент цепочки, который применяет + выбранные настройки (сжатие видно в превью и в цепочке, ошибки + недостижимости ложатся на тайл шага). Нужен флоу передачи индикатора из + такого шага в кнопку Download (ошибка/предупреждение/итоговый размер). + Связано с пунктами про индикацию активного сжатия и превью итогового + результата - [ ] кнопку редактирования названия пайплайна на этой же странице - [ ] при загрузке нескольких файлов, показывает source не той же высоты, как result diff --git a/web/src/lib/components/schema/PreviewTile.svelte b/web/src/lib/components/schema/PreviewTile.svelte index 14bcd35..304eb5f 100644 --- a/web/src/lib/components/schema/PreviewTile.svelte +++ b/web/src/lib/components/schema/PreviewTile.svelte @@ -18,6 +18,9 @@ // Runtime warnings (chain/download events): shown as badges next to the // derived result note, but from a different source. warnings?: string[]; + // Runtime errors: danger badges; detail goes to the native tooltip until + // a popover with full text exists. + errors?: { label: string; detail?: string }[]; actions?: Snippet; } let { @@ -31,6 +34,7 @@ note = undefined, noteTone = "warning", warnings = [], + errors = [], actions = undefined, }: Props = $props(); @@ -56,6 +60,14 @@ {#if note} {/if} + {#each errors as error (error.label + (error.detail ?? ""))} + + {/each} {#each warnings as warning (warning)} {/each} diff --git a/web/src/lib/components/schema/SchemaActions.svelte b/web/src/lib/components/schema/SchemaActions.svelte index d206072..f4efaa7 100644 --- a/web/src/lib/components/schema/SchemaActions.svelte +++ b/web/src/lib/components/schema/SchemaActions.svelte @@ -1,6 +1,7 @@ - + {#if mode === "text"}
(stepState.steps = v), touchedFor: (key) => stepState.touchedFor(key), reportError: (e) => runner.reportError(e), - clearError: () => (runner.displayError = null), + clearError: () => runner.clearSourceError(), }); const stepState = createSchemaToolState({ @@ -122,12 +123,36 @@ const stepResults = $derived(runner.stepResults); const stepFileSets = $derived(runner.stepFileSets); const runWarnings = $derived(runner.runWarnings); - const displayError = $derived(runner.displayError); - const warningTexts = $derived( - [...upload.sourceWarnings, ...runWarnings, ...runner.downloadWarnings].map( - chainWarningText, - ), + // Feedback zones: source tile / step tiles / download button area. + const sourceWarningTexts = $derived( + upload.sourceWarnings.map(chainWarningText), ); + const warningsByStep = $derived.by(() => { + const byStep: string[][] = steps.map(() => []); + for (const w of runWarnings) { + if ("step" in w && w.step >= 1 && w.step <= byStep.length) { + byStep[w.step - 1].push(chainWarningText(w)); + } + } + return byStep; + }); + const downloadWarningTexts = $derived( + runner.downloadWarnings.map(chainWarningText), + ); + const stepError = $derived(runner.stepError); + const sourceError = $derived(runner.sourceError); + const actionError = $derived(runner.actionError); + + function displayText(e: DisplayError | null): string { + if (!e) return ""; + return e.kind === "i18n" ? t(e.key, e.vars) : e.text; + } + + function errorBadge(e: DisplayError | null) { + return e + ? [{ label: t("actions.actionFailed"), detail: displayText(e) }] + : []; + } const masks = createStepMasks({ steps: () => steps, @@ -176,14 +201,6 @@ : null, ); - const errorText = $derived.by(() => { - const current = displayError; - if (!current) return ""; - return current.kind === "i18n" - ? t(current.key, current.vars) - : current.text; - }); - const debouncedRun = debounce(() => run(), 200); $effect(() => { @@ -288,6 +305,8 @@ textSource={upload.textSource} running={busy} fileCount={upload.sourceFiles.length} + warnings={sourceWarningTexts} + errors={errorBadge(sourceError)} ontextinput={(v) => (upload.textSource = v)} onrendertext={run} onupload={upload.handleFile} @@ -320,7 +339,10 @@ resultKind={displayResultKind} result={out} {resultNote} - warnings={warningTexts} + warnings={warningsByStep[i] ?? []} + errors={stepError && stepError.step === i + ? [{ label: stepError.label, detail: stepError.detail }] + : []} {fileResult} {textResult} textVars={verdictVars} @@ -341,6 +363,10 @@ viewMode="image" dims={out ? `${out.width} × ${out.height}` : undefined} parts={outSet.length > 1 ? outSet.length : undefined} + warnings={warningsByStep[i] ?? []} + errors={stepError && stepError.step === i + ? [{ label: stepError.label, detail: stepError.detail }] + : []} > {#snippet actions()} {@render maskChip(i)} @@ -381,6 +407,13 @@ quality={output.quality} limitKb={output.limitKb} alphaLoss={output.alphaLoss} + warnings={downloadWarningTexts} + error={actionError + ? { + label: t("actions.actionFailed"), + detail: displayText(actionError), + } + : null} onupload={upload.handleFile} ondownload={download} onformat={(v) => (output.format = v)} @@ -442,9 +475,6 @@
{/if} - {#if errorText} - - {/if} @@ -611,11 +641,6 @@ border-radius: 0 0 var(--radius-m) var(--radius-m); background: var(--color-panel); } - .error { - margin: var(--space-l) 0 0; - color: var(--color-danger); - font: var(--font-size-s) var(--font-mono); - } .empty { font: var(--font-size-s) var(--font-mono); } diff --git a/web/src/lib/components/schema/schema-tool-runner.svelte.ts b/web/src/lib/components/schema/schema-tool-runner.svelte.ts index a49c1f0..00d0f96 100644 --- a/web/src/lib/components/schema/schema-tool-runner.svelte.ts +++ b/web/src/lib/components/schema/schema-tool-runner.svelte.ts @@ -58,7 +58,12 @@ interface RunnerState { runWarnings: ChainWarning[]; // Size-limit feedback from the last download (limit unreachable etc.). downloadWarnings: ChainWarning[]; - displayError: DisplayError | null; + // Chain step failure: badge lands on that step's tile (0-based index). + stepError: { step: number; label: string; detail: string } | null; + // Source decode/upload failures (via reportError): badge on the source tile. + sourceError: DisplayError | null; + // Download/copy and non-chain run failures: badge near the download button. + actionError: DisplayError | null; } function toDisplayError(e: unknown): DisplayError { @@ -101,7 +106,8 @@ async function run(s: RunnerState, ctx: SchemaToolRunnerCtx) { const steps = ctx.steps(); if (!schema || steps.length === 0) return; const gen = ++s.runGen; - s.displayError = null; + s.stepError = null; + s.actionError = null; s.running = true; try { const source = ctx.source(); @@ -125,16 +131,17 @@ async function run(s: RunnerState, ctx: SchemaToolRunnerCtx) { if (e instanceof ChainStepError) { const base = toDisplayError(e.cause); const msg = base.kind === "i18n" ? t(base.key, base.vars) : base.text; - s.displayError = { - kind: "plain", - text: t("toolPage.stepError", { + s.stepError = { + step: e.stepIndex, + label: t("chain.stepFailed", { n: e.stepIndex + 1 }), + detail: t("toolPage.stepError", { n: e.stepIndex + 1, title: chainStepTitle(e.toolId), msg, }), }; } else { - s.displayError = toDisplayError(e); + s.actionError = toDisplayError(e); } } finally { if (gen === s.runGen) s.running = false; @@ -144,7 +151,7 @@ async function run(s: RunnerState, ctx: SchemaToolRunnerCtx) { async function download(s: RunnerState, ctx: SchemaToolRunnerCtx) { if (!ctx.schema()) return; s.downloading = true; - s.displayError = null; + s.actionError = null; try { if (s.fileResult) { await downloadZip(s.fileResult.files, `${ctx.pageSlug()}.zip`); @@ -179,7 +186,7 @@ async function download(s: RunnerState, ctx: SchemaToolRunnerCtx) { downloadBlob(blob, `${ctx.pageSlug()}.${out.ext}`); } } catch (e) { - s.displayError = toDisplayError(e); + s.actionError = toDisplayError(e); } finally { s.downloading = false; } @@ -195,7 +202,7 @@ async function copyText(s: RunnerState, ctx: SchemaToolRunnerCtx) { try { await navigator.clipboard.writeText(copyValue); } catch (e) { - s.displayError = toDisplayError(e); + s.actionError = toDisplayError(e); } } @@ -205,7 +212,7 @@ async function downloadText(s: RunnerState, ctx: SchemaToolRunnerCtx) { const blob = new Blob([s.textResult], { type: "text/plain" }); downloadBlob(blob, `${ctx.pageSlug()}.txt`); } catch (e) { - s.displayError = toDisplayError(e); + s.actionError = toDisplayError(e); } } @@ -215,7 +222,8 @@ function clearResults(s: RunnerState) { s.textResult = null; s.verdictVars = undefined; s.downloadWarnings = []; - s.displayError = null; + s.stepError = null; + s.actionError = null; } // Execution state and actions of SchemaToolView: run/download/clipboard and @@ -234,7 +242,9 @@ export function createSchemaToolRunner(ctx: SchemaToolRunnerCtx) { stepFileSets: [], runWarnings: [], downloadWarnings: [], - displayError: null, + stepError: null, + sourceError: null, + actionError: null, }); return { get running() { @@ -270,11 +280,17 @@ export function createSchemaToolRunner(ctx: SchemaToolRunnerCtx) { get downloadWarnings() { return s.downloadWarnings; }, - get displayError() { - return s.displayError; + get stepError() { + return s.stepError; }, - set displayError(v: DisplayError | null) { - s.displayError = v; + get sourceError() { + return s.sourceError; + }, + get actionError() { + return s.actionError; + }, + clearSourceError: () => { + s.sourceError = null; }, run: () => run(s, ctx), download: () => download(s, ctx), @@ -290,7 +306,7 @@ export function createSchemaToolRunner(ctx: SchemaToolRunnerCtx) { }, clearResults: () => clearResults(s), reportError: (e: unknown) => { - s.displayError = toDisplayError(e); + s.sourceError = toDisplayError(e); }, }; } diff --git a/web/src/lib/i18n/en.ts b/web/src/lib/i18n/en.ts index e592e07..e58bad6 100644 --- a/web/src/lib/i18n/en.ts +++ b/web/src/lib/i18n/en.ts @@ -69,6 +69,7 @@ export const en: Dict = { warnSourceSkip: "{skipped} of {total} files could not be read", warnSizeLimitMiss: "Could not fit into {limit} KB — downloaded file is {actual} KB", + stepFailed: "Step {n} failed", }, savedChains: { pageTitle: "Saved pipelines — easy-png-tools", @@ -141,6 +142,7 @@ export const en: Dict = { sizeLimitKb: "Max size, KB", sizeLimitBmp: "Size limit does not apply to BMP — it is always uncompressed", + actionFailed: "Failed", }, textSource: { placeholder: "Paste base64 / hex / bytes…", diff --git a/web/src/lib/i18n/ru.ts b/web/src/lib/i18n/ru.ts index a750fa9..9b6a9ca 100644 --- a/web/src/lib/i18n/ru.ts +++ b/web/src/lib/i18n/ru.ts @@ -71,6 +71,7 @@ export const ru: Dict = { warnSourceSkip: "{skipped} из {total} файлов не удалось прочитать", warnSizeLimitMiss: "Не удалось ужаться до {limit} КБ — скачан файл на {actual} КБ", + stepFailed: "Шаг {n}: ошибка", }, savedChains: { pageTitle: "Сохранённые пайплайны — easy-png-tools", @@ -142,6 +143,7 @@ export const ru: Dict = { alphaLoss: "В результате есть прозрачность — {format} её уберёт", sizeLimitKb: "Лимит, КБ", sizeLimitBmp: "Лимит размера не действует на BMP — он всегда несжатый", + actionFailed: "Ошибка", }, textSource: { placeholder: "Вставьте base64 / hex / байты…",