Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
919c64b
build(release): v2.3.2
enjeck Oct 12, 2026
b2902b8
chore: remove unused babel config dependency from devDependencies
enjeck Oct 12, 2026
b0027d3
feat(views): allow views without a table and give views a type
remko48 Oct 12, 2026
631def2
fix(views): tell standalone views apart from federated views
remko48 Oct 12, 2026
2ebf8c7
fix(views): name views without a table by their id
remko48 Oct 12, 2026
18ffab4
Merge pull request #3073 from nextcloud/remove-babel
blizzz Oct 12, 2026
41f5365
Merge branch 'main' into feat/noid/views-without-table
remko48 Oct 12, 2026
5cdeaa2
fix(sidebar): keep views the table listing does not carry
blizzz Sep 4, 2026
4a25495
fix(views): address review on views without a table
remko48 Oct 13, 2026
475a169
fix(views): give federated proxy views their own uuid
remko48 Oct 13, 2026
f9a9f67
Merge pull request #3072 from nextcloud/release/2.3.2
enjeck Oct 14, 2026
543782a
Merge pull request #2953 from nextcloud/fix/noid/missing-tables-views
enjeck Oct 14, 2026
322644d
feat: add persistent per-view layout modes
Rello May 11, 2026
86bf04c
fix(views): resolve test feedback on the layout settings
Rello Jun 17, 2026
382abb9
style(views): apply lint and rector fixes
enjeck Aug 29, 2026
f140807
style(views): apply the php-cs-fixer arrow function spacing
AndyScherzinger Sep 1, 2026
e5c54c2
fix(views): reject invalid layout values when creating a view
AndyScherzinger Sep 1, 2026
ff56baf
fix(views): remap card sources when importing a scheme
AndyScherzinger Sep 1, 2026
2179dc1
fix(views): render the saved card layout on public shares
AndyScherzinger Sep 1, 2026
a4fe0c3
fix(views): keep the actions menu available in card layouts
AndyScherzinger Sep 1, 2026
8a71f14
chore(migration): rename layout migration to the 2.4.0 target version
AndyScherzinger Sep 1, 2026
fdab555
fix(views): stop card source validation from blocking unrelated updates
AndyScherzinger Sep 1, 2026
dcc447c
fix(tables): clamp the page number in a watcher instead of updated()
AndyScherzinger Sep 1, 2026
a8881c1
refactor(tables): reuse PaginationBlock for the card layout footer
AndyScherzinger Sep 1, 2026
963b0ae
test(tables): cover pagination across the toolbar and card footer
AndyScherzinger Sep 1, 2026
e245359
chore(migration): correct the date part of the layout migration
AndyScherzinger Sep 1, 2026
7b6d0ec
build: bump the app version to 2.4.0-dev.0
AndyScherzinger Sep 1, 2026
28286c6
fix(tables): make card titles fit, fill and stay readable
AndyScherzinger Sep 1, 2026
6fda65f
fix(share): render the saved card layout on public share links
AndyScherzinger Sep 2, 2026
daed45f
fix(reference): render the saved card layout in the embedded widget
AndyScherzinger Sep 2, 2026
9c32808
fix(picker): preview a view in its own layout before inserting it
AndyScherzinger Sep 2, 2026
63533a8
fix(tables): keep rich text formatted in the card layouts
AndyScherzinger Sep 2, 2026
f506c9f
fix(tables): drop the card image where no preview can be fetched
AndyScherzinger Sep 2, 2026
942ea66
fix(views): keep the saved layout when resetting local adjustments
AndyScherzinger Sep 9, 2026
656bd01
fix(views): stop forcing card sources on every view save
AndyScherzinger Sep 9, 2026
6f0c372
fix(views): reject card sources when a view has no columns
AndyScherzinger Sep 9, 2026
6b630de
fix(views): tolerate malformed view settings when reading
AndyScherzinger Sep 9, 2026
17ae74c
refactor(views): drop the unreleased flat card source format
AndyScherzinger Sep 9, 2026
214043a
fix(tables): rebuild card preview URLs from the file id alone
AndyScherzinger Sep 9, 2026
8dce0e9
perf(views): only clear orphaned card sources when the columns change
AndyScherzinger Sep 9, 2026
cc89abe
fix(tables): scope the pagination event to the table that raised it
AndyScherzinger Sep 9, 2026
ef9a91a
fix(tables): keep the card title measurement stable
AndyScherzinger Sep 9, 2026
fddce2e
perf(picker): load the content preview data in parallel
AndyScherzinger Sep 9, 2026
bb9fca3
refactor(views): centralise the layout values
AndyScherzinger Sep 9, 2026
8d185a4
perf(tables): derive the card data once per rendered page
AndyScherzinger Sep 9, 2026
68368de
chore(tables): clean up the smaller review findings
AndyScherzinger Sep 9, 2026
c5fbdea
refactor(views): separate the saved layout from a viewer's own choice
AndyScherzinger Sep 9, 2026
950f5f4
feat(views): let viewers switch layout without changing the view
AndyScherzinger Sep 9, 2026
7012ba0
fix(tables): stop nesting interactive content inside the card button
AndyScherzinger Sep 9, 2026
9b187ef
docs(agents): carve out presentational references from soft-invalidation
AndyScherzinger Sep 9, 2026
2581311
feat(federation): ship the view layout to a federated receiver
AndyScherzinger Sep 9, 2026
0d837c0
fix(contexts): render an application opened by its own URL
AndyScherzinger Sep 12, 2026
55d49d2
refactor(tables): derive a file preview link in one place
AndyScherzinger Sep 17, 2026
eb87922
feat(tables): add title validation and improve card layout handling
enjeck Oct 12, 2026
9ea82db
feat(tables): enhance ViewSettings handling for card sources and upda…
enjeck Oct 12, 2026
91910a0
Merge pull request #2407 from nextcloud/codex/implement-persistent-la…
enjeck Oct 14, 2026
2aeada5
Merge remote-tracking branch 'upstream/main' into main-forked
remko48 Oct 14, 2026
a5c5e25
Merge branch 'main-forked' into stack/g1-views-without-table
remko48 Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .nextcloudignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
/docs/
/screenshots/
/tests
/babel.config.cjs
/babel.config.js
/cypress.config.mjs
/.editorconfig
/.eslintrc.cjs
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ On import/export, selection option IDs change. `ColumnService::importColumn()` m

When a stored rule, filter, or condition references a column or option that no longer exists (due to deletion, type change, or import remapping), mark it `broken: true` instead of deleting it. Surface the broken state in the UI. Provide an auto-clear path that removes the flag when the rule is next saved in a valid state.

**Exception — presentational references.** A reference that only selects how a row is *displayed*, rather than which rows or values are returned, is dropped to `null` instead. Card sources (`viewSettings.cardBackgroundSource`, `viewSettings.cardTitleSource`) are the current case, handled in `ViewSettings::resolveSource()`, `ViewSettings::remapSources()` (called from `ApiTablesController::createFromScheme()`) and `TablesMigrator::remapViewCardSources()`.

Two reasons this differs from a filter or sort rule:

- Column ids are only unique per instance, so an id that fails to remap on import is not merely stale — after import it may address a live column of an unrelated table. Keeping it would point the card at someone else's data, which is worse than losing the setting.
- Losing it degrades gracefully. A view with no card sources renders the text-only card layout, which is a supported state the UI already handles, so there is no broken condition for the user to repair.

A reference that changes which rows or values are returned still follows the rule above.

### XSS / CSS injection

- Never use `v-html`, `innerHTML`, `eval`, or `new Function` with user-supplied values.
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@

# Changelog

## 2.3.2

### Added
* [Feat: Allow image previews of Files image links (tables#2946)](https://github.com/nextcloud/tables/pull/2946)
* [Feat: Add rector to CI (tables#2949)](https://github.com/nextcloud/tables/pull/2949)

### Fixed
* [Fix(search): search as the user the provider is given (tables#3011)](https://github.com/nextcloud/tables/pull/3011)
* [Fix(Handlers): use a composable for (un)subscribing to events (tables#3023)](https://github.com/nextcloud/tables/pull/3023)
* [Fix(rows): add id tie-breaker so limit/offset pages are stable (tables#3058)](https://github.com/nextcloud/tables/pull/3058)
* [Fix(migration): use mappers instead of services in db structure repair (tables#3064)](https://github.com/nextcloud/tables/pull/3064)
* [Fix(federation): accept federated requests from instances on non-default ports (tables#3065)](https://github.com/nextcloud/tables/pull/3065)

### Dependencies
* [Update nextcloud/ocp dependency (tables#3038)](https://github.com/nextcloud/tables/pull/3038)
* [Fix(deps): update dependency @nextcloud/dialogs to ^7.5.0 (main) (tables#2963)](https://github.com/nextcloud/tables/pull/2963)
* [Chore(deps): update dependency psalm/phar to ^6.17.2 (main) (tables#3015)](https://github.com/nextcloud/tables/pull/3015)
* [Fix(deps): update dependency @nextcloud/files to ^4.1.0 (main) (tables#3016)](https://github.com/nextcloud/tables/pull/3016)
* [Fix(deps): update dependency @nextcloud/vue to ^9.13.1 (main) (tables#3017)](https://github.com/nextcloud/tables/pull/3017)
* [Chore(deps): update dependency @rollup/rollup-linux-x64-gnu to ^4.63.3 (main) (tables#3018)](https://github.com/nextcloud/tables/pull/3018)
* [Fix(deps): update dependency @material-symbols/svg-400 to ^0.47.4 (main) (tables#3031)](https://github.com/nextcloud/tables/pull/3031)
* [Chore(deps): update dependency rector/rector to ^2.6.7 (main) (tables#3030)](https://github.com/nextcloud/tables/pull/3030)
* [Chore(deps): update dependency eslint to ^10.11.0 (main) (tables#3032)](https://github.com/nextcloud/tables/pull/3032)
* [Fix(deps): update dependency @nextcloud/router to ^3.2.0 (main) (tables#3033)](https://github.com/nextcloud/tables/pull/3033)
* [Chore(deps): update cypress-io/github-action action to v7.4.4 (main) (tables#3039)](https://github.com/nextcloud/tables/pull/3039)
* [Chore(deps): update dependency @nextcloud/e2e-test-server to ^0.6.1 (main) (tables#3040)](https://github.com/nextcloud/tables/pull/3040)
* [Chore(deps): update icewind1991/nextcloud-version-matrix action to v1.3.3 (main) (tables#3041)](https://github.com/nextcloud/tables/pull/3041)
* [Update nextcloud/ocp dependency (tables#3020)](https://github.com/nextcloud/tables/pull/3020)
* [Fix npm audit (tables#3060)](https://github.com/nextcloud/tables/pull/3060)

## 2.3.1

### Added
Expand Down
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ appstore: build
assemble:
mkdir -p $(build_dir)
rsync -a \
--exclude=babel.config.js \
--exclude=build \
--exclude=composer.* \
--exclude=CONTRIBUTING.md \
Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Share your tables and views with users and groups within your cloud.
Have a good time and manage whatever you want.

]]></description>
<version>2.3.1</version>
<version>2.4.0-dev.0</version>
<licence>AGPL-3.0-or-later</licence>
<author>Nextcloud GmbH and Nextcloud contributors</author>
<namespace>Tables</namespace>
Expand Down
7 changes: 0 additions & 7 deletions babel.config.cjs

This file was deleted.

135 changes: 135 additions & 0 deletions cypress/component/CardActivation.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import CustomTable from '../../src/shared/components/ncTable/sections/CustomTable.vue'
import { ColumnTypes } from '../../src/shared/components/ncTable/mixins/columnHandler.js'
import TextRichColumn from '../../src/shared/components/ncTable/mixins/columnsTypes/textRich.js'

const CARD = '[data-cy="galleryLayoutCard"]'
const TITLE = '[data-cy="galleryLayoutCardTitle"]'

describe('Card activation', () => {
let columns = []
let rows = []

beforeEach(() => {
columns = [
new TextRichColumn({ id: 1, title: 'Notes', type: ColumnTypes.TextRich }),
new TextRichColumn({ id: 2, title: 'More', type: ColumnTypes.TextRich }),
]
rows = [{
id: 7,
data: [
{ columnId: 1, value: 'a card title' },
{ columnId: 2, value: '[a link](https://example.invalid/page)' },
],
}]
})

it('is a plain container with an explicitly named control', () => {
mountCards(columns, rows)

// A button role would make the metadata list and its link presentational.
cy.get(CARD).should('not.have.attr', 'role')
cy.get(CARD).should('not.have.attr', 'tabindex')
cy.get(CARD).should($card => expect($card[0].tagName).to.equal('DIV'))
cy.get(TITLE).should($title => expect($title[0].tagName).to.equal('BUTTON'))
// Named from the title element rather than carrying a second copy of the string,
// so the control and the text it stands for cannot drift apart.
cy.get(TITLE).should('not.have.attr', 'aria-label')
cy.get(TITLE).then($button => {
cy.get(`#${$button.attr('aria-labelledby')}`).should('contain', 'a card title')
})
// The title stays in the text flow, where the line clamp can act on it.
cy.get(`${CARD} .layout-card__title-text`).should('contain', 'a card title')
})

it('renders a rich title and keeps its link on top of the control', () => {
rows[0].data[0].value = '**bold** [t](https://example.invalid/p)'
mountCards(columns, rows)

cy.get(`${CARD} .layout-card__title-text strong`).should('exist')
cy.get(`${CARD} .layout-card__title-banner a`).should($link => {
const box = $link[0].getBoundingClientRect()
const top = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2)
// The overlay must not cover a link the title brings with it.
expect(top.closest('a')).to.equal($link[0])
})
})

it('keeps a link inside the card reachable', () => {
mountCards(columns, rows)

cy.get(`${CARD} a`).should('exist').and('have.attr', 'href')
})

it('opens the row once from the focusable control', () => {
mountCards(columns, rows).then(({ wrapper }) => {
// A real button, so the platform handles enter and space; what needs asserting
// is that it can be reached and that activating it does not open the row twice
// by also running the card's own handler.
cy.get(TITLE).focus().should('be.focused')
cy.get(TITLE).click({ force: true })
.then(() => {
expect(wrapper.emitted('edit-row')).to.deep.equal([[7]])
})
})
})

it('opens the row when the card body is clicked', () => {
mountCards(columns, rows).then(({ wrapper }) => {
cy.get(`${CARD} .layout-card__image-wrapper`).click({ force: true })
.then(() => {
expect(wrapper.emitted('edit-row')).to.deep.equal([[7]])
})
})
})

it('exposes the card image as content rather than decoration', () => {
// The image says something the title does not, so it keeps a name and stays in the
// accessibility tree; nothing else in the card repeats it.
rows[0].data[1].value = '/index.php/f/42'
mountCards(columns, rows, 2)

cy.get(`${CARD} img.layout-card__image`).invoke('attr', 'alt').should('not.be.empty')
cy.get(`${CARD} img.layout-card__image`).should('not.have.attr', 'aria-hidden')
cy.get(`${CARD} img.layout-card__image`).should('not.have.attr', 'role')
})

it('follows a link inside a card without also opening the row', () => {
mountCards(columns, rows).then(({ wrapper }) => {
cy.get(`${CARD} a`).then($link => {
// Keep the harness on the page; the click still bubbles as it would in the app.
$link.on('click', event => event.preventDefault())
})
cy.get(`${CARD} a`).click({ force: true })
.then(() => {
expect(wrapper.emitted('edit-row')).to.equal(undefined)
})
})
})
})

/**
* Mounts CustomTable rendering the given rows as a gallery.
*
* @param {Array} columns the parsed columns
* @param {Array} rows the rows to render
* @param {number|null} backgroundColumnId the column a card reads its image from
* @return {Cypress.Chainable} the mount result
*/
function mountCards(columns, rows, backgroundColumnId = null) {
return cy.mount(CustomTable, {
props: {
columns,
rows,
elementId: 1,
isView: true,
layout: 'gallery',
viewSettings: { cardBackgroundSource: backgroundColumnId, cardTitleSource: 1 },
viewSetting: {},
config: { canReadRows: true },
},
})
}
69 changes: 69 additions & 0 deletions cypress/component/CardPreviewUrl.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import CustomTable from '../../src/shared/components/ncTable/sections/CustomTable.vue'
import { ColumnTypes } from '../../src/shared/components/ncTable/mixins/columnHandler.js'
import TextLineColumn from '../../src/shared/components/ncTable/mixins/columnsTypes/textLine.js'

const CARD = '[data-cy="tilesLayoutCard"]'

describe('Card preview URL', () => {
const columns = [
new TextLineColumn({ id: 1, title: 'Name', type: ColumnTypes.TextLine }),
new TextLineColumn({ id: 2, title: 'Image', type: ColumnTypes.TextLine }),
]

it('rebuilds the request instead of forwarding the stored one', () => {
mountCards(`${window.location.origin}/index.php/core/preview?fileId=7&x=9999&evil=1`)

cy.get(`${CARD} img`).should('have.attr', 'src').then(src => {
expect(src).to.match(/\/core\/preview\?fileId=7&x=\d+&y=\d+&a=1$/)
expect(src).not.to.contain('evil')
expect(src).not.to.contain('9999')
})
})

it('reads a link cell stored as JSON', () => {
mountCards(JSON.stringify({ value: `${window.location.origin}/index.php/f/55`, title: 'x' }))

cy.get(`${CARD} img`).should('have.attr', 'src').and('match', /fileId=55&/)
})

it('prefers the file id a picked file stores over the link it also carries', () => {
mountCards(JSON.stringify({
resourceUrl: `${window.location.origin}/index.php/f/55`,
attributes: { fileId: 66 },
}))

cy.get(`${CARD} img`).should('have.attr', 'src').and('match', /fileId=66&/)
})

it('renders no image for a value it will not follow', () => {
mountCards('https://evil.example/index.php/f/42')

cy.get(CARD).should('exist')
cy.get(`${CARD} img`).should('not.exist')
})

/**
* Mounts one tile whose background cell holds the given value.
*
* @param {string} imageValue the raw value of the image cell
* @return {Cypress.Chainable} the mount result
*/
function mountCards(imageValue) {
return cy.mount(CustomTable, {
props: {
columns,
rows: [{ id: 1, data: [{ columnId: 1, value: 'a title' }, { columnId: 2, value: imageValue }] }],
elementId: 1,
isView: true,
layout: 'tiles',
viewSettings: { cardBackgroundSource: 2, cardTitleSource: 1 },
viewSetting: {},
config: { canReadRows: true },
},
})
}
})
41 changes: 38 additions & 3 deletions cypress/component/ContentReferenceWidget.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('ContentReferenceWidget', () => {

it('can edit a row', () => {
mountContentWidget(richObject)

// Load a fixture which is used to reply to the edit row request
cy.fixture('widgets/editRow.json')
.then((rowData) => {
Expand Down Expand Up @@ -110,9 +110,34 @@ describe('ContentReferenceWidget', () => {
cy.get('@rows').first().children().as('editedRow')
cy.get('@editedRow').first().contains('Giraffe')
})
it('renders a view saved as tiles without falling back to the table', () => {
mountWidget({ ...richObject, layout: 'tiles', viewSettings: { cardBackgroundSource: null, cardTitleSource: 159 } })

cy.get('[data-cy="tilesLayoutCard"]').should('have.length', richObject.rows.length)
cy.get('[data-cy="contentReferenceWidget"] tr[data-cy="customTableRow"]').should('not.exist')
})

it('renders a view saved as a gallery without falling back to the table', () => {
mountWidget({ ...richObject, layout: 'gallery', viewSettings: { cardBackgroundSource: null, cardTitleSource: 159 } })

cy.get('[data-cy="galleryLayoutCard"]').should('have.length', richObject.rows.length)
cy.get('[data-cy="contentReferenceWidget"] tr[data-cy="customTableRow"]').should('not.exist')
})

it('keeps the table layout when the reference carries none', () => {
mountContentWidget(richObject)

cy.get('[data-cy="contentReferenceWidget"] tr[data-cy="customTableRow"]').should('have.length', richObject.rows.length)
cy.get('[data-cy$="LayoutCard"]').should('not.exist')
})
})

function mountContentWidget(richObject) {
/**
* Mounts the widget without assuming a particular layout.
*
* @param {object} richObject the reference payload to render
*/
function mountWidget(richObject) {
cy.reply('**/apps/tables/row/table/*', richObject.rows)

cy.mount(ContentReferenceWidget, {
Expand All @@ -121,8 +146,18 @@ function mountContentWidget(richObject) {
},
})

// Get some often used elements
cy.get('[data-cy="contentReferenceWidget"] .options').as('options')
}

/**
* Mounts the widget and aliases the table elements the row tests rely on.
*
* @param {object} richObject the reference payload to render
*/
function mountContentWidget(richObject) {
mountWidget(richObject)

// Get some often used elements
cy.get('[data-cy="contentReferenceWidget"] .NcTable table').as('table')
cy.get('@table').find('tr[data-cy="customTableRow"]').as('rows')
}
Loading
Loading