Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
104 changes: 104 additions & 0 deletions cypress/e2e/groupfoldersNavigation.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*!
* 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 { 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/<fileid> 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')
cy.location('pathname').should('include', '/apps/files/groupfolders')

// 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')
cy.get('[data-cy-files-list-row-fileid]').first()
.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=/${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')
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')
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')
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')
cy.get('[data-cy-files-list-row-fileid]').first()
.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')
})
})
24 changes: 17 additions & 7 deletions src/actions/openGroupfolderAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
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.

Those should already be handled by file router in server (if not fixed there)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

NavigationDuplicated is handled at the server router layer (so my original catch for it was redundant), but Redirected is not handled anywhere, it bubbles as an uncaught promise rejection from every goToRoute call in every app. Neverthless, it should be handled there too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

return true
}
throw e
}
},

default: DefaultType.DEFAULT,
Expand Down
Loading