Repository navigation
Open
Labels
Description
Part of #3099. We want to store pages and widgets relationally without changing too much of the structure of views, rows, permissions or existing application content.
Schema
erDiagram
CONTEXTS_CONTEXT ||--o{ CONTEXTS_REL_CONTEXT_NODE : "has nodes"
CONTEXTS_CONTEXT ||--o{ CONTEXTS_PAGE : "has pages"
CONTEXTS_PAGE ||--o{ CONTEXTS_PAGE_CONTENT : "start page rows"
CONTEXTS_PAGE ||--o{ CONTEXTS_WIDGET : "grid page widgets"
CONTEXTS_WIDGET ||--o| CONTEXTS_WIDGET_SOURCE : "if it shows data"
CONTEXTS_REL_CONTEXT_NODE ||--o{ CONTEXTS_WIDGET_SOURCE : "shown by"
CONTEXTS_CONTEXT {
int id PK
string name
string owner_id
}
CONTEXTS_REL_CONTEXT_NODE {
int id PK
int context_id FK
int node_id
int node_type
int permissions
}
CONTEXTS_PAGE {
int id PK
int context_id FK "new index"
string page_type "startpage or grid"
string title "new, nullable"
string emoji "new, nullable"
text description "new, nullable"
}
CONTEXTS_PAGE_CONTENT {
int id PK
int page_id FK
int node_rel_id FK
int order
}
CONTEXTS_WIDGET {
int id PK
int page_id FK "indexed"
string type "header, text or data, defined on the server"
string title "nullable"
smallint grid_x
int grid_y
smallint grid_width
smallint grid_height
json settings "nullable, checked against the type schema"
}
CONTEXTS_WIDGET_SOURCE {
int widget_id PK, FK
int node_rel_id FK "indexed"
}
- Pages get a title, an emoji, a description and a new grid page type.
- A widget row holds the type, an optional title and the position and size on the grid.
- Widget settings are stored as one row per setting. The plan has about 25 widget types, and other apps may add more. A new type then needs no new table. The server checks every setting against the type’s schema on save.
- A data widget links to the node row of its application in a separate source table. This keeps the link a real column that the server can check and clean up. It also guarantees a widget only shows data of its own application.
- The start page rows stay as they are.
Behavior
- Saving a page syncs its widgets by id in one transaction.
- Deleting a page or an application deletes its widgets, settings and sources.
- Removing a table or view from the application deletes only the source row. The widget stays and is shown as needing a new table.
- Scheme export and import copy widgets, settings and sources, and remap the node with the mapping importContext() already builds.
API
GET /api/2/contexts/{contextId}/pages/{pageId}returns a grid page with its widgets.POST /api/2/contexts/{contextId}/pagesadds a grid page.PATCH /api/2/contexts/{contextId}/pages/{pageId}changes the page or its widgets.DELETE /api/2/contexts/{contextId}/pages/{pageId}deletes a grid page.GET /api/2/widget-typesreturns the widget types and their schemas.
Reading needs read access to the application. Changes need manage access.
Activity
feat(widgets): store widget settings as one JSON column on the widget
rubenvdlinde commentedon Nov 2, 2026
Thanks for the review on #3131 to #3137, @enjeck.
You're right that the menu never got the same agreement here as pages and widgets. So here's a proposal for the menu, , with your suggestions in it.
#3135 to #3138 are on hold until we agree, and then we rebuild them on this, as two lean PRs with a test per behaviour: the model and the API first, then export and import.
Schema
erDiagram CONTEXTS_CONTEXT ||--o{ CONTEXTS_MENU_ITEM : "has a menu" CONTEXTS_PAGE ||--o{ CONTEXTS_MENU_ITEM : "opened by" CONTEXTS_REL_CONTEXT_NODE ||--o{ CONTEXTS_MENU_ITEM : "opened by" CONTEXTS_MENU_ITEM ||--o{ CONTEXTS_MENU_ITEM : "group holds items, one level" CONTEXTS_MENU_ITEM { int id PK int context_id FK "indexed" int parent_id FK "nullable, a group of the same application" int order string title "nullable, falls back to the title of what it opens" string icon "nullable, the icons pages use" text description "nullable" int page_id FK "nullable, indexed, set for a page item" int node_rel_id FK "nullable, indexed, set for a table or view item" string url "nullable, set for a link item" string technical_name "unique per application, item_<id> by default" }page_id), a table or view of the application (node_rel_id, as you suggested), or a link (url, http or https only). In the API that's onetarget:{pageId},{nodeRelId}or{url}, like a widget'ssource.parent_idand groups can wait for a later step.item_<id>unless one is given.Behavior
nodes, or because the table or view itself is deleted, also when its owner is deleted), when the application is deleted, and on scheme import. Items in a deleted group move to the top level. Without foreign keys a race can still leave an item whose target is gone, so reading the menu leaves those out.importContext()maps nodes, validates every item like the API does, and skips items whose target didn't come along, with a reason. The import preview and moving a user's data carry the menu too.API
The menu gets its own endpoints, like columns: a resource of its own, filtered by application. That also keeps
/api/2/contexts/{context}/{table}free for the row routes agreed in #3094.GET /api/2/menu?context={contextId}returns the menu of an application, as a flat list withparentId.POST /api/2/menuadds an item to the end of the menu:context,title,icon,description,target,technicalName.PATCH /api/2/menu/{itemId}changes an item's title, icon, description, target or technical name. Setting a target replaces the previous one.DELETE /api/2/menu/{itemId}deletes an item; the items in a deleted group move to the top level.PUT /api/2/menu/ordertakescontextanditems: [{id, parentId}]in display order. It must list every item of the menu exactly once, the parent must be a group, and it either saves all of it or nothing ("changed in between, reload", like page saves). It returns the whole menu. Position and parent change only here, so two edits can't build a deeper tree or a loop.Reading needs read access to the application, changes need manage access, checked once through the item's application. Outsiders and items of another application get a 404, readers who try to change something a 403, and a target outside the application a 400. Create and change are rate limited like pages. The application response stays as it is on main.
Who sees what
Per-audience visibility of pages and menu items is a separate topic we'll open an issue for. It would hide things, not protect data, which stays with the table permissions, and nothing in this design blocks it.
Does this match what you had in mind? In particular: removing the item when its table leaves the application (instead of keeping it marked as broken), and groups for one level of nesting.
blizzz commentedon Nov 3, 2026
Thank you for the proposal @rubenvdlinde!
Yes, definitely, this is what we expect.
I do not see a necessity to leave it flat in the first place, probably it makes sense to take this feature in right now. On this functionality and behaviour I have a few questions:
And more thoughts and questions:
page_id,node_rel_idandurlof which only one can be set, while others have to be null. This cannot be enforce by DDL, only application logic. Not a problem per se, but something to keep in mind, especially on future work.That said, all in all it sounds round, nothing fundamental to change 👍
rubenvdlinde commentedon Nov 5, 2026
Thanks @blizzz, good to hear. Then groups go in right away.
Deleting targets. You read it right: when a page, a table or view in the application, or the application itself is deleted, the items that open it are deleted too, not turned into groups. That happens in the same transaction as the delete. The case I meant was the other way round: someone adds an item for a table at the same moment another person removes that table from the application. Adding or changing an item now checks its target inside its own transaction as well. That narrows the window but doesn't close it, because there are no row locks or foreign keys. If it ever happens, the leftover row is harmless: reading the menu skips it, and it goes when the application is deleted. I don't think that's worth a cleanup job, but tell me if you'd rather have one.
Items in a deleted group move to the top level; that's the only case where an item survives a delete.
Empty groups. An empty group stays, so whoever manages the application can fill it again. People who can only read the application don't see it.
The three target columns. Yes, I considered a type and a value. I'd keep the three:
page_idandnode_rel_idare indexed integer references, so removing the items of a deleted page or node is a plain indexed delete, and a widget's source uses the same shape. The rule that at most one is set lives in the service, is tested, and is noted on the entity for later work.Technical names and uuids. Agreed, so I dropped the technical name on menu items. Within an instance an item is addressed by its id. In a scheme export an item refers to its group by position in the exported menu, to tables and views by uuid (moving between instances is what they're for), and to pages by the page's technical name. Once #3094 settles how technical names work for tables in an application, we'll follow it.
Routes. One small change from the proposal: the application id is
contextId, like on the other application routes, soGET /api/2/menu?contextId=5. Your last point about/contexts/{id}/scheme|transfer|pagesseems to have been cut off. What did you mean there?blizzz commentedon Nov 6, 2026
As a follow up, possibly can be a repair job that just once after each Nextcloud upgrade. I am good having an issue about it, and otherwise leave it aside for now.
Oh, I meant to strike that, please ignore.
blizzz commentedon Nov 6, 2026
I remembered wrongly. We have contexts routes collected within:
/api/2/contexts/{contextId}/. Menu related endpoints could also be in the same fashion within/api/2/contexts/{contextId}/menu.