Repository navigation
Conversation
Signed-off-by: Enjeck C. <patrathewhiz@gmail.com>
Signed-off-by: Enjeck C. <patrathewhiz@gmail.com>
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>
chore: remove unused babel config dependency from devDependencies
5b9b7e3 to
2ebf8c7
Compare
Views come from more source (owned, and shared) and there was a race condition that would reset the list of views instead of extending it. Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de> Assisted-by: ClaudeCode:claude-opus-5
- 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>
rjzondervan
left a comment
There was a problem hiding this comment.
Checked 4a25495 against blizzz's review on nextcloud#3075 and the replies there. Every change promised in those replies is in this commit: isStandalone() in both Row2Mapper checks, the cleanup moved to ViewService, grid views refused with a table on create and update, the 404 for row changes through a view without a table, and the uuid-only index with a migration and an import that keeps a free uuid. Psalm and cs are clean, and the new tests pass. In a mutation check, the new rules are all caught; what survives is moved or older code (the listener call, the loop continuing after one failed delete, the Uuid::isValid() check on import).
Inline: one 🟡 on the new index, plus two smaller points. Two things from the upstream replies are not in this commit:
- Technical name: blizzz's general question in his review summary (why the technical name changes, since it does not depend on a table) has no answer on nextcloud#3075 yet, and this commit does not touch technical names.
- TablesMigrator: the reply on grid views not being exported says "we will fix that", but this commit does not touch
TablesMigrator. Worth saying on nextcloud#3075 whether it comes in this PR or a later one.
- 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>
build(release): v2.3.2
…les-views fix(sidebar): keep views the table listing does not carry
Signed-off-by: Rello <github@scherello.de> Signed-off-by: Rello <Rello@users.noreply.github.com> AI-assistant: Codex (GPT-5) Co-Authored-By: Andy Scherzinger <info@andy-scherzinger.de> Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
View::getLayoutNormalized(), ViewUpdateInput::normalizeLayout() and importView() each spelled out the set of layouts, and the frontend repeated the card layouts in two components. A backing enum and matching frontend constants now hold the values once. importView() keeps accepting a layout it cannot resolve by dropping it, so a scheme written by another version still imports, but it says so rather than leaving the leniency implicit in a second copy of the list. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The template called getPreviewUrl twice per row, once in the v-if and once in the src binding, each parsing the cell as JSON and looking its column up again, while the title and metadata helpers repeated the same column lookups for every row. A page of a hundred cards paid for all of it on each render. The values are now derived in one computed pass over the page, with the title and background columns resolved once for the whole set. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
getDisplayValue() guarded against a column without getValueString, but every column reaching the card layouts is a columnClass instance, so the branch never ran. The layout picker wrapped a single radio row in a presentation table, which is a layout table for something flex does, and its title rule matched no element. The fallback to the second column as a card title now says why it is the second one. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
layout and viewSettings were copied into viewSetting, the object holding a viewer's unsaved tweaks, so a manage gated property of the view and a local adjustment became indistinguishable. That is what let "Reset local adjustments" drop the layout. Both now reach the table as their own props, sourced from the view itself, and viewSetting.layout means only "this viewer switched layout". The rendered layout is that override falling back to the saved one, so resetting returns to the view's layout instead of erasing it, and the reset button appears once a viewer has switched. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Only a manager can open the view settings, so a recipient of a shared view had no way to read it as anything but the layout it was saved with. The toolbar gains a menu button at its trailing edge listing the three layouts, each with its own icon. Choosing one writes the local view setting, so it applies at once, is never persisted, and is cleared again by "Reset local adjustments" or by leaving the view. The trigger carries the icon of the current layout and is labelled "Switch layout", since an icon-only button would otherwise have no accessible name. It appears only where the view was set up for cards, meaning a card title source is configured or the saved layout is already tiles or gallery: cards of a view without a title source would carry nothing but row numbers. That is a presentation choice, not a constraint, so nothing on the write path enforces it. An embedded widget declines the switch outright, being a snapshot of a view inside someone else's document, and the smart picker preview inherits that; public links and contexts keep it. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
A card was a <button> holding a div, a list and, wherever a rich text column carries one, a link. A button may contain none of those: it takes phrasing content with no interactive descendant, and the parser does not repair the tree, so the invalid markup reached the DOM. A link inside a card also bubbled its click to the button, which followed the link and opened the row editor at the same time. The card is now a div with the button role, keyboard activation on enter and space, and a focus ring of its own. Clicks coming from a link are left to the link. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Card sources drop to null when they no longer resolve, which contradicted the soft-invalidation rule as written. The rule now says why a reference that only selects how a row is displayed is treated differently from one that decides which rows or values are returned: an id that fails to remap on import may address a live column of an unrelated table, and a view without card sources falls back to the text-only card layout, so there is no broken state for a user to repair. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
notifyNodeUpdate() and the share payload carried only title and emoji, so a federated gallery view rendered as a table on the receiving instance with nothing to say the layout had been lost. The layout travels with both, and a value the receiving version does not know is ignored rather than stored. The card sources stay behind on purpose: they are column ids of the sending instance and would address unrelated columns after the hop, so a federated card layout renders text-only. Adding them later means carrying column uuids, which the payload can gain without breaking a receiver, since unknown keys are ignored. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Opening an application by its URL, rather than by clicking it in the navigation, left the page blank: setContext() looked the context up with findIndex() and assigned straight to that index, so on the first load -- when the list has not been fetched yet and the lookup returns -1 -- contexts[-1] named a property instead of appending. The getters iterate the array, so activeContext stayed undefined and every branch of the template was false. Reading the icon off it threw first, leaving "An error occurred while loading the application" instead. Append when the context is not in the list yet, as the store convention for findIndex lookups requires, and drop the second copy of the icon assignment. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The card layout and the link cell each grew their own way of reading a file id out of a link and building a preview request, and the two disagreed. The link cell matched a lowercase fileid only, which is not the spelling the server writes, so a preview URL never resolved through it; it also read an id out of a link to another host and then requested that id from this one. The card layout, in turn, anchored the id to the end of the path and did not know the openfile parameter that a Files deep link carries. Move the matcher and the URL builder to a shared helper that takes what both need: the link is parsed rather than pattern matched, a foreign origin is refused, fileid and openfile are read whatever their case, and the id may be followed by more path. Each caller keeps what is its own -- the link cell only previews a files reference, while a card background may be any column and so probes the fields a stored link may hold -- and both now prefer an id the picker recorded outright. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Signed-off-by: Enjeck C. <patrathewhiz@gmail.com>
…te related tests Signed-off-by: Enjeck C. <patrathewhiz@gmail.com>
…istent-layout-modes-in-nextcloud-tables 🖼️ Add persistent per-view layout modes (table / tiles / gallery)
|
@remko48 a heads-up on what is happening in this fork while Ruben is away. WhyOn nextcloud#3075, enjeck asked why grid pages are views without a table, when Tables already has a page model for applications ( Ruben will post the reply on nextcloud#3075 himself. The full design is in that reply. What I'm building hereEverything happens in this fork, branched from Menu stack (
New page stack (
Application stack ( What this means for upstream
The old I'll update this comment with the PR numbers and test results as each stack finishes. Prepared with Claude Code on Ruben's instruction. StatusMenu stack: done (15 Oct)
Each branch got its own commit, then the one below it merged in. No rebase, no force-push. Per PR:
Tests (PHPUnit inside a throwaway container, one file at a time, on each branch):
Three things for you:
Page stack: done (15 Oct)
Where the code differs from the reply on nextcloud#3075:
Tests: all on PostgreSQL 16 / NC 34, PHPUnit one file at a time. MySQL, MariaDB and SQLite are left to CI.
p3 is about 1,580 lines without the lock file. It wasn't split, because it is one set of components. Application stack: done (15 Oct)
#25 to #31 each have a "superseded by" comment and are still open. Behaviour that changed compared with the old PRs (each PR body explains it):
Tests: PostgreSQL 16 / NC 34, Playwright at 1600 px with
Open for you (and Ruben):
Suggested upstream order
|
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)