Files
OSIT-AE-App-Svelte/documentation/PROJECT__AE_Obj_Field_Editor_New.md
Scott Idem 528fb9b33f feat: scaffold element_ae_obj_field_editor_new — parallel-run rewrite of inline field editor
Creates the _new version of the field editor element alongside the working
original (which remains untouched). The _new file starts from the original's
hardened optimistic-update state machine and adds:

- Svelte 5 generics (T) on current_value/draft_value instead of any
- email / url / tel added to the field_type union with edit-mode branches
- object_reload prop removed (was declared, never implemented — on_success
  is and remains the caller's cache-refresh hook)
- to_input_value() / from_input_value() stubs at the two right call sites
  for datetime conversion (both directions, TODO #4 to implement)
- coerce_select_value() stub for select type-mismatch fix (TODO #5)
- Inline TODO mini how-to checked-list mirroring the project doc
- Styling still uses Skeleton classes — tagged for Tailwind/Flowbite swap
  (TODO #6) and a11y gaps marked at their exact markup locations (TODO #7)

Companion files:
- documentation/PROJECT__AE_Obj_Field_Editor_New.md — full plan, migration
  order for all 8 call sites, naming convention note
- documentation/TODO__Agents.md — new entry pointing to the project doc
- documentation/README__Docs_Index.md — project doc added to Active Projects

npx svelte-check: 0 errors, 1 expected benign warning (state_referenced_locally
on the field_type initializer, documented in-file).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 16:47:36 -04:00

11 KiB
Raw Blame History

Project: AE Obj Field Editor — _new Rewrite

Status: 🟡 Planning Priority: Medium — quality-of-life primitive, no event deadline attached Created: 2026-06-16 Last Updated: 2026-06-16 Related: BOOTSTRAP__AI_Agent_Quickstart.md, AE__Naming_Conventions.md


Background

src/lib/elements/element_ae_obj_field_editor.svelte is the generic inline field editor: given object_type + object_id + field_name + current_value, it renders a "click pencil → edit one field → Save" UI and PATCHes just that field via api.update_ae_obj(). It's used in 8 call sites today (session_view, presenter_view, location_view, person_view, device list, presentation list, location list, leads manage tab).

This component already went through one rename cycle — it was originally built as element_ae_obj_field_editor_v3.svelte (replacing an older CRUD v1/v2 pattern), then renamed to drop the _v3 suffix once it became canonical. That migration (build new → migrate callers one at a time → delete old → drop suffix) worked well and is the template for this project.

Review findings (2026-06-16) — see REFERENCE__Common_Agent_Mistakes.md context and the chat log for full detail:

  1. Built entirely on Skeleton UI classes (btn-icon, variant-soft-*, variant-filled-*, .input/.select/.textarea/.checkbox, badge). Conflicts with the stated Tailwind v4 + Flowbite/ShadCN direction. This is the highest-leverage place to do that swap — fix once in elements/, every call site benefits.
  2. object_reload prop is dead. Declared, defaults to true, commented "SWR pattern" — never read in the script. Every call site compensates by hand-rolling identical on_success={() => events_func.load_ae_obj_id__*(...)} boilerplate. Either implement it for real or remove it and document on_success as the actual refresh mechanism.
  3. Datetime fields need manual format conversion at every call site, only on the way in, never on the way out. Confirmed in session_view.svelte: callers must pre-convert with to_datetime_local(...) before passing current_value because the component does no normalization between the stored datetime string and <input type="datetime-local">'s expected format. On save, the native datetime-local string goes straight to the PATCH body with no reverse conversion. Works today because the backend is forgiving, but the contract is leaky.
  4. Select-binding type mismatch is a latent landmine. Object.entries() always yields string vals for <option value>. Today's usages (event_location_id, poc_person_id) are string IDs already, so it's invisible — but a true numeric/boolean enum field would break the optimistic-clear check (current_value === draft_value, strict equality), leaving the field stuck on the optimistic value.
  5. Missing field types: no email (no native keyboard/validation), no url/tel. Original design intent explicitly wanted email support.
  6. current_value/draft_value typed any — no compile-time safety. Svelte 5 supports generics (<script generics="T">); this primitive should use them.
  7. Minor a11y/polish gaps: invisible edit-trigger button stays tab-focusable when $ae_loc.edit_mode is off; icon-only Save/Cancel buttons lack aria-label; error message is tooltip-only (title attr, not visible/announced); no Escape-to-cancel; no autofocus on entering edit mode.
  8. Coarse-store cost (not a bug, just a known tradeoff): reads $ae_loc.edit_mode directly in the template. Fine in isolation, but this component is instantiated per-row in list tables, so any unrelated $ae_loc write re-renders every instance on the page. Not chasing this now — noting it in case a list ever feels janky.

What's not broken and should be preserved as-is: the optimistic-display state machine (has_optimistic / draft_value / clearing once current_value catches up) is already hardened — it went through a real bug-fix round (layout shift, bindable crash, optimistic display) and works correctly for every field type in production use today.


Goals

  1. Near drop-in replacement. Same prop contract (names, types, defaults) wherever possible, so migrating a call site is an import swap plus maybe one or two new optional props — not a rewrite of the call site.
  2. Don't let "drop-in" block real fixes. Where the old contract is part of the problem (e.g. object_reload doing nothing, datetime conversion being the caller's job), change it — call sites get updated during migration anyway, so this is the moment to fix it properly rather than carry the leak forward.
  3. Run both versions in parallel during migration. The old file stays in place and fully working until every call site has migrated and been verified. No "migrate and pray" — verify each call site, then move to the next.
  4. One canonical "_new" rewrite. Per project naming convention going forward: when rewriting an existing thing, the in-progress replacement is named _new (not _v2/_v4/etc.) regardless of how many older versioned files may have existed historically for other components. There should only ever be one "new version of whatever we're working on" at a time. Once migration is complete, drop the _new suffix and delete the old file — exactly as was done for the previous _v3 → (no suffix) rename.

Non-Goals

  • Multi-field / batch editing in one widget — out of scope, this stays a single-field editor by design.
  • JSON sub-field editing (cfg_json.some_key) — not a supported field_type today, not adding it here either.
  • Changing the underlying api.update_ae_obj() PATCH semantics.

New Component: element_ae_obj_field_editor_new.svelte

Prop contract changes from the original

Prop Change Why
object_reload Removed. Never implemented; on_success is and remains the real refresh hook. Removing rather than silently leaving a dead, misleading prop.
field_type Add 'email' | 'url' | 'tel'. Explicitly wanted in the original design, never added.
current_value / draft_value Typed via generic (<script generics="T">) instead of any. Compile-time safety for callers; this is meant to be used consistently project-wide.
Datetime conversion Owned by the component, both directions (stored format ↔ datetime-local/date input format). Removes the to_datetime_local(...) boilerplate currently required at every datetime call site, and fixes the missing reverse conversion on save.
Select value coercion Coerce draft_value back to typeof current_value after a select change (or compare via String() in the optimistic-clear check). Prevents the stuck-optimistic-state landmine for any future numeric/boolean enum field.
Everything else (object_type, object_id, field_name, allow_null, select_options, edit_label, display_block, display_absolute_edit, placeholder, class_li, textarea_rows, log_lvl, on_success, on_error, children) Unchanged. Drop-in for every call site that doesn't use object_reload.

Styling

Rebuilt on Tailwind v4 utility classes + Flowbite, matching whatever the current non-Skeleton convention is elsewhere in the app (check a recently touched component, e.g. the Pres Mgmt Config page, for the current baseline look). No Skeleton classes (btn-icon, variant-*, .input/.select/etc.) in the new file.

Polish fixed opportunistically while rewriting

  • aria-label on icon-only Save/Cancel/edit-trigger buttons.
  • tabindex="-1" on the edit-trigger button when invisible ($ae_loc.edit_mode off), so it's not in the tab order while non-interactive.
  • Visible inline error text (not just a title tooltip) when patch_status === 'error'.
  • Escape key cancels edit mode (mirrors existing Enter-to-save on text/number).
  • Autofocus the input when entering edit mode.

Migration Plan

  1. Build element_ae_obj_field_editor_new.svelte in src/lib/elements/, alongside the existing file. Keep the old file completely untouched and working during this phase.
  2. Smoke-test in isolation — add it to src/routes/testing/ae_obj_field_editor/+page.svelte (the existing test playground for the old component) alongside the old one, covering every field_type including the new email/url/tel.
  3. Migrate call sites one at a time, in this order (simplest/lowest-risk first, datetime-heavy ones last since they exercise the riskiest contract change):
    • src/routes/core/person_view.svelte
    • src/routes/events/[event_id]/(pres_mgmt)/device/device/ae_comp__event_device_obj_li.svelte
    • src/routes/events/[event_id]/(pres_mgmt)/locations/ae_comp__event_location_obj_li.svelte
    • src/routes/events/[event_id]/(pres_mgmt)/location/[event_location_id]/location_view.svelte
    • src/routes/events/ae_comp__event_presentation_obj_li.svelte
    • src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte
    • src/routes/events/[event_id]/(pres_mgmt)/presenter/[presenter_id]/presenter_view.svelte
    • src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/session_view.svelte (has the datetime fields — migrate last, drop the to_datetime_local(...) wrapper at the call site once the component owns that conversion)
  4. Per call site: swap the import, remove any now-unnecessary object_reload usage (there is none today — confirmed dead) and any manual datetime pre-conversion, run npx svelte-check, visually verify the field in the running app (view mode, edit mode, save, optimistic display, error path).
  5. Once all 8 call sites + the test playground are migrated and verified: delete element_ae_obj_field_editor.svelte (move to ~/tmp/agents_trash, never rm), rename element_ae_obj_field_editor_new.svelteelement_ae_obj_field_editor.svelte, update the now-stale import paths left by the rename, run npx svelte-check one final time.
  6. Update docs: AE__Naming_Conventions.md if a "_new" rewrite convention note belongs there, BOOTSTRAP__AI_Agent_Quickstart.md / REFERENCE__Common_Agent_Mistakes.md if anything generalizable came out of the datetime/select-coercion fixes, and mark this doc complete.

Both files coexist for the full duration of steps 14. Nothing is deleted until every consumer is migrated and verified.


Open Questions

  • None blocking the start of step 1. Revisit field-by-field styling choices (e.g. exact Tailwind/Flowbite input classes) against whatever the most recently styled form in the app is using, since Skeleton phase-out is ongoing and conventions may still be settling.

Naming Convention Note (for future rewrites like this)

Going forward, an in-progress full rewrite of an existing component/module is named _new regardless of how many historically-versioned files (_v1/_v2/_v3/etc.) may exist elsewhere in the codebase for unrelated components. There should be at most one "new version of the thing we're currently rewriting" in flight at a time. Once migration completes, the suffix is dropped and the old file is deleted — there is no permanent _new file left behind, just like there's no permanent _v3 file left behind from the previous round of this same component.