Skip to content

feat(contexts): save the menu of an application - #21

Open
remko48 wants to merge 8 commits into
stack/m1-menu-modelfrom
stack/m2-menu-saving
Open

remko48 wants to merge 8 commits into
stack/m1-menu-modelfrom
stack/m2-menu-saving

Conversation

@remko48

@remko48 remko48 commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Applications (contexts) can now save their menu: create and update take a menuItems list that replaces the whole menu.

What changes

  • Input (MenuItemInput): every item is validated before the service is called; an invalid item returns a 400.
    • label: required, at most 200 characters
    • icon: optional, at most 64 characters
    • target type: view, table or url (url when left out)
    • a view or table needs its positive id; a link must be an http or https address with a host, at most 2000 characters
    • technical name: optional, lowercase letters, numbers and underscores, starting with a letter
  • Saving: sending menuItems replaces the whole menu; leaving it out keeps the stored menu. The order of the list is the order of the menu.
  • Technical names: an item without one keeps the name stored for the same target, so a rename or reorder doesn't change it; otherwise one is derived from the label, with a number added when the menu already uses it. Two items sending the same name is a 400.
  • Targets become resources: the views and tables in the menu are added to the application as resources with read permission, and to its start page, so everyone the application is shared with can open them. A new target must exist and be manageable by the user; every unusable target gets the same 403, so a save reveals nothing about other people's views and tables. A stored target that no longer exists is kept and comes back broken. Removing a menu item leaves its view or table in the application as a resource.
  • Concurrent saves: an application update that loses its write lock to another request (SQLite reports "database is locked" at once) is retried.
  • Errors: invalid input on creating an application now returns a 400 instead of a 500.
  • API: menuItems is added to the create and update endpoints (OpenAPI and TS types regenerated).

Page target

  • Saving a menu accepts a page item only if it points at a page of the same application. Anything else gets a 400. A page item adds no node.
  • Creating an application puts a "Dashboard" item first (technical name dashboard, with the usual suffix if that name is taken). It opens the start page, which the server creates in the same transaction. On create only, a page item without targetId means "the new start page", and the dashboard item is then not added twice (MenuItemInput::createListFromInputArray(..., allowNewStartPage: true)). On update, the same item is a 400. Existing applications get no item added automatically.
  • ContextService now takes IL10N for the label.
  • A stored page item whose page is gone stays in the menu and stays broken, like a view or table, so it does not block saving. Only a page target that is new in the save and not a page of this application gets the 400 (testAStoredPageItemWhosePageIsGoneIsKept, testANewPageTargetOutsideTheApplicationIsRefusedEvenWithABrokenStoredOne for a foreign and a missing page id).

Tests: ContextServiceTest (own page accepted and no node, foreign page 400, id-less page on update 400, create without a menu, create with a menu prepends, create with an id-less page item resolves it without a duplicate, update adds nothing), MenuItemInputTest, new ContextControllerTest (create passes the id-less page item on, update gives a 400). All OK, psalm and cs:check clean, OpenAPI regenerated. Prepared in the Conduction fork for porting upstream by @remko48. Prepared with Claude Code.

🖼️ Screenshots

Backend only, no UI changes.

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔙 Backport requests are created or not needed: /backport to stableX.X
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

Creating or updating an application (context) can now set its menu. The
menu items are validated at the controller boundary, and invalid input is
answered with a 400 instead of a 500.

- every save replaces the whole menu inside the same transaction as the
  rest of the update
- the views and tables the menu opens become nodes of the application,
  with read permission, so everyone it is shared with can open them; the
  owner must be able to manage them
- a menu item without a technical name gets one derived from its label,
  with a numeric suffix when the menu already uses it
- only http and https links are accepted

Co-authored-by: Ruben van der Linde <ruben@conduction.nl>
Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Remko <remko@conduction.nl>
@remko48
remko48 added this pull request to stack #24 October 12, 2026 11:39
Comment thread lib/Service/ContextService.php Outdated
Comment thread lib/Service/ContextService.php
Comment thread lib/Model/MenuItemInput.php
Comment thread lib/Service/ContextService.php Outdated
@remko48 remko48 closed this Oct 13, 2026
@remko48 remko48 reopened this Oct 13, 2026
@github-actions github-actions Bot added the AI assisted This PR contains AI-assisted commits label Oct 13, 2026
Comment thread tests/unit/Service/ContextServiceTest.php Outdated
…hnical names

- a new menu target that does not exist gets the same refusal as one the
  owner cannot manage, so a save reveals nothing about others' ids; a target
  that was already in the menu is kept and marked broken
- an item sent without a technical name keeps the name stored for the same
  target, so renaming or reordering no longer changes it
- removing a menu item keeps its view or table as a resource, as documented
- the start page mock gets an id, which PHP 8.5 needs for the array key
- a javascript link with a host is tested, which only the scheme check stops
- the menu list is mapped with a first-class callable

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Remko <remko@conduction.nl>
The update runs in one transaction since the menu is saved with it. On SQLite a
concurrent request can take the write lock between its read and its write, and the
update then failed at once with a 500. It now runs again on a retryable database
error, as atomicRetry() does in core.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Remko <remko@conduction.nl>
Comment thread lib/Service/ContextService.php Outdated
remko48 and others added 4 commits October 14, 2026 09:49
A saved page target must be a page of the same application, otherwise the
save is refused with a 400; it adds no node. Creating an application puts a
"Dashboard" item first that opens its new start page. A page item sent
without an id on create opens that start page; on update it is refused.

Assisted-by: ClaudeCode:claude-opus-5-5
Saving a menu no longer fails on an item that points at a page that was
in the stored menu but no longer exists; it is kept and stays broken,
like a view or table. A page target that is new in the save must still
be a page of the application.

Assisted-by: ClaudeCode:claude-opus-5-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI assisted This PR contains AI-assisted commits

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants