Skip to content

🖼️ Add persistent per-view layout modes (table / tiles / gallery) - #2407

Open
Rello wants to merge 23 commits into
mainfrom
codex/implement-persistent-layout-modes-in-nextcloud-tables
Open

🖼️ Add persistent per-view layout modes (table / tiles / gallery)#2407
Rello wants to merge 23 commits into
mainfrom
codex/implement-persistent-layout-modes-in-nextcloud-tables

Conversation

@Rello

@Rello Rello commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Motivation

  • Implement optional gallery and tile views, as available in SharePoint
  • Both views are using the first column to get the picture url for the preview
  • Tile is only showing the picture plus its title from the 2nd column. Use case: Index pages
  • Gallery view is showing all additional columns from the current view. Use case: Product catalogue or inventory
  • Provide persistent, per-view layout modes so users can choose between table, tiles and gallery and have that preference stored with the view.
  • Expose layout selection in the View settings modal and apply the persisted layout automatically when a view is opened, while keeping backward compatibility (missing/old values => table).

Implementation

Source data

Bildschirmfoto 2026-03-19 um 20 34 35

Tile View

Bildschirmfoto 2026-03-19 um 20 52 47

Gallery View

Bildschirmfoto 2026-03-19 um 20 53 05

Settings modal

Bildschirmfoto 2026-03-19 um 20 54 28

Sharepoint reference

565593259-fda68f2f-2a04-4ace-b1f9-fb2efbc6c77a-2

🖼️ Screenshots

🌞 Light 🌚 Dark
view-3 view-3
view-4 view-4
view-5 view-5
view-6 view-6
view-7 view-7
view-8 view-8

🧪 Andy: I tested:

  • gallery/tiles view in the app as views and part of an app/context, text-only, image-only, image+text and opened as user share
  • embedded widget in text file/app for tile/gallery
  • public links tile/gallery
  • import/export of the structure

@Rello
Rello requested review from blizzz and enjeck as code owners March 18, 2026 11:50
@Rello Rello linked an issue Mar 18, 2026 that may be closed by this pull request
@Rello

Rello commented Mar 18, 2026

Copy link
Copy Markdown
Contributor Author

@jancborchardt as new layouts will be relevant for your opinion

@jancborchardt jancborchardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcoambrosini one for you, check it out. :)

@Rello do you have reference screenshots from Sharepoint as you mention?
List and Gallery view currently also look very similar (with Gallery in need of some design improvement).

@Rello
Rello marked this pull request as draft March 19, 2026 15:16
@Rello

Rello commented Mar 19, 2026

Copy link
Copy Markdown
Contributor Author

@marcoambrosini can you please have a look now? the PR was now implemented and tested on the branch and I added the actual screenshots from the working app

@Rello
Rello marked this pull request as ready for review March 19, 2026 20:37
@jancborchardt jancborchardt moved this to 🏗️ At engineering in 🖍 Design team Mar 30, 2026
@AndyScherzinger AndyScherzinger added enhancement New feature or request 3. to review Waiting for reviews labels Apr 12, 2026
@Rello Rello changed the title Add persistent per-view layout modes (table / tiles / gallery) 🖼️ Add persistent per-view layout modes (table / tiles / gallery) Apr 13, 2026
@Rello
Rello requested a review from jancborchardt April 13, 2026 10:47
marcoambrosini

This comment was marked as outdated.

@Rello

This comment was marked as resolved.

Comment thread lib/Migration/Version2400Date20260901000000.php

@enjeck enjeck left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming that the state when there's no image is designer-approved? Here, for a tile view, I don't see why say "No image" instead of just text-based tiling like in https://media.geeksforgeeks.org/wp-content/uploads/20231218102144/Screenshot-2023-12-18-102058.png, for example.

Image

cc @marcoambrosini

Comment thread src/modules/modals/ViewSettings.vue
Comment thread src/modules/modals/ViewSettings.vue
@Rello
Rello marked this pull request as draft April 28, 2026 08:37
@Rello

This comment was marked as outdated.

This comment was marked as outdated.

@Rello

Rello commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

Just confirming that the state when there's no image is designer-approved? Here, for a tile view, I don't see why say "No image" instead of just text-based tiling like in https://media.geeksforgeeks.org/wp-content/uploads/20231218102144/Screenshot-2023-12-18-102058.png, for example.
2BK1ZfxUS9CBdrnTws71-prkJ5-t8t_fhbs">
cc @marcoambrosini

@enjeck what do you mean by this? I am not sure if I understand. what would you show instead of the "no image"?

@AndyScherzinger
AndyScherzinger force-pushed the codex/implement-persistent-layout-modes-in-nextcloud-tables branch from 4c234f0 to e873fe7 Compare September 2, 2026 11:38
@AndyScherzinger
AndyScherzinger force-pushed the codex/implement-persistent-layout-modes-in-nextcloud-tables branch from 3d1f736 to 49d5ac9 Compare September 2, 2026 18:19
@AndyScherzinger AndyScherzinger added 3. to review Waiting for reviews and removed 2. developing Work in progress labels Sep 3, 2026
@AndyScherzinger
AndyScherzinger force-pushed the codex/implement-persistent-layout-modes-in-nextcloud-tables branch from 49d5ac9 to e8fbc46 Compare September 5, 2026 06:34
Rello and others added 23 commits September 5, 2026 12:44
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: Rello <github@scherello.de>
Co-Authored-By: Andy Scherzinger <info@andy-scherzinger.de>
Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Rello <github@scherello.de>
Co-Authored-By: Andy Scherzinger <info@andy-scherzinger.de>
Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Enjeck C. <patrathewhiz@gmail.com>
Revert the unintended package-lock.json changes. package.json was never
modified, but the lockfile dropped @vue/devtools-kit while keeping
@vue/devtools-api, which imports it. That broke the vite build with
"Rollup failed to resolve import @vue/devtools-kit" and took down both
the NPM build and the Cypress component tests.

Also apply the php-cs-fixer arrow-function spacing (fn () instead of
fn()) in ViewController and TablesMigratorTest.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
View creation silently coerced any unsupported layout to the table
layout and still reported success, while updates reject the same values
in ViewUpdateInput::normalizeLayout(). Reuse that normalizer on create
so a typo returns a bad request instead of persisting a different
layout than the caller asked for.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Importing a table or context scheme remapped columnSettings, sort and
filter through the uuid map but persisted the exported card source
column ids unchanged, so tiles/gallery views could reference a
nonexistent column or, after an id collision, show data from an
unrelated column of the target table.

Resolve both card sources through the columnId/columnUuid pairs the
exported column settings already carry, and drop a source that cannot
be resolved rather than keeping a stale id.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
PublicElement bound a localViewSetting that was never declared and
passed no view setting to TableView, so a view saved as tiles or
gallery always fell back to the table layout when opened through a
public link. Initialise the setting from the shared view and pass it
down to the renderer.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The named actions slot was only rendered inside the table header, which
is skipped for tiles and gallery, so opening a card layout hid the whole
action menu including the view settings entry used to switch the layout
back. Render the slot above the card grid when a card layout is active.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The migration was still named Version1000Date20260318000000, a prefix far below
the migrations added to the repository since this branch started, so it sorted
ahead of everything already released. Rename it to the 2.4.0 target version so
the ordering reflects the release it ships in.

The step guards every change with hasTable()/hasColumn(), so re-running it under
the new name on an installation that already applied the old one is a no-op.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
ViewService::update() validated the stored cardBackgroundSource and
cardTitleSource on every request, so removing a column from a view failed with
400 even when the request never mentioned viewSettings. Callers were forced to
resend the card settings just to make an unrelated column change succeed.

Validate strictly only when the request actually carries view settings, and
otherwise drop a card source the new column set no longer contains.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The updated() hook assigned pageNumber on every re-render, and its
totalPages === 1 branch fired on each update regardless of whether anything
changed. Mutating reactive state from that hook risks feedback loops.

Move the clamp into a totalPages watcher so it only runs when the page count
actually changes, and keep the number at 1 or above when a filter empties the
table.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
CustomTable carried its own copy of the pagination controls, which duplicated
PaginationBlock and left the two controls out of sync: changing the page in the
footer never reached the block in the toolbar, because only the toolbar copy
emitted tables:pagination-changed.

Render PaginationBlock in the footer instead and let it adopt pagination changes
from the event bus, guarded so an adopted change is not echoed back. This drops
the duplicated markup, its icon and component imports and the now unused
allPageNumbersArray computed.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The existing component test mounts a three row fixture, so the footer pagination
never rendered and the two pagination controls were never exercised together.
Mount NcTable with 150 generated rows instead and assert that paging from either
control moves the other, that the footer only appears beyond the first page, and
that it renders in the gallery layout.

Removing the event bus subscription from PaginationBlock fails the two sync
assertions, so they guard the behaviour rather than the markup.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The date part still read 20260318, which predates the file itself: it was added
to the branch on 2026-05-11. Set it to the day the migration ships so the name
orders correctly against the other 2.4.0 migrations.

The class name is what gets recorded in oc_migrations, and it already changed
with the version prefix rename, so this carries no additional upgrade cost. The
step guards every change with hasTable()/hasColumn() and is a no-op where it has
already run.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The layout migration targets 2.4.0, but the app still declared 2.3.1-rc.1, which
is lower than the version development instances already have installed. Nextcloud
refuses to load an app whose declared version is below the installed one, so the
branch could not be deployed for testing.

Bumps appinfo/info.xml along with package.json and package-lock.json, which this
repository keeps in sync.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Cards are grid items and stretch to the tallest card in their row, but the card
was a block holding a fixed height image and a body of natural height, so every
card carrying less metadata than its neighbours ended in dead space: 22px for
two short values, 76px for a single one. Make the card a flex column so the
body absorbs the leftover height.

The title banner had no height limit. Absolutely positioned against an image
wrapper pinned to aspect-ratio 3 / 2, a long title grew past the top of the
image and the card's overflow sliced the first line away. It now gets a line
budget derived from the rendered card size rather than a fixed count, kept in
step by a ResizeObserver on the card container. The observer watches the title
as well, because the container keeps its size when only the text size changes,
which would otherwise leave the budget stale at 200%. MIN_TITLE_LINES makes the
share a preference rather than a hard cap, so an enlarged font shortens the
title instead of dropping it to a single line. The clamp sits on an inner span:
clamping the banner itself still painted a partial line into its padding.

Cards without a background column are not covering an image, so their title may
use the whole card. A tile then reads like a note and starts at the top, while a
gallery card keeps its title next to the metadata below it.

Finally the scrim is darkened from 0.4 to 0.55. White text over a white image
region measured 2.85:1, below the 4.5:1 WCAG AA needs at this size and weight,
and compliance otherwise depended on which picture a user happened to pick.
0.55 puts the worst case at 4.76:1 in both colour schemes. The accompanying
text shadow only firms up the glyph edges where the title crosses a bright part
of the image; contrast ratios ignore shadows, so it carries no part of AA.

A file that cannot be read made the browser paint the alt text into the card,
where it ran into the banner already showing the same words. The image keeps its
alt attribute, and with it its accessible name, but the visual fallback is now
suppressed so only the broken image indicator remains.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
A view shared by link always came out as a table. The renderer takes the layout
from the view setting it is handed, but nothing ever put one there: the public
page ships its node through an allowlist of six keys, and neither layout nor
viewSettings was among them, so the browser never learned that the view had been
saved as tiles or a gallery.

Let both keys through and carry them into the element the public page builds. A
shared table has no layout of its own and keeps rendering as a table.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
A view embedded in a text or markdown file rendered as a table whatever layout it
had been saved with. The widget mounts the same table component as the app, so it
only lacked the setting: the reference payload carried no layout, and the widget
started from an empty view setting that nothing filled in.

Send layout and viewSettings along with a view reference, and seed the widget
from them. A table reference carries neither and keeps the table layout.

Covered by component tests for both card layouts and for a reference without one.
Splitting the mount helper keeps the row tests aliasing a table while the card
cases mount without assuming one.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Picking a view as content showed a table preview whatever layout the view had
been saved with, so the preview did not match what the inserted widget renders.

The picker assembles its own rich object from the search result, and the search
result is a light shape carrying no layout. It already fetches columns and rows
for the preview, so fetch the layout the same way and hand it to the widget. A
table has no layout of its own and keeps its table preview.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
A rich text column came out as its raw markdown on a tile or a gallery card,
while the table rendered it formatted. The cards took the value as a plain
string, so the asterisks and hashes of the stored text were shown as written.

Render a rich text value as the formatted text it is, in the one place both card
layouts take their values from, so tiles, galleries, the embedded widget, the
picker preview and public links all follow. Markdown brings block elements with
it, so they are held on the text flow inside a title, where the line clamp needs
them, and stripped of their own spacing inside the metadata.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
A preview is served against the viewer's session, so on a public link there is
none to authenticate with and the image can never arrive. Cards still asked for
one, leaving a broken image on every card of a shared tiles or gallery view.

Ask for no preview at all when there is no session. The card then has no
background to show and falls into the layout a view already uses when no image
column is configured, so the shared card needs no styling of its own.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
@AndyScherzinger
AndyScherzinger force-pushed the codex/implement-persistent-layout-modes-in-nextcloud-tables branch from e8fbc46 to 832a782 Compare September 5, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews AI assisted codex enhancement New feature or request

Projects

Status: 🏗️ At engineering

Development

Successfully merging this pull request may close these issues.

Add view mode: Tiles

8 participants