diff --git a/cypress/e2e/page-list.spec.js b/cypress/e2e/page-list.spec.js index b51048713..5bff0b0e6 100644 --- a/cypress/e2e/page-list.spec.js +++ b/cypress/e2e/page-list.spec.js @@ -146,4 +146,48 @@ describe('Page list', function() { }) }) }) + + describe('Page trash', function() { + it('allows to trash and restore page with subpage and attachment', function() { + cy.visit('/apps/collectives/Our%20Garden/Day%201') + + // Insert attachment + cy.intercept({ method: 'POST', url: '**/text/attachment/upload*' }).as('attachmentUpload') + cy.get('input[data-text-el="attachment-file-input"]') + .selectFile('cypress/fixtures/test.png', { force: true }) + cy.wait('@attachmentUpload') + cy.switchPageMode(0) + + // Trash page + cy.contains('.page-list .app-content-list-item', 'Day 1') + .find('.action-item__menutoggle') + .click({ force: true }) + cy.get('button.action-button') + .contains('Delete page and subpages') + .click() + cy.get('.page-list .app-content-list-item') + .should('not.contain', 'Day 1') + + // Restore page + cy.get('.page-trash') + .click() + cy.contains('table tr', 'Day 1') + .find('button') + .contains('Restore') + .click() + cy.get('table tr') + .should('not.exist') + + cy.visit('/apps/collectives/Our%20Garden/Day%201') + if (Cypress.env('ncVersion') === 'stable25') { + cy.getEditor() + .find('img.image__main') + .should('be.visible') + } else { + cy.getReadOnlyEditor() + .find('img.image__main') + .should('be.visible') + } + }) + }) }) diff --git a/cypress/e2e/pages.spec.js b/cypress/e2e/pages.spec.js index 486c9ef07..a252377f7 100644 --- a/cypress/e2e/pages.spec.js +++ b/cypress/e2e/pages.spec.js @@ -261,7 +261,7 @@ describe('Page', function() { }) describe('Using the search providers to search for a page', function() { - it('Search for page and page content', function() { + it('Search for page title', function() { cy.get('.unified-search a').click() cy.get('.unified-search__form input') .type('Day') @@ -271,7 +271,7 @@ describe('Page', function() { }) describe('Using the search providers to search page content', function() { - it('Search for page and page content', function() { + it('Search for page content', function() { cy.get('.unified-search a').click() cy.get('.unified-search__form input') .type('share your thoughts') diff --git a/lib/Fs/NodeHelper.php b/lib/Fs/NodeHelper.php index ea3b0590e..64435aea8 100644 --- a/lib/Fs/NodeHelper.php +++ b/lib/Fs/NodeHelper.php @@ -174,8 +174,9 @@ public static function isPage(File $file): bool { * @return bool */ public static function isLandingPage(File $file): bool { - $internalPath = $file->getInternalPath(); - return ($internalPath === PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX); + $internalPath = $file->getInternalPath() ?: ''; + return ($internalPath === PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX) + || preg_match('/^appdata_\w+\/collectives\/\d+\/' . PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX . '$/', $internalPath); } /** diff --git a/lib/Service/PageService.php b/lib/Service/PageService.php index 26468f1bd..077024528 100644 --- a/lib/Service/PageService.php +++ b/lib/Service/PageService.php @@ -843,11 +843,6 @@ public function trash(int $collectiveId, int $parentId, int $id, string $userId) try { if (NodeHelper::isIndexPage($file)) { - // Don't delete if still page has subpages - if (NodeHelper::indexPageHasOtherContent($file)) { - throw new NotPermittedException('Failed to delete page ' . $id . ' with subpages'); - } - // Delete folder if it's an index page without subpages $file->getParent()->delete(); } else { diff --git a/lib/Trash/PageTrashBackend.php b/lib/Trash/PageTrashBackend.php index a2cf6321e..bf8f2a6e0 100644 --- a/lib/Trash/PageTrashBackend.php +++ b/lib/Trash/PageTrashBackend.php @@ -169,14 +169,14 @@ public function restoreItem(ITrashItem $item): void { } // Get pageId for restoring page in collective page database - $restorePageId = null; + $restorePageId = $node->getId(); if ($node instanceof Folder) { // Try to use index page if folder is deleted - if (null !== $indexNode = $node->get(PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX)) { + try { + $indexNode = $node->get(PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX); $restorePageId = $indexNode->getId(); + } catch (NotFoundException $e) { } - } else { - $restorePageId = $node->getId(); } $targetLocation = $targetFolder->getInternalPath() . '/' . $originalLocation; @@ -214,25 +214,25 @@ public function removeItem(ITrashItem $item): void { } // Get original parent folder of item to revert subfolders further down - $targetFolder = $this->collectiveFolderManager->getFolder($collectiveId); + $collectiveFolder = $this->collectiveFolderManager->getFolder($collectiveId); $targetFolderPath = substr($item->getOriginalLocation(), 0, -strlen($item->getName())); if ($targetFolderPath) { try { - $targetFolder = $targetFolder->get($targetFolderPath); + $targetFolder = $collectiveFolder->get($targetFolderPath); } catch (NotFoundException $e) { $targetFolder = null; } } // Get pageId for deleting page from collective page database - $deletePageId = null; + $deletePageId = $node->getId(); if ($node instanceof Folder) { // Try to use index page if folder is deleted - if (null !== $indexNode = $node->get(PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX)) { + try { + $indexNode = $node->get(PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX); $deletePageId = $indexNode->getId(); + } catch (NotFoundException $e) { } - } else { - $deletePageId = $node->getId(); } if ($node->getStorage()->unlink($node->getInternalPath()) === false) { @@ -259,7 +259,7 @@ public function removeItem(ITrashItem $item): void { } // Try to revert subfolders of target folder parent - if ($targetFolder) { + if ($targetFolder && $targetFolder->getId() !== $collectiveFolder->getId()) { try { NodeHelper::revertSubFolders($targetFolder->getParent()); } catch (\OCA\Collectives\Service\NotFoundException | \OCA\Collectives\Service\NotPermittedException $e) { diff --git a/src/components/Collective.vue b/src/components/Collective.vue index aad82f92f..ccde1857a 100644 --- a/src/components/Collective.vue +++ b/src/components/Collective.vue @@ -16,7 +16,7 @@ import { mapActions, mapGetters, mapMutations } from 'vuex' import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' import { listen } from '@nextcloud/notify_push' import { NcAppContentDetails, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue' -import { GET_PAGES } from '../store/actions.js' +import { GET_PAGES, GET_TRASH_PAGES } from '../store/actions.js' import { SELECT_VERSION } from '../store/mutations.js' import displayError from '../util/displayError.js' import Page from './Page.vue' @@ -50,6 +50,7 @@ export default { computed: { ...mapGetters([ 'currentCollective', + 'currentCollectiveCanEdit', 'currentFileIdPage', 'currentPage', 'collectivePage', @@ -100,6 +101,7 @@ export default { ...mapActions({ dispatchGetPages: GET_PAGES, + dispatchGetTrashPages: GET_TRASH_PAGES, }), initCollective() { @@ -173,6 +175,10 @@ export default { async getPages() { await this.dispatchGetPages() .catch(displayError('Could not fetch pages')) + if (this.currentCollectiveCanEdit) { + await this.dispatchGetTrashPages() + .catch(displayError('Could not fetch page trash')) + } }, /** @@ -181,6 +187,10 @@ export default { async getPagesBackground() { await this.dispatchGetPages(false) .catch(displayError('Could not fetch pages')) + if (this.currentCollectiveCanEdit) { + await this.dispatchGetTrashPages() + .catch(displayError('Could not fetch page trash')) + } }, closeNav() { diff --git a/src/components/Page/PageActionMenu.vue b/src/components/Page/PageActionMenu.vue index 247ac8fba..8afe179a3 100644 --- a/src/components/Page/PageActionMenu.vue +++ b/src/components/Page/PageActionMenu.vue @@ -62,11 +62,9 @@ {{ deletePageString }} @@ -87,7 +85,6 @@ import { NcActions, NcActionButton, NcActionLink, NcActionSeparator } from '@nex import isMobile from '@nextcloud/vue/dist/Mixins/isMobile.js' import CollectiveActions from '../Collective/CollectiveActions.vue' import DeleteIcon from 'vue-material-design-icons/Delete.vue' -import DeleteOffIcon from 'vue-material-design-icons/DeleteOff.vue' import EmoticonOutlineIcon from 'vue-material-design-icons/EmoticonOutline.vue' import FormatListBulletedIcon from 'vue-material-design-icons/FormatListBulleted.vue' import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue' @@ -107,7 +104,6 @@ export default { NcActionLink, NcActionSeparator, DeleteIcon, - DeleteOffIcon, EmoticonOutlineIcon, FormatListBulletedIcon, PagesTemplateIcon, @@ -173,6 +169,7 @@ export default { ...mapGetters([ 'currentCollective', 'currentCollectiveCanEdit', + 'hasSubpages', 'loading', 'pagesTreeWalk', 'showing', @@ -201,8 +198,8 @@ export default { }, deletePageString() { - return this.hasSubpages - ? t('collectives', 'Cannot delete page with subpages') + return this.hasSubpages(this.pageId) + ? t('collectives', 'Delete page and subpages') : this.isTemplate ? t('collectives', 'Delete template') : t('collectives', 'Delete page') @@ -212,10 +209,6 @@ export default { return !!this.templatePage(this.pageId) }, - hasSubpages() { - return !!this.visibleSubpages(this.pageId).length || !!this.hasTemplate - }, - /** * Other apps can register an extra collective action via * OCA.Collectives.CollectiveExtraAction diff --git a/src/components/PageList.vue b/src/components/PageList.vue index 748a7282b..69e9ea62e 100644 --- a/src/components/PageList.vue +++ b/src/components/PageList.vue @@ -105,6 +105,7 @@ class="page-list-drag-item" /> + @@ -117,6 +118,7 @@ import CloseIcon from 'vue-material-design-icons/Close.vue' import Draggable from './PageList/Draggable.vue' import SubpageList from './PageList/SubpageList.vue' import Item from './PageList/Item.vue' +import PageTrash from './PageList/PageTrash.vue' import SortAlphabeticalAscendingIcon from 'vue-material-design-icons/SortAlphabeticalAscending.vue' import SortAscendingIcon from 'vue-material-design-icons/SortAscending.vue' import SortClockAscendingOutlineIcon from 'vue-material-design-icons/SortClockAscendingOutline.vue' @@ -138,6 +140,7 @@ export default { Draggable, Item, PagesTemplateIcon, + PageTrash, SubpageList, SortAlphabeticalAscendingIcon, SortAscendingIcon, @@ -200,6 +203,12 @@ export default { disableSorting() { return this.filterString !== '' }, + + displayTrash() { + return this.currentCollectiveCanEdit + && ('files_trashbin' in this.OC.appswebroots) + && !this.loading('collectives') + }, }, methods: { diff --git a/src/components/PageList/Item.vue b/src/components/PageList/Item.vue index 5fe56e078..f3c72d6cf 100644 --- a/src/components/PageList/Item.vue +++ b/src/components/PageList/Item.vue @@ -9,6 +9,7 @@ highlight: isHighlighted, 'dragged-over-target': isDraggedOverTarget, 'highlight-target': isHighlightedTarget, + 'highlight-animation': isHighlightAnimation, }" draggable @dragstart="onDragstart" @@ -180,6 +181,7 @@ export default { computed: { ...mapState({ highlightPageId: (state) => state.pages.highlightPageId, + highlightAnimationPageId: (state) => state.pages.highlightAnimationPageId, isDragoverTargetPage: (state) => state.pages.isDragoverTargetPage, draggedPageId: (state) => state.pages.draggedPageId, }), @@ -260,6 +262,10 @@ export default { return this.isPotentialDropTarget && this.isDragoverTargetPage }, + + isHighlightAnimation() { + return this.highlightAnimationPageId === this.pageId + }, }, mounted() { @@ -340,6 +346,8 @@ export default { diff --git a/src/components/PageSidebar/SidebarTabAttachments.vue b/src/components/PageSidebar/SidebarTabAttachments.vue index 19005e0ae..e421aff8f 100644 --- a/src/components/PageSidebar/SidebarTabAttachments.vue +++ b/src/components/PageSidebar/SidebarTabAttachments.vue @@ -436,15 +436,11 @@ export default { } - diff --git a/src/css/animation.scss b/src/css/animation.scss new file mode 100644 index 000000000..b11c87493 --- /dev/null +++ b/src/css/animation.scss @@ -0,0 +1,5 @@ +@keyframes highlight-animation { + 0% { background-color: var(--color-background-hover); } + 50% { background-color: var(--color-background-hover); } + 100% { background-color: rgba(var(--color-background-hover), 0); } +} diff --git a/src/mixins/pageMixin.js b/src/mixins/pageMixin.js index 47e572caa..d9e245a8e 100644 --- a/src/mixins/pageMixin.js +++ b/src/mixins/pageMixin.js @@ -1,7 +1,8 @@ import { mapActions, mapGetters, mapMutations, mapState } from 'vuex' import { showError, showSuccess } from '@nextcloud/dialogs' +import { emit } from '@nextcloud/event-bus' import { - DELETE_PAGE, + TRASH_PAGE, GET_PAGES, MOVE_PAGE, NEW_PAGE, @@ -45,7 +46,7 @@ export default { dispatchSetPageEmoji: SET_PAGE_EMOJI, dispatchSetPageSubpageOrder: SET_PAGE_SUBPAGE_ORDER, dispatchMovePage: MOVE_PAGE, - dispatchDeletePage: DELETE_PAGE, + dispatchTrashPage: TRASH_PAGE, }), /** @@ -170,7 +171,7 @@ export default { const currentPageId = this.currentPage?.id try { - await this.dispatchDeletePage({ parentId, pageId }) + await this.dispatchTrashPage({ parentId, pageId }) } catch (e) { console.error(e) showError(t('collectives', 'Could not delete the page')) @@ -182,9 +183,7 @@ export default { this.$router.push(`/${encodeURIComponent(this.currentCollective.name)}`) } - // Delete pageId from parent page subpageOrder - this.subpageOrderDelete(parentId, pageId) - + emit('collectives:page-list:page-trashed') showSuccess(t('collectives', 'Page deleted')) }, diff --git a/src/store/actions.js b/src/store/actions.js index 8dba4c2b3..205742218 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -18,7 +18,9 @@ export const UPDATE_COLLECTIVE_PAGE_MODE = 'UPDATE_COLLECTIVE_PAGE_MODE' export const SET_COLLECTIVE_USER_SETTING_PAGE_ORDER = 'SET_COLLECTIVE_USER_SETTING_PAGE_ORDER' export const MARK_COLLECTIVE_DELETED = 'MARK_COLLECTIVE_DELETED' export const UNMARK_COLLECTIVE_DELETED = 'UNMARK_COLLECTIVE_DELETED' +export const EXPAND_PARENTS = 'EXPAND_PARENTS' export const GET_PAGES = 'GET_PAGES' +export const GET_TRASH_PAGES = 'GET_TRASH_PAGES' export const GET_PAGE = 'GET_PAGE' export const NEW_PAGE = 'NEW_PAGE' export const NEW_TEMPLATE = 'NEW_TEMPLATE' @@ -27,6 +29,8 @@ export const RENAME_PAGE = 'RENAME_PAGE' export const MOVE_PAGE = 'MOVE_PAGE' export const SET_PAGE_EMOJI = 'SET_PAGE_EMOJI' export const SET_PAGE_SUBPAGE_ORDER = 'SET_PAGE_SUBPAGE_ORDER' +export const TRASH_PAGE = 'TRASH_PAGE' +export const RESTORE_PAGE = 'RESTORE_PAGE' export const DELETE_PAGE = 'DELETE_PAGE' export const GET_ATTACHMENTS = 'GET_ATTACHMENTS' export const GET_BACKLINKS = 'GET_BACKLINKS' diff --git a/src/store/circles.js b/src/store/circles.js index d89c2c67b..541a65abf 100644 --- a/src/store/circles.js +++ b/src/store/circles.js @@ -1,6 +1,6 @@ import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' -import { GET_CIRCLES, RENAME_CIRCLE, ADD_MEMBERS_TO_CIRCLE, LEAVE_CIRCLE, GET_PAGES } from './actions.js' +import { GET_CIRCLES, RENAME_CIRCLE, ADD_MEMBERS_TO_CIRCLE, LEAVE_CIRCLE, GET_PAGES, GET_TRASH_PAGES } from './actions.js' import { SET_CIRCLES, UPDATE_CIRCLE, @@ -81,6 +81,7 @@ export default { if (collective.id === getters.currentCollective?.id) { // Update page list, properties like `collectivePath` might have changed await dispatch(GET_PAGES) + await dispatch(GET_TRASH_PAGES) } commit(PATCH_COLLECTIVE_WITH_CIRCLE, response.data.ocs.data) }, diff --git a/src/store/mutations.js b/src/store/mutations.js index 41fc2880e..c5e150813 100644 --- a/src/store/mutations.js +++ b/src/store/mutations.js @@ -11,9 +11,12 @@ export const RESTORE_COLLECTIVE_FROM_TRASH = 'RESTORE_COLLECTIVE_FROM_TRASH' export const DELETE_COLLECTIVE_FROM_TRASH = 'DELETE_COLLECTIVE_FROM_TRASH' export const REMOVE_COLLECTIVE = 'REMOVE_COLLECTICE' export const SET_PAGES = 'SET_PAGES' +export const SET_TRASH_PAGES = 'SET_TRASH_PAGES' export const UPDATE_PAGE = 'UPDATE_PAGE' export const ADD_PAGE = 'ADD_PAGE' -export const DELETE_PAGE_BY_ID = 'DELETE_PAGE_BY_ID' +export const MOVE_PAGE_INTO_TRASH = 'MOVE_PAGE_INTO_TRASH' +export const RESTORE_PAGE_FROM_TRASH = 'RESTORE_PAGE_FROM_TRASH' +export const DELETE_PAGE_FROM_TRASH_BY_ID = 'DELETE_PAGE_FROM_TRASH_BY_ID' export const SELECT_VERSION = 'SELECT_VERSION' export const SET_VERSIONS = 'SET_VERSIONS' export const SET_ATTACHMENTS = 'SET_ATTACHMENTS' diff --git a/src/store/pages.js b/src/store/pages.js index 3446d0d99..f37b36cea 100644 --- a/src/store/pages.js +++ b/src/store/pages.js @@ -7,9 +7,12 @@ import * as sortOrders from '../util/sortOrders.js' import { SET_PAGES, + SET_TRASH_PAGES, ADD_PAGE, UPDATE_PAGE, - DELETE_PAGE_BY_ID, + MOVE_PAGE_INTO_TRASH, + RESTORE_PAGE_FROM_TRASH, + DELETE_PAGE_FROM_TRASH_BY_ID, SET_ATTACHMENTS, SET_ATTACHMENT_DELETED, SET_ATTACHMENT_UNDELETED, @@ -19,7 +22,9 @@ import { } from './mutations.js' import { + EXPAND_PARENTS, GET_PAGES, + GET_TRASH_PAGES, GET_PAGE, NEW_PAGE, NEW_TEMPLATE, @@ -28,6 +33,8 @@ import { MOVE_PAGE, SET_PAGE_EMOJI, SET_PAGE_SUBPAGE_ORDER, + TRASH_PAGE, + RESTORE_PAGE, DELETE_PAGE, GET_ATTACHMENTS, GET_BACKLINKS, @@ -38,6 +45,7 @@ export const TEMPLATE_PAGE = 'Template' export default { state: { pages: [], + trashPages: [], newPage: undefined, sortBy: undefined, collapsed: {}, @@ -46,6 +54,7 @@ export default { deletedAttachments: [], backlinks: [], highlightPageId: null, + highlightAnimationPageId: null, isDragoverTargetPage: false, draggedPageId: null, }, @@ -167,6 +176,12 @@ export default { return state.pages.find(p => (p.id === fileId)) }, + hasSubpages(state, _getters) { + return (pageId) => { + return state.pages.filter(p => p.parentId === pageId).length > 0 + } + }, + sortedSubpages(state, getters) { return (parentId, sortOrder) => { const parentPage = state.pages.find(p => p.id === parentId) @@ -274,6 +289,14 @@ export default { return (parentId, pageId) => `${getters.pageUrl(parentId, pageId)}/backlinks` }, + trashIndexUrl(_state, getters) { + return `${getters.pagesUrl}/trash` + }, + + trashActionUrl(_state, getters) { + return (pageId) => `${getters.pagesUrl}/trash/${pageId}` + }, + pageTitle(state, getters) { return pageId => { const page = state.pages.find(p => p.id === pageId) @@ -293,6 +316,10 @@ export default { keptSortable(state) { return (pageId) => state.pages.find(p => p.id === pageId)?.keepSortable }, + + trashPages(state) { + return state.trashPages.sort((a, b) => b.trashTimestamp - a.trashTimestamp) + }, }, mutations: { @@ -300,6 +327,10 @@ export default { state.pages = pages }, + [SET_TRASH_PAGES](state, trashPages) { + state.trashPages = trashPages + }, + [UPDATE_PAGE](state, page) { state.pages.splice( state.pages.findIndex(p => p.id === page.id), @@ -313,8 +344,22 @@ export default { state.newPage = page }, - [DELETE_PAGE_BY_ID](state, id) { - state.pages.splice(state.pages.findIndex(p => p.id === id), 1) + [MOVE_PAGE_INTO_TRASH](state, page) { + const trashPage = { ...page } + state.pages.splice(state.pages.findIndex(p => p.id === page.id), 1) + trashPage.trashTimestamp = Date.now() / 1000 + state.trashPages.unshift(trashPage) + }, + + [RESTORE_PAGE_FROM_TRASH](state, trashPage) { + const page = { ...trashPage } + page.trashTimestamp = null + state.pages.unshift(page) + state.trashPages.splice(state.trashPages.findIndex(p => p.id === trashPage.id), 1) + }, + + [DELETE_PAGE_FROM_TRASH_BY_ID](state, id) { + state.trashPages.splice(state.trashPages.findIndex(p => p.id === id), 1) }, [SET_ATTACHMENTS](state, { attachments }) { @@ -394,6 +439,10 @@ export default { state.highlightPageId = pageId }, + setHighlightAnimationPageId(state, pageId) { + state.highlightAnimationPageId = pageId + }, + setDragoverTargetPage(state, bool) { state.isDragoverTargetPage = bool }, @@ -404,6 +453,21 @@ export default { }, actions: { + /** + * Expand all parents of a page + * Needs to be an action to have access to the getter `pageParents` + * + * @param {object} store the vuex store + * @param {Function} store.commit commit changes + * @param {object} store.getters getters of the store + * @param {number} pageId Page ID + */ + [EXPAND_PARENTS]({ commit, getters }, pageId) { + for (const page of getters.pageParents(pageId)) { + commit('expand', page.id) + } + }, + /** * Get list of all pages * @@ -421,6 +485,20 @@ export default { commit('done', 'collective') }, + /** + * Get list of all pages in trash + * + * @param {object} store the vuex store + * @param {Function} store.commit commit changes + * @param {object} store.getters getters of the store + */ + async [GET_TRASH_PAGES]({ commit, getters }) { + commit('load', 'pageTrash') + const response = await axios.get(getters.trashIndexUrl) + commit(SET_TRASH_PAGES, response.data.data) + commit('done', 'pageTrash') + }, + /** * Get a single page and update it in the store * @@ -605,8 +683,7 @@ export default { }, /** - * - * Delete the current page + * Trash the page with the given id * * @param {object} store the vuex store * @param {Function} store.commit commit changes @@ -615,13 +692,41 @@ export default { * @param {number} page.parentId ID of the parent page * @param {number} page.pageId ID of the page */ - async [DELETE_PAGE]({ commit, getters }, { parentId, pageId }) { + async [TRASH_PAGE]({ commit, getters }, { parentId, pageId }) { commit('load', 'page') - await axios.delete(getters.pageUrl(parentId, pageId)) - commit(DELETE_PAGE_BY_ID, pageId) + const response = await axios.delete(getters.pageUrl(parentId, pageId)) + commit(MOVE_PAGE_INTO_TRASH, response.data.data) commit('done', 'page') }, + /** + * Restore the page with the given id from trash + * + * @param {object} store the vuex store + * @param {Function} store.commit commit changes + * @param {object} store.getters getters of the store + * @param {object} page the page + * @param {number} page.pageId ID of the page to restore + */ + async [RESTORE_PAGE]({ commit, getters }, { pageId }) { + const response = await axios.patch(getters.trashActionUrl(pageId)) + commit(RESTORE_PAGE_FROM_TRASH, response.data.data) + }, + + /** + * Delete the page with the given id from trash + * + * @param {object} store the vuex store + * @param {Function} store.commit commit changes + * @param {object} store.getters getters of the store + * @param {object} page the page + * @param {number} page.pageId ID of the page to delete + */ + async [DELETE_PAGE]({ commit, getters }, { pageId }) { + axios.delete(getters.trashActionUrl(pageId)) + commit(DELETE_PAGE_FROM_TRASH_BY_ID, pageId) + }, + /** * Get list of attachments for a page * diff --git a/tests/Integration/features/pages.feature b/tests/Integration/features/pages.feature index 755341ad3..3f6fd64e6 100644 --- a/tests/Integration/features/pages.feature +++ b/tests/Integration/features/pages.feature @@ -46,10 +46,6 @@ Feature: pages And user "jane" sets subpageOrder for page "firstpage" to "[1,2]" with parentPath "Readme.md" in "BehatPagesCollective" And user "jane" fails to set subpageOrder for page "firstpage" to "[invalid]" with parentPath "Readme.md" in "BehatPagesCollective" - Scenario: Fail to trash a page with subpages - When user "jane" fails to trash page "firstpage" with parentPath "Readme.md" in "BehatPagesCollective" - Then user "jane" sees pagePath "firstpage/Readme.md" in "BehatPagesCollective" - Scenario: Rename parent page When user "jane" renames page "firstpage" to "parentpage" with parentPath "Readme.md" in "BehatPagesCollective" Then user "jane" sees pagePath "parentpage/Readme.md" in "BehatPagesCollective" @@ -78,6 +74,11 @@ Feature: pages When user "jane" restores page "subpage" from trash in "BehatPagesCollective" Then user "jane" sees pagePath "parentpage/subpage.md" in "BehatPagesCollective" + Scenario: Trash and restore a page with subpages + When user "jane" trashes page "parentpage" with parentPath "Readme.md" in "BehatPagesCollective" + And user "jane" doesn't see pagePath "parentpage/Readme.md" in "BehatPagesCollective" + Then user "jane" restores page "parentpage" from trash in "BehatPagesCollective" + Scenario: Trash and delete all subpages reverts subfolders When user "jane" trashes page "subpage" with parentPath "parentpage/Readme.md" in "BehatPagesCollective" And user "jane" trashes page "subpage2" with parentPath "parentpage/Readme.md" in "BehatPagesCollective"