feat(ui): edit a listed sharing in place through the same inline form - #334
Merged
Merged
Conversation
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).
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
Three things I checked by hand rather than trusting the tests:
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/:idnow 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-leveleditingShareId; there is no second form and no whole-row inline editor (ui/README.mdv1.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 sharevalidate_listing.ui/js/app.js— the pre-fill walks provider → Plan → model in order, dispatching achangeat each level (fillPlans/fillModelsare wired to those selects only, so assigning values directly would leave an empty model dropdown); the "every day" shortcut's property-baseddisabledand the seven day chips' checked state are restored from the row, per the existingresetShareAvailconvention.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 toplaceholderand never tovalue: a mask invaluewould be submitted as the new key, the server would encrypt the mask intoencrypted_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 guardedif (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 sameid, so listing time and earnings ownership do not move.ui/js/app.js—parseShareDaysis now the single parse point foravailable_days; the list renderer and the pre-fill previously each had their ownJSON.parse(one datum, two readings).ui/js/i18n.js,src/i18n_pack.rs—share.form.editTitle/share.edit.ok/share.edit.failin both packs;common.saveleaves the unreachable-key list because the edit-mode submit button now consumes it. Counters re-measured, not adjusted by hand: keys796 → 799,T("…")literals546 → 549, distinct literals435 → 437.ui/index.html—id="sf-submit"on the form's submit button (the edit/create mode switch rewrites itsdata-i18nhook); cache-busterapp.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.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-keynorkeyis 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 singleplaceholderwrite, fed from the row's member. Plusthe_r134_roster_is_derived,the_r134_rules_have_teeth(four variants, each turning exactly its own rule red) andthe_r134_scanners_have_teeth.Tests
cargo test— 455 passed, 0 failed (baselineeb2c8ca: 451) — 4 new cases.cargo fmt --check— clean.cargo clippy --all-targets -- -D warnings— clean.ui/js/app.jsswapped back to the pre-change revision (md595be2ffb02d4d77bcf19e73233a59a08, bloba13c5d0), 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 (md523b4f2528c9139ed1633bcac627bafe5, blobb2ffe51) the four are green.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 intovalue; 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
feat/…)(#N)in the title