feat(jobs): one surface for the job detail sheet #213
No reviewers
Labels
No labels
adr
android
area/calendar
area/design-system
area/i18n
area/jobs
area/offline
area/server
area/testing
bug
ci
duplicate
enhancement
help wanted
invalid
notifications
question
reliability
security
severity/low
severity/medium
tracking
web
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
eagraiclainne/app!213
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/job-detail-sheet"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #172 and #173. First screen built on the field row (#211).
A job sheet you read, then press Edit, then fill in a form, then Save,
asks a household to make every decision at once to change one thing.
Changing who does the bins meant opening an editor over the title, the
note, the date and the points, and a half-finished form was a decision
about all of them. Rows that are their own controls ask for one change at
a time and commit it where it is shown.
Structure follows §5: the title in place, the description as the job's own
words rather than a labelled field, the progress block, then Who / When /
Worth as picker rows, the To do list, and a footer that names the next
thing the reader can actually do — four states, because
CompleteItemrequires the owner and a button that cannot do what it says is worse than
no button.
Why the pickers are in this PR
The first version of this branch wired the three chevron rows to the
pickers the app already had, leaving #173 to replace them. Deployed and
looked at, that was plainly wrong: the app's existing pickers are inline
form fragments with Save buttons, so reusing them re-imported the exact
grammar this redesign exists to delete. The sheet broke two of the five
laws it was built to satisfy — a chevron unfolded a form instead of
opening a picker, and three explicit Saves sat on a screen whose premise
is commit-on-blur.
So #173 is folded in. A chevron now opens a real picker (§8): Who
commits and closes on a tap, because a single-select list has nothing to
confirm; When confirms, because the date and the time are two
decisions, and its footer states the outcome rather than saying Done;
Worth re-filters when the owner changes, since the server's
eligibility rule depends on who the job belongs to. No Save button
remains on this screen, and repricing a linked reward commits in place
like every other value.
Sheets also gained a grab handle and a stack, so a picker over a sheet
dismisses itself rather than the screen beneath it — that one is a real
bug found by building the thing.
Nothing shipped was dropped
Points on a sub-job, removing one, creating a reward from inside the job
and repricing a linked one all survive, in the places §5 puts them. Taking
an unassigned job folded into the Who picker, which is the server's taker
rule rather than a second affordance for it. Ticking a sub-job with points
now celebrates instead of passing in silence, and the sheet honours the
admin bypass the server has always had on completion. Android gained
sub-job promotion, which that surface never had.
A completed repeating job takes §6.3's shape: title struck, successor
named and tappable, and a refusal to reopen that says which job took over
instead of saying no.
The queue debt this collected
Carrying
spawned_nextinto Android's read cache needed a schema bump,which exposed something worse than the feature:
CacheDb.onUpgradedropped
pending_opalong with the caches. The queue is not a cache. Itholds writes that never reached the server — a job renamed on the bus, a
sub-job ticked with no signal — and dropping them loses a household's work
with no symptom at all. The comment in that file had been warning about
this since v12.
It now survives an upgrade, with a test that was checked to fail against
the old behaviour rather than merely pass against the new one.
Left thinner, on purpose
The repeat rule and delete keep their current affordances inside an
interim options block until #175 and #176. That block is now the only
stand-in in the sheet.
conformance/render-job-sheet.jsonandrender-job-pickers.jsonpin thesheet's and the pickers' string inventories across both surfaces. No
screenshot golden moved: none photographs a
Sheet.Jobs.tsx2,565 → 903 lines;ItemSheets.kt1,434 → 327.🤖 Generated with Claude Code
Test report
Coverage: 27.0%
Updated by the check workflow · commit
846dee7442Android test report
Coverage:
Updated by the android workflow · commit
846dee744290ef92aea94b39b5957b4b39b5957b3fab3be704Force-pushed: four defects found by deploying it and using it, all fixed in
this commit.
Closing a picker closed the whole sheet. React synthetic events propagate
through the React tree, not the DOM tree, so a picker rendered inside the
sheet's children is a React descendant of the sheet's own element — and the
picker's leave animation bubbled into the sheet's
onAnimationEnd, which readsheet-downand closed it. Guarded withe.target === e.currentTargetinSheet.tsx, so it holds for every sheet in the app. The overlay's backdropclick turned out to be unguarded too, and only accidentally safe; it now guards
itself instead of leaning on a child's
stopPropagation.This one was unreachable from a test: jsdom ships no
AnimationEventconstructor, and React skips registering
animationendwhen it is missing. Ashim now makes that class of bug catchable.
The linked reward rendered as a broken row — its name in the label column,
its value as a bare number, unstyled Unlink and Delete beneath. And the Worth
picker never showed what was already linked, because it lists eligible
rewards and eligibility excludes anything already attached.
Those were one mistake seen twice. The picker now owns the answer: the linked
reward first and drawn as selected, its amount editing in place, Unlink and
Delete beside it, then the eligible rewards,
Make a new one,Worth nothing.The block under the row is deleted, component and CSS, on both surfaces. The
Worth row is §5's shape again — a value and a chevron.
The add-a-sub-job row was the shipped quick-add form, never restyled. It is
now one more row on the list's 48px rhythm, keeping the name-and-price-in-one-
gesture capability.
Also fixed while in there: unticked checkboxes drew a ghosted tick at 15%
opacity. §5 wants an empty box with a 2px muted border.
Every web fix has a test confirmed to fail against the previous commit. Android
did not have the first defect — Compose does not bubble — and the report says so
rather than claiming a fix that was not made.
Known parity gap, left deliberately: Android's checkbox is Material's 20dp/2dp
rather than §5's 22dp/7dp. Replacing Material's toggleable semantics over 2dp is
not worth it; #178's pass can settle it.
3fab3be7047451a4bbc0Force-pushed again: the Worth picker's last three styling defects, found on the
same deploy.
Make a new oneopened a raw form field — a bordered number input withbrowser spinner arrows, between rows that are all clean 56px picker rows. It
is now one more row, with a digit-filtered text field and no spinner. Android
got the same shape, built from
PickerRow's layout rather than anOutlinedTextField.Worth 0 points, disabled, while the field wasempty. A reward worth nothing is not something the server accepts, so the
button was stating an outcome that could not happen — and a disabled control
is what law 5 bans anyway. There is now no footer until the amount is one the
picker could commit, and then it states the real outcome, the way the When
picker's footer states
Due Tuesday 09:45.UnlinkandDelete this rewardwere bare underlined links, with theunderline running under the trash icon. They are quiet buttons in the
picker's own voice now. Android needed no change here — Compose's
TextButtonnever underlined, and already matched the sheet's delete-triggergrammar.
Each fix has a test proven to fail first, by swapping the pre-fix sources back
in with
jj file showand watching the assertions break, on both surfaces.All four styling defects on this screen came from the same place: the shipped
app's form fragments reused inside the new row grammar. The sheet is clean now;
the next ticket to touch one of those surfaces should check for it rather than
wait for a deploy to find it.
7451a4bbc0aac0e632beForce-pushed: two more form fragments fixed, the whole screen swept, and the
goldens recorded.
The sweep found a data-loss bug.
Remove iton a sub-job hard-deleted onthe first tap — no question, no undo. Delete is one of exactly two acts the laws
send to a dialog, precisely because it cannot be taken back. It now asks
Delete "Wash up"? This one can't be undone.The two defects reported from the deploy: the When picker's
Timewas a boxedfield with a clock button beside it and is now a row (24-hour kept, and clearing
moved into the list as
Any time, so the empty row names its emptiness insteadof going blank);
Make it its own jobandRemove itwere underlined links andare now the last two of §5's five things, as rows.
Nineteen hits swept, each with a verdict. Nine fixed, including several nobody
reported: the
⋯entries and both delete confirms were still buttons in aform-actionsblock, Android tintedUnlinkwith the accent where web usesink-soft, and a sub-job's checkbox was disabled-and-faded when it belongs to
someone else — law 5 wants a fact drawn at full strength and simply not offered.
Two left deliberately: the
⋯repeat block and the convert chooser are the last.fieldfragments here, and #175 replaces that whole block with the designedrepeat sheet. Styling them now is work thrown away; their triggers are rows.
One reported, not fixed:
Jobs.tsxstill holds the shipped create form withfour
(optional)labels. That is §8's create screen — #177 owns it.Goldens recorded: 12 cases, 24 images — the sheet with sub-jobs, a sub-job
expanded, a plain job, and all three pickers. Hearth light and dark, because
every colour in them comes from components whose own goldens already cross all
19 families; what these pin is layout. The When cases pose on the frozen e2e
anchor, or a month grid with today ringed would re-record itself every midnight.
Thirteen tests, each proven to fail against the previous commit first.
AGENTS.mdnow carries the form-fragment sweep as a hand-off condition with itshunt table, so the next screen built from rows gets it without being asked.
aac0e632be298dcb9275Force-pushed: a behaviour regression this branch introduced, now reverted.
What broke. The rebuild widened the tick rule to
isAdmin || owner === me || owner === "", on both surfaces, for sub-jobs and for the job's own footer. Soan admin was offered a tick on everyone's work. It was done on purpose, with the
reasoning that the server's
RequireOwnerbypasses for an admin and the shippedweb "denied a tick the server allows".
That reasoning is backwards. What the server permits is not what the interface
should offer. The bypass exists so a parent can correct data through the API or
after reassigning — not so they can tick a child's chore off on their behalf.
And since completing a sub-job with points banks them to its owner, an admin
ticking someone else's sub-job credits that person for work they may not have
done, which quietly breaks what the reward system is for.
The design already said so, and this branch had already implemented the other
half of it: §5's footer shows
Waiting on Ali, unfilled and not a button, for asub-job that belongs to someone else, and §7b calls a sub-job the reader owns
"the only interactive row on the sheet". The footer states were right; the
checkbox rule under them was not, so the same screen contradicted itself.
Fixed by dropping
isAdminfromcanTickJob/tickableStep(web) andcanTickJob/tickable(Android). The server guard is untouched.One correction to the report that prompted this. The household remembered
1.8 as letting the job's owner tick an unassigned sub-job. It did not — it let
anyone:
v1.8.1 web/src/pages/Jobs.tsx:1677reads(stepOwner === me || stepOwner === ""), with no parent-owner check and no admin clause, and Androidagreed. §7b says the same thing, because that is what
CompleteItemdoes with anunowned item. So 1.8's rule is restored exactly, including that row. Narrowing it
to the job's owner would be a new decision, not a restoration, and is left open.
The
isAdmincapability is kept on the Android caps object, deliberately unused,so a test that really flags an admin pins this rather than the absence of a line
nobody would notice coming back.
298dcb9275846dee7442