Repository navigation
Conversation
A view can now exist without a table and carries a type (table or grid), so an application can hold pages that are not tied to one table. - tables_views gets a type column and table_id becomes nullable - a view without a table is owned by its creator and listed for them - row, column, import, relation, permission and activity code skips views without a table instead of failing on the missing table - views without a table are removed with their owner Co-authored-by: Ruben van der Linde <ruben@conduction.nl> Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Remko <remko@conduction.nl>
Federated views have no table_id either, so a missing table alone does not mean a view is standalone. View::isStandalone() checks for no table and no external id, and the guards, the owner fallback and the query for a user's standalone views now use it. - a row update, delete or presence check against a view without a table no longer ends in a TypeError: the view holds no rows - the view type is validated with the rest of the input and is typed as table or grid in the API - the migration no longer makes table_id nullable, it already is Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Remko <remko@conduction.nl>
The unique index on (table_id, technical_name) does not hold for a missing table on MySQL, PostgreSQL and SQLite, and on Oracle it makes a name unique across the whole instance. A view without a table is therefore always named view_<id>: create and update refuse a chosen name, and an import ignores the name from its source. The page a user names is the application's menu item, whose technical name is unique within the application. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Remko <remko@conduction.nl>
blizzz
left a comment
There was a problem hiding this comment.
And a general question:
- What is the motivation on the change on Technical name? They are not dependent upon a table. Technical names are user-provided aliases for easier access in in external handlers (e.g. external Flows).
Correct a later pr in the 3rd stack wil cover the export per application but not the account migration so we will fix that
We traced it back and we see that this was already a 500 for normal views too but with the gridviews we have made it is now easier to reach. This is now done in 4a25495
Correct. The app always creates a fresh UUID (v7), so duplicates only come from import or manual database work. Tables already has an index on |
- the row checks use isStandalone(), like the rest of the row code - deleting a user's views without a table lives in ViewService; the listener only calls it - a grid view cannot have a table: refused on create and when changing the type - changing or deleting a row through a view without a table is a 404 instead of a 500 - a view's uuid is unique on its own: duplicates get a fresh uuid in a migration before the index is added, and an import only keeps the source uuid when it is still free Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Remko <remko@conduction.nl>
- a proxy view no longer copies the uuid of the remote view, so one view federated to several users on the same instance does not clash on the uuid index of views; tables keep the source uuid as on main - the migration keeps the uuid on the local view when a proxy shares it, and drops the old (table_id, uuid) index that the uuid-only index covers - the row checks say that federated views read their rows through the federation proxy Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Remko <remko@conduction.nl>
Nah, we cannot have UUIDs on views alone. The naming is not ideal here. While the value is created from UUID provider, the use case is slightly bigger. We introduced them to be able to update a Table schema, and we had to have a reliable identifier, that would also work across instances. At the same time, you should be able to import the same schema in different tables. Hence the connector between table ID and view UUID. This will break, when view UUIDs turn unique. The difference was tables – looks like a inconsistency here – is that they were not needed at all originally. Only when we applied the same mechanics to contexts. And thus one other question, unless @enjeck asked it elsewhere already, what is the underlying idea to use extend the case of views, instead of adding such a functionality within Contexts/Applications? My understand is to use it as an element on a context page, but it would not strictly need to be a view. Unless all other View functionalities would be used (but where do the rows come from? I assume I'd find the answer in follow-up works?) |
|
Hello!
I too am confused why you chose to store grid pages as views without a table. Until now, a view in Tables has always been a projection of one table. By changing this, we're now forced to add null checks to rows, columns, imports, relations, permissions and activity. And to changes the uuid index and the technical names of views. Why not just use Applications, which already have a page model. tables_contexts_page has a page_type column, and today only the start page uses it. Did you consider adding a grid page type there instead? Data widgets would then point at the application's own tables and views. Sharing the application would also share the data on its pages. Unless I'm missing something. |
|
Thanks both, and especially @enjeck for the direct question. You're right, and we'd like to explain how we got here and then change course. Why we built pages as viewsOur starting point was openbuild, which stores its pages in OpenRegister. There, a page is one entity holding its widgets and their layout. When we brought that to Tables, we looked for the smallest change to existing code. Views already had a grid-like shape, a UUID, a technical name and an API. Adding a Applications felt like the riskier place. That trade-off was wrong. The null checks in rows, columns, relations, permissions and activity are the price of it. So are the UUID index and technical name changes @blizzz raised. A view is a projection of one table, and it should stay one. Changing the course (or as we say in dutch Beter ten halve gekeerd dan ten hele gedwaald)Tables already has the split openbuild keeps in one entity. A page belongs to an application. A content row places one of the application's nodes on that page. That content row is our widget, only without a type, a configuration or a position. So we would need to add that there. Building on it also fixes the permission question. A data widget can only point at a node of its own application. Sharing the application then shares the data on its pages, with the permissions already set on the node. So yeah... Better achitecture all together. Extending it without breaking anything
One query needs real work: Two side effects are worth knowing. The scheme import already sets Existing start pagesWe would need to do something to keep uniformity here. the start page moves to the grid as well, so applications have one way of showing pages. Existing content rows load like this:
Existing start pages therefore show their tables and views in the same order as today. Nothing in the database changes until someone saves the page in the grid editor (so its backwards compatible). Every save also fills in So whats next?This PR and three of the PRs stacked on it store pages in views. Those we'll close:
The canvas (#3079) and the widget forms (#3081) are frontend code, and most of it carries over. They are stacked on the PRs above, so they'll come back as new PRs on top of the page model. The data widget will point at a node of the application instead of a view. The application menu (#3086 to #3089) is a separate stack on
Until the page model lands, the dashboard item opens today's start page. Once it lands, the same page shows as a grid, and the menu item stays as it is. Existing applications get no automatic item; their menu stays as saved. What we'll do
|

Views can now exist without a table.
With this PR we prepare for the use of pages inside an application.
Views also get a
type(tableorgrid).Every existing view becomes
table, so nothing changes for current users.What changes
tables_viewsgets atypecolumn with the defaulttable.table_idwas already nullable (for federated views), so a view without a table needs no other schema change.ViewService::create()accepts no table when the type is nottable. Atableview still needs a table, and a view without a table cannot be changed totablelater.view_<id>). Setting a technical name on it is rejected, and an import drops it. The unique index on(table_id, technical_name)treats a missing table differently per database.tableIdis now nullable andtypeis added to the view response (OpenAPI and TS types regenerated).typecan already be changed through the existing view update endpoints. There is no new endpoint yet; creating a view without a table through the API comes in a later PR of this stack.🖼️ Screenshots
Backend only, no UI changes.
🏁 Checklist
/backport to stableX.X🤖 AI (if applicable)