diff --git a/cypress/e2e/circle-with-group.spec.js b/cypress/e2e/circle-with-group.spec.js index 9fc448a378..e45bcc8ade 100644 --- a/cypress/e2e/circle-with-group.spec.js +++ b/cypress/e2e/circle-with-group.spec.js @@ -32,7 +32,6 @@ describe('Pages are accessible via group membership to circle', function() { before(function() { cy.loginAs('jane') - cy.visit('apps/collectives') cy.deleteAndSeedCollective('Group Collective') cy.circleFind('Group Collective') .circleAddMember('Bobs Group', 2) diff --git a/cypress/e2e/collective-members.spec.js b/cypress/e2e/collective-members.spec.js index b764eae5d8..b87b5f5b72 100644 --- a/cypress/e2e/collective-members.spec.js +++ b/cypress/e2e/collective-members.spec.js @@ -27,7 +27,6 @@ describe('Collective members', function() { before(function() { cy.loginAs('bob') - cy.visit('apps/collectives') cy.deleteAndSeedCollective('Members Collective') }) diff --git a/cypress/e2e/collective-page-mode.spec.js b/cypress/e2e/collective-page-mode.spec.js new file mode 100644 index 0000000000..afa9ae30a4 --- /dev/null +++ b/cypress/e2e/collective-page-mode.spec.js @@ -0,0 +1,61 @@ +/** + * @copyright Copyright (c) 2021 Azul + * + * @author Azul + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +describe('Collective page mode', function() { + + before(function() { + cy.loginAs('bob') + cy.deleteAndSeedCollective('Our Garden') + .seedPage('Day 1', '', 'Readme.md') + .seedPage('Day 2', '', 'Readme.md') + }) + + beforeEach(function() { + cy.loginAs('bob') + }) + + describe('Changing page mode', function() { + it('Opens edit mode per default', function() { + cy.seedCollectivePageMode('Our Garden', 1) + cy.visit('/apps/collectives/Our Garden') + // make sure the page list loaded properly + cy.contains('.app-content-list-item a', 'Day 1') + cy.openPage('Day 2') + cy.getEditor() + .should('be.visible') + cy.getReadOnlyEditor() + .should('not.be.visible') + }) + + it('Opens view mode per default', function() { + cy.seedCollectivePageMode('Our Garden', 0) + cy.visit('/apps/collectives/Our Garden') + // make sure the page list loaded properly + cy.contains('.app-content-list-item a', 'Day 1') + cy.openPage('Day 2') + cy.getReadOnlyEditor() + .should('be.visible') + cy.getEditor() + .should('not.be.visible') + }) + }) +}) diff --git a/cypress/e2e/collective-readonly.spec.js b/cypress/e2e/collective-readonly.spec.js index afc92f4ea8..27458fa272 100644 --- a/cypress/e2e/collective-readonly.spec.js +++ b/cypress/e2e/collective-readonly.spec.js @@ -24,12 +24,11 @@ describe('Read-only collective', function() { before(function() { cy.loginAs('alice') - cy.visit('apps/collectives') cy.deleteAndSeedCollective('PermissionCollective') - cy.seedPage('SecondPage', '', 'Readme.md') - cy.seedCollectivePermissions('PermissionCollective', 'edit', 4) + .seedPage('SecondPage') cy.circleFind('PermissionCollective') .circleAddMember('bob') + cy.seedCollectivePermissions('PermissionCollective', 'edit', 4) }) describe('in read-only collective', function() { diff --git a/cypress/e2e/collective-settings.spec.js b/cypress/e2e/collective-settings.spec.js index 144b80b302..4f837a30c3 100644 --- a/cypress/e2e/collective-settings.spec.js +++ b/cypress/e2e/collective-settings.spec.js @@ -27,7 +27,6 @@ describe('Collective settings', function() { before(function() { cy.loginAs('bob') - cy.visit('apps/collectives') cy.deleteCollective('Change me now') cy.deleteAndSeedCollective('Change me') }) diff --git a/cypress/e2e/collective-share.spec.js b/cypress/e2e/collective-share.spec.js index 6e9a15939c..017d641352 100644 --- a/cypress/e2e/collective-share.spec.js +++ b/cypress/e2e/collective-share.spec.js @@ -29,7 +29,6 @@ describe('Collective Share', function() { before(function() { cy.loginAs('bob') - cy.visit('/apps/collectives') cy.deleteAndSeedCollective('Share me') }) diff --git a/cypress/e2e/collective.spec.js b/cypress/e2e/collective.spec.js index a1e2b65c2b..1f00817bf0 100644 --- a/cypress/e2e/collective.spec.js +++ b/cypress/e2e/collective.spec.js @@ -29,7 +29,6 @@ describe('Collective', function() { before(function() { cy.loginAs('bob') - cy.visit('apps/collectives') cy.deleteCollective('Preexisting Circle') cy.deleteCollective('History Club') cy.deleteCollective(specialCollective) @@ -39,7 +38,6 @@ describe('Collective', function() { cy.seedCircle('Preexisting Circle') cy.seedCircle('History Club', { visible: true, open: true }) cy.loginAs('jane') - cy.visit('apps/collectives') cy.deleteCollective('Foreign Circle') cy.seedCircle('Foreign Circle', { visible: true, open: true }) }) diff --git a/cypress/e2e/collectives-trash.spec.js b/cypress/e2e/collectives-trash.spec.js index cc4f0efe78..df25d62e02 100644 --- a/cypress/e2e/collectives-trash.spec.js +++ b/cypress/e2e/collectives-trash.spec.js @@ -28,7 +28,6 @@ describe('Collective', function() { describe('move collective to trash and restore', function() { before(function() { cy.loginAs('bob') - cy.visit('apps/collectives') cy.deleteAndSeedCollective('Delete me') }) it('Allows moving the collective to trash', function() { diff --git a/cypress/e2e/dashboard-widget.spec.js b/cypress/e2e/dashboard-widget.spec.js index c9535e531d..4671f4aada 100644 --- a/cypress/e2e/dashboard-widget.spec.js +++ b/cypress/e2e/dashboard-widget.spec.js @@ -30,9 +30,8 @@ describe('Collectives dashboard widget', function() { before(function() { cy.loginAs('bob') cy.enableDashboardWidget('collectives-recent-pages') - cy.visit('apps/collectives') cy.deleteAndSeedCollective('Dashboard Collective1') - cy.seedPage('Page 1', '', 'Readme.md') + .seedPage('Page 1', '', 'Readme.md') }) it('Lists pages in the dashboard widget', function() { cy.visit('/apps/dashboard/') diff --git a/cypress/e2e/page-details.spec.js b/cypress/e2e/page-details.spec.js index 34249b501f..ddfd98d842 100644 --- a/cypress/e2e/page-details.spec.js +++ b/cypress/e2e/page-details.spec.js @@ -27,11 +27,10 @@ describe('Page details', function() { before(function() { cy.loginAs('bob') - cy.visit('/apps/collectives') cy.deleteAndSeedCollective('Our Garden') - cy.seedPage('Day 1', '', 'Readme.md') + .seedPage('Day 1', '', 'Readme.md') + .seedPage('TableOfContents', '', 'Readme.md') cy.seedPageContent('Our Garden/Day 2.md', 'A test string with Day 2 in the middle and a [link to Day 1](/index.php/apps/collectives/Our%20Garden/Day%201).') - cy.seedPage('TableOfContents', '', 'Readme.md') cy.seedPageContent('Our Garden/TableOfContents.md', '## Second-Level Heading') }) diff --git a/cypress/e2e/page-landingpage.spec.js b/cypress/e2e/page-landingpage.spec.js index 4d92fa2db7..a7d8710e9e 100644 --- a/cypress/e2e/page-landingpage.spec.js +++ b/cypress/e2e/page-landingpage.spec.js @@ -29,14 +29,13 @@ const collective = 'Landingpage Collective' describe('Page landing page', function() { before(function() { cy.loginAs('bob') - cy.visit('/apps/collectives') cy.deleteAndSeedCollective(collective) + .seedPage('Page 1', '', 'Readme.md') + .seedPage('Page 2', '', 'Readme.md') + .seedPage('Page 3', '', 'Readme.md') cy.circleFind(collective).circleAddMember('alice') cy.circleFind(collective).circleAddMember('jane') cy.circleFind(collective).circleAddMember('john') - cy.seedPage('Page 1', '', 'Readme.md') - cy.seedPage('Page 2', '', 'Readme.md') - cy.seedPage('Page 3', '', 'Readme.md') }) beforeEach(function() { diff --git a/cypress/e2e/page-list.spec.js b/cypress/e2e/page-list.spec.js index 24fa8df5c0..74b0419434 100644 --- a/cypress/e2e/page-list.spec.js +++ b/cypress/e2e/page-list.spec.js @@ -27,24 +27,25 @@ describe('Page list', function() { before(function() { cy.loginAs('bob') - cy.visit('apps/collectives') cy.deleteAndSeedCollective('Our Garden') - cy.seedPage('Target', '', 'Readme.md') - cy.seedPage('Target Subpage', '', 'Target.md') + .as('garden') + .seedPage('Target', '', 'Readme.md') + .seedPage('Target Subpage', '', 'Target.md') // Wait 1 second to make sure that page order by time is right cy.wait(1000) // eslint-disable-line cypress/no-unnecessary-waiting - cy.seedPage('Day 1', '', 'Readme.md') - cy.seedPage('Subpage Title', '', 'Day 1.md') - cy.seedPage('Day 2', '', 'Readme.md') - cy.seedPage('Page Title', '', 'Readme.md') - cy.seedPage('Move me internal', '', 'Readme.md') - cy.seedPage('Copy me internal', '', 'Readme.md') - cy.seedPage('Move me external', '', 'Readme.md') - cy.seedPage('Copy me external', '', 'Readme.md') - cy.seedPage('#% special chars', '', 'Readme.md') + cy.then(() => this.garden) + .seedPage('Day 1', '', 'Readme.md') + .seedPage('Subpage Title', '', 'Day 1.md') + .seedPage('Day 2', '', 'Readme.md') + .seedPage('Page Title', '', 'Readme.md') + .seedPage('Move me internal', '', 'Readme.md') + .seedPage('Copy me internal', '', 'Readme.md') + .seedPage('Move me external', '', 'Readme.md') + .seedPage('Copy me external', '', 'Readme.md') + .seedPage('#% special chars', '', 'Readme.md') cy.deleteAndSeedCollective('MoveCopyTargetCollective') - cy.seedPage('Target external', '', 'Readme.md') - cy.seedPage('Target Subpage external', '', 'Target external.md') + .seedPage('Target external', '', 'Readme.md') + .seedPage('Target Subpage external', '', 'Target external.md') }) beforeEach(function() { diff --git a/cypress/e2e/pages-links.spec.js b/cypress/e2e/pages-links.spec.js index f00b22a4c4..355b4f3fa2 100644 --- a/cypress/e2e/pages-links.spec.js +++ b/cypress/e2e/pages-links.spec.js @@ -32,19 +32,18 @@ let anotherCollectiveFirstPageId, linkTargetPageId describe('Page Link Handling', function() { before(function() { cy.loginAs('bob') - cy.visit('/apps/collectives') cy.deleteAndSeedCollective('Another Collective') - cy.seedPage('First Page', '', 'Readme.md').then((id) => { - anotherCollectiveFirstPageId = id - }) + .seedPage('First Page', '', 'Readme.md').then(({ pageId }) => { + anotherCollectiveFirstPageId = pageId + }) cy.deleteAndSeedCollective('Link Testing') - cy.seedPage('Parent', '', 'Readme.md') - cy.seedPage('Child', '', 'Parent.md') - cy.seedPage('Link Target', '', 'Readme.md').then((id) => { - linkTargetPageId = id - }) + .seedPage('Parent', '', 'Readme.md') + .seedPage('Child', '', 'Parent.md') + .seedPage('Link Target', '', 'Readme.md').then(({ pageId }) => { + linkTargetPageId = pageId + }) + .seedPage('Link Source', '', 'Readme.md') cy.seedPageContent('Link%20Testing/Link%20Target.md', 'Some content') - cy.seedPage('Link Source', '', 'Readme.md') cy.uploadFile('test.md', 'text/markdown').then((id) => { textId = id }).then(() => { diff --git a/cypress/e2e/pages.spec.js b/cypress/e2e/pages.spec.js index 982501bd14..5585cc1180 100644 --- a/cypress/e2e/pages.spec.js +++ b/cypress/e2e/pages.spec.js @@ -25,18 +25,19 @@ */ describe('Page', function() { + before(function() { cy.loginAs('bob') - cy.visit('/apps/collectives') - cy.deleteAndSeedCollective('Our Garden') - cy.seedPage('Day 1', '', 'Readme.md') + cy.deleteAndSeedCollective('Our Garden').as('garden') + .seedPage('Day 1', '', 'Readme.md') // Wait 1 second to make sure that page order by time is right cy.wait(1000) // eslint-disable-line cypress/no-unnecessary-waiting - cy.seedPage('Day 2', '', 'Readme.md') - cy.seedPage('Page Title', '', 'Readme.md') - cy.seedPage('#% special chars', '', 'Readme.md') + cy.then(() => this.garden) + .seedPage('Day 2', '', 'Readme.md') + .seedPage('Page Title', '', 'Readme.md') + .seedPage('#% special chars', '', 'Readme.md') + .seedPage('Template', '', 'Readme.md') cy.seedPageContent('Our Garden/Day 2.md', 'A test string with Day 2 in the middle and a [link to Day 1](/index.php/apps/collectives/Our%20Garden/Day%201).') - cy.seedPage('Template', '', 'Readme.md') cy.seedPageContent('Our Garden/Template.md', 'This is going to be our template.') }) @@ -264,26 +265,6 @@ describe('Page', function() { }) } - describe('Changing page mode', function() { - it('Opens edit mode per default', function() { - cy.seedCollectivePageMode('Our Garden', 1) - cy.openPage('Day 2') - cy.getEditor() - .should('be.visible') - cy.getReadOnlyEditor() - .should('not.be.visible') - }) - - it('Opens view mode per default', function() { - cy.seedCollectivePageMode('Our Garden', 0) - cy.openPage('Day 2') - cy.getReadOnlyEditor() - .should('be.visible') - cy.getEditor() - .should('not.be.visible') - }) - }) - describe('Full width view', function() { it('Allows to toggle persistent full-width view', function() { cy.openPage('Day 2') diff --git a/cypress/e2e/settings.spec.js b/cypress/e2e/settings.spec.js index caa4630eae..341a2d6f29 100644 --- a/cypress/e2e/settings.spec.js +++ b/cypress/e2e/settings.spec.js @@ -27,7 +27,6 @@ describe('Settings', function() { before(function() { cy.loginAs('bob') - cy.visit('apps/collectives') cy.deleteAndSeedCollective('A Collective') }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 128e8a118e..d95c651cff 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,20 +1,9 @@ import { login, logout } from '@nextcloud/cypress/commands' import { User } from '@nextcloud/cypress' -import { - GET_COLLECTIVES, - GET_TRASH_COLLECTIVES, - NEW_COLLECTIVE, - TRASH_COLLECTIVE, - DELETE_COLLECTIVE, - UPDATE_COLLECTIVE_EDIT_PERMISSIONS, - UPDATE_COLLECTIVE_SHARE_PERMISSIONS, - UPDATE_COLLECTIVE_PAGE_MODE, - GET_PAGES, - NEW_PAGE, - GET_CIRCLES, -} from '../../src/store/actions.js' +import * as api from '../../src/apis/collectives/index.js' import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' const url = Cypress.config('baseUrl').replace(/\/index.php\/?$/g, '') Cypress.env('baseUrl', url) @@ -96,8 +85,8 @@ Cypress.Commands.add('disableApp', appName => { Cypress.Commands.add('setAppEnabled', (appName, value = true) => { const verb = value ? 'enable' : 'disable' - const api = `${Cypress.env('baseUrl')}/index.php/settings/apps/${verb}` - return axios.post(api, + const url = `${Cypress.env('baseUrl')}/index.php/settings/apps/${verb}` + return axios.post(url, { appIds: [appName] }, ) }) @@ -107,8 +96,8 @@ Cypress.Commands.add('setAppEnabled', (appName, value = true) => { */ Cypress.Commands.add('enableDashboardWidget', (widgetName) => { Cypress.log() - const api = `${Cypress.env('baseUrl')}/index.php/apps/dashboard/layout` - return axios.post(api, + const url = `${Cypress.env('baseUrl')}/index.php/apps/dashboard/layout` + return axios.post(url, { layout: widgetName }, ) }) @@ -176,11 +165,20 @@ Cypress.Commands.add('findBy', Cypress.Commands.add('deleteAndSeedCollective', (name) => { Cypress.log() cy.deleteCollective(name) - cy.dispatch(NEW_COLLECTIVE, { name }) - cy.store('getters.updatedCollectivePath') - .then(path => cy.routeTo(path)) - // Make sure new collective is loaded - cy.get('#titleform input').should('have.value', name) + cy.seedCollective(name) + cy.getCollectives() + .findBy({ name }) +}) + +Cypress.Commands.add('seedCollective', (name) => { + return api.newCollective({ name }) + .catch(e => { + if (e.request && e.request.status === 422) { + // The collective already existed... carry on. + } else { + throw e + } + }) }) /** @@ -202,18 +200,45 @@ Cypress.Commands.add('createCollective', (name, members = []) => { }) /** - * Delete a collective if exists and clean it from the trash. + * Delete a collective - no matter if it is in use or trashed. + * + * This command will succeed if the collective does not exist at all. */ Cypress.Commands.add('deleteCollective', (name) => { - cy.dispatch(GET_COLLECTIVES) - cy.store('state.collectives.collectives') + cy.trashCollective(name) + cy.deleteCollectiveFromTrash(name) +}) + +Cypress.Commands.add('getCollectives', () => { + return api.getCollectives() + .then(response => response.data.data) +}) + +/** + * Move a collective into the trash if it exists. + * + * This command will succeed if the collective does not exist at all. + */ +Cypress.Commands.add('trashCollective', (name) => { + cy.getCollectives() .findBy({ name }) - .dispatch(TRASH_COLLECTIVE) - // Try to find and delete collective from trash - cy.dispatch(GET_TRASH_COLLECTIVES) - cy.store('state.collectives.trashCollectives') + .then((found) => found && api.trashCollective(found.id)) +}) + +Cypress.Commands.add('getTrashCollectives', () => { + return api.getTrashCollectives() + .then(response => response.data.data) +}) + +/** + * Clear a collective from the trash if it is in there. + * + * This command will succeed if the collective does not exist at all. + */ +Cypress.Commands.add('deleteCollectiveFromTrash', (name) => { + cy.getTrashCollectives() .findBy({ name }) - .dispatch(DELETE_COLLECTIVE, { circle: true }) + .then((found) => found && api.deleteCollective(found.id, true)) }) /** @@ -222,11 +247,11 @@ Cypress.Commands.add('deleteCollective', (name) => { Cypress.Commands.add('seedCollectivePermissions', (name, type, level) => { Cypress.log() const action = (type === 'edit') - ? UPDATE_COLLECTIVE_EDIT_PERMISSIONS - : UPDATE_COLLECTIVE_SHARE_PERMISSIONS - cy.store('state.collectives.collectives') + ? api.updateCollectiveEditPermissions + : api.updateCollectiveSharePermissions + cy.getCollectives() .findBy({ name }) - .dispatch(action, { level }) + .then((found) => action(found.id, level)) }) /** @@ -234,54 +259,47 @@ Cypress.Commands.add('seedCollectivePermissions', (name, type, level) => { */ Cypress.Commands.add('seedCollectivePageMode', (name, mode) => { Cypress.log() - cy.store('state.collectives.collectives') + cy.getCollectives() .findBy({ name }) - .dispatch(UPDATE_COLLECTIVE_PAGE_MODE, { mode }) + .then((found) => api.updateCollectivePageMode(found.id, mode)) }) /** - * Add a page to a collective + * Context for the given collective for a logged in user. + * + * @param {object} collective - Collective to provide the context for. */ -Cypress.Commands.add('seedPage', (name, parentFilePath, parentFileName) => { - Cypress.log() - cy.dispatch(GET_PAGES) - cy.store('state.pages.pages') - .findBy({ filePath: parentFilePath, fileName: parentFileName }) - .its('id') - .as('parentId') - .then(id => ({ parentId: id })) - .dispatch(NEW_PAGE, { title: name, pagePath: name }) - // Return pageId of created page - cy.get('@parentId').then(parentId => { - return cy.store('state.pages.pages') - .findBy({ parentId, title: name }) - .its('id') - }) +function collectiveContext(collective) { + return { + isPublic: false, + collectiveId: collective.id, + shareTokenParam: null, + } +} + +Cypress.Commands.add('getPages', collective => { + return api.getPages(collectiveContext(collective)) + .then(response => response.data.data) }) /** - * Upload a file + * Add a page to a collective */ -Cypress.Commands.add('uploadFile', (path, mimeType, remotePath = '') => { - Cypress.log() - // Get fixture - return cy.fixture(path, 'base64').then(file => { - // convert the base64 string to a blob - const blob = Cypress.Blob.base64StringToBlob(file, mimeType) - try { - const file = new File([blob], path, { type: mimeType }) - return cy.uploadContent(remotePath + path, file, mimeType) - .then(response => { - const ocFileId = response.headers['oc-fileid'] - const fileId = parseInt(ocFileId.substring(0, ocFileId.indexOf('oc'))) - return fileId - }) - } catch (error) { - cy.log(error) - throw new Error(`Unable to process file ${path}`) - } +Cypress.Commands.add('seedPage', + { prevSubject: true }, + (subject, name, parentFilePath = '', parentFileName = 'Readme.md') => { + Cypress.log() + cy.getPages(subject) + .findBy({ filePath: parentFilePath, fileName: parentFileName }) + .then(({ id: parentId }) => { + return api.createPage( + collectiveContext(subject), + { parentId, title: name, pagePath: name }, + ) + }) + .its('data.data.id') + .then(pageId => ({ ...subject, pageId })) }) -}) /** * Upload content of a page @@ -294,22 +312,37 @@ Cypress.Commands.add('seedPageContent', (pagePath, content) => { cy.uploadContent(`Collectives/${pagePath}`, content) }) +Cypress.Commands.add('uploadFile', (path, mimeType, remotePath = '') => { + Cypress.log() + // Get fixture + return cy.fixture(path, 'base64').then(data => { + // convert the base64 string to a blob + const blob = Cypress.Blob.base64StringToBlob(data, mimeType) + const file = new File([blob], path, { type: mimeType }) + return cy.uploadContent(remotePath + path, file, mimeType) + .then(response => { + const ocFileId = response.headers['oc-fileid'] + const fileId = parseInt(ocFileId.substring(0, ocFileId.indexOf('oc'))) + return fileId + }) + }) +}) + /** * Generic upload of content - used by seedPageContent and uploadPage */ Cypress.Commands.add('uploadContent', (path, content, mimetype = 'text/markdown') => { // @nextcloud/axios automatic handling for request tokens does not work for webdav - cy.window() - .its('app.OC.requestToken') - .then(requesttoken => { - const url = `${Cypress.env('baseUrl')}/remote.php/webdav/${path}` - return axios.put(url, content, { - headers: { - requesttoken, - 'Content-Type': mimetype, - }, - }) + cy.request('/csrftoken').then(({ body }) => { + const requesttoken = body.token + const url = `${Cypress.env('baseUrl')}/remote.php/webdav/${path}` + return axios.put(url, content, { + headers: { + requesttoken, + 'Content-Type': mimetype, + }, }) + }) }) /** @@ -317,14 +350,12 @@ Cypress.Commands.add('uploadContent', (path, content, mimetype = 'text/markdown' */ Cypress.Commands.add('seedCircle', (name, config = null) => { Cypress.log() - cy.dispatch(GET_CIRCLES) - cy.store('state.circles.circles') - .findBy({ sanitizedName: name }) + cy.circleFind(name) .then(async circle => { - const api = `${Cypress.env('baseUrl')}/ocs/v2.php/apps/circles/circles` + const url = `${Cypress.env('baseUrl')}/ocs/v2.php/apps/circles/circles` let circleId if (!circle) { - const response = await axios.post(api, + const response = await axios.post(url, { name, personal: false, local: true }, ) circleId = response.data.ocs.data.id @@ -340,23 +371,27 @@ Cypress.Commands.add('seedCircle', (name, config = null) => { const value = bits .filter(([k, v]) => config[k]) .reduce((sum, [k, v]) => sum + v, 0) - await axios.put(`${api}/${circleId}/config`, + await axios.put(`${url}/${circleId}/config`, { value }, ) } }) }) -/** - * Add someone to a circle - */ +Cypress.Commands.add('getCircles', () => { + return axios.get(generateOcsUrl('apps/circles/circles')) + .then(response => response.data.ocs.data) +}) + Cypress.Commands.add('circleFind', (name) => { Cypress.log() - cy.dispatch(GET_CIRCLES) - return cy.store('state.circles.circles') + cy.getCircles() .findBy({ sanitizedName: name }) }) +/** + * Add someone to a circle + */ Cypress.Commands.add('circleAddMember', { prevSubject: true }, async ({ id }, userId, type = 1) => { diff --git a/cypress/support/navigation.js b/cypress/support/navigation.js index a02469cba9..566d02612f 100644 --- a/cypress/support/navigation.js +++ b/cypress/support/navigation.js @@ -17,8 +17,7 @@ Cypress.Commands.add('openPageMenu', (pageName) => { Cypress.Commands.add('openCollective', (collectiveName) => { Cypress.log() - cy.get(`.collectives_list_item a[title="${collectiveName}"]`) - .click() + cy.routeTo(collectiveName) }) Cypress.Commands.add('openCollectiveMenu', (collectiveName) => { diff --git a/src/apis/collectives/collectives.js b/src/apis/collectives/collectives.js new file mode 100644 index 0000000000..0afd736940 --- /dev/null +++ b/src/apis/collectives/collectives.js @@ -0,0 +1,151 @@ +import axios from '@nextcloud/axios' +import { collectivesUrl } from './urls.js' + +/** + * Get all active (i.e. not trashed) collectives for the current user + * + * Will return the shared collective if a share token is given. + * + * @param {string} shareToken authentication token from the share + */ +export function getCollectives(shareToken = false) { + return shareToken + ? axios.get(collectivesUrl('p', shareToken)) + : axios.get(collectivesUrl()) +} + +/** + * Get all trashed collectives for the current user + */ +export function getTrashCollectives() { + return axios.get(collectivesUrl('trash')) +} + +/** + * Create a new collective with the given properties. + * + * @param {object} collective - properties for the new collective + */ +export function newCollective(collective) { + return axios.post( + collectivesUrl(), + collective, + ) +} + +/** + * Trash the collective with the given id + * + * @param {number} collectiveId - Id of the collective to trash. + */ +export function trashCollective(collectiveId) { + return axios.delete(collectivesUrl(collectiveId)) +} + +/** + * Delete the collective with the given id. + * + * @param {number} collectiveId - id of the collective to delete + * @param {boolean} removeCircle - also remove the circle if true + */ +export function deleteCollective(collectiveId, removeCircle) { + const query = removeCircle ? '?circle=1' : '' + return axios.delete(collectivesUrl('trash', collectiveId + query)) +} + +/** + * Restore a collective with the given id from trash + * + * @param {number} collectiveId Id of the colletive to be restored + */ +export function restoreCollective(collectiveId) { + return axios.patch(collectivesUrl('trash', collectiveId)) +} + +/** + * Update a collective with the given properties + * + * @param {object} collective Properties for the collective + */ +export function updateCollective(collective) { + return axios.put( + collectivesUrl(collective.id), + collective, + ) +} + +/** + * Set the permission level required for editing. + * + * @param {number} collectiveId - id of the collective to update + * @param {number} level - required level for editing + */ +export function updateCollectiveEditPermissions(collectiveId, level) { + return axios.put( + collectivesUrl(collectiveId, 'editLevel'), + { level }, + ) +} + +/** + * Set the permission level required for sharing. + * + * @param {number} collectiveId - id of the collective to update + * @param {number} level - required level for sharing + */ +export function updateCollectiveSharePermissions(collectiveId, level) { + return axios.put( + collectivesUrl(collectiveId, 'shareLevel'), + { level }, + ) +} + +/** + * Set the edit mode for the given collective + * + * @param {number} collectiveId - id of the collective to update + * @param {number} mode - pageMode to use. + * + * Possible modes: pageModes.MODE_VIEW or pageModes.MODE_EDIT + */ +export function updateCollectivePageMode(collectiveId, mode) { + return axios.put( + collectivesUrl(collectiveId, 'pageMode'), + { mode }, + ) +} + +/** + * Create a public collective share + * + * @param {number} collectiveId Id of the colletive to be shared + */ +export function shareCollective(collectiveId) { + return axios.post(collectivesUrl(collectiveId, 'share')) +} + +/** + * Update a public collective share + * + * @param {number} collectiveId Id of the colletive + * @param {string} shareToken Token of the share to be updated + * @param {boolean} shareEditable Is collective share editable + */ +export function updateShareCollective(collectiveId, shareToken, shareEditable) { + return axios.put( + collectivesUrl(collectiveId, 'share', shareToken), + { editable: shareEditable }, + ) +} + +/** + * Delete a public collective share + * + * @param {number} collectiveId Id of the colletive + * @param {string} shareToken Token of the share to be removed + */ +export function unshareCollective(collectiveId, shareToken) { + return axios.delete( + collectivesUrl(collectiveId, 'share', shareToken), + ) +} diff --git a/src/apis/collectives/index.js b/src/apis/collectives/index.js new file mode 100644 index 0000000000..6ab18e6d35 --- /dev/null +++ b/src/apis/collectives/index.js @@ -0,0 +1,4 @@ +export * from './urls.js' +export * from './collectives.js' +export * from './pages.js' +export * from './userSettings.js' diff --git a/src/apis/collectives/pages.js b/src/apis/collectives/pages.js new file mode 100644 index 0000000000..c0b3c1264a --- /dev/null +++ b/src/apis/collectives/pages.js @@ -0,0 +1,24 @@ +import axios from '@nextcloud/axios' +import { pagesUrl } from './urls.js' + +/** + * Get all pages in the given context (collective or public share) + * + * @param {object} context - either the current collective or a share context + */ +export function getPages(context) { + return axios.get(pagesUrl(context)) +} + +/** + * Create a new page in the given context (collective or public share) + * + * @param {object} context - either the current collective or a share context + * @param {object} page - properties of the new page + */ +export function createPage(context, page) { + return axios.post( + pagesUrl(context, page.parentId), + page, + ) +} diff --git a/src/apis/collectives/urls.js b/src/apis/collectives/urls.js new file mode 100644 index 0000000000..ff6d27e058 --- /dev/null +++ b/src/apis/collectives/urls.js @@ -0,0 +1,24 @@ +import { generateUrl } from '@nextcloud/router' + +/** + * Url for the collectives api + * + * @param {...any} parts - url parts to append - will be joined with `/` + */ +export function collectivesUrl(...parts) { + const path = ['apps/collectives/_api', ...parts] + .join('/') + return generateUrl(path) +} + +/** + * Url for pages paths inside the given context. + * + * @param {object} context - either the current collective or a share context + * @param {...any} parts - url parts to append. + */ +export function pagesUrl(context, ...parts) { + return context.isPublic + ? collectivesUrl('p', context.shareTokenParam, '_pages', ...parts) + : collectivesUrl(context.collectiveId, '_pages', ...parts) +} diff --git a/src/apis/collectives/userSettings.js b/src/apis/collectives/userSettings.js new file mode 100644 index 0000000000..1fabeea083 --- /dev/null +++ b/src/apis/collectives/userSettings.js @@ -0,0 +1,28 @@ +import axios from '@nextcloud/axios' +import { collectivesUrl } from './urls.js' + +/** + * Set the page order for the current user + * + * @param {number} collectiveId ID of the colletive to be updated + * @param {number} pageOrder the desired page order for the current user + */ +export function setCollectiveUserSettingPageOrder(collectiveId, pageOrder) { + return axios.put( + collectivesUrl(collectiveId, 'userSettings', 'pageOrder'), + { pageOrder }, + ) +} + +/** + * Set the the `show recent pages` toggle for the current user + * + * @param {number} collectiveId ID of the colletive to be updated + * @param {boolean} showRecentPages the desired value + */ +export function setCollectiveUserSettingShowRecentPages(collectiveId, showRecentPages) { + return axios.put( + collectivesUrl(collectiveId, 'userSettings', 'pageOrder'), + { showRecentPages }, + ) +} diff --git a/src/store/collectives.js b/src/store/collectives.js index ba3240bedd..e33183a4f8 100644 --- a/src/store/collectives.js +++ b/src/store/collectives.js @@ -1,8 +1,8 @@ -import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { byName } from '../util/sortOrders.js' import randomEmoji from '../util/randomEmoji.js' import { memberLevels } from '../constants.js' +import * as api from '../apis/collectives/index.js' import { SET_COLLECTIVES, @@ -234,9 +234,7 @@ export default { async [GET_COLLECTIVES]({ commit, getters }) { commit('load', 'collectives') try { - const response = getters.isPublic - ? await axios.get(generateUrl(`/apps/collectives/_api/p/${getters.shareTokenParam}`)) - : await axios.get(generateUrl('/apps/collectives/_api')) + const response = await api.getCollectives(getters.isPublic && getters.shareTokenParam) commit(SET_COLLECTIVES, response.data.data) } finally { commit('done', 'collectives') @@ -252,7 +250,7 @@ export default { */ async [GET_TRASH_COLLECTIVES]({ commit, getters }) { commit('load', 'collectiveTrash') - const response = await axios.get(generateUrl('/apps/collectives/_api/trash')) + const response = await api.getTrashCollectives() commit(SET_TRASH_COLLECTIVES, response.data.data) commit('done', 'collectiveTrash') }, @@ -267,10 +265,7 @@ export default { * @param {object} collective Properties for the new collective */ async [NEW_COLLECTIVE]({ commit, rootState, dispatch }, collective) { - const response = await axios.post( - generateUrl('/apps/collectives/_api'), - collective, - ) + const response = await api.newCollective(collective) commit('info', response.data.message) commit(ADD_OR_UPDATE_COLLECTIVE, response.data.data) // If collectives folder wasn't initialized already, now it should be there @@ -287,10 +282,7 @@ export default { * @param {object} collective Properties for the collective */ async [UPDATE_COLLECTIVE]({ commit }, collective) { - const response = await axios.put( - generateUrl('/apps/collectives/_api/' + collective.id), - collective, - ) + const response = await api.updateCollective(collective) commit(ADD_OR_UPDATE_COLLECTIVE, response.data.data) }, @@ -303,7 +295,7 @@ export default { * @param {number} collective.id ID of the colletive to be trashed */ async [TRASH_COLLECTIVE]({ commit }, { id }) { - const response = await axios.delete(generateUrl('/apps/collectives/_api/' + id)) + const response = await api.trashCollective(id) commit(MOVE_COLLECTIVE_INTO_TRASH, response.data.data) }, @@ -316,7 +308,7 @@ export default { * @param {number} collective.id ID of the colletive to be restored */ async [RESTORE_COLLECTIVE]({ commit }, { id }) { - const response = await axios.patch(generateUrl('/apps/collectives/_api/trash/' + id)) + const response = await api.restoreCollective(id) commit(RESTORE_COLLECTIVE_FROM_TRASH, response.data.data) }, @@ -330,11 +322,7 @@ export default { * @param {boolean} collective.circle Whether to delete the circle as well */ async [DELETE_COLLECTIVE]({ commit }, { id, circle }) { - let doCircle = '' - if (circle) { - doCircle = '?circle=1' - } - const response = await axios.delete(generateUrl('/apps/collectives/_api/trash/' + id + doCircle)) + const response = await api.deleteCollective(id, circle) commit(DELETE_COLLECTIVE_FROM_TRASH, response.data.data) if (circle) { commit(DELETE_CIRCLE_FOR, response.data.data) @@ -351,13 +339,13 @@ export default { */ async [SHARE_COLLECTIVE]({ commit }, { id }) { commit('load', 'share') - const response = await axios.post(generateUrl('/apps/collectives/_api/' + id + '/share')) + const response = await api.shareCollective(id) commit(ADD_OR_UPDATE_COLLECTIVE, response.data.data) commit('done', 'share') }, /** - * Create a public collective share + * Update a public collective share * * @param {object} store the vuex store * @param {Function} store.commit commit changes @@ -368,11 +356,7 @@ export default { */ async [UPDATE_SHARE_COLLECTIVE]({ commit }, { id, shareToken, shareEditable }) { commit('load', 'shareEditable') - const response = await axios.put( - generateUrl('/apps/collectives/_api/' + id + '/share/' + shareToken), - { editable: shareEditable }, - - ) + const response = await api.updateShareCollective(id, shareToken, shareEditable) commit(ADD_OR_UPDATE_COLLECTIVE, response.data.data) commit('done', 'shareEditable') }, @@ -383,13 +367,11 @@ export default { * @param {object} store the vuex store * @param {Function} store.commit commit changes * @param {object} store.getters getters of the store - * @param {object} collective the collective with id + * @param {object} collective the collective with id and shareToken */ async [UNSHARE_COLLECTIVE]({ commit, getters }, collective) { commit('load', 'unshare') - const response = await axios.delete( - generateUrl('/apps/collectives/_api/' + collective.id + '/share/' + collective.shareToken), - ) + const response = await api.unshareCollective(collective.id, collective.shareToken) commit(ADD_OR_UPDATE_COLLECTIVE, response.data.data) commit('done', 'unshare') }, @@ -402,10 +384,7 @@ export default { * @param {number} data.level new minimum level for sharing */ async [UPDATE_COLLECTIVE_EDIT_PERMISSIONS]({ commit }, { id, level }) { - const response = await axios.put( - generateUrl('/apps/collectives/_api/' + id + '/editLevel'), - { level }, - ) + const response = await api.updateCollectiveEditPermissions(id, level) commit(ADD_OR_UPDATE_COLLECTIVE, response.data.data) }, @@ -417,10 +396,7 @@ export default { * @param {number} data.level new minimum level for sharing */ async [UPDATE_COLLECTIVE_SHARE_PERMISSIONS]({ commit }, { id, level }) { - const response = await axios.put( - generateUrl('/apps/collectives/_api/' + id + '/shareLevel'), - { level }, - ) + const response = await api.updateCollectiveSharePermissions(id, level) commit(ADD_OR_UPDATE_COLLECTIVE, response.data.data) }, @@ -432,27 +408,27 @@ export default { * @param {number} data.mode page mode */ async [UPDATE_COLLECTIVE_PAGE_MODE]({ commit }, { id, mode }) { - const response = await axios.put( - generateUrl('/apps/collectives/_api/' + id + '/pageMode'), - { mode }, - ) + const response = await api.updateCollectivePageMode(id, mode) commit(ADD_OR_UPDATE_COLLECTIVE, response.data.data) }, + /** + * Set the page order for the current user + * + * @param {object} store the vuex store + * @param {Function} store.commit commit changes + * @param {object} data the data object + * @param {number} data.id ID of the colletive to be updated + * @param {number} data.pageOrder the desired page order for the current user + */ async [SET_COLLECTIVE_USER_SETTING_PAGE_ORDER]({ commit }, { id, pageOrder }) { - await axios.put( - generateUrl('/apps/collectives/_api/' + id + '/_userSettings/pageOrder'), - { pageOrder }, - ) + await api.setCollectiveUserSettingPageOrder(id, pageOrder) commit(PATCH_COLLECTIVE_WITH_PROPERTY, { id, property: 'userPageOrder', value: pageOrder }) }, async [SET_COLLECTIVE_USER_SETTING_SHOW_RECENT_PAGES]({ commit, getters }, { id, showRecentPages }) { commit(PATCH_COLLECTIVE_WITH_PROPERTY, { id, property: 'userShowRecentPages', value: showRecentPages }) - await axios.put( - generateUrl('/apps/collectives/_api/' + id + '/_userSettings/showRecentPages'), - { showRecentPages }, - ) + await api.setCollectiveUserSettingShowRecentPages(id, showRecentPages) }, [MARK_COLLECTIVE_DELETED]({ commit }, collective) { diff --git a/src/store/pages.js b/src/store/pages.js index d11f47f1be..bffcdba1bb 100644 --- a/src/store/pages.js +++ b/src/store/pages.js @@ -2,10 +2,11 @@ import { set } from 'vue' import { getCurrentUser } from '@nextcloud/auth' import { getBuilder } from '@nextcloud/browser-storage' import axios from '@nextcloud/axios' -import { generateRemoteUrl, generateUrl } from '@nextcloud/router' +import { generateRemoteUrl } from '@nextcloud/router' /* eslint import/namespace: ['error', { allowComputed: true }] */ import * as sortOrders from '../util/sortOrders.js' import { sortedSubpages, pageParents } from './pageExtracts.js' +import * as api from '../apis/collectives/index.js' import { SET_PAGES, @@ -242,46 +243,40 @@ export default { return state.newPage && getters.pagePath(state.newPage) }, - pagesUrl(_state, getters) { - return getters.isPublic - ? generateUrl(`/apps/collectives/_api/p/${getters.shareTokenParam}/_pages`) - : generateUrl(`/apps/collectives/_api/${getters.currentCollective.id}/_pages`) - }, - - pageCreateUrl(_state, getters) { - return parentId => `${getters.pagesUrl}/${parentId}` - }, - - pageUrl(_state, getters) { - return (pageId) => `${getters.pagesUrl}/${pageId}` + context(_state, getters) { + return { + isPublic: getters.isPublic, + collectiveId: getters.currentCollective.id, + shareTokenParam: getters.shareTokenParam, + } }, emojiUrl(_state, getters) { - return (pageId) => `${getters.pageUrl(pageId)}/emoji` + return (pageId) => api.pagesUrl(getters.context, pageId, 'emoji') }, subpageOrderUrl(_state, getters) { - return (pageId) => `${getters.pageUrl(pageId)}/subpageOrder` + return (pageId) => api.pagesUrl(getters.context, pageId, 'subpageOrder') }, touchUrl(_state, getters) { - return `${getters.pageUrl(getters.currentPage.id)}/touch` + return api.pagesUrl(getters.context, getters.currentPage.id, 'touch') }, attachmentsUrl(_state, getters) { - return (pageId) => `${getters.pageUrl(pageId)}/attachments` + return (pageId) => api.pagesUrl(getters.context, pageId, 'attachments') }, backlinksUrl(_state, getters) { - return (pageId) => `${getters.pageUrl(pageId)}/backlinks` + return (pageId) => api.pagesUrl(getters.context, pageId, 'backlinks') }, trashIndexUrl(_state, getters) { - return `${getters.pagesUrl}/trash` + return api.pagesUrl(getters.context, 'trash') }, trashActionUrl(_state, getters) { - return (pageId) => `${getters.pagesUrl}/trash/${pageId}` + return (pageId) => api.pagesUrl(getters.context, 'trash', pageId) }, pageTitle(state, getters) { @@ -507,7 +502,7 @@ export default { if (setLoading) { commit('load', 'collective') } - const response = await axios.get(getters.pagesUrl) + const response = await api.getPages(getters.context) commit(SET_PAGES, { pages: response.data.data, current: getters.currentPage, @@ -539,7 +534,7 @@ export default { * @param {number} pageId Page ID */ async [GET_PAGE]({ commit, getters, state }, pageId) { - const response = await axios.get(getters.pageUrl(pageId)) + const response = await axios.get(api.pagesUrl(getters.context, pageId)) commit(UPDATE_PAGE, response.data.data) }, @@ -557,7 +552,7 @@ export default { // We'll be done when the editor is loaded. commit('load', 'newPageContent') - const response = await axios.post(getters.pageCreateUrl(page.parentId), page) + const response = await api.createPage(getters.context, page) // Add new page to the beginning of pages array commit(ADD_PAGE, response.data.data) }, @@ -579,7 +574,7 @@ export default { // We'll be done when the editor is loaded. commit('load', 'newPageContent') - const response = await axios.post(getters.pageCreateUrl(page.parentId), page) + const response = await api.createPage(getters.context, page) // Add new page to the beginning of pages array commit(ADD_PAGE, response.data.data) }, @@ -605,7 +600,7 @@ export default { * @param {string} newTitle new title for the page */ async [RENAME_PAGE]({ commit, getters }, newTitle) { - const url = getters.pageUrl(getters.currentPage.id) + const url = api.pagesUrl(getters.context, getters.currentPage.id) const response = await axios.put(url, { title: newTitle }) await commit(UPDATE_PAGE, response.data.data) }, @@ -635,7 +630,7 @@ export default { index += 1 } - const url = getters.pageUrl(pageId) + const url = api.pagesUrl(getters.context, pageId) try { await axios.put(url, { index, parentId: newParentId, copy: true }) // Reload the page list to make new page appear @@ -674,7 +669,7 @@ export default { page.parentId = newParentId commit(UPDATE_PAGE, page) - const url = getters.pageUrl(pageId) + const url = api.pagesUrl(getters.context, pageId) try { const response = await axios.put(url, { index, parentId: newParentId }) commit(UPDATE_PAGE, response.data.data) @@ -709,7 +704,7 @@ export default { async [COPY_PAGE_TO_COLLECTIVE]({ commit, getters, state, dispatch }, { collectiveId, newParentId, pageId, index }) { commit('load', 'pagelist') - const url = `${getters.pageUrl(pageId)}/to/${collectiveId}` + const url = api.pagesUrl(getters.context, pageId, 'to', collectiveId) await axios.put(url, { index, parentId: newParentId, copy: true }) commit('done', 'pagelist') }, @@ -733,7 +728,7 @@ export default { const page = { ...state.pages.find(p => p.id === pageId) } const hasSubpages = getters.visibleSubpages(pageId).length > 0 - const url = `${getters.pageUrl(pageId)}/to/${collectiveId}` + const url = api.pagesUrl(getters.context, pageId, 'to', collectiveId) await axios.put(url, { index, parentId: newParentId }) commit(REMOVE_PAGE, page) commit('done', 'pagelist') @@ -809,7 +804,7 @@ export default { * @param {number} page.pageId ID of the page */ async [TRASH_PAGE]({ commit, getters }, { pageId }) { - const response = await axios.delete(getters.pageUrl(pageId)) + const response = await axios.delete(api.pagesUrl(getters.context, pageId)) commit(MOVE_PAGE_INTO_TRASH, response.data.data) },