Skip to content

feat(ui): edit a listed sharing in place through the same inline form - #334

Merged
argszero merged 1 commit into
mainfrom
feat/sharing-edit-form
Sep 30, 2026
Merged

argszero merged 1 commit into
mainfrom
feat/sharing-edit-form

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The sharing list offered pause / resume / relist and delete, but no edit — so a wrong quota, plan, model, note or time window could only be fixed by deleting the row and listing it again, which also threw away the row's id, its listing time and its earnings attribution.

This is the frontend half of the host work order 2026-09-30T13:12:07 (translated from the Chinese original: "A listed shared key should be editable in page, and every setting from listing time must be changeable"). The endpoint half landed as #333: PATCH /api/sharings/:id now accepts the full create field set as a partial update, with a three-state key.

Related Issue

None — this is the second PR of the two-PR host work order 2026-09-30T13:12:07; the first (the endpoint) is #333.

Changes

  • ui/js/app.js — every row gains an edit action that opens the same inline form #share-form-card, pre-filled with that row's current values. Edit mode is a module-level editingShareId; there is no second form and no whole-row inline editor (ui/README.md v1.13: prefer inline interaction, no second form). Create and edit share one submit handler, one payload builder (sharePayload), one availability encoding and one validation path — the same shape the server uses, where create and patch share validate_listing.
  • ui/js/app.js — the pre-fill walks provider → Plan → model in order, dispatching a change at each level (fillPlans / fillModels are wired to those selects only, so assigning values directly would leave an empty model dropdown); the "every day" shortcut's property-based disabled and the seven day chips' checked state are restored from the row, per the existing resetShareAvail convention.
  • ui/js/app.js — the key field is three-state, and that is the invariant under a gate. The server hands the client only the mask (mask_upstream_key: first 2 chars + - + last 4), so the mask is written to placeholder and never to value: a mask in value would be submitted as the new key, the server would encrypt the mask into encrypted_key, the original key would fail every later call and nothing in the UI would say so. The field's only way into the payload is the guarded if (key) payload.key = key; (omitted / blank = keep the stored ciphertext), and every programmatic write to that field writes the empty string. The edit request carries the same id, so listing time and earnings ownership do not move.
  • ui/js/app.js — parseShareDays is now the single parse point for available_days; the list renderer and the pre-fill previously each had their own JSON.parse (one datum, two readings).
  • ui/js/i18n.js, src/i18n_pack.rs — share.form.editTitle / share.edit.ok / share.edit.fail in both packs; common.save leaves the unreachable-key list because the edit-mode submit button now consumes it. Counters re-measured, not adjusted by hand: keys 796 → 799, T("…") literals 546 → 549, distinct literals 435 → 437.
  • ui/index.html — id="sf-submit" on the form's submit button (the edit/create mode switch rewrites its data-i18n hook); cache-buster app.js?v=20260930-2, i18n.js?v=20260930-1.
  • ui/README.md — the sharing section names the edit capability, and a new section documents the key three-state invariant, the one-entry-point rule for the payload, and the gate's (lexical) scope.
  • New gate src/state_gate.rs::the_share_edit_form_never_submits_the_stored_key_mask — three rules whose inputs are all derived (the field selector comes from the guard binding, the member name from the payload assignment itself; neither #sf-key nor key is written into the gate). R1 the payload member has exactly one entry point and it is conditional (the field is not in the payload literal); R2 every programmatic write to the committable field writes the empty string (so its content can only come from typing); R3 the converse — the mask must actually be shown, and only via the single placeholder write, fed from the row's member. Plus the_r134_roster_is_derived, the_r134_rules_have_teeth (four variants, each turning exactly its own rule red) and the_r134_scanners_have_teeth.
  • No config / data-structure change.

Tests

  • cargo test — 455 passed, 0 failed (baseline eb2c8ca: 451) — 4 new cases.
  • cargo fmt --check — clean.
  • cargo clippy --all-targets -- -D warnings — clean.
  • A/B on the gate (tree restored byte-identically afterwards): with ui/js/app.js swapped back to the pre-change revision (md5 95be2ffb02d4d77bcf19e73233a59a08, blob a13c5d0), the main gate and all three companion tests fail — the derivation finds 0 guarded payload assignments, which is exactly the pre-change shape; with the shipped revision (md5 23b4f2528c9139ed1633bcac627bafe5, blob b2ffe51) the four are green.
  • Runtime instrument (maintained outside the repository — CI has no JS runner): a jsdom probe loads the real app.js, mocks the sharing API and drives an actual edit — 13 legs × 8 tree variants, all "AS DECLARED": the shipped tree, the pre-change tree (the edit entry does not exist, so the axis legs are recorded FAIL with a reason rather than silently skipped), and six mutations (mask into value; mask not shown; no provider dispatch; day chips left disabled; POST instead of PATCH; key required in edit mode). Each mutation turns its own legs red, and the pre-change tree is not mistaken for a healthy one.

Checklist

  • Branch name follows convention (feat/…)
  • Conventional Commits format, no (#N) in the title
  • Single responsibility, minimal change
  • Gate scope stated honestly: it is lexical — it proves where the mask may go and that the payload member can only come from typing, not what the browser does at that instant (that half is the runtime instrument's, and the repo has no JS runner in CI).

A listing could only be paused, resumed or deleted, so a wrong quota,
plan, model, note or time window could only be fixed by deleting the row
and listing it again — which also threw away the row's id, its listing
time and its earnings attribution.

This is the frontend half of the host's work order
`2026-09-30T13:12:07` ("a listed shared key should be editable in page,
and every listing-time setting must be changeable"); the endpoint half
landed as #333, where PATCH /api/sharings/:id started accepting the full
create field set as a partial update.

Each row gains an edit action that opens the **same** inline form
(`#share-form-card`), pre-filled with that row's current values: edit mode
is a module-level `editingShareId`, and create and edit share one submit
handler, one payload builder, one availability encoding and one
validation path — mirroring the server, where create and patch share
`validate_listing`. No second form, no whole-row inline editor.

The pre-fill walks provider -> Plan -> model in order, dispatching a
`change` at each level, because the model list is built from the plan's
provider; the "every day" shortcut's property-based `disabled` is restored
from the row's day set on the same path.

The key field is the part that is easy to get wrong, so it is the part
under a gate. The client only ever holds the server-made mask, and the
mask belongs in `placeholder`, never in `value`: a mask in `value` would
be submitted as the new key, the server would encrypt the mask into
`encrypted_key`, the original key would stop working permanently and
nothing in the UI would say so. The field's only way into the payload is
the guarded `if (key) payload.key = key;` (omitted = keep the stored
ciphertext), and every programmatic write to that field writes the empty
string.

Also: `parseShareDays` becomes the single parse point for
`available_days` (the list renderer and the pre-fill had two copies of the
same `JSON.parse`), and i18n gains `share.form.editTitle` /
`share.edit.ok` / `share.edit.fail` in both packs.

Tests: `state_gate::the_share_edit_form_never_submits_the_stored_key_mask`
with three derived rules plus roster / teeth / scanner self-checks.
`cargo test` 455 passed (baseline 451).
@argszero

Copy link
Copy Markdown
Owner Author

Self-review (this repo allows self-merge; GitHub will not accept an approval from the author, so the review is recorded here).

Read through the diff on the pushed tree and re-ran everything locally at 30f0c09:

  • cargo test — 455 passed, 0 failed (baseline eb2c8ca: 451).
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings — both clean.
  • Gate A/B on the pushed bytes: with ui/js/app.js swapped to the pre-change revision (md5 95be2ffb02d4d77bcf19e73233a59a08) the main gate plus all three companions fail (0 guarded payload assignments — the pre-change shape); with the shipped revision (md5 23b4f2528c9139ed1633bcac627bafe5) all four are green. Tree restored byte-identically afterwards.
  • jsdom runtime instrument (kept outside the repository — CI has no JS runner): 13 legs × 8 tree variants, all AS DECLARED, including the pre-change tree and six mutations. The pre-change tree is not skipped into a false green: legs whose entry point does not exist are recorded FAIL with a reason.
  • CI on this PR — test / fmt / clippy and msrv both green.

Three things I checked by hand rather than trusting the tests:

  1. The mask cannot be submitted. sharePayload is the only payload builder, and the field's only way into it is if (key) payload.key = key;. openShareEdit passes s.key — the server-made mask — into syncShareFormMode, which writes it to placeholder and deletes the data-i18n-ph hook so a later applyStatic() cannot overwrite it with the static hint. Every write to #sf-key writes "". An edit request with the field left blank therefore sends no key field at all, and the endpoint keeps the stored ciphertext.
  2. The pre-fill actually produces a usable form. provider → Plan → model are filled in order with a change dispatched at each level, because fillPlans / fillModels are wired only to those selects — assigning values directly leaves an empty model dropdown. The row's day set is restored into the seven chips, and the "every day" shortcut's property-based disabled is recomputed from that set (the C-series recycle-path rule).
  3. The edit path is an update, not a re-create. It PATCHes /api/sharings/<existing id>; editingShareId is read into a local before hideShareForm() clears it, and create-vs-edit differs in exactly two places (endpoint/method, and whether an empty key is an error).

Scope note: this completes the host work order's second half — the first (the endpoint) is #333, which is why the field semantics are asserted on both sides. The new gate is lexical: it proves where the mask may go and that the payload member can only come from typing, not what the browser does at that instant; that half belongs to the runtime instrument, and the repository has no JS runner in CI.

@argszero
argszero merged commit c701584 into main Sep 30, 2026
2 checks passed
@argszero
argszero deleted the feat/sharing-edit-form branch September 30, 2026 07:06
@argszero argszero mentioned this pull request Sep 30, 2026
12 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant