From 7e99409e56b08dd701429b49100b6e1686558165 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 13 Jan 2026 16:01:40 +0100 Subject: [PATCH 1/9] feat(userFolder): Make collectives user folder hidden per default Fixes: #2095 Signed-off-by: Jonas --- cypress/e2e/files.spec.js | 4 +-- cypress/e2e/page-links.spec.js | 2 +- cypress/support/commands.js | 2 +- lib/Fs/UserFolderHelper.php | 2 +- tests/Integration/features/mountpoint.feature | 26 +++++++++---------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cypress/e2e/files.spec.js b/cypress/e2e/files.spec.js index 10bb53361..34fc6558d 100644 --- a/cypress/e2e/files.spec.js +++ b/cypress/e2e/files.spec.js @@ -13,8 +13,8 @@ describe('Files app', function() { const breadcrumbsSelector = '[data-cy-files-content-breadcrumbs] :is(a, button)' cy.visit('/apps/files') - cy.openFile('Collectives') - cy.get(breadcrumbsSelector).should('contain', 'Collectives') + cy.openFile('.Collectives') + cy.get(breadcrumbsSelector).should('contain', '.Collectives') cy.openFile('Preexisting Collective') cy.get(breadcrumbsSelector).should('contain', 'Preexisting Collective') cy.fileList().should('contain', 'Readme') diff --git a/cypress/e2e/page-links.spec.js b/cypress/e2e/page-links.spec.js index 6c0e487a6..d83e60c85 100644 --- a/cypress/e2e/page-links.spec.js +++ b/cypress/e2e/page-links.spec.js @@ -32,7 +32,7 @@ describe('Page link handling', function() { cy.uploadFile('test.png', 'image/png').then((id) => { imageId = id }) - cy.uploadFile('test.pdf', 'application/pdf', 'Collectives/Link%20Testing/').then((id) => { + cy.uploadFile('test.pdf', 'application/pdf', '.Collectives/Link%20Testing/').then((id) => { pdfId = id }) }).then(() => { diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 012c56264..2eede28fa 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -289,7 +289,7 @@ Cypress.Commands.add('seedPageContent', (pagePath, content) => { ? content.substring(0, 200) + '…' : content Cypress.log({ message: `${pagePath}, ${contentForLog}` }) - cy.uploadContent(`Collectives/${pagePath}`, content) + cy.uploadContent(`.Collectives/${pagePath}`, content) }) Cypress.Commands.add('uploadFile', (path, mimeType, remotePath = '') => { diff --git a/lib/Fs/UserFolderHelper.php b/lib/Fs/UserFolderHelper.php index f92ca2122..e13b732dc 100644 --- a/lib/Fs/UserFolderHelper.php +++ b/lib/Fs/UserFolderHelper.php @@ -35,7 +35,7 @@ public function getUserFolderSetting(string $userId): string { $user = $this->userManager->get($userId); $userLang = $this->l10nFactory->getUserLanguage($user); $l10n = $this->l10nFactory->get('collectives', $userLang); - $userCollectivesPath = '/' . $l10n->t('Collectives'); + $userCollectivesPath = DIRECTORY_SEPARATOR . '.' . $l10n->t('Collectives'); $this->config->setUserValue($userId, 'collectives', 'user_folder', $userCollectivesPath); } diff --git a/tests/Integration/features/mountpoint.feature b/tests/Integration/features/mountpoint.feature index 064766549..b5eaefee2 100644 --- a/tests/Integration/features/mountpoint.feature +++ b/tests/Integration/features/mountpoint.feature @@ -21,16 +21,16 @@ Feature: mountpoint When user "jane" creates folder "MyCollectives" And user "jane" sees webdav node "MyCollectives" And user "jane" creates collective "BehatMountPoint2" - And user "jane" sees webdav node "Collectives/BehatMountPoint2" + And user "jane" sees webdav node ".Collectives/BehatMountPoint2" Then user "jane" sets setting "user_folder" to value "/MyCollectives" - And user "jane" fails to see webdav node "Collectives" + And user "jane" fails to see webdav node ".Collectives" And user "jane" sees webdav node "MyCollectives" And user "jane" sees webdav node "MyCollectives/BehatMountPoint2" And user "jane" fails to see webdav node "MyCollectives (1)" - Then user "jane" sets setting "user_folder" to value "/Collectives" + Then user "jane" sets setting "user_folder" to value "/.Collectives" And user "jane" fails to see webdav node "MyCollectives" - And user "jane" sees webdav node "Collectives" - And user "jane" sees webdav node "Collectives/BehatMountPoint2" + And user "jane" sees webdav node ".Collectives" + And user "jane" sees webdav node ".Collectives/BehatMountPoint2" Then user "jane" trashes and deletes collective "BehatMountPoint2" Scenario: Rename non-empty node when in conflict with mountpoint @@ -38,32 +38,32 @@ Feature: mountpoint And user "jane" creates folder "MyCollectives/subfolder" And user "jane" sees webdav node "MyCollectives/subfolder" And user "jane" creates collective "BehatMountPoint3" - And user "jane" sees webdav node "Collectives/BehatMountPoint3" + And user "jane" sees webdav node ".Collectives/BehatMountPoint3" Then user "jane" sets setting "user_folder" to value "/MyCollectives" - And user "jane" fails to see webdav node "Collectives" + And user "jane" fails to see webdav node ".Collectives" And user "jane" sees webdav node "MyCollectives" And user "jane" sees webdav node "MyCollectives/BehatMountPoint3" And user "jane" fails to see webdav node "MyCollectives/subfolder" And user "jane" sees webdav node "MyCollectives (1)/subfolder" - Then user "jane" sets setting "user_folder" to value "/Collectives" + Then user "jane" sets setting "user_folder" to value "/.Collectives" And user "jane" fails to see webdav node "MyCollectives" - And user "jane" sees webdav node "Collectives" - And user "jane" sees webdav node "Collectives/BehatMountPoint3" + And user "jane" sees webdav node ".Collectives" + And user "jane" sees webdav node ".Collectives/BehatMountPoint3" And user "jane" deletes folder "MyCollectives (1)" Then user "jane" trashes and deletes collective "BehatMountPoint3" Scenario: Change collectives user folder for user When user "bob" creates folder "some" And user "bob" sets setting "user_folder" to value "/some/folder" - And user "bob" fails to see webdav node "Collectives" + And user "bob" fails to see webdav node ".Collectives" And user "bob" sees webdav node "some/folder" Then user "jane" has webdav access to "BehatMountPoint" with permissions "RMGDNVCK" And user "john" has webdav access to "BehatMountPoint" with permissions "RMGDNVCK" And user "alice" has webdav access to "BehatMountPoint" with permissions "RMG" And user "bob" has webdav access to "BehatMountPoint" with permissions "MG" - Then user "bob" sets setting "user_folder" to value "/Collectives" + Then user "bob" sets setting "user_folder" to value "/.Collectives" And user "bob" fails to see webdav node "some/folder" - And user "bob" sees webdav node "Collectives" + And user "bob" sees webdav node ".Collectives" And user "bob" deletes folder "some" Scenario: Trash page via webdav From 8ed6f8ea5394d963b4fcaf327975881b045c1f8b Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 23 Feb 2026 17:10:52 +0100 Subject: [PATCH 2/9] test(unit): adjust default user folder path Signed-off-by: Jonas --- tests/Unit/Fs/MarkdownHelperTest.php | 4 ++-- tests/Unit/Fs/UserFolderHelperTest.php | 4 ++-- tests/Unit/Model/PageInfoTest.php | 4 ++-- tests/Unit/Service/AttachmentServiceTest.php | 6 +++--- tests/Unit/Service/PageServiceTest.php | 14 +++++++------- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/Unit/Fs/MarkdownHelperTest.php b/tests/Unit/Fs/MarkdownHelperTest.php index 9287c51af..60a476d3a 100644 --- a/tests/Unit/Fs/MarkdownHelperTest.php +++ b/tests/Unit/Fs/MarkdownHelperTest.php @@ -37,7 +37,7 @@ public function linksContentProvider(): array { ['[link](https://example.org/?foo=3#fragment)', [['link', 'https://example.org/?foo=3#fragment', '']]], ['[link](#fragment)', [['link', '#fragment', '']]], - // With markdown marks in text + // With Markdown marks in text ['#Title\n\nLink: [*italic* **bold** link](https://example.org/)\n\nMore text...', [['italic bold link', 'https://example.org/', '']]], // Multiple links @@ -100,7 +100,7 @@ public function testGetLinkedPageIds(): void { $pageInfo = new PageInfo(); $pageInfo->setId(123); - $pageInfo->setCollectivePath('Collectives/' . $collective->getName()); + $pageInfo->setCollectivePath('.Collectives/' . $collective->getName()); $pageInfo->setFilePath('page1/pageX'); $pageInfo->setFileName('subpage2.md'); $pageInfo->setTitle('subpage2'); diff --git a/tests/Unit/Fs/UserFolderHelperTest.php b/tests/Unit/Fs/UserFolderHelperTest.php index 46d8da894..0ad3dcef5 100644 --- a/tests/Unit/Fs/UserFolderHelperTest.php +++ b/tests/Unit/Fs/UserFolderHelperTest.php @@ -36,7 +36,7 @@ protected function setUp(): void { ->disableOriginalConstructor() ->getMock(); $this->collectivesUserFolder->method('getName') - ->willReturn('Collectives'); + ->willReturn('.Collectives'); $this->userFolder = $this->getMockBuilder(Folder::class) ->disableOriginalConstructor() @@ -83,7 +83,7 @@ public function testGetUserFolderSetting(): void { $this->l10n->method('t') ->willReturn('Collectif'); - self::assertEquals('/Collectif', $this->helper->getUserFolderSetting('jane')); + self::assertEquals('/.Collectif', $this->helper->getUserFolderSetting('jane')); $this->config->method('setUserValue') ->willThrowException(new PreConditionNotMetException('')); diff --git a/tests/Unit/Model/PageInfoTest.php b/tests/Unit/Model/PageInfoTest.php index 171c6c3c7..49aedcc54 100644 --- a/tests/Unit/Model/PageInfoTest.php +++ b/tests/Unit/Model/PageInfoTest.php @@ -22,8 +22,8 @@ public function testFromFile(): void { $fileMTime = 0; $fileSize = 100; $fileName = 'name.md'; - $fileMountPoint = '/files/user/Collectives/collective/'; - $fileCollectivePath = 'Collectives/collective'; + $fileMountPoint = '/files/user/.Collectives/collective/'; + $fileCollectivePath = '.Collectives/collective'; $parentInternalPath = 'path/to/file'; $internalPath = $parentInternalPath . '/' . $fileName; $userId = 'jane'; diff --git a/tests/Unit/Service/AttachmentServiceTest.php b/tests/Unit/Service/AttachmentServiceTest.php index 2b42686a3..464846817 100644 --- a/tests/Unit/Service/AttachmentServiceTest.php +++ b/tests/Unit/Service/AttachmentServiceTest.php @@ -37,7 +37,7 @@ protected function setUp(): void { ->with($this->attachmentFolderName) ->willReturn(true); $this->parentFolder->method('getRelativePath') - ->willReturn('/Collectives/x/path/to/' . $this->attachmentFolderName . '/attachmentFile1'); + ->willReturn('/.Collectives/x/path/to/' . $this->attachmentFolderName . '/attachmentFile1'); $attachmentFolder = $this->createMock(Folder::class); $attachmentFolder->method('getName') ->willReturn($this->attachmentFolderName); @@ -45,7 +45,7 @@ protected function setUp(): void { $attachmentFile->method('getId') ->willReturn(2); $attachmentFile->method('getPath') - ->willReturn('/' . $this->userId . '/files/Collectives/x/path/to/' . $this->attachmentFolderName . '/attachmentFile1'); + ->willReturn('/' . $this->userId . '/files/.Collectives/x/path/to/' . $this->attachmentFolderName . '/attachmentFile1'); $attachmentFile->method('getParent') ->willReturn($attachmentFolder); $attachmentFile->method('getInternalPath') @@ -72,7 +72,7 @@ public function testGetAttachments(): void { 'filesize' => null, 'mimetype' => '', 'timestamp' => null, - 'path' => '/Collectives/x/path/to/' . $this->attachmentFolderName . '/attachmentFile1', + 'path' => '/.Collectives/x/path/to/' . $this->attachmentFolderName . '/attachmentFile1', 'internalPath' => '/path/to/' . $this->attachmentFolderName . '/attachmentFile1', 'hasPreview' => false, 'src' => $this->attachmentFolderName . DIRECTORY_SEPARATOR . 'attachmentFile1', diff --git a/tests/Unit/Service/PageServiceTest.php b/tests/Unit/Service/PageServiceTest.php index ab31342c6..7cd6a9491 100644 --- a/tests/Unit/Service/PageServiceTest.php +++ b/tests/Unit/Service/PageServiceTest.php @@ -204,7 +204,7 @@ private function prepareFile(string $fileName, Folder $parent, IMountPoint $moun $file->method('getMountPoint') ->willReturn($mountPoint); $file->method('getInternalPath') - ->willReturn('Collectives/testfolder/' . $fileName); + ->willReturn('.Collectives/testfolder/' . $fileName); $file->method('getMTime') ->willReturn(0); $file->method('getSize') @@ -228,7 +228,7 @@ public function testGetPagesFromFolderWithSubfolderWithoutRecurse(): void { $mountPoint = $this->getMockBuilder(MountPoint::class) ->disableOriginalConstructor() ->getMock(); - $mountPoint->method('getMountPoint')->willReturn('/files/user/Collectives/collective/'); + $mountPoint->method('getMountPoint')->willReturn('/files/user/.Collectives/collective/'); $indexFile = $this->prepareFile('Readme.md', $folder, $mountPoint, 101); $folder->method('get') @@ -267,7 +267,7 @@ public function testGetPagesFromFolderWithSubfolderWithoutRecurse(): void { $subfolder->method('getMountPoint') ->willReturn($mountPoint); $subfolder->method('getInternalPath') - ->willReturn('Collectives/testfolder/' . $fileName); + ->willReturn('.Collectives/testfolder/' . $fileName); $subfolder->method('getMTime') ->willReturn(0); $subfolder->method('getSize') @@ -305,7 +305,7 @@ public function testGetPagesFromFolderRecursive(): void { ->willReturn('testfolder'); $mountPoint = $this->createMock(IMountPoint::class); - $mountPoint->method('getMountPoint')->willReturn('/files/user/Collectives/collective/'); + $mountPoint->method('getMountPoint')->willReturn('/files/user/.Collectives/collective/'); $indexFile = $this->prepareFile('Readme.md', $folder, $mountPoint, 101); $folder->method('get') @@ -359,7 +359,7 @@ public function testGetPagesFromFolderWithMissingIndex(): void { $mountPoint = $this->getMockBuilder(MountPoint::class) ->disableOriginalConstructor() ->getMock(); - $mountPoint->method('getMountPoint')->willReturn('/files/user/Collectives/collective/'); + $mountPoint->method('getMountPoint')->willReturn('/files/user/.Collectives/collective/'); $folder = $this->createMock(Folder::class); $folder->method('getParent') @@ -380,7 +380,7 @@ public function testGetPagesFromFolderWithMissingIndex(): void { $file1->method('getMountPoint') ->willReturn($mountPoint); $file1->method('getInternalPath') - ->willReturn('Collectives/testfolder/' . $file1Name); + ->willReturn('.Collectives/testfolder/' . $file1Name); $file1->method('getMTime') ->willReturn(0); $file1->method('getSize') @@ -407,7 +407,7 @@ public function testGetPagesFromFolderWithMissingIndex(): void { $indexFile->method('getMountPoint') ->willReturn($mountPoint); $indexFile->method('getInternalPath') - ->willReturn('Collectives/testfolder/Readme.md'); + ->willReturn('.Collectives/testfolder/Readme.md'); $indexFile->method('getMTime') ->willReturn(0); $indexFile->method('getSize') From 4ce2f2bf5f676f66b6abea09ac29085f926958cb Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 13 Jan 2026 17:15:16 +0100 Subject: [PATCH 3/9] feat(userFolder): Migrate user folder settings to new default Update the setting if the user never touched the default value. Signed-off-by: Jonas --- appinfo/info.xml | 3 +- lib/Migration/MigrateUserFolderSettings.php | 85 +++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 lib/Migration/MigrateUserFolderSettings.php diff --git a/appinfo/info.xml b/appinfo/info.xml index c0ffcae8c..f30077ef6 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -26,7 +26,7 @@ In your Nextcloud instance, simply navigate to **»Apps«**, find the **»Teams«** and **»Collectives«** apps and enable them. ]]> - 3.6.1 + 3.7.0 agpl CollectiveCloud Team Collectives @@ -63,6 +63,7 @@ In your Nextcloud instance, simply navigate to **»Apps«**, find the OCA\Collectives\Migration\GenerateSlugs OCA\Collectives\Migration\MigrateBacklinks + OCA\Collectives\Migration\MigrateUserFolderSettings diff --git a/lib/Migration/MigrateUserFolderSettings.php b/lib/Migration/MigrateUserFolderSettings.php new file mode 100644 index 000000000..03c1c70b6 --- /dev/null +++ b/lib/Migration/MigrateUserFolderSettings.php @@ -0,0 +1,85 @@ +appConfig->getValueBool('collectives', 'migrated_user_folder_settings')) { + $output->info('User folder settings already migrated'); + return; + } + + $output->info('Migrating user folder settings ...'); + $output->startProgress(); + + $this->userManager->callForSeenUsers(function (IUser $user) use ($output) { + $oldDefaultUserFolderPath = DIRECTORY_SEPARATOR . 'Collectives'; + $newDefaultUserFolderPath = DIRECTORY_SEPARATOR . '.' . 'Collectives'; + $userFolderPath = $this->config->getUserValue($user->getUID(), 'collectives', 'user_folder', ''); + if ($userFolderPath === '') { + // No user folder path configured, doesn't use Collectives + return; + } + + if ($userFolderPath === $newDefaultUserFolderPath) { + // New default English user folder path, already migrated + return; + } + + if ($userFolderPath === $oldDefaultUserFolderPath) { + // Old default English user folder path, update setting + $this->config->setUserValue($user->getUID(), 'collectives', 'user_folder', $newDefaultUserFolderPath); + $output->advance(); + return; + } + + $userLang = $this->l10nFactory->getUserLanguage($user); + $l10n = $this->l10nFactory->get('collectives', $userLang); + $oldDefaultUserFolderPathL10n = DIRECTORY_SEPARATOR . $l10n->t('Collectives'); + $newDefaultUserFolderPathL10n = DIRECTORY_SEPARATOR . '.' . $l10n->t('Collectives'); + + if ($userFolderPath === $oldDefaultUserFolderPathL10n) { + // new default localized user folder path, already migrated + return; + } + + if ($userFolderPath === $oldDefaultUserFolderPathL10n) { + // Old default localized user folder path, update setting + $this->config->setUserValue($user->getUID(), 'collectives', 'user_folder', $newDefaultUserFolderPathL10n); + $output->advance(); + } + }); + + $output->finishProgress(); + $output->info('done'); + + $this->appConfig->setValueBool('collectives', 'migrated_user_folder_settings', true); + } +} From 936168129814b8de0fdd51a1f2bf8f8064aabe3f Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 23 Feb 2026 13:36:39 +0100 Subject: [PATCH 4/9] test(playwright): refactor files app fixture Signed-off-by: Jonas --- playwright/e2e/settings.spec.ts | 10 +++--- playwright/support/fixtures/filesApp.ts | 21 ++++-------- .../support/sections/FilesAppSection.ts | 33 +++++++++++++++++++ 3 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 playwright/support/sections/FilesAppSection.ts diff --git a/playwright/e2e/settings.spec.ts b/playwright/e2e/settings.spec.ts index 65d340bb0..2d9023944 100644 --- a/playwright/e2e/settings.spec.ts +++ b/playwright/e2e/settings.spec.ts @@ -26,7 +26,7 @@ test.describe('Settings', () => { await collective.openApp() }) - test('Can change collectives folder', async ({ getFileListEntry, navigation, openFilesApp, openFile }) => { + test('Can change collectives folder', async ({ navigation, filesApp }) => { const randomFolder = Math.random().toString(36).replace(/[^a-z]+/g, '').slice(0, 10) await navigation.setUserFolder(randomFolder) await navigation.openCollectivesSettings() @@ -34,9 +34,9 @@ test.describe('Settings', () => { // Input field has new value after setting it await expect(navigation.collectivesFolderInputEl).toHaveValue(`/${randomFolder}`) - await openFilesApp() - await openFile(randomFolder) - await expect(getFileListEntry(collectiveName1)).toBeVisible() - await expect(getFileListEntry(collectiveName2)).toBeVisible() + await filesApp.open() + await filesApp.openFile(randomFolder) + await expect(filesApp.getFileListEntry(collectiveName1)).toBeVisible() + await expect(filesApp.getFileListEntry(collectiveName2)).toBeVisible() }) }) diff --git a/playwright/support/fixtures/filesApp.ts b/playwright/support/fixtures/filesApp.ts index 3bdf23a06..8b437807e 100644 --- a/playwright/support/fixtures/filesApp.ts +++ b/playwright/support/fixtures/filesApp.ts @@ -3,25 +3,16 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { type Locator } from '@playwright/test' import { test as baseTest } from '@playwright/test' +import { FilesAppSection } from '../sections/FilesAppSection.ts' type FilesAppFixture = { - openFilesApp: () => Promise - getFileListEntry: (fileName: string) => Locator - openFile: (fileName: string) => Promise + filesApp: FilesAppSection } export const test = baseTest.extend({ - openFilesApp: ({ page }, use) => use(async () => { - await page.goto('/apps/files/') - }), - getFileListEntry: ({ page }, use) => use((fileName: string) => { - return page.locator(`[data-cy-files-list-row-name="${fileName}"]`) - }), - openFile: ({ page }, use) => use(async (fileName: string) => { - // Open the file by clicking on it in the file list - const fileEntry = page.locator(`[data-cy-files-list-row-name="${fileName}"]`) - await fileEntry.click() - }), + filesApp: async ({ page }, use) => { + const filesApp = new FilesAppSection(page) + await use(filesApp) + }, }) diff --git a/playwright/support/sections/FilesAppSection.ts b/playwright/support/sections/FilesAppSection.ts new file mode 100644 index 000000000..e2466dc37 --- /dev/null +++ b/playwright/support/sections/FilesAppSection.ts @@ -0,0 +1,33 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { type Locator, type Page } from '@playwright/test' +import { expect } from '@playwright/test' + +export class FilesAppSection { + public readonly fileListEl: Locator + + constructor(public readonly page: Page) { + this.fileListEl = this.page.locator('.files-list') + } + + public async open(): Promise { + await this.page.goto('/apps/files/') + } + + public getFileListEntry(fileName: string): Locator { + return this.fileListEl.locator(`[data-cy-files-list-row-name="${fileName}"]`) + } + + public async openFile(fileName: string): Promise { + const fileEntry = this.getFileListEntry(fileName) + await fileEntry.click() + } + + public async hasCollectivesHeader(): Promise { + return await expect(this.fileListEl.locator('.filelist-collectives-wrapper')) + .toContainText('The content of this folder is best viewed in the Collectives app.') + } +} From b897e950b9c074ce085593368d5a48dfda7f7787 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 23 Feb 2026 16:52:24 +0100 Subject: [PATCH 5/9] test(playwright): make sure we always send authenticated requests Signed-off-by: Jonas --- playwright/support/fixtures/User.ts | 3 ++- .../support/fixtures/create-collectives.ts | 2 +- playwright/support/fixtures/random-user.ts | 24 +++++++++++-------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/playwright/support/fixtures/User.ts b/playwright/support/fixtures/User.ts index 68919b9ed..f06eccc8d 100644 --- a/playwright/support/fixtures/User.ts +++ b/playwright/support/fixtures/User.ts @@ -3,13 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { type User as Account } from '@nextcloud/e2e-test-server' import { type Page } from '@playwright/test' import { createCollective, trashAndDeleteCollective } from './Collective.ts' export class User { constructor( + public readonly account: Account, public readonly page: Page, - public readonly userId: string, ) { } diff --git a/playwright/support/fixtures/create-collectives.ts b/playwright/support/fixtures/create-collectives.ts index 1be5caddf..cf74f3202 100644 --- a/playwright/support/fixtures/create-collectives.ts +++ b/playwright/support/fixtures/create-collectives.ts @@ -56,7 +56,7 @@ export const test = base.extend({ await runOcc([ 'collectives:import:markdown', `--collective-id=${collective.data.id}`, - `--user-id=${user.userId}`, + `--user-id=${user.account.userId}`, '--', config.markdownImportPath, ]) diff --git a/playwright/support/fixtures/random-user.ts b/playwright/support/fixtures/random-user.ts index 3d1e54f2d..20f866162 100644 --- a/playwright/support/fixtures/random-user.ts +++ b/playwright/support/fixtures/random-user.ts @@ -3,14 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { type User as Account } from '@nextcloud/e2e-test-server' import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright' import { test as base } from '@playwright/test' import { User } from './User.ts' -type RandomUser = Awaited> - export interface UserFixture { - randomUser: RandomUser + account: Account user: User } @@ -19,24 +18,29 @@ export interface UserFixture { */ export const test = base.extend({ // eslint-disable-next-line no-empty-pattern - randomUser: async ({}, use) => { - const randomUser = await createRandomUser() - await use(randomUser) + account: async ({}, use) => { + const account = await createRandomUser() + await use(account) }, - page: async ({ browser, baseURL, randomUser }, use) => { + page: async ({ account, browser, baseURL }, use) => { // Important: make sure we authenticate in a clean environment by unsetting storage state. const page = await browser.newPage({ storageState: undefined, baseURL, }) - await login(page.request, randomUser) + await login(page.request, account) + const tokenResponse = await page.request.get('./csrftoken', { + failOnStatusCode: true, + }) + const { token } = (await tokenResponse.json()) as { token: string } + await page.context().setExtraHTTPHeaders({ requesttoken: token }) await use(page) await page.close() }, - user: async ({ page, randomUser }, use) => { - const user = new User(page, randomUser.userId) + user: async ({ account, page }, use) => { + const user = new User(account, page) await use(user) }, }) From e9952642a8ae155d6b3a3243c1af24101ba92243 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 23 Feb 2026 14:22:40 +0100 Subject: [PATCH 6/9] test(playwright): migrate files app tests to playwright Signed-off-by: Jonas --- cypress/e2e/files.spec.js | 25 ---------------------- cypress/support/navigation.js | 2 -- playwright/e2e/files.spec.ts | 28 +++++++++++++++++++++++++ playwright/support/fixtures/filesApp.ts | 17 +++++++++++++++ 4 files changed, 45 insertions(+), 27 deletions(-) delete mode 100644 cypress/e2e/files.spec.js create mode 100644 playwright/e2e/files.spec.ts diff --git a/cypress/e2e/files.spec.js b/cypress/e2e/files.spec.js deleted file mode 100644 index 34fc6558d..000000000 --- a/cypress/e2e/files.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -describe('Files app', function() { - before(function() { - cy.loginAs('bob') - cy.deleteAndSeedCollective('Preexisting Collective') - }) - - it('has a matching folder', function() { - const breadcrumbsSelector = '[data-cy-files-content-breadcrumbs] :is(a, button)' - cy.visit('/apps/files') - - cy.openFile('.Collectives') - cy.get(breadcrumbsSelector).should('contain', '.Collectives') - cy.openFile('Preexisting Collective') - cy.get(breadcrumbsSelector).should('contain', 'Preexisting Collective') - cy.fileList().should('contain', 'Readme') - cy.fileList().should('contain', '.md') - cy.get('.filelist-collectives-wrapper') - .should('contain', 'The content of this folder is best viewed in the Collectives app.') - }) -}) diff --git a/cypress/support/navigation.js b/cypress/support/navigation.js index 60a57fa92..c6d9c4765 100644 --- a/cypress/support/navigation.js +++ b/cypress/support/navigation.js @@ -56,5 +56,3 @@ Cypress.Commands.add('clickMenuButton', (title) => { const FILE_LIST_SELECTOR = '.files-fileList a, [data-cy-files-list-row] [data-cy-files-list-row-name-link]' Cypress.Commands.add('fileList', () => cy.get(FILE_LIST_SELECTOR)) - -Cypress.Commands.add('openFile', (name) => cy.fileList().contains(name).click()) diff --git a/playwright/e2e/files.spec.ts b/playwright/e2e/files.spec.ts new file mode 100644 index 000000000..8d7f435bc --- /dev/null +++ b/playwright/e2e/files.spec.ts @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, mergeTests } from '@playwright/test' +import { test as createCollectivesTest } from '../support/fixtures/create-collectives.ts' +import { test as filesAppTest } from '../support/fixtures/filesApp.ts' +import { test as navigationTest } from '../support/fixtures/navigation.ts' + +const test = mergeTests(createCollectivesTest, filesAppTest, navigationTest) + +test.describe('Files app', () => { + test('Collectives folder is visible in files app', async ({ collective, filesApp, setShowHiddenFiles }) => { + await collective.openCollective() + + await setShowHiddenFiles(true) + await filesApp.open() + + await expect(filesApp.getFileListEntry('.Collectives')).toBeVisible() + await filesApp.openFile('.Collectives') + await expect(filesApp.getFileListEntry('Test Collective 1')).toBeVisible() + await filesApp.hasCollectivesHeader() + await filesApp.openFile('Test Collective 1') + await expect(filesApp.getFileListEntry('Readme.md')).toBeVisible() + await filesApp.hasCollectivesHeader() + }) +}) diff --git a/playwright/support/fixtures/filesApp.ts b/playwright/support/fixtures/filesApp.ts index 8b437807e..c761385aa 100644 --- a/playwright/support/fixtures/filesApp.ts +++ b/playwright/support/fixtures/filesApp.ts @@ -8,6 +8,7 @@ import { FilesAppSection } from '../sections/FilesAppSection.ts' type FilesAppFixture = { filesApp: FilesAppSection + setShowHiddenFiles: (show: boolean) => Promise } export const test = baseTest.extend({ @@ -15,4 +16,20 @@ export const test = baseTest.extend({ const filesApp = new FilesAppSection(page) await use(filesApp) }, + + setShowHiddenFiles: ({ page }, use) => use(async (show: boolean) => { + await page.request.put( + '/index.php/apps/files/api/v1/config/show_hidden', + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + data: { + value: show, + }, + failOnStatusCode: true, + }, + ) + }), }) From 582c1689854dc3566dfb8ee526a2e170392ae248 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 23 Feb 2026 14:37:24 +0100 Subject: [PATCH 7/9] docs: document hidden .Collectives folder in user documentation Contains some additional drive-by fixes. Signed-off-by: Jonas --- docs/content/usage/_index.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/content/usage/_index.md b/docs/content/usage/_index.md index ce87438aa..a14888998 100644 --- a/docs/content/usage/_index.md +++ b/docs/content/usage/_index.md @@ -5,14 +5,13 @@ weight = 1 alwaysopen = true +++ -This tutorial will walk you through the collectives app in less than 10 -minutes. -It assumes you already have a user account on a nextcloud installation -with the collectives app enabled. +This tutorial will walk you through the basics of the Collectives app. +It assumes you already have a user account on a Nextcloud installation +with the Collectives app enabled. ## ✨ Create a new collective -Visit the collectives app by clicking its icon in the toolbar at the top +Visit the Collectives app by clicking its icon in the toolbar at the top of the screen: ![Screenshot of the app icon in the main toolbar](/images/apps.png) @@ -20,17 +19,17 @@ of the screen: On the left of the screen you will find a list of all of your collectives. It's probably empty if you have not been added to any yet. -Click "Create new collective" and type a name for your collective. +Click "New collective" and type a name for your collective. You can also pick an emoji to easily find your collective later: -![Screencast of clicking the "Create new collective" button](/images/create-collective.gif) +![Screencast of clicking the "New collective" button](/images/create-collective.gif) ## 🌱 Bring life to your collective Create pages and share the knowledge that really matters. -Click the "Create a Page" button in the upper left +Click the "Add a page" button in the upper left and a new page will appear. You can type in a title right away or add some content first @@ -53,11 +52,16 @@ can even add entire groups to your collectives. ## Also good to know * Multiple people can edit the same page simultaneously. -* Link local pages by selecting text and choosing "link file". - Drag & drop from page list into the editor also works. +* Link pages by drag & dropping them from the page list into the editor. * Add templates for future subpages via "Manage templates" in the landing page three-dot-menu. * Ask [the community](https://help.nextcloud.com/c/apps/collectives/174) for help in case of questions. ## Searching Collectives -Use the search input on top of the page list (top left) to filter your collecives by page titles. Use the Nextcloud unified search (top right) to search within the contents of Collectives. +Use the search input on top of the page list (top left) to filter your collectives by page titles. Use the Nextcloud unified search (top right) to search within the contents of Collectives. + +## Access the Markdown files via Files app + +The pages of your collectives are stored in Markdown files. You can access them in the Files app in a +hidden folder called ".Collectives" (or a translated version of it). To access the hidden folder, enable +"Show hidden files" in the Files app settings. From 8d7c63116486deabec6aa3c07b5ce7ef563d7ffa Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 26 Feb 2026 12:36:54 +0100 Subject: [PATCH 8/9] docs(usage): slightly improve docs how to change collectives folder Signed-off-by: Jonas --- docs/content/usage/_index.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/content/usage/_index.md b/docs/content/usage/_index.md index a14888998..2514f13dd 100644 --- a/docs/content/usage/_index.md +++ b/docs/content/usage/_index.md @@ -62,6 +62,10 @@ Use the search input on top of the page list (top left) to filter your collectiv ## Access the Markdown files via Files app -The pages of your collectives are stored in Markdown files. You can access them in the Files app in a -hidden folder called ".Collectives" (or a translated version of it). To access the hidden folder, enable -"Show hidden files" in the Files app settings. +The pages of your collectives are stored in Markdown files. You can access them via the Files app. +Per default, the folder is hidden as it's called ".Collectives" (or a translated version of it). +To access the hidden folder, enable "Show hidden files" in the Files app settings. + +You can also change the name of the folder in the Collectives settings. The setting is located at +the bottom of the Collectives list. If you want the folder to be visible permanently without showing +other hidden files, set the folder setting to a name without leading dot (e.g. "Collectives"). From 1ccfa7b182ba791348907f7a63e836bdb083503e Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 26 Feb 2026 16:50:14 +0100 Subject: [PATCH 9/9] fix(Migration): remove superfluous check for localized collectives folder Signed-off-by: Jonas --- lib/Migration/MigrateUserFolderSettings.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/Migration/MigrateUserFolderSettings.php b/lib/Migration/MigrateUserFolderSettings.php index 03c1c70b6..4b4e1e9e4 100644 --- a/lib/Migration/MigrateUserFolderSettings.php +++ b/lib/Migration/MigrateUserFolderSettings.php @@ -65,11 +65,6 @@ public function run(IOutput $output): void { $oldDefaultUserFolderPathL10n = DIRECTORY_SEPARATOR . $l10n->t('Collectives'); $newDefaultUserFolderPathL10n = DIRECTORY_SEPARATOR . '.' . $l10n->t('Collectives'); - if ($userFolderPath === $oldDefaultUserFolderPathL10n) { - // new default localized user folder path, already migrated - return; - } - if ($userFolderPath === $oldDefaultUserFolderPathL10n) { // Old default localized user folder path, update setting $this->config->setUserValue($user->getUID(), 'collectives', 'user_folder', $newDefaultUserFolderPathL10n);