From f0f34602e2ca9623fe7c4d84ee05b9df6f6f92fb Mon Sep 17 00:00:00 2001 From: "Alexander R. Krug" Date: Tue, 29 Sep 2026 16:50:02 +0200 Subject: [PATCH 1/6] fix(context): scope view settings per resource Previously all resources of an application (context) shared a single viewSetting object. Because the per-view localViewSetting chain ends up referencing that same object, entering a search term above one view mutated the shared object and thereby filtered every view in the application at once. Clearing the search once decoupled only that one view (the deep clone in setSearchString), which made the behavior inconsistent depending on interaction order. View settings (search string, filters, hidden columns, sorting) are now kept per resource, keyed by the resource key, and reset when the application is reloaded. Fixes: #3008 Assisted-by: pi:glm-5.3-flash Signed-off-by: Alexander R. Krug --- src/pages/Context.vue | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/Context.vue b/src/pages/Context.vue index 3e85a8167e..722eac08f1 100644 --- a/src/pages/Context.vue +++ b/src/pages/Context.vue @@ -23,14 +23,14 @@
@@ -80,7 +80,7 @@ export default { return { loading: true, icon: null, - viewSetting: {}, + viewSettings: {}, context: null, contextResources: [], errorMessage: null, @@ -175,6 +175,7 @@ export default { this.isReloading = true this.loading = true this.contextResources = [] + this.viewSettings = {} try { await this.loadContext({ id: this.activeContextId }) @@ -223,6 +224,7 @@ export default { }) table.key = (table.id).toString() table.isView = false + this.viewSettings[table.key] = {} this.contextResources.push(table) } @@ -242,6 +244,7 @@ export default { }) view.key = 'view-' + (view.id).toString() view.isView = true + this.viewSettings[view.key] = {} this.contextResources.push(view) } } From 626afb41b528985d1af0005394943a44cb162bd8 Mon Sep 17 00:00:00 2001 From: "Alexander R. Krug" Date: Thu, 1 Oct 2026 13:04:20 +0200 Subject: [PATCH 2/6] test(context): add regression test for per-view search scope Add a Cypress spec asserting that a search term entered above one view of an application filters only that view while the other views of the application stay uninterrupted, and that clearing the search un-filters it. Guards against a regression of the shared viewSetting object that previously leaked the search across all views (fixes #3008). To make the fixture buildable and the assertions robust: - extend the createContext command with optional resource titles, driven through the creation modal's contextResourceForm typeahead - add stable data-cy hooks: contextTableNode / contextViewNode on the context page's per-resource wrappers and elementSearchInput on the shared per-element search input, so the tests can scope to one resource's search input without depending on markup or class order - add the spec to the committed e2e test list - collapse two pre-existing double blank lines in commands.js flagged by lint Signed-off-by: Alexander R. Krug --- cypress/e2e/context-view-search.cy.js | 73 +++++++++++++++++++ cypress/support/commands.js | 8 +- package.json | 2 +- src/pages/Context.vue | 4 +- .../ncTable/partials/SearchForm.vue | 2 +- 5 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 cypress/e2e/context-view-search.cy.js diff --git a/cypress/e2e/context-view-search.cy.js b/cypress/e2e/context-view-search.cy.js new file mode 100644 index 0000000000..ad0e69884c --- /dev/null +++ b/cypress/e2e/context-view-search.cy.js @@ -0,0 +1,73 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +let localUser +const tableName = 'fruit stock' +const viewOneTitle = 'apple view' +const viewTwoTitle = 'banana view' +const contextTitle = 'test application search scope' + +describe('Test view search inside an application', () => { + before(function() { + cy.createRandomUser().then(user => { + localUser = user + }) + }) + + beforeEach(function() { + cy.login(localUser) + cy.visit('apps/tables') + cy.get('[aria-label="Create new table"]').should('be.visible') + }) + + it('Create table with rows, two views of it and an application containing both views', () => { + cy.createTable(tableName) + cy.loadTable(tableName) + cy.createTextLineColumn('Name', null, null, true) + + // two rows with distinct content so the search can discriminate + cy.get('[data-cy="createRowBtn"]').click() + cy.get('[data-cy="createRowModal"] input').first().should('be.visible').clear().type('apple row') + cy.get('[data-cy="createRowSaveButton"]').click() + cy.get('[data-cy="createRowBtn"]').click() + cy.get('[data-cy="createRowModal"] input').first().should('be.visible').clear().type('banana row') + cy.get('[data-cy="createRowSaveButton"]').click() + + cy.createView(viewOneTitle) + cy.loadTable(tableName) + cy.createView(viewTwoTitle) + + cy.createContext(contextTitle, false, [viewOneTitle, viewTwoTitle]) + }) + + it('Search in one view must not filter the other views', () => { + cy.loadContext(contextTitle) + cy.get('[data-cy="contextViewNode"]').should('have.length', 2) + + // type into the first view's search input only + cy.get('[data-cy="contextViewNode"]:contains("' + viewOneTitle + '")') + .find('[data-cy="elementSearchInput"] input') + .type('apple') + // wait for the search to be applied (debounced) + cy.wait(700) + + // the searched view is filtered + cy.get('[data-cy="contextViewNode"]:contains("' + viewOneTitle + '")') + .find('td').should('contain.text', 'apple row') + .and('not.contain.text', 'banana row') + + // the other view keeps being uninterrupted + cy.get('[data-cy="contextViewNode"]:contains("' + viewTwoTitle + '")') + .find('td').should('contain.text', 'apple row') + .and('contain.text', 'banana row') + + // clearing the search un-filters the searched view + cy.get('[data-cy="contextViewNode"]:contains("' + viewOneTitle + '")') + .find('[data-cy="elementSearchInput"] input').clear() + cy.wait(700) + cy.get('[data-cy="contextViewNode"]:contains("' + viewOneTitle + '")') + .find('td').should('contain.text', 'banana row') + }) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 683ec6f4f4..bf3725cd1f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -118,7 +118,6 @@ Cypress.Commands.add('deleteTable', (title) => { cy.get('[data-cy="navigationTableItem"]').contains(title).should('not.exist') }) - Cypress.Commands.add('deleteRow', (rowIndex) => { cy.get('[data-cy="ncTable"] [data-cy="editRowBtn"]').eq(rowIndex).click() cy.get('[data-cy="editRowDeleteButton"]').click({ force: true }) @@ -149,13 +148,17 @@ Cypress.Commands.add('openCreateColumnModal', (isFirstColumn) => { } }) -Cypress.Commands.add('createContext', (title, showInNav = false) => { +Cypress.Commands.add('createContext', (title, showInNav = false, resourceTitles = []) => { cy.get('ul:nth-of-type(2) [data-cy="createContextIcon"]').click({ force: true }) cy.get('[data-cy="createContextModal"]').should('be.visible') cy.get('[data-cy="createContextTitle"]').clear().type(title) if (showInNav) { cy.get('[data-cy="createContextShowInNavSwitch"]').click() } + for (const resourceTitle of resourceTitles) { + cy.get('[data-cy="contextResourceForm"] input').first().clear().type(resourceTitle) + cy.contains('.vs__dropdown-menu li', resourceTitle).click() + } cy.get('[data-cy="createContextSubmitBtn"]').click() // verify context was created properly @@ -282,7 +285,6 @@ Cypress.Commands.add('createSelectionColumn', (title, options, defaultOption, is cy.get('.custom-table table tr th .cell').contains(title).should('exist') }) - Cypress.Commands.add('createSelectionMultiColumn', (title, options, defaultOptions, isFirstColumn) => { cy.openCreateColumnModal(isFirstColumn) diff --git a/package.json b/package.json index 5f223a21c5..7ca82abc61 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "stylelint:fix": "stylelint 'css/*.css' 'css/*.scss' 'src/**/*.scss' 'src/**/*.vue' --fix", "test:component": "cypress run --component", "test:e2e": "playwright test", - "test:e2e:cypress": "cypress run --browser chrome --no-runner-ui --spec cypress/e2e/context-navigation-hidden.cy.js,cypress/e2e/context-navigation-shared.cy.js,cypress/e2e/tables-import.cy.js,cypress/e2e/tables-table.cy.js,cypress/e2e/view-filtering-selection-row-checked.cy.js,cypress/e2e/view-filtering-selection-row-unchecked.cy.js,cypress/e2e/view-filtering-selection-row-removal.cy.js", + "test:e2e:cypress": "cypress run --browser chrome --no-runner-ui --spec cypress/e2e/context-navigation-hidden.cy.js,cypress/e2e/context-navigation-shared.cy.js,cypress/e2e/tables-import.cy.js,cypress/e2e/context-view-search.cy.js,cypress/e2e/tables-table.cy.js,cypress/e2e/view-filtering-selection-row-checked.cy.js,cypress/e2e/view-filtering-selection-row-unchecked.cy.js,cypress/e2e/view-filtering-selection-row-removal.cy.js", "test:e2e:ui": "playwright test --ui", "test:e2e:report": "playwright show-report", "start:nextcloud": "node playwright/start-nextcloud-server.mjs" diff --git a/src/pages/Context.vue b/src/pages/Context.vue index 722eac08f1..109ab3d082 100644 --- a/src/pages/Context.vue +++ b/src/pages/Context.vue @@ -21,14 +21,14 @@
-
+
-
+