Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Public page shares #1041

Merged
merged 26 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7b06da2
feat(share): Add basic backend implementation for page shares
mejo- Dec 6, 2023
7823847
feat(CollectiveInfo): Add `isPageShare` flag to CollectiveInfo
mejo- Dec 7, 2023
35f6f40
feat(frontend): Add basic support for page shares
mejo- Dec 7, 2023
d6ef56b
fix(pageShare): No page deletion and page trash in page shares
mejo- Dec 7, 2023
e266e0e
test(behat): Add integration tests for page share API calls
mejo- Dec 11, 2023
520080e
fix(PagePicker): Adjustments for page shares
mejo- Dec 12, 2023
061454d
fix(PageActionMenu): Show 'open sidebar' when sidebar is closed
mejo- Dec 12, 2023
3d8a3bb
feat(share): Let share API endpoints return shares, not collectives
mejo- Dec 12, 2023
0472441
feat(sharing): Add page sidebar tab to manage collective/page shares
mejo- Dec 12, 2023
16e5843
feat(sharing): Replace sharing options with link in collective actions
mejo- Dec 13, 2023
a8f02db
test(cypress): Adjust sharing tests to new UI and add page share tests
mejo- Dec 13, 2023
e42391c
feat(pages): Don't revert subfolders for leaf pages
mejo- Dec 18, 2023
d409d1c
feat(pageList): Add 'Share' button to page actions in page list
mejo- Dec 18, 2023
ce6945b
fix(backend): Transform single page to subfolder before sharing
mejo- Dec 18, 2023
6aa6a10
fix(pageShare): Move page only to parents within the page share
mejo- Dec 18, 2023
cf4583a
test(behat): Allow to upload attachments to subfolders
mejo- Dec 18, 2023
f743bed
fix(sharing): Get shared collective by token
mejo- Dec 18, 2023
2b08f4e
chore(vuex): Clean up getter arguments
mejo- Dec 18, 2023
6b419e3
fix(ui): Change share action button title to 'Share with guests'
mejo- Dec 18, 2023
45d39a8
test(ci): Set unique name when uploading cypress job artifacts
mejo- Dec 18, 2023
0abbb36
chore(backend): Rename variable to improve code readability
mejo- Dec 20, 2023
f9ffb08
fix(backend): check if collective and share token match
mejo- Dec 20, 2023
c6b0943
fix(backend): Remove superfluous check from `createShare`
mejo- Dec 20, 2023
621d416
fix(frontend): Display error when creating a share failed
mejo- Dec 20, 2023
cd07cd0
fix(backend): Allow to create several shares for a collective
mejo- Dec 20, 2023
e5c2985
test(cypress): Move page share tests into own spec
mejo- Dec 20, 2023
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
4 changes: 2 additions & 2 deletions .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,15 @@ jobs:
uses: actions/upload-artifact@v4
if: failure()
with:
name: Screenshots.${{ matrix.server-versions }}
name: Screenshots.${{ matrix.server-versions }}-${{ matrix.containers }}
path: apps/${{ env.APP_NAME }}/cypress/screenshots/
retention-days: 5

- name: Upload nextcloud logs
uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ matrix.server-versions }}.log
name: ${{ matrix.server-versions }}-${{ matrix.containers }}.log
path: data/nextcloud.log
retention-days: 5

Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ In your Nextcloud instance, simply navigate to **Β»AppsΒ«**, find the
**Β»CirclesΒ«** and **Β»CollectivesΒ«** apps and enable them.

]]></description>
<version>2.9.2</version>
<version>2.10.0</version>
<licence>agpl</licence>
<author>CollectiveCloud Team</author>
<namespace>Collectives</namespace>
Expand Down
22 changes: 16 additions & 6 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@
'requirements' => ['id' => '\d+']],
['name' => 'collective#trash', 'url' => '/_api/{id}', 'verb' => 'DELETE',
'requirements' => ['id' => '\d+']],
['name' => 'collective#createShare', 'url' => '/_api/{id}/share', 'verb' => 'POST',
'requirements' => ['id' => '\d+']],
['name' => 'collective#updateShare', 'url' => '/_api/{id}/share/{token}', 'verb' => 'PUT',
'requirements' => ['id' => '\d+']],
['name' => 'collective#deleteShare', 'url' => '/_api/{id}/share/{token}', 'verb' => 'DELETE',
'requirements' => ['id' => '\d+']],

// collectives trash API
['name' => 'trash#index', 'url' => '/_api/trash', 'verb' => 'GET'],
Expand All @@ -41,6 +35,22 @@
['name' => 'collectiveUserSettings#showRecentPages', 'url' => '/_api/{id}/_userSettings/showRecentPages', 'verb' => 'PUT',
'requirements' => ['id' => '\d+']],

// share API
['name' => 'share#getCollectiveShares', 'url' => '/_api/{collectiveId}/shares', 'verb' => 'GET',
'requirements' => ['collectiveId' => '\d+']],
['name' => 'share#createCollectiveShare', 'url' => '/_api/{collectiveId}/share', 'verb' => 'POST',
'requirements' => ['collectiveId' => '\d+']],
['name' => 'share#updateCollectiveShare', 'url' => '/_api/{collectiveId}/share/{token}', 'verb' => 'PUT',
'requirements' => ['collectiveId' => '\d+']],
['name' => 'share#deleteCollectiveShare', 'url' => '/_api/{collectiveId}/share/{token}', 'verb' => 'DELETE',
'requirements' => ['collectiveId' => '\d+']],
['name' => 'share#createPageShare', 'url' => '/_api/{collectiveId}/_pages/{pageId}/share', 'verb' => 'POST',
'requirements' => ['collectiveId' => '\d+', 'pageId' => '\d+']],
['name' => 'share#updatePageShare', 'url' => '/_api/{collectiveId}/_pages/{pageId}/share/{token}', 'verb' => 'PUT',
'requirements' => ['collectiveId' => '\d+', 'pageId' => '\d+']],
['name' => 'share#deletePageShare', 'url' => '/_api/{collectiveId}/_pages/{pageId}/share/{token}', 'verb' => 'DELETE',
'requirements' => ['collectiveId' => '\d+', 'pageId' => '\d+']],

// pages API
['name' => 'page#index', 'url' => '/_api/{collectiveId}/_pages',
'verb' => 'GET', 'requirements' => ['collectiveId' => '\d+']],
Expand Down
46 changes: 24 additions & 22 deletions cypress/e2e/collective-share.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,17 @@ describe('Collective Share', function() {
},
})
cy.openCollectiveMenu('Share me')
cy.clickMenuButton('Share with guests')
cy.intercept('POST', '**/_api/*/share').as('createShare')
cy.clickMenuButton('Share link')
cy.get('.sharing-entry button.new-share-link')
.click()
cy.wait('@createShare')
cy.get('div.open ul')
.contains('Share link').should('not.be.visible')
cy.get('div.open ul')
.contains('Copy share link').should('be.visible')
cy.get('div.open ul')
.contains('Unshare').should('be.visible')
cy.get('button')
.contains('Copy share link')
cy.get('.toast-success').should('contain', 'Collective "Share me" has been shared')
cy.get('.sharing-entry .share-select')
.should('contain', 'View only')
cy.get('button.sharing-entry__copy')
.click()
cy.get('.toast-success').should('contain', 'Link copied to the clipboard.')
cy.get('.toast-success').should('contain', 'Link copied')
cy.get('@clipBoardWriteText').should('have.been.calledOnce')
})
it('Allows opening a shared (non-editable) collective', function() {
Expand All @@ -81,16 +79,21 @@ describe('Collective Share', function() {
.should('not.exist')
cy.getEditor().should('not.exist')
})
it('Allows toggling the editable flag for a collective share', function() {
it('Allows setting a collective share to editable', function() {
cy.loginAs('bob')
cy.visit('apps/collectives')
cy.openCollectiveMenu('Share me')
cy.clickMenuButton('Share with guests')
cy.get('.sharing-entry .share-select')
.click()
cy.intercept('PUT', '**/_api/*/share/*').as('updateShare')
cy.get('input#shareEditable')
.check({ force: true })
cy.get('.sharing-entry .share-select .dropdown-item')
.contains('Can edit')
.click()
cy.wait('@updateShare')
cy.get('input#shareEditable')
.should('be.checked')
cy.get('.toast-success').should('contain', 'Share link of collective "Share me" has been updated')
cy.get('.sharing-entry .share-select')
.should('contain', 'Can edit')
})
it('Allows opening and editing a shared (editable) collective', function() {
cy.logout()
Expand Down Expand Up @@ -123,15 +126,14 @@ describe('Collective Share', function() {
cy.loginAs('bob')
cy.visit('apps/collectives')
cy.openCollectiveMenu('Share me')
cy.clickMenuButton('Share with guests')
cy.get('.sharing-entry__actions')
.click()
cy.intercept('DELETE', '**/_api/*/share/*').as('deleteShare')
cy.clickMenuButton('Unshare')
cy.get('.unshare-button')
.click()
cy.wait('@deleteShare')
cy.get('div.open ul')
.contains('Share link').should('be.visible')
cy.get('div.open ul')
.contains('Copy share link').should('not.be.visible')
cy.get('div.open ul')
.contains('Unshare').should('not.be.visible')
cy.get('.toast-success').should('contain', 'Collective "Share me" has been unshared')
})
it('Opening unshared collective fails', function() {
cy.logout()
Expand Down
129 changes: 129 additions & 0 deletions cypress/e2e/page-share.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* @copyright Copyright (c) 2021 Jonas <[email protected]>
*
* @author Jonas <[email protected]>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

/**
* Tests for basic Collectives functionality.
*/

describe('Collective Share', function() {
let shareUrl

before(function() {
cy.loginAs('bob')
cy.visit('/apps/collectives')
cy.deleteAndSeedCollective('Share me')
cy.seedPage('Sharepage', '', 'Readme.md')
cy.seedPage('Sharesubpage', '', 'Sharepage.md')
cy.seedPageContent('Share%20me/Sharepage/Readme.md', '## Shared page')
})

describe('page share', function() {
it('Allows sharing a page', function() {
cy.loginAs('bob')
cy.visit('/apps/collectives', {
onBeforeLoad(win) {
// navigator.clipboard doesn't exist on HTTP requests (in CI), so let's create it
if (!win.navigator.clipboard) {
win.navigator.clipboard = {
__proto__: {
writeText: () => {},
},
}
}
// overwrite navigator.clipboard.writeText with cypress stub
cy.stub(win.navigator.clipboard, 'writeText', (text) => {
shareUrl = text
})
.as('clipBoardWriteText')
},
})
cy.openCollective('Share me')
cy.openPage('Sharepage')
cy.get('button.action-item .icon-menu-sidebar').click()
cy.get('a#sharing').click()
cy.intercept('POST', '**/_api/*/_pages/*/share').as('createShare')
cy.get('.sharing-entry button.new-share-link')
.click()
cy.wait('@createShare')
cy.get('.toast-success').should('contain', 'Page "Sharepage" has been shared')
cy.get('.sharing-entry .share-select')
.should('contain', 'View only')
cy.get('button.sharing-entry__copy')
.click()
cy.get('.toast-success').should('contain', 'Link copied')
cy.get('@clipBoardWriteText').should('have.been.calledOnce')
})
it('Allows opening a shared (non-editable) page', function() {
cy.logout()
cy.visit(shareUrl)
cy.get('#titleform input').should('have.value', 'Sharepage')
cy.get('button.titleform-button').should('not.exist')
cy.getReadOnlyEditor()
.should('be.visible')
.find('h2').should('contain', 'Shared page')
cy.get('.app-content-list-item.toplevel')
.should('contain', 'Sharepage')
cy.get('.app-content-list-item.toplevel')
.find('button.icon.add')
.should('not.exist')
cy.getEditor().should('not.exist')
})
it('Allows setting a page share to editable', function() {
cy.loginAs('bob')
cy.visit('/apps/collectives')
cy.openCollective('Share me')
cy.openPage('Sharepage')
cy.get('button.action-item .icon-menu-sidebar').click()
cy.get('a#sharing').click()
cy.get('.sharing-entry .share-select')
.click()
cy.intercept('PUT', '**/_api/*/_pages/*/share/*').as('updateShare')
cy.get('.sharing-entry .share-select .dropdown-item')
.contains('Can edit')
.click()
cy.wait('@updateShare')
cy.get('.toast-success').should('contain', 'Share link of page "Sharepage" has been updated')
cy.get('.sharing-entry .share-select')
.should('contain', 'Can edit')
})
it('Allows unsharing a page', function() {
cy.loginAs('bob')
cy.visit('/apps/collectives')
cy.openCollective('Share me')
cy.openPage('Sharepage')
cy.get('button.action-item .icon-menu-sidebar').click()
cy.get('a#sharing').click()
cy.get('.sharing-entry__actions')
.click()
cy.intercept('DELETE', '**/_api/*/_pages/*/share/*').as('deleteShare')
cy.get('.unshare-button')
.click()
cy.wait('@deleteShare')
cy.get('.toast-success').should('contain', 'Page "Sharepage" has been unshared')
})
it('Opening unshared page fails', function() {
cy.logout()
cy.visit(shareUrl, { failOnStatusCode: false })
cy.get('.body-login-container').contains(/(File|Page) not found/)
})
})
})
16 changes: 9 additions & 7 deletions cypress/e2e/pages-links.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,17 +342,19 @@ describe('Page Link Handling', function() {
},
})
cy.openCollectiveMenu('Link Testing')
cy.clickMenuButton('Share with guests')
cy.intercept('POST', '**/_api/*/share').as('createShare')
cy.clickMenuButton('Share link')
cy.get('.sharing-entry button.new-share-link')
.click()
cy.wait('@createShare')
cy.get('.sharing-entry .share-select')
.click()
cy.intercept('PUT', '**/_api/*/share/*').as('updateShare')
cy.get('input#shareEditable')
.check({ force: true })
cy.get('.sharing-entry .share-select .dropdown-item')
.contains('Can edit')
.click()
cy.wait('@updateShare')
cy.get('input#shareEditable')
.should('be.checked')
cy.get('button')
.contains('Copy share link')
cy.get('button.sharing-entry__copy')
.click()
cy.get('@clipBoardWriteText').should('have.been.calledOnce')
})
Expand Down
65 changes: 1 addition & 64 deletions lib/Controller/CollectiveController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use OCA\Collectives\Db\Collective;
use OCA\Collectives\Fs\NodeHelper;
use OCA\Collectives\Service\CollectiveService;
use OCA\Collectives\Service\CollectiveShareService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\Constants;
Expand All @@ -24,7 +23,6 @@ class CollectiveController extends Controller {
private IFactory $l10nFactory;
private LoggerInterface $logger;
private NodeHelper $nodeHelper;
private CollectiveShareService $shareService;

use ErrorHelper;

Expand All @@ -34,15 +32,13 @@ public function __construct(string $AppName,
IUserSession $userSession,
IFactory $l10nFactory,
LoggerInterface $logger,
NodeHelper $nodeHelper,
CollectiveShareService $shareService) {
NodeHelper $nodeHelper) {
parent::__construct($AppName, $request);
$this->service = $service;
$this->userSession = $userSession;
$this->l10nFactory = $l10nFactory;
$this->logger = $logger;
$this->nodeHelper = $nodeHelper;
$this->shareService = $shareService;
}

/**
Expand Down Expand Up @@ -206,63 +202,4 @@ public function trash(int $id): DataResponse {
];
});
}

/**
* @NoAdminRequired
*
* @param int $id
*
* @return DataResponse
*/
public function createShare(int $id): DataResponse {
return $this->prepareResponse(function () use ($id): array {
$userId = $this->getUserId();
$collectiveInfo = $this->service->getCollectiveInfo($id, $userId);
$share = $this->shareService->createShare($userId, $collectiveInfo);
$collectiveInfo->setShareToken($share->getToken());
return [
"data" => $collectiveInfo
];
});
}

/**
* @NoAdminRequired
*
* @param int $id
* @param string $token
* @param bool $editable
*
* @return DataResponse
*/
public function updateShare(int $id, string $token, bool $editable = false): DataResponse {
return $this->prepareResponse(function () use ($id, $token, $editable): array {
$userId = $this->getUserId();
$collectiveInfo = $this->service->getCollectiveInfo($id, $userId);
$this->shareService->updateShare($userId, $collectiveInfo, $token, $editable);
$collectiveInfo = $this->service->getCollectiveWithShare($id, $userId);
return [
"data" => $collectiveInfo
];
});
}

/**
* @NoAdminRequired
*
* @param int $id
* @param string $token
*
* @return DataResponse
*/
public function deleteShare(int $id, string $token): DataResponse {
return $this->prepareResponse(function () use ($id, $token): array {
$userId = $this->getUserId();
$collectiveInfo = $this->service->getCollectiveInfo($id, $userId);
$this->shareService->deleteShare($userId, $id, $token);
return [
"data" => $collectiveInfo
];
});
}
}
Loading
Loading