Skip to content

feat(bin-designer): add synced organization tags to saved designs#1936

Merged
andymai merged 2 commits into
mainfrom
feat/design-tags-foundation
May 29, 2026
Merged

feat(bin-designer): add synced organization tags to saved designs#1936
andymai merged 2 commits into
mainfrom
feat/design-tags-foundation

Conversation

@andymai
Copy link
Copy Markdown
Owner

@andymai andymai commented May 29, 2026

What & why

Adds an optional tags field to saved bin designs so users can organize a growing library. Production cloud-sync data shows design counts are long-tailed — median 1, but p90=10, p95=15, max 69 per user — so the designs library needs real organization, not just a flat list.

This PR is the data-model foundation (no UI yet). It's stage 1 of 3:

  1. Tags foundationthis PR
  2. Designs-manager rebuild (wider grid, tag chips + filtering, bulk delete/export/tag)
  3. Header thumbnail quick-switch + management-only layout modal

How tags work

Tags sync across a user's devices by riding alongside the design name (a sibling of params, not inside it):

  • SavedDesign.tags + a shared normalizeTags helper (trim, dedupe case-insensitively, cap at 12 tags × 32 chars)
  • Persisted in IndexedDB via saveDesign / duplicateDesign; new updateDesignTags helper
  • Carried through DesignSyncPayload and the design sync adapter — LWW: a remote tag array (even empty) wins; a legacy payload with no tags falls back to local
  • Sanitized + stored server-side in the design envelope (sanitizeTags); client and server caps are kept identical as a documented cross-boundary contract

Safety

  • tags is optional everywhere (absent = untagged) → no destructive migration; existing saved designs are untouched.
  • The sync-admin drift accounting is unaffected: tags appears with the same key+value on both the blob envelope and the index-size calc, so it cancels in blob.size − sizeBytes exactly like name. Verified by new regression tests and a clean read-only production audit (0 findings, 731 blobs / 122 users).

Tests

  • normalizeTags / sanitizeTags unit tests (client + server caps)
  • Storage round-trip, duplicate carry-over, update + clear, preserve-on-omit
  • Sync adapter: tag carry-through in list/get/applyRemote incl. LWW + legacy fallback
  • Sync API endpoint: tag sanitize + round-trip + always-array envelope
  • sync-admin: tagged & untagged designs are drift-free

typecheck ✅ · lint ✅ (0 errors) · knip ✅ · affected tests ✅

Introduces an optional `tags` field on saved designs that scales with the
power-user tail (production: design counts reach p95=15, max 69 per user).
This is the data-model foundation for the upcoming designs-manager rebuild
(tag chips, filtering, bulk tag actions).

Tags sync cross-device by riding alongside the design `name` (a sibling of
`params`, not inside it):
- `SavedDesign.tags` + `normalizeTags` (trim/dedupe/cap: 12 tags, 32 chars)
- persisted in IndexedDB (saveDesign/duplicate); `updateDesignTags` helper
- carried through `DesignSyncPayload` + the design sync adapter (LWW: a
  remote tag array wins; a legacy payload without tags falls back to local)
- sanitized + stored server-side in the design envelope (`sanitizeTags`),
  with matching client/server caps as a documented cross-boundary contract

`tags` is optional everywhere (absent = untagged) so there is no destructive
migration and existing saved designs are untouched. The sync-admin drift
math is unaffected: tags appear with the same key+value on both the blob and
the index-size sides, so they cancel in `blob.size - sizeBytes` exactly like
`name` (verified by a regression test + a clean prod audit).
Copilot AI review requested due to automatic review settings May 29, 2026 21:09
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gridfinity-layout-tool Ready Ready Preview, Comment May 29, 2026 9:25pm

Request Review

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR adds an optional tags field to saved bin designs as the data-model foundation for organizing a growing design library. Tags are optional everywhere so no migration is needed, and the implementation is carefully symmetric across client and server.

  • Client: normalizeTags in src/features/bin-designer/utils/tags.ts strips control chars ([\\x00-\\x1F\\x7F]), trims, caps at 32 chars, dedupes case-insensitively, and caps at 12 tags — identical to the server's sanitizeTags via sanitizeString, preserving the sync contract so a tag the client keeps is never silently rewritten on pull.
  • Storage + sync: saveDesign preserves existing tags when an update omits them (via existing?.tags fallback) and clears them on explicit []; applyRemote applies correct LWW semantics — an explicit remote array (even empty) wins; a legacy payload with no tags key falls back to local.
  • Byte accounting: tags appears with the same key+value in both the blob envelope and the index-size calculation, so it cancels in blob.size − sizeBytes and the envelope delta (13 + digits(modifiedAt)) is unchanged.

Confidence Score: 5/5

Safe to merge — tags are optional everywhere, no destructive migration, and the client/server normalization functions are provably identical.

All changed paths are additive and backwards-compatible. The cross-boundary sync contract (client normalizeTags ↔ server sanitizeTags) uses the same regex, trim, and slice order, verified by the test suite. LWW semantics correctly distinguish an explicit empty array from an absent key. The byte-delta accounting for the sync-admin drift check is mathematically unchanged by the addition of tags.

No files require special attention.

Important Files Changed

Filename Overview
src/features/bin-designer/utils/tags.ts New utility: normalizeTags with identical control-char stripping and caps to the server's sanitizeTags (regex [\x00-\x1F\x7F], trim, slice, dedupe, max 12×32)
api/lib/designerValidation.ts Adds sanitizeTags and DESIGN_TAG_MAX_COUNT/LENGTH constants; delegates to sanitizeString which applies the same regex/trim/slice as the client-side normalizeTags
api/sync/designs/[id].ts Tags extracted from unwrapped payload, sanitized, included in sizeBytes and preValidationBytes, stored in the design envelope; delta arithmetic verified unchanged
src/features/bin-designer/storage/DesignerStorage.ts saveDesign preserves existing tags when update omits them (via existing?.tags fallback), clears when empty array, and adds updateDesignTags helper
src/features/bin-designer/sync/designAdapter.ts LWW tag merging in applyRemote: explicit remote array (even []) wins via ?? over local; legacy payloads with no tags key fall back to local via undefined ?? base?.tags
src/core/sync/adapters/types.ts Adds optional tags?: readonly string[] to DesignSyncPayload; correctly optional to preserve backward compatibility with existing sync payloads
scripts/sync-admin/lib/delta.ts Comment updated to document that tags appear with the same key+value on both blob and index sides, canceling in the delta formula; net delta = 13 + digits(modifiedAt) unchanged
src/features/bin-designer/types/index.ts Adds optional readonly tags?: readonly string[] to SavedDesign; absent on pre-tags designs, consistent with the optional-everywhere safety approach

Reviews (2): Last reviewed commit: "refactor(bin-designer): address tag revi..." | Re-trigger Greptile

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional tags field to saved bin designs, threading it end-to-end from IndexedDB storage through the sync adapter, sync engine payload type, and server PUT/GET endpoint, with matching client/server normalization helpers and drift-accounting updates. This is the data-model groundwork for a forthcoming designs-manager rebuild (no UI yet).

Changes:

  • New normalizeTags (client) and sanitizeTags (server) helpers sharing a 12 × 32-char cap as an explicit cross-boundary contract.
  • SavedDesign / DesignSyncPayload / design envelope gain optional tags; storage, duplicateDesign, and new updateDesignTags persist and preserve them; adapter applies LWW (remote array wins, missing field falls back to local).
  • sync-admin drift accounting + tests updated so tags cancels on both sides of blob.size − sizeBytes.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/features/bin-designer/utils/tags.ts New client tag normalizer with shared limits
src/features/bin-designer/utils/tags.test.ts Unit tests for normalizeTags
src/features/bin-designer/types/index.ts Adds optional tags to SavedDesign
src/features/bin-designer/storage/DesignerStorage.ts Persists tags, preserves on omit, adds updateDesignTags, carries through duplicate
src/features/bin-designer/storage/DesignerStorage.test.ts Round-trip / normalize / preserve / clear / duplicate tests
src/features/bin-designer/sync/designAdapter.ts Unwraps remote tags, LWW apply, list/get include tags
src/features/bin-designer/sync/designAdapter.test.ts Tag carry-through and LWW/legacy fallback tests
src/core/sync/adapters/types.ts Adds optional tags to DesignSyncPayload
api/lib/designerValidation.ts Server sanitizeTags + matching constants
api/sync/designs/[id].ts Unwrap/sanitize/store/tiebreak tags; envelope always carries tags array
api/sync/designs/[id].test.ts Sanitize round-trip + always-array envelope tests; size-accounting updated
scripts/sync-admin/lib/delta.ts Comment update: tags are byte-neutral in envelope/index delta
scripts/sync-admin/tests/findings.test.ts Regression fixtures for tagged/untagged drift-free designs

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- normalizeTags now strips control chars to match the server's sanitizeTags,
  so the cross-boundary contract holds for all inputs (prevents a one-cycle
  sync flicker on control-char tags)
- updateDesignTags passes tags through raw and lets saveDesign normalize,
  matching updateDesignName (drops redundant double-normalization)
- document the tags field, storage helper, and sync semantics in the README
@github-actions github-actions Bot added the docs label May 29, 2026
@andymai
Copy link
Copy Markdown
Owner Author

andymai commented May 29, 2026

Thanks for the review! Addressed in dbfe88a:

  • Control-char parity (the contract gap): normalizeTags now strips [\x00-\x1F\x7F] exactly like the server's sanitizeTags, so a tag the client keeps can't be rewritten by the server on the next sync pull. Added a test covering interior-tab / null-byte tags.
  • P2 — double normalization: updateDesignTags now passes tags through raw and lets saveDesign normalize, matching updateDesignName.
  • README: documented the tags field, updateDesignTags, and the sync semantics (name-sibling envelope, LWW, omit-preserves / []-clears, client↔server caps must stay identical).

@andymai andymai merged commit 7d732ba into main May 29, 2026
16 checks passed
@andymai andymai deleted the feat/design-tags-foundation branch May 29, 2026 21:28
andymai added a commit that referenced this pull request May 30, 2026
…bulk actions (#1939)

* feat(bin-designer): organize saved designs with tags, filtering, and bulk actions

Rebuilds the saved-designs manager so it scales with the power-user tail
(production design counts reach p95=15, max 69 per user) instead of the
cramped single-column list.

- Wider dialog (max-w-lg → max-w-4xl) with a denser auto-fill thumbnail grid
- Per-design tag chips on cards; "Edit tags" action opens a tag editor
  (single design) backed by updateDesignTags
- Tag filter bar (chip toggles, AND-match, clear-filters) above the list
- Bulk-selection mode: multi-select to Delete (single confirm), Export, or
  Tag a batch at once

New pieces, each unit-tested: useDesignSelection reducer, tagFilter
(collect/filter) utils, DesignTagChips, TagInput, TagEditDialog, TagFilterBar,
BulkActionBar. Tags reuse the synced field added in #1936. i18n added across
all 9 locales. Verified visually (normal / filtered / selection) via Playwright.

* chore: bump Total JS size-limit to 1652 kB for designs-manager UI

* fix(bin-designer): address designs-manager review feedback

- handleBulkDelete: remove only successfully-deleted IDs from the list so a
  partial storage failure can't drop a still-present design from the UI
  (Greptile)
- prune active tag filters whose tag no longer exists, so the list can't get
  stuck showing nothing while the (hidden) filter bar offers no way to clear it
- skip the tag write (updatedAt bump + sync) when a single/bulk tag edit
  doesn't actually change a design's tag set (new tagsEqual helper, tested)
- TagEditDialog: pass a localized close-button aria-label (common.closeDialog)

* refactor(bin-designer): tighten designs-manager after review pass

- wire the previously-dead useDesignSelection.prune: drop selected IDs for
  designs removed via the row menu while in selection mode, so the count and
  bulk actions stay accurate
- drop unused selection API (CLEAR action, enter() id-seeding) — nothing in
  the UI used them and they can't be caught by knip as object properties
- extract toggleTag() helper for the case-insensitive filter chip toggle
  (was an inline ternary in JSX); tested
- remove restatement/section-divider comments and the redundant DesignTagChips
  container aria-label; keep only WHY comments

* style(bin-designer): bottom-align design card date row

Pin the date/actions row to the card bottom (mt-auto) so dates line up across
a grid row regardless of how many tags each card shows — cards with fewer tags
were floating their date higher, breaking the row's visual baseline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants