fix(meal-plan) Share via list-shareable using a generic ShareDialog #52

Merged
araemer merged 2 commits from generic-share-dialog into stage 2026-10-03 11:45:31 +00:00
Owner

Merge order: merge #47 (shopping lists) first. This branch is based on 001-shopping-lists, because it needs what #47 introduces (GET /user/list-shareable in the app, CompactUserModel, UserDisplayModel, IconButton's touchTarget). Until #47 is merged, the diff below also shows #47's changes; its own changes are 024a898 and 8d21e3b. After #47 is in, this PR's diff shrinks to those two commits.

Fixes #46. Follow-up to the review of #47 (comment on ShoppingListShareDialog: "isn't this basically the same as for meal plans? Something to generalize?").

The bug (#46)

MealPlanShareDialog loaded its user list with fetchAllUsers() (GET /user/all), which is admin-only: for every other user it returned 403 and the user dropdown stayed empty, so non-admins could not share a meal plan. Verified against the dev backend: as user, GET /user/all → 403, GET /user/list-shareable → 200.

Changes

  • hooks/useShareableUsers: loads the share candidates from GET /user/list-shareable (any authenticated user) once on mount, with reload. Used by both dialogs; this is the fix.

  • basics/ShareDialog: one generic "Teilen" dialog, generic over the permission enum:

    • an optional owner row
    • one row per share: avatar, name, "(Du)" for the own share, level select (or plain label when the feature can't change levels) and remove
    • an add section that hides users who already have access
    • errors inline
  • ShoppingListShareDialog: now a thin wrapper. Same behaviour as before: owner row, two levels, the context's mutations and error texts.

  • MealPlanShareDialog: now a thin wrapper with the three meal plan levels. Meal plans gain:

    • avatars
    • changing a share's level: MealPlanPage gets a handler that calls saveMealPlanShare with id and version; the API already supported it. A concurrent change gives a 409 and an inline message.

    A meal plan carries no owner name, so it has no owner row. Errors are now shown inline; before they surfaced as unhandled promise rejections.

  • docs/architecture.md updated: ShareDialog, useShareableUsers, MealPlanShareDialog.

  • BasicDialog scrolls when taller than the screen (8d21e3b): the overlay scrolls and the card is centred with my-auto instead of items-center. A dialog that fits stays centred; a taller one starts at the top and scrolls to its actions. Affects every dialog; found while verifying this PR on a phone in landscape. Verified: short dialogs still centred, backdrop click still closes, the tall share dialog scrolls from its close button to "Hinzufügen", no horizontal overflow on the five viewports.

Verification

  • npm run build, npm run lint (only the 2 existing warnings in MealPlanDayProvider) and npm run test (372 tests) pass.
  • /verify-ui as the non-admin user:
    • Meal plan, on a throwaway plan that was deleted afterwards: 9 candidates load. Sharing with pro, changing the level to Vollzugriff and removing the share all work, with no errors.
    • Shopping list: owner row present; adding (default Lesen & Schreiben), changing to Vollzugriff and removing all work.
    • Both dialogs, desktop, both tablet orientations and phone portrait: fit.
    • Phone landscape (890 × 400): the shopping list dialog with an owner and one share was taller than the screen and got cut off, because BasicDialog couldn't scroll. Fixed in 8d21e3b (below).

🤖 Generated with Claude Code

**Merge order: merge #47 (shopping lists) first.** This branch is based on `001-shopping-lists`, because it needs what #47 introduces (`GET /user/list-shareable` in the app, `CompactUserModel`, `UserDisplayModel`, `IconButton`'s `touchTarget`). Until #47 is merged, the diff below also shows #47's changes; its own changes are 024a898 and 8d21e3b. After #47 is in, this PR's diff shrinks to those two commits. Fixes #46. Follow-up to the review of #47 (comment on `ShoppingListShareDialog`: "isn't this basically the same as for meal plans? Something to generalize?"). ## The bug (#46) `MealPlanShareDialog` loaded its user list with `fetchAllUsers()` (`GET /user/all`), which is admin-only: for every other user it returned 403 and the user dropdown stayed empty, so non-admins could not share a meal plan. Verified against the dev backend: as `user`, `GET /user/all` → 403, `GET /user/list-shareable` → 200. ## Changes - **`hooks/useShareableUsers`:** loads the share candidates from `GET /user/list-shareable` (any authenticated user) once on mount, with `reload`. Used by both dialogs; this is the fix. - **`basics/ShareDialog`:** one generic "Teilen" dialog, generic over the permission enum: - an optional owner row - one row per share: avatar, name, "(Du)" for the own share, level select (or plain label when the feature can't change levels) and remove - an add section that hides users who already have access - errors inline - **`ShoppingListShareDialog`:** now a thin wrapper. Same behaviour as before: owner row, two levels, the context's mutations and error texts. - **`MealPlanShareDialog`:** now a thin wrapper with the three meal plan levels. Meal plans gain: - avatars - changing a share's level: `MealPlanPage` gets a handler that calls `saveMealPlanShare` with id and version; the API already supported it. A concurrent change gives a 409 and an inline message. A meal plan carries no owner name, so it has no owner row. Errors are now shown inline; before they surfaced as unhandled promise rejections. - `docs/architecture.md` updated: `ShareDialog`, `useShareableUsers`, `MealPlanShareDialog`. - **`BasicDialog` scrolls when taller than the screen** (8d21e3b): the overlay scrolls and the card is centred with `my-auto` instead of `items-center`. A dialog that fits stays centred; a taller one starts at the top and scrolls to its actions. Affects every dialog; found while verifying this PR on a phone in landscape. Verified: short dialogs still centred, backdrop click still closes, the tall share dialog scrolls from its close button to "Hinzufügen", no horizontal overflow on the five viewports. ## Verification - `npm run build`, `npm run lint` (only the 2 existing warnings in `MealPlanDayProvider`) and `npm run test` (372 tests) pass. - `/verify-ui` as the non-admin `user`: - **Meal plan, on a throwaway plan that was deleted afterwards:** 9 candidates load. Sharing with `pro`, changing the level to Vollzugriff and removing the share all work, with no errors. - **Shopping list:** owner row present; adding (default Lesen & Schreiben), changing to Vollzugriff and removing all work. - **Both dialogs, desktop, both tablet orientations and phone portrait:** fit. - **Phone landscape (890 × 400):** the shopping list dialog with an owner and one share was taller than the screen and got cut off, because `BasicDialog` couldn't scroll. Fixed in 8d21e3b (below). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs(adr) Add shopping list refresh ADR for spec 001
All checks were successful
/ test (pull_request) Successful in 1m9s
a5661d6382
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(plan) Add shopping list implementation plan (spec 001)
All checks were successful
/ test (pull_request) Successful in 1m2s
eb28899d1b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApiError now carries code: ErrorCode, read from the error body or derived
from the HTTP status when the body has no known code. The error body is read
once as text and then parsed, fixing the double-consume when it is not JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New ADR partially supersedes 2026-09-20-contract-enums-in-models (only its
explicit list of 9 enums). CLAUDE.md states the rule by kind and points
server-refreshed contexts at the shopping list refresh ADR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response-only mappers for lists, items, shares and compact users;
ShoppingListRestResource returning only models; GET /user/list-shareable;
item input parsing, display texts, error classification and optimistic
state transforms, each with tests. Name helpers and UserAvatar accept
CompactUserModel via DisplayableUser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entry, overview and list routes; ShoppingListProvider loads and refreshes
the list in place (polling, visibility, pull to refresh, settle-refresh)
with optimistic tick/remove/move/reorder/remove-ticked; item rows with tap
panel and drag handle, add form with duplicate prompt, edit/move/share
dialogs; PullToRefresh and NameInputDialog basics; menu entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On desktop the three buttons wrapped into a ragged right-aligned stack in
the narrow dialog card; stack them full-width with the primary on top, as
on phones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entfernen in an item's panel and Erledigte entfernen now ask for
confirmation (DeleteModal), revising plan D8 after review. The item
confirmation lives on the page, not in the dnd-kit-transformed row.
Kept as its own commit so it can be reverted on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(shopping-list) give a full-access user's own share the usual controls
All checks were successful
/ test (pull_request) Successful in 1m4s
a2be4dfac8
Consistent with the meal plan share dialog: the user's own share row,
marked (Du), has the permission select and the remove button. Removing it
ends their access (the page goes to the overview with the access-lost
notice); losing FULL closes the share dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(shopping-list) keep the rename dialog's draft across polling
All checks were successful
/ test (pull_request) Successful in 1m4s
184e659029
The list page's rename dialog was keyed on the live list version, so a
rename by someone else reset the user's typing on the next poll, and the
save used the live version, so it never got the 409. The dialog now
snapshots name and version when it opens; renameList takes that version,
and a conflict re-seeds the dialog from the refreshed list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(shopping-list) cover the error-message selection and form value helpers
All checks were successful
/ test (pull_request) Successful in 1m3s
977b2b22ae
itemToInputValues and EMPTY_ITEM_INPUT_VALUES move into
shoppingListItemInputUtils next to formatAmountForInput, with tests.
shoppingListMessages stays component-side and gets its own test, since it
decides when an error is shown and when navigation makes it unnecessary;
CLAUDE.md's Tier 1 scope lists it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(shopping-list) don't flash "item gone" when the whole list is gone
All checks were successful
/ test (pull_request) Successful in 1m3s
dea07c397b
An item route's 404/403 set the item-gone message in the same tick as the
reload that finds the list deleted or inaccessible, so the popup could
paint before the page navigated to the overview. The page now shows its
error popup only while the list is READY (plan 10.4 / 10.7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(shopping-list) keep (Du) visible, use the plan's prompt text, document children
All checks were successful
/ test (pull_request) Successful in 1m5s
9a46c30495
- Share dialog: only the name truncates, so the (Du) marker stays visible
  on narrow phones.
- Overview: the choose-a-default prompt uses the plan's wording; the extra
  touch-specific sentence is gone.
- JSDoc on the children props of ShoppingListProvider and PullToRefresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(shopping-list) centre the list column, make add primary, tone down remove-ticked
All checks were successful
/ test (pull_request) Successful in 1m9s
294ea7b63e
- Overview and list page bodies use ContentBody (centred max-w-2xl column,
  like the recipe pages) instead of spanning the full desktop width; px-0
  keeps the full width on phones.
- "Eintrag hinzufügen" uses the primary style, like "Schritt hinzufügen".
- "Erledigte entfernen" is a small link-style action (TextLinkButton) with
  a 44 px tap area instead of a full button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(shopping-list) light hover tooltip in the InfoTooltip style
All checks were successful
/ test (pull_request) Successful in 1m4s
55b616e0b7
The item hover was a dark grey bubble, unlike every other tooltip in the
app. It now uses InfoTooltip's white/bordered style, sizes to its lines
(one line per reason on desktop), and never exceeds the row width, so a
narrow desktop window doesn't scroll sideways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(shopping-list) show the item hover tooltip only after a short rest
All checks were successful
/ test (pull_request) Successful in 1m3s
06bff72488
The tooltip appeared the moment the pointer touched a row, so moving the
mouse across the list made bubbles flicker. It now appears after the
pointer rests on a row for 500 ms and hides immediately on leave
(DELAYED_HOVER_TOOLTIP_CLASS in config/layout.ts). Touch devices are
unchanged: no tooltip, the tap panel shows the same text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refactor(meal-plan) use NameInputDialog for creating and renaming meal plans
All checks were successful
/ test (pull_request) Successful in 1m9s
f49a258665
MealPlanCreateDialog and MealPlanRenameDialog were the same single-field
dialog as the new NameInputDialog (trimmed, non-empty, rename disabled
while unchanged). The meal plan page now uses NameInputDialog with its own
texts, and the two files are gone (follow-up from plan 001, D8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-on: #50
- Inputs in NameInputDialog and ShoppingListItemFields rely on the global
  input style from index.css instead of restating (and drifting from) it;
  the item fields keep only min-h-11 for the tap target.
- DisplayableUser becomes UserDisplayModel in src/models, a documented
  interface that UserModel and CompactUserModel both satisfy.
- Button renders type="button" and gains submitsForm; the add form uses
  Button for both actions instead of hand-composed native buttons.
- ShoppingListBadge becomes basics/Badge with a BadgeVariant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SearchField, LinkInput and MealPlanDayNoteEditor restated the global
input/textarea style from index.css (border, background, focus ring).
They now keep only their deliberate differences: the search field's icon
padding and compact toolbar look, the link input's tiny toolbar size and
thinner ring, the note's small text and fixed size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Badge gains success and neutral variants (dev console an/aus) and a
colorClassName override for domain palettes; the AI import status keeps
its colours in aiAnalyticsColors.ts, shared with the chart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HeaderSwitchTitle's plain title positioned the bee outside its box
(-right-12), so a long title that wrapped to the full width on a phone
pushed the bee past the screen edge. It now reserves the bee's width as
padding, as LogoHeading does; one-line titles look the same as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-on: #51
- Text buttons, inputs and selects keep the app's defaults (Button ~40 px,
  global input ~42 px); the per-use min-h-11 on them is gone. The 44 px
  touch target applies only to small controls: IconButton's touchTarget
  prop for icon buttons in rows, and TOUCH_TARGET_CLASS /
  TOUCH_TARGET_HEIGHT_CLASS in config/layout.ts for the drag handle, the
  row body and the remove-ticked link. Revises plan step 10's blanket
  44 px rule.
- basics/Checkbox replaces the item row's hand-built checkbox.
- basics/CollapsibleSection replaces the done section's fold header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(plan) record the deviations from plan 001 in an append-only section
All checks were successful
/ test (pull_request) Successful in 1m3s
58611570a5
The approved plan stays unchanged; a Deviations section lists each place
the implementation departs from it (plan reference, what changed, why,
who decided), the status of the GATE C points, and the follow-up issues.
First example of a convention proposed for all plans in recipe-platform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ShoppingListErrorKind becomes ShoppingListErrorType (the app's const
  enums are named ...Type); action results report errorType.
- NOTICE_MESSAGES documents why router state carries a notice id.
- formatItemQuantity becomes formatQuantity in utils/quantityUtils and is
  used for recipe ingredients too: German decimal comma, scaled amounts
  rounded to three fraction digits.
- IngredientMapper turns the backend's null amount/unit into undefined.
  The backend sends null contrary to the contract's optional fields; the
  shared formatter exposed it as a crash of the recipe detail page during
  verification, before this was pushed.
- Plan deviations recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(meal-plan) share via list-shareable using a generic ShareDialog
All checks were successful
/ test (pull_request) Successful in 1m7s
024a8987d7
MealPlanShareDialog loaded its user list with fetchAllUsers, which is
admin-only, so non-admins could not share a meal plan (#46). Both share
dialogs now use a generic basics/ShareDialog with candidates from
GET /user/list-shareable (hooks/useShareableUsers). Each feature keeps its
permission levels, API calls and error texts in a thin wrapper. Meal plans
gain avatars and changing a share's level (saveMealPlanShare with id and
version), which the API already supported.

Fixes #46

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(ui) let dialogs taller than the screen scroll
All checks were successful
/ test (pull_request) Successful in 1m5s
8d21e3b882
BasicDialog centred its card with items-center in a fixed overlay that
couldn't scroll, so a dialog taller than the screen (e.g. the share dialog
on a phone in landscape) was cut off at the top and bottom with no way to
reach the close button or the actions. The overlay now scrolls and the
card is centred with my-auto, which top-aligns it when it doesn't fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
araemer merged commit f697a959c3 into stage 2026-10-03 11:45:31 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
araemer/recipe-app!52
No description provided.