From 7e74fae908e98e04fce1efed69792c66860d60b3 Mon Sep 17 00:00:00 2001 From: Git'Fellow <12234510+solracsf@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:37:11 +0200 Subject: [PATCH 1/5] fix(Action): await navigation and handle router rejections Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com> --- src/actions/openGroupfolderAction.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/actions/openGroupfolderAction.ts b/src/actions/openGroupfolderAction.ts index c3ed92617..393e5727a 100644 --- a/src/actions/openGroupfolderAction.ts +++ b/src/actions/openGroupfolderAction.ts @@ -16,13 +16,23 @@ export const action: IFileAction = { enabled: ({ view }) => view.id === appName, async exec({ nodes }) { - const dir = nodes[0].attributes.mountPoint - window.OCP.Files.Router.goToRoute( - null, // use default route - { view: 'files', fileid: nodes[0].id }, - { dir }, - ) - return null + try { + await window.OCP.Files.Router.goToRoute( + null, // use default route + { view: 'files', fileid: nodes[0].id }, + { dir: nodes[0].attributes.mountPoint }, + ) + return true + } catch (e) { + // Vue Router throws on duplicated/redirected navigations; those are not + // real failures from the user's perspective — the target view is reached. + const name = (e as { name?: string })?.name + const message = (e as { message?: string })?.message ?? '' + if (name === 'NavigationDuplicated' || /Redirected/.test(message)) { + return true + } + throw e + } }, default: DefaultType.DEFAULT, From fa4f4cd2480e95ef930897af2afd0307ed0926f2 Mon Sep 17 00:00:00 2001 From: Git'Fellow <12234510+solracsf@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:52:57 +0200 Subject: [PATCH 2/5] test(cypress): cover team folder navigation from the Team folders view Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com> --- cypress/e2e/groupfoldersNavigation.cy.ts | 100 +++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 cypress/e2e/groupfoldersNavigation.cy.ts diff --git a/cypress/e2e/groupfoldersNavigation.cy.ts b/cypress/e2e/groupfoldersNavigation.cy.ts new file mode 100644 index 000000000..eb116aab2 --- /dev/null +++ b/cypress/e2e/groupfoldersNavigation.cy.ts @@ -0,0 +1,100 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { User } from '@nextcloud/e2e-test-server/cypress' + +import { + PERMISSION_READ, + PERMISSION_WRITE, + addUserToGroup, + createGroup, + createGroupFolder, + deleteGroupFolder, + fileOrFolderExists, +} from './groupfoldersUtils.ts' +import { getRowForFile } from './files/filesUtils.ts' +import { randHash } from '../utils/index.js' + +// Regression coverage for https://github.com/nextcloud/groupfolders/issues/4499: +// clicking a team folder from the Team folders view must navigate to the +// target route *with* the fileid segment (PR #4524) and render content — including +// on repeated navigation, which is where the stale-route / abort-race failures +// used to surface. +describe('Team folders view navigation', () => { + let user: User + let groupFolderId: string + let groupName: string + let groupFolderName: string + + beforeEach(() => { + if (groupFolderId) { + deleteGroupFolder(groupFolderId) + } + groupName = `test_group_${randHash()}` + groupFolderName = `test_group_folder_${randHash()}` + + cy.createRandomUser().then(_user => { + user = _user + createGroup(groupName).then(() => { + addUserToGroup(groupName, user.userId) + createGroupFolder(groupFolderName, groupName, [PERMISSION_READ, PERMISSION_WRITE]) + .then(_id => { + groupFolderId = _id + }) + }) + }) + }) + + it('clicking a team folder navigates to /apps/files/files/ with dir query and renders content', () => { + cy.uploadContent(user, new Blob(['hello']), 'text/plain', `/${groupFolderName}/file1.txt`) + + cy.login(user) + cy.visit('/apps/files/groupfolders') + + getRowForFile(groupFolderName).should('be.visible') + + cy.intercept({ method: 'PROPFIND', url: `**/dav/files/**/${groupFolderName}` }).as('propFindFolder') + getRowForFile(groupFolderName) + .find('[data-cy-files-list-row-name-link]') + .click({ force: true }) + cy.wait('@propFindFolder') + + // PR 4524: route must include the fileid segment, not just /apps/files/files?dir=... + cy.location('pathname').should('match', /\/apps\/files\/files\/\d+$/) + cy.location('search').should('contain', `dir=%2F${groupFolderName}`) + + // If exec returns before navigation settles or lets a router rejection bubble, + // the file list stays empty / shows "folder not found". + fileOrFolderExists('file1.txt') + }) + + it('navigating back to the Team folders view and re-entering the same folder still works', () => { + cy.uploadContent(user, new Blob(['hello']), 'text/plain', `/${groupFolderName}/file1.txt`) + + cy.login(user) + cy.visit('/apps/files/groupfolders') + getRowForFile(groupFolderName).should('be.visible') + + cy.intercept({ method: 'PROPFIND', url: `**/dav/files/**/${groupFolderName}` }).as('propFind1') + getRowForFile(groupFolderName) + .find('[data-cy-files-list-row-name-link]') + .click({ force: true }) + cy.wait('@propFind1') + fileOrFolderExists('file1.txt') + + cy.visit('/apps/files/groupfolders') + getRowForFile(groupFolderName).should('be.visible') + + // Second click — the stale-router path from the bug report. + cy.intercept({ method: 'PROPFIND', url: `**/dav/files/**/${groupFolderName}` }).as('propFind2') + getRowForFile(groupFolderName) + .find('[data-cy-files-list-row-name-link]') + .click({ force: true }) + cy.wait('@propFind2') + + cy.location('pathname').should('match', /\/apps\/files\/files\/\d+$/) + fileOrFolderExists('file1.txt') + }) +}) From 020a37e94b293a733035f92058ccc70ea51534e1 Mon Sep 17 00:00:00 2001 From: Git'Fellow <12234510+solracsf@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:37:36 +0200 Subject: [PATCH 3/5] test(cypress): locate team folder rows by fileid Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com> --- cypress/e2e/groupfoldersNavigation.cy.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/groupfoldersNavigation.cy.ts b/cypress/e2e/groupfoldersNavigation.cy.ts index eb116aab2..c8c47b74f 100644 --- a/cypress/e2e/groupfoldersNavigation.cy.ts +++ b/cypress/e2e/groupfoldersNavigation.cy.ts @@ -14,7 +14,6 @@ import { deleteGroupFolder, fileOrFolderExists, } from './groupfoldersUtils.ts' -import { getRowForFile } from './files/filesUtils.ts' import { randHash } from '../utils/index.js' // Regression coverage for https://github.com/nextcloud/groupfolders/issues/4499: @@ -52,11 +51,14 @@ describe('Team folders view navigation', () => { cy.login(user) cy.visit('/apps/files/groupfolders') + cy.location('pathname').should('include', '/apps/files/groupfolders') - getRowForFile(groupFolderName).should('be.visible') + // The Team folders view is served by the groupfolders DAV endpoint and keys + // rows by group-folder id rather than mount-point name — locate by fileid. + cy.get('[data-cy-files-list-row-fileid]').should('have.length.at.least', 1) cy.intercept({ method: 'PROPFIND', url: `**/dav/files/**/${groupFolderName}` }).as('propFindFolder') - getRowForFile(groupFolderName) + cy.get('[data-cy-files-list-row-fileid]').first() .find('[data-cy-files-list-row-name-link]') .click({ force: true }) cy.wait('@propFindFolder') @@ -75,21 +77,23 @@ describe('Team folders view navigation', () => { cy.login(user) cy.visit('/apps/files/groupfolders') - getRowForFile(groupFolderName).should('be.visible') + cy.location('pathname').should('include', '/apps/files/groupfolders') + cy.get('[data-cy-files-list-row-fileid]').should('have.length.at.least', 1) cy.intercept({ method: 'PROPFIND', url: `**/dav/files/**/${groupFolderName}` }).as('propFind1') - getRowForFile(groupFolderName) + cy.get('[data-cy-files-list-row-fileid]').first() .find('[data-cy-files-list-row-name-link]') .click({ force: true }) cy.wait('@propFind1') fileOrFolderExists('file1.txt') cy.visit('/apps/files/groupfolders') - getRowForFile(groupFolderName).should('be.visible') + cy.location('pathname').should('include', '/apps/files/groupfolders') + cy.get('[data-cy-files-list-row-fileid]').should('have.length.at.least', 1) // Second click — the stale-router path from the bug report. cy.intercept({ method: 'PROPFIND', url: `**/dav/files/**/${groupFolderName}` }).as('propFind2') - getRowForFile(groupFolderName) + cy.get('[data-cy-files-list-row-fileid]').first() .find('[data-cy-files-list-row-name-link]') .click({ force: true }) cy.wait('@propFind2') From 766d19168a699bd23aae71c09993a7c77ee1a13b Mon Sep 17 00:00:00 2001 From: Git'Fellow <12234510+solracsf@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:03:55 +0200 Subject: [PATCH 4/5] test(cypress): assert dir query with literal slash Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com> --- cypress/e2e/groupfoldersNavigation.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/groupfoldersNavigation.cy.ts b/cypress/e2e/groupfoldersNavigation.cy.ts index c8c47b74f..ea47043ec 100644 --- a/cypress/e2e/groupfoldersNavigation.cy.ts +++ b/cypress/e2e/groupfoldersNavigation.cy.ts @@ -65,7 +65,7 @@ describe('Team folders view navigation', () => { // PR 4524: route must include the fileid segment, not just /apps/files/files?dir=... cy.location('pathname').should('match', /\/apps\/files\/files\/\d+$/) - cy.location('search').should('contain', `dir=%2F${groupFolderName}`) + cy.location('search').should('contain', `dir=/${groupFolderName}`) // If exec returns before navigation settles or lets a router rejection bubble, // the file list stays empty / shows "folder not found". From 4bfefe4832124850fb069bd7e1b5d22ae0288faa Mon Sep 17 00:00:00 2001 From: Git'Fellow <12234510+solracsf@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:23:40 +0200 Subject: [PATCH 5/5] fix(Action): drop router-rejection catch, rely on server-side router Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com> --- src/actions/openGroupfolderAction.ts | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/actions/openGroupfolderAction.ts b/src/actions/openGroupfolderAction.ts index 393e5727a..1a4443d69 100644 --- a/src/actions/openGroupfolderAction.ts +++ b/src/actions/openGroupfolderAction.ts @@ -16,23 +16,12 @@ export const action: IFileAction = { enabled: ({ view }) => view.id === appName, async exec({ nodes }) { - try { - await window.OCP.Files.Router.goToRoute( - null, // use default route - { view: 'files', fileid: nodes[0].id }, - { dir: nodes[0].attributes.mountPoint }, - ) - return true - } catch (e) { - // Vue Router throws on duplicated/redirected navigations; those are not - // real failures from the user's perspective — the target view is reached. - const name = (e as { name?: string })?.name - const message = (e as { message?: string })?.message ?? '' - if (name === 'NavigationDuplicated' || /Redirected/.test(message)) { - return true - } - throw e - } + await window.OCP.Files.Router.goToRoute( + null, // use default route + { view: 'files', fileid: nodes[0].id }, + { dir: nodes[0].attributes.mountPoint }, + ) + return true }, default: DefaultType.DEFAULT,