Import: a browsable preset panel with multi-select (#48) #98

Merged
claude-bot merged 1 commit from feat/browsable-preset-import into main 2026-09-01 03:07:10 +00:00
Contributor

Batch C item 7 of #48 — the last one, and the largest.

The gap

Spec §3.6 has always called for parsing a WLED presets.json and "displaying them in a browsable import panel" where the user "selects which presets to import". What existed was a textarea, a name-only <select>, and one preset per round trip.

Frontend

The dialog moves out of Toolbar.tsx (which was carrying it inline) into ImportPanel.tsx:

  • File picker, with paste kept behind a disclosure rather than being the only route in.
  • Browsable list — one row per preset: colour swatch, name, source segment ids, a checkbox, and a per-row event + phase selector once checked.
  • Segment mapping becomes a row per source segment mapped onto this install's named segments, replacing the raw JSON field. It's seeded with what the backend would do on its own, so the table shows the real outcome instead of every row reading "skip".

Backend

PresetPreview gains a representative palette — the non-black colour slots across a preset's segments, deduped and capped — which drives the swatches via the existing segmentGradient.

New POST /import/presets/batch applies the whole selection in one transaction under a single backup. All-or-nothing matters here specifically: a scheme id is derived as {event_id}_{phase}, so a partial import would leave phases the user never chose. import_preset's core is extracted to _apply_preset and shared by both endpoints.

Collisions are resolved before sending

Because one bad row would discard every good one, the panel checks the selection against the current library up front: an approved slot blocks, a candidate warns it will be replaced, and two rows targeting one phase are caught client-side as well as server-side. Those rules live in importUtils.ts so they're testable without rendering the dialog.

One behaviour change worth flagging

segment_mapping now distinguishes None ("no opinion" → match by WLED index) from {} ("map nothing"), which the panel needs in order to express skipping every segment. Previously both fell through to the default mapping. No existing caller sent {} — the old UI's empty-field path is removed in this PR — and there's a test pinning both behaviours.

Verification

  • Backend: 287 passed (8 new), ruff check, ruff format --check, mypy clean.
  • Frontend, in a node:22 container: tsc -b, eslint, vitest (70 passed, 14 new in importUtils.test.ts) and npm run build all clean.
  • The E2E suite doesn't exercise the import dialog, so nothing there depends on the old markup.

🤖 Generated with Claude Code

Batch C item 7 of #48 — the last one, and the largest. ## The gap Spec §3.6 has always called for parsing a WLED `presets.json` and *"displaying them in a browsable import panel"* where the user *"selects which presets to import"*. What existed was a textarea, a name-only `<select>`, and one preset per round trip. ## Frontend The dialog moves out of `Toolbar.tsx` (which was carrying it inline) into `ImportPanel.tsx`: - **File picker**, with paste kept behind a disclosure rather than being the only route in. - **Browsable list** — one row per preset: colour swatch, name, source segment ids, a checkbox, and a per-row event + phase selector once checked. - **Segment mapping** becomes a row per source segment mapped onto this install's named segments, replacing the raw JSON field. It's **seeded with what the backend would do on its own**, so the table shows the real outcome instead of every row reading "skip". ## Backend `PresetPreview` gains a representative palette — the non-black colour slots across a preset's segments, deduped and capped — which drives the swatches via the existing `segmentGradient`. New `POST /import/presets/batch` applies the whole selection in **one transaction under a single backup**. All-or-nothing matters here specifically: a scheme id is derived as `{event_id}_{phase}`, so a partial import would leave phases the user never chose. `import_preset`'s core is extracted to `_apply_preset` and shared by both endpoints. ## Collisions are resolved before sending Because one bad row would discard every good one, the panel checks the selection against the current library up front: an approved slot **blocks**, a candidate **warns** it will be replaced, and two rows targeting one phase are caught client-side as well as server-side. Those rules live in `importUtils.ts` so they're testable without rendering the dialog. ## One behaviour change worth flagging `segment_mapping` now distinguishes `None` ("no opinion" → match by WLED index) from `{}` ("map nothing"), which the panel needs in order to express skipping every segment. Previously both fell through to the default mapping. No existing caller sent `{}` — the old UI's empty-field path is removed in this PR — and there's a test pinning both behaviours. ## Verification - Backend: **287 passed** (8 new), `ruff check`, `ruff format --check`, `mypy` clean. - Frontend, in a `node:22` container: `tsc -b`, `eslint`, `vitest` (**70 passed**, 14 new in `importUtils.test.ts`) and `npm run build` all clean. - The E2E suite doesn't exercise the import dialog, so nothing there depends on the old markup. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Import: a browsable preset panel with multi-select (#48)
All checks were successful
CI / Alembic migration check (pull_request) Successful in 1m32s
CI / Python lint & type-check (pull_request) Successful in 1m57s
CI / Frontend lint, test & build (pull_request) Successful in 2m8s
CI / Python tests (pull_request) Successful in 3m22s
CI / Docker build, health smoke & E2E (pull_request) Successful in 1m46s
fecd720b2a
Spec §3.6 has always called for parsing a WLED presets.json and "displaying
them in a browsable import panel" where the user "selects which presets to
import". What existed was a textarea, a name-only dropdown, and one preset
per round trip.

The dialog moves out of Toolbar.tsx into ImportPanel.tsx: a file picker
(paste kept behind a disclosure) over a scrollable list, one row per preset
showing a colour swatch, name and source segment ids, with a checkbox and a
per-row event/phase selector. Segment mapping becomes a row per source
segment mapped onto this install's named segments, replacing the raw JSON
field, and is seeded with what the backend would do on its own so the table
shows the real outcome rather than every row reading "skip".

Backend: PresetPreview gains a representative palette (non-black slots
across the segments, deduped, capped) to drive the swatches, and a new
POST /import/presets/batch applies the whole selection in one transaction
under a single backup. All-or-nothing matters here: a scheme id is derived
as {event_id}_{phase}, so a partial import would leave phases the user never
chose. import_preset's core is extracted to _apply_preset and shared.

Because one bad row would discard every good one, the panel resolves
collisions against the current library before sending -- an approved slot
blocks, a candidate warns it will be replaced, and two rows on one phase are
caught client-side as well as server-side. Those rules live in importUtils.ts
so they are testable without rendering the dialog.

One behaviour change worth noting: segment_mapping now distinguishes None
("no opinion" -> match by WLED index) from {} ("map nothing"), which the
panel needs to express skipping every segment. No existing caller sent {}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/browsable-preset-import 2026-09-01 03:07:11 +00:00
Sign in to join this conversation.
No description provided.