Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ In your Nextcloud instance, simply navigate to **»Apps«**, find the
**»Teams«** and **»Collectives«** apps and enable them.

]]></description>
<version>3.6.1</version>
<version>3.7.0</version>
<licence>agpl</licence>
<author>CollectiveCloud Team</author>
<namespace>Collectives</namespace>
Expand Down Expand Up @@ -63,6 +63,7 @@ In your Nextcloud instance, simply navigate to **»Apps«**, find the
<live-migration>
<step>OCA\Collectives\Migration\GenerateSlugs</step>
<step>OCA\Collectives\Migration\MigrateBacklinks</step>
<step>OCA\Collectives\Migration\MigrateUserFolderSettings</step>
</live-migration>
</repair-steps>
<commands>
Expand Down
25 changes: 0 additions & 25 deletions cypress/e2e/files.spec.js

This file was deleted.

2 changes: 1 addition & 1 deletion cypress/e2e/page-links.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '') => {
Expand Down
2 changes: 0 additions & 2 deletions cypress/support/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
30 changes: 19 additions & 11 deletions docs/content/usage/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,31 @@ 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)

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
Expand All @@ -53,11 +52,20 @@ 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 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").
2 changes: 1 addition & 1 deletion lib/Fs/UserFolderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
80 changes: 80 additions & 0 deletions lib/Migration/MigrateUserFolderSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Collectives\Migration;

use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;

class MigrateUserFolderSettings implements IRepairStep {
public function __construct(
private readonly IConfig $config,
private readonly IAppConfig $appConfig,
private readonly IUserManager $userManager,
private readonly IFactory $l10nFactory,
) {
}

public function getName():string {
return 'Migrate user folder settings to new default name';
}

public function run(IOutput $output): void {
if ($this->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) {
// 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);
}
}
28 changes: 28 additions & 0 deletions playwright/e2e/files.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
10 changes: 5 additions & 5 deletions playwright/e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ 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()

// 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()
})
})
3 changes: 2 additions & 1 deletion playwright/support/fixtures/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
}

Expand Down
2 changes: 1 addition & 1 deletion playwright/support/fixtures/create-collectives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const test = base.extend<CollectivesFixture>({
await runOcc([
'collectives:import:markdown',
`--collective-id=${collective.data.id}`,
`--user-id=${user.userId}`,
`--user-id=${user.account.userId}`,
'--',
config.markdownImportPath,
])
Expand Down
36 changes: 22 additions & 14 deletions playwright/support/fixtures/filesApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,33 @@
* 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<void>
getFileListEntry: (fileName: string) => Locator
openFile: (fileName: string) => Promise<void>
filesApp: FilesAppSection
setShowHiddenFiles: (show: boolean) => Promise<void>
}

export const test = baseTest.extend<FilesAppFixture>({
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)
},

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,
},
)
}),
})
24 changes: 14 additions & 10 deletions playwright/support/fixtures/random-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof createRandomUser>>

export interface UserFixture {
randomUser: RandomUser
account: Account
user: User
}

Expand All @@ -19,24 +18,29 @@ export interface UserFixture {
*/
export const test = base.extend<UserFixture>({
// 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)
},
})
Loading
Loading