Skip to content

feat(ui): save the shopping list as a menu - #604

Draft
LeMyst wants to merge 1 commit into
cooklang:mainfrom
LeMyst:feat/shopping-list-to-menu
Draft

LeMyst wants to merge 1 commit into
cooklang:mainfrom
LeMyst:feat/shopping-list-to-menu

Conversation

@LeMyst

@LeMyst LeMyst commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #671 (the #380 request in the comments of #385).

I was imagining adding recipes to a shopping list using the web interface, and then having a button to "save" that collection as a .menu file. […] Then, the .menu file would be saved and I could open it and convert it to a shopping list again, based on the current pantry at another time.

What changes

Save as Menu on the shopping list page writes the recipes on the list to a new .menu. Open the menu another week and Add All to Shopping List puts the same list together again, against whatever the pantry holds then.

---
title: Week 41
---

- @./Breakfast/Easy Pancakes{2} \
- @./Risotto{} \
- @./Salads/Caprese{1.5}
  • One bullet per recipe, in list order, with the recipe's factor on the list ({} at ×1). add_menu stores a unitless factor unchanged, so the saved menu round-trips to the same entries and scales. An integration test checks this, including a menu on the list whose {4%servings} reference became ×4.
  • The \ continuation keeps the bullets one list, as the menu editor writes them. Without it, Cooklang joins the lines of a paragraph.
  • A menu on the list is written as its recipes, at the factors the menu gave them. A reference to the menu itself would need menus inside menus, a separate PR.
  • Sub-recipe choices (a recipe added without some of its @./ sub-recipes) are dropped: a menu cannot say "this recipe, but not its dough". Adding the saved menu back includes them all. The dialog and the docs say so.
  • The new menu opens on its menu page rather than in the editor. The point is to look at what was saved and add it back, and the page has Edit one click away.
  • Shown to whoever may edit recipes (creating a file is a recipe edit), never on a static site, and only while the list has something on it.

Design

  • POST /api/shopping_list/save_as_menu {name}:
    • responds 201 {path, skipped}
    • responds 400 for an empty list or a name with nothing left in it
    • responds 409 when the menu exists
    • skipped lists paths a reference cannot spell, e.g. a file name containing {.
  • Auth: the route sits under /api/shopping_list, whose prefix maps to EditLists. It is listed first in the WRITES table at EditRecipes, so a shopper can still change the list but cannot create files. A middleware unit test and the per-role integration test cover it.
  • Shared creation code: /new's creation logic moves to server::new_file::create, used by both routes. It covers the name clean-up, the containment check through symlinks from create_recipe deletes a pre-existing directory when the parent resolves outside the recipe folder through a symlink #549/fix(server): never delete a folder when refusing a new recipe path #582, create_new and the flush before redirecting.
  • Name clean-up fix: each path segment is now trimmed too. Before, a name like " /// " made a folder called " " holding " .menu"; this affects the New Recipe/Menu form as well.
  • Dialog: role="dialog", aria-modal, labelled and described. Focus starts on the name and stays inside the dialog. Escape or Cancel hands focus back to the button. Errors are announced with role="alert" and aria-invalid.
  • Strings in all 9 locales; a section in docs/server.md; the endpoint in the API reference and docs/api.md.

Screenshots

The list Naming the menu The saved menu
list dialog menu

Tests

  • Unit:
    • references, factors and menu expansion;
    • skipped paths;
    • the menu text;
    • name clean-up and titles;
    • the auth table.
  • tests/save_as_menu_test.rs:
    • the round trip through add_menu and the menu page's links;
    • an empty list;
    • a name that is taken, and one with nothing left after clean-up;
    • ../.. in the name;
    • a symlinked folder that leads out of the collection (refused, the link kept).
  • server_auth_test.rs: reader and shopper get 403, editor and admin get through, and the button only shows for the latter two.
  • Playwright shopping-list-save-menu.spec.ts:
    • save and open;
    • a taken name, with axe on the dialog and Escape returning focus;
    • no button on an empty list.
  • Checks: cargo fmt, cargo clippy --all-targets (also --no-default-features: no new warnings), cargo test all clean.
  • Full chromium Playwright run: all pass. When run in parallel, the specs that share seed/.shopping-list (shopping-list-copy/-live, recipe-scaling, this one) can trip over each other locally. They pass with --workers=1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RXA3hAKY9Q8hLCZGeokLPK

"Save as Menu" on the shopping list page writes the recipes on the list
to a new `.menu`, so the same list can be put together again another
week with the menu's "Add All to Shopping List" (cooklang#380, carried into
cooklang#385).

- One `- @./Path/Recipe{factor}` bullet per recipe, in list order, `{}`
  at x1, joined with ` \` so they stay one list. `add_menu` reads a
  unitless factor back unchanged, so adding the saved menu stores the
  same recipes at the same factors.
- A menu on the list is written as its recipes. Which sub-recipes a
  recipe was added without is dropped: a menu cannot say it.
- `POST /api/shopping_list/save_as_menu {name}` → 201 `{path, skipped}`,
  400 for an empty list or name, 409 when the menu exists. It writes a
  file, so it needs `EditRecipes`: listed before `/api/shopping_list` in
  the auth WRITES table, whose prefix would otherwise make it a list
  edit for shoppers.
- The dialog (name, Escape, focus kept inside and handed back) is shown
  to those who may edit recipes; the new menu opens once saved.
- `/new`'s file creation (name clean-up, containment check through
  symlinks from cooklang#549, create_new + flush) moves to
  `server::new_file::create`, shared by both. Clean-up now also trims
  each path segment: "  /// " no longer makes a folder named "  "
  holding " .menu".
- Strings in all 9 locales; docs/server.md and the API reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXA3hAKY9Q8hLCZGeokLPK
@dubadub

dubadub commented Oct 3, 2026

Copy link
Copy Markdown
Member

I'd make it more universal. Maybe just add a copy button to the list of recipes which copies to clipboard all recipes in menu format. Then user decide what to do with that.

list-button-2

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.

2 participants