diff --git a/.eslintrc.js b/.eslintrc.js index 6c897d35111..66b5254b3c5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,7 @@ module.exports = { 'plugin:prettier/recommended', 'plugin:jest/recommended' ], - parser: "@babel/eslint-parser", + parser: '@babel/eslint-parser', parserOptions: { requireConfigFile: false, sourceType: 'module' @@ -23,13 +23,14 @@ module.exports = { 'require-await': 'warn', 'no-new': 'off', 'jest/no-standalone-expect': 'off', - 'node/no-callback-literal': 'off' + 'node/no-callback-literal': 'off', + 'unused-imports/no-unused-imports': 'error' }, globals: { require: false, requirejs: false }, - plugins: ['jest'], + plugins: ['jest', 'unused-imports'], overrides: [ { files: ['**/*.vue'], diff --git a/changelog/unreleased/enhancement-trash-bin b/changelog/unreleased/enhancement-trash-bin new file mode 100644 index 00000000000..8537df1eeb5 --- /dev/null +++ b/changelog/unreleased/enhancement-trash-bin @@ -0,0 +1,12 @@ +Enhancement: Trash bin + +We've improved the trash bin in general: +* Add compatibility with owncloud-sdk 3.0.0-alpha 1 +* Add a confirmation dialog while hitting the `Empty trash bin` button +* Add trash bin for project spaces +* Change personal trash bin route from `files/trash` to `files/trash/personal` + +https://github.com/owncloud/web/pull/6566 +https://github.com/owncloud/web/issues/6544 +https://github.com/owncloud/web/issues/5974 + diff --git a/package.json b/package.json index a630e0bb74c..9eb5a44c677 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-promise": "^5.2.0", + "eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-vue": "^7.13.0", "eslint-plugin-vuejs-accessibility": "^0.7.1", "focus-trap": "^6.4.0", diff --git a/packages/web-app-files/src/components/AppBar/AppBar.vue b/packages/web-app-files/src/components/AppBar/AppBar.vue index 2a8a576f688..2c217a7ae73 100644 --- a/packages/web-app-files/src/components/AppBar/AppBar.vue +++ b/packages/web-app-files/src/components/AppBar/AppBar.vue @@ -63,7 +63,8 @@ import { DavProperties } from 'web-pkg/src/constants' import { isLocationPublicActive, isLocationSharesActive, - isLocationSpacesActive + isLocationSpacesActive, + isLocationTrashActive } from '../../router' import { useActiveLocation } from '../../composables' @@ -92,7 +93,11 @@ export default { isPersonalLocation: useActiveLocation(isLocationSpacesActive, 'files-spaces-personal-home'), isPublicLocation: useActiveLocation(isLocationPublicActive, 'files-public-files'), isSpacesProjectsLocation: useActiveLocation(isLocationSpacesActive, 'files-spaces-projects'), - isSpacesProjectLocation: useActiveLocation(isLocationSpacesActive, 'files-spaces-project') + isSpacesProjectLocation: useActiveLocation(isLocationSpacesActive, 'files-spaces-project'), + isTrashSpacesProjectActive: useActiveLocation( + isLocationTrashActive, + 'files-trash-spaces-project' + ) } }, data: () => ({ @@ -113,6 +118,9 @@ export default { ...mapState('Files', ['areHiddenFilesShown']), showContextActions() { + if (this.isTrashSpacesProjectActive) { + return false + } if (this.isSpacesProjectLocation) { return this.currentFolder && this.breadcrumbs.length > 2 } @@ -166,12 +174,30 @@ export default { this.isPublicLocation || this.isPersonalLocation || this.isSpacesProjectsLocation || - this.isSpacesProjectLocation + this.isSpacesProjectLocation || + this.isTrashSpacesProjectActive ) ) { return [] } + if (this.isTrashSpacesProjectActive) { + return [ + { + text: this.$gettext('Spaces'), + to: '/files/spaces/projects' + }, + { + text: this.$route.params.storageId, + to: `/files/spaces/projects/${this.$route.params.storageId}` + }, + { + text: this.$gettext('Deleted Files'), + onClick: () => bus.publish('app.files.list.load') + } + ] + } + const { params: routeParams, path: routePath } = this.$route const requestedItemPath = routeParams.item || '' const basePaths = diff --git a/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue b/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue index 8af2921ff43..ce0b718d9e8 100644 --- a/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue +++ b/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue @@ -32,6 +32,7 @@ import { mapGetters } from 'vuex' import ActionMenuItem from '../../ActionMenuItem.vue' import Rename from '../../../mixins/spaces/actions/rename' import Delete from '../../../mixins/spaces/actions/delete' +import DeletedFiles from '../../../mixins/spaces/actions/deletedFiles' import Disable from '../../../mixins/spaces/actions/disable' import Restore from '../../../mixins/spaces/actions/restore' import EditDescription from '../../../mixins/spaces/actions/editDescription' @@ -51,6 +52,7 @@ export default { mixins: [ Rename, Delete, + DeletedFiles, EditDescription, EditReadmeContent, Disable, @@ -70,6 +72,7 @@ export default { ...this.$_uploadImage_items, ...this.$_editReadmeContent_items, ...this.$_editQuota_items, + ...this.$_deletedFiles_items, ...this.$_restore_items, ...this.$_delete_items, ...this.$_disable_items diff --git a/packages/web-app-files/src/components/SideBar/FileInfo.vue b/packages/web-app-files/src/components/SideBar/FileInfo.vue index ae85da3a161..717eb4e6403 100644 --- a/packages/web-app-files/src/components/SideBar/FileInfo.vue +++ b/packages/web-app-files/src/components/SideBar/FileInfo.vue @@ -38,7 +38,7 @@ import Mixins from '../../mixins' import MixinResources from '../../mixins/resources' import MixinFavorite from '../../mixins/actions/favorite' -import { isLocationCommonActive } from '../../router' +import { isLocationTrashActive } from '../../router' export default { name: 'FileInfo', @@ -61,7 +61,10 @@ export default { return obj } - if (isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return interpolate({ sourceTime: this.file.ddate, infoString: this.$pgettext('inline info about deletion date', 'deleted %{timeRelative}'), diff --git a/packages/web-app-files/src/components/SideBar/SideBar.vue b/packages/web-app-files/src/components/SideBar/SideBar.vue index 3d1ced36e80..8be8ff881b6 100644 --- a/packages/web-app-files/src/components/SideBar/SideBar.vue +++ b/packages/web-app-files/src/components/SideBar/SideBar.vue @@ -92,11 +92,7 @@ import { VisibilityObserver } from 'web-pkg/src/observer' import { DavProperties } from 'web-pkg/src/constants' import { buildResource } from '../../helpers/resources' -import { - isLocationCommonActive, - isLocationPublicActive, - isLocationSharesActive -} from '../../router' +import { isLocationPublicActive, isLocationSharesActive, isLocationTrashActive } from '../../router' import { computed } from '@vue/composition-api' import FileInfo from './FileInfo.vue' @@ -310,7 +306,8 @@ export default { } if ( - isLocationCommonActive(this.$router, 'files-common-trash') || + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') || this.highlightedFileIsSpace ) { this.selectedFile = this.highlightedFile diff --git a/packages/web-app-files/src/components/Trashbin.vue b/packages/web-app-files/src/components/Trashbin.vue new file mode 100644 index 00000000000..11fa15ac363 --- /dev/null +++ b/packages/web-app-files/src/components/Trashbin.vue @@ -0,0 +1,123 @@ + + + diff --git a/packages/web-app-files/src/fileSideBars.js b/packages/web-app-files/src/fileSideBars.js index 4ffff12a55f..3afc4b5ac7e 100644 --- a/packages/web-app-files/src/fileSideBars.js +++ b/packages/web-app-files/src/fileSideBars.js @@ -8,7 +8,7 @@ import NoSelection from './components/SideBar/NoSelection.vue' import SpaceActions from './components/SideBar/Actions/SpaceActions.vue' import SpaceDetails from './components/SideBar/Details/SpaceDetails.vue' import SpaceShares from './components/SideBar/Shares/SpaceShares.vue' -import { isLocationCommonActive, isLocationPublicActive, isLocationSpacesActive } from './router' +import { isLocationSpacesActive, isLocationTrashActive, isLocationPublicActive } from './router' import { spaceRoleEditor, spaceRoleManager } from './helpers/share' export default [ @@ -27,10 +27,15 @@ export default [ app: 'details-item', icon: 'questionnaire-line', component: FileDetails, - default: !isLocationCommonActive(router, 'files-common-trash'), + default: + !isLocationTrashActive(router, 'files-trash-personal') && + !isLocationTrashActive(router, 'files-trash-spaces-project'), get enabled() { return ( - !isLocationCommonActive(router, 'files-common-trash') && !multipleSelection && !rootFolder + !isLocationTrashActive(router, 'files-trash-personal') && + !isLocationTrashActive(router, 'files-trash-spaces-project') && + !multipleSelection && + !rootFolder ) } }), @@ -48,7 +53,9 @@ export default [ component: FileActions, icon: 'slideshow-3', iconFillType: 'line', - default: isLocationCommonActive(router, 'files-common-trash'), + default: + isLocationTrashActive(router, 'files-trash-personal') || + isLocationTrashActive(router, 'files-trash-spaces-project'), get enabled() { return !multipleSelection && !rootFolder } @@ -60,7 +67,8 @@ export default [ get enabled() { if (multipleSelection || rootFolder) return false if ( - isLocationCommonActive(router, 'files-common-trash') || + isLocationTrashActive(router, 'files-trash-personal') || + isLocationTrashActive(router, 'files-trash-spaces-project') || isLocationPublicActive(router, 'files-public-files') ) { return false @@ -79,7 +87,8 @@ export default [ get enabled() { if (multipleSelection || rootFolder) return false if ( - isLocationCommonActive(router, 'files-common-trash') || + isLocationTrashActive(router, 'files-trash-personal') || + isLocationTrashActive(router, 'files-trash-spaces-project') || isLocationPublicActive(router, 'files-public-files') ) { return false @@ -98,7 +107,8 @@ export default [ get enabled() { if (multipleSelection || rootFolder) return false if ( - isLocationCommonActive(router, 'files-common-trash') || + isLocationTrashActive(router, 'files-trash-personal') || + isLocationTrashActive(router, 'files-trash-spaces-project') || isLocationPublicActive(router, 'files-public-files') ) { return false diff --git a/packages/web-app-files/src/helpers/resources.js b/packages/web-app-files/src/helpers/resources.js index ad22a4cab26..ab2e3d6fbc1 100644 --- a/packages/web-app-files/src/helpers/resources.js +++ b/packages/web-app-files/src/helpers/resources.js @@ -240,10 +240,18 @@ export function buildWebDavFilesPath(userId, path) { return '/' + `files/${userId}/${path}`.split('/').filter(Boolean).join('/') } +export function buildWebDavFilesTrashPath(userId, path = '') { + return '/' + `trash-bin/${userId}/${path}`.split('/').filter(Boolean).join('/') +} + export function buildWebDavSpacesPath(storageId, path) { return '/' + `spaces/${storageId}/${path}`.split('/').filter(Boolean).join('/') } +export function buildWebDavSpacesTrashPath(storageId, path = '') { + return '/' + `/spaces/trash-bin/${storageId}/${path}`.split('/').filter(Boolean).join('/') +} + export function attachIndicators(resource, sharesTree) { return (resource.indicators = getIndicators(resource, sharesTree)) } diff --git a/packages/web-app-files/src/index.js b/packages/web-app-files/src/index.js index 97a0abee8b4..0f1ec9bcee9 100644 --- a/packages/web-app-files/src/index.js +++ b/packages/web-app-files/src/index.js @@ -10,6 +10,7 @@ import SharedWithMe from './views/shares/SharedWithMe.vue' import SharedWithOthers from './views/shares/SharedWithOthers.vue' import SharedViaLink from './views/shares/SharedViaLink.vue' import SpaceProject from './views/spaces/Project.vue' +import SpaceTrashbin from './views/spaces/Trashbin.vue' import SpaceProjects from './views/spaces/Projects.vue' import Trashbin from './views/Trashbin.vue' import translations from '../l10n/translations.json' @@ -104,7 +105,8 @@ export default { SharedWithOthers, Spaces: { Project: SpaceProject, - Projects: SpaceProjects + Projects: SpaceProjects, + Trashbin: SpaceTrashbin }, Trashbin }), diff --git a/packages/web-app-files/src/mixins/actions/delete.js b/packages/web-app-files/src/mixins/actions/delete.js index cbc82bcc5d9..4925aabd032 100644 --- a/packages/web-app-files/src/mixins/actions/delete.js +++ b/packages/web-app-files/src/mixins/actions/delete.js @@ -1,10 +1,6 @@ import MixinDeleteResources from '../../mixins/deleteResources' import { mapState } from 'vuex' -import { - isLocationCommonActive, - isLocationPublicActive, - isLocationSpacesActive -} from '../../router' +import { isLocationPublicActive, isLocationSpacesActive, isLocationTrashActive } from '../../router' export default { mixins: [MixinDeleteResources], @@ -44,7 +40,10 @@ export default { label: () => this.$gettext('Delete'), handler: this.$_delete_trigger, isEnabled: ({ resources }) => { - if (!isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + !isLocationTrashActive(this.$router, 'files-trash-personal') && + !isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return false } return resources.length > 0 diff --git a/packages/web-app-files/src/mixins/actions/emptyTrashBin.js b/packages/web-app-files/src/mixins/actions/emptyTrashBin.js index 04fd25e4a55..ce7db9a75fb 100644 --- a/packages/web-app-files/src/mixins/actions/emptyTrashBin.js +++ b/packages/web-app-files/src/mixins/actions/emptyTrashBin.js @@ -1,9 +1,11 @@ -import { mapActions, mapGetters } from 'vuex' -import { isLocationCommonActive } from '../../router' +import { mapActions, mapGetters, mapState } from 'vuex' +import { isLocationTrashActive } from '../../router' +import { buildWebDavFilesTrashPath, buildWebDavSpacesTrashPath } from '../../helpers/resources' export default { computed: { ...mapGetters('Files', ['activeFiles']), + ...mapState(['user']), $_emptyTrashBin_items() { return [ { @@ -12,7 +14,10 @@ export default { label: () => this.$gettext('Empty trash bin'), handler: this.$_emptyTrashBin_trigger, isEnabled: ({ resources }) => { - if (!isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + !isLocationTrashActive(this.$router, 'files-trash-personal') && + !isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return false } // empty trash bin is not available for individual resources, but only for the trash bin as a whole @@ -27,12 +32,35 @@ export default { } }, methods: { - ...mapActions(['showMessage']), + ...mapActions(['showMessage', 'createModal', 'hideModal', 'toggleModalConfirmButton']), ...mapActions('Files', ['clearTrashBin']), + $_emptyTrashBin_trigger() { - this.$client.fileTrash - .clearTrashBin() + const modal = { + variation: 'danger', + title: this.$gettext('Empty trash bin'), + cancelText: this.$gettext('Cancel'), + confirmText: this.$gettext('Delete'), + icon: 'alarm-warning', + message: this.$gettext( + 'Are you sure you want to permanently delete your items in the trash bin? You can’t undo this action.' + ), + hasInput: false, + onCancel: this.hideModal, + onConfirm: () => this.$_emptyTrashBin_emptyTrashBin() + } + + this.createModal(modal) + }, + $_emptyTrashBin_emptyTrashBin() { + const path = isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ? buildWebDavSpacesTrashPath(this.$route.params.storageId) + : buildWebDavFilesTrashPath(this.user.id) + + return this.$client.fileTrash + .clearTrashBin(path) .then(() => { + this.hideModal() this.showMessage({ title: this.$gettext('All deleted files were removed') }) diff --git a/packages/web-app-files/src/mixins/actions/fetch.js b/packages/web-app-files/src/mixins/actions/fetch.js index 1edc66fd910..45dfa3b0b21 100644 --- a/packages/web-app-files/src/mixins/actions/fetch.js +++ b/packages/web-app-files/src/mixins/actions/fetch.js @@ -1,4 +1,4 @@ -import { isLocationCommonActive, isLocationPublicActive } from '../../router' +import { isLocationPublicActive, isLocationTrashActive } from '../../router' export default { computed: { @@ -18,7 +18,10 @@ export default { return this.$gettext('Open in browser') }, isEnabled: ({ resources }) => { - if (isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return false } if (resources.length !== 1) { diff --git a/packages/web-app-files/src/mixins/actions/navigate.js b/packages/web-app-files/src/mixins/actions/navigate.js index c8290860051..e1634bd34e4 100644 --- a/packages/web-app-files/src/mixins/actions/navigate.js +++ b/packages/web-app-files/src/mixins/actions/navigate.js @@ -3,10 +3,10 @@ import { isSameResource } from '../../helpers/resource' import { createLocationPublic, createLocationSpaces, - isLocationCommonActive, isLocationPublicActive, isLocationSharesActive, - isLocationSpacesActive + isLocationSpacesActive, + isLocationTrashActive } from '../../router' import { ShareStatus } from '../../helpers/share' import merge from 'lodash-es/merge' @@ -22,7 +22,10 @@ export default { label: () => this.$pgettext('Action in the files list row to open a folder', 'Open folder'), isEnabled: ({ resources }) => { - if (isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return false } if (resources.length !== 1) { diff --git a/packages/web-app-files/src/mixins/actions/rename.js b/packages/web-app-files/src/mixins/actions/rename.js index 0ca0089ebda..59b20d4b2a3 100644 --- a/packages/web-app-files/src/mixins/actions/rename.js +++ b/packages/web-app-files/src/mixins/actions/rename.js @@ -3,7 +3,7 @@ import { mapActions, mapGetters } from 'vuex' import { isSameResource } from '../../helpers/resource' import { getParentPaths } from '../../helpers/path' import { buildResource } from '../../helpers/resources' -import { isLocationCommonActive } from '../../router' +import { isLocationTrashActive } from '../../router' export default { computed: { @@ -19,7 +19,10 @@ export default { }, handler: this.$_rename_trigger, isEnabled: ({ resources }) => { - if (isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return false } if (resources.length !== 1) { diff --git a/packages/web-app-files/src/mixins/actions/restore.js b/packages/web-app-files/src/mixins/actions/restore.js index ae36048976a..28c7f9d3061 100644 --- a/packages/web-app-files/src/mixins/actions/restore.js +++ b/packages/web-app-files/src/mixins/actions/restore.js @@ -1,9 +1,17 @@ -import { mapActions } from 'vuex' +import { mapActions, mapState } from 'vuex' import PQueue from 'p-queue' -import { isLocationCommonActive } from '../../router' +import { isLocationTrashActive } from '../../router' +import { + buildWebDavFilesTrashPath, + buildWebDavFilesPath, + buildWebDavSpacesTrashPath, + buildWebDavSpacesPath +} from '../../helpers/resources' export default { computed: { + ...mapState(['user']), + $_restore_items() { return [ { @@ -12,7 +20,10 @@ export default { label: () => this.$gettext('Restore'), handler: this.$_restore_trigger, isEnabled: ({ resources }) => { - if (!isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + !isLocationTrashActive(this.$router, 'files-trash-personal') && + !isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return false } return resources.length > 0 @@ -25,6 +36,7 @@ export default { }, methods: { ...mapActions('Files', ['removeFilesFromTrashbin']), + ...mapActions(['showMessage']), async $_restore_trigger({ resources }) { const restoredResources = [] @@ -32,10 +44,17 @@ export default { const restorePromises = [] const restoreQueue = new PQueue({ concurrency: 4 }) resources.forEach((resource) => { + const path = isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ? buildWebDavSpacesTrashPath(this.$route.params.storageId) + : buildWebDavFilesTrashPath(this.user.id) + const restorePath = isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ? buildWebDavSpacesPath(this.$route.params.storageId, resource.path) + : buildWebDavFilesPath(this.user.id, resource.path) + restorePromises.push( restoreQueue.add(async () => { try { - await this.$client.fileTrash.restore(resource.id, resource.path) + await this.$client.fileTrash.restore(path, resource.id, restorePath) restoredResources.push(resource) } catch (e) { console.error(e) diff --git a/packages/web-app-files/src/mixins/actions/showActions.js b/packages/web-app-files/src/mixins/actions/showActions.js index dd3cc9c80be..a92253f3148 100644 --- a/packages/web-app-files/src/mixins/actions/showActions.js +++ b/packages/web-app-files/src/mixins/actions/showActions.js @@ -1,5 +1,5 @@ import { mapActions } from 'vuex' -import { isLocationCommonActive } from '../../router' +import { isLocationTrashActive } from '../../router' import isFilesAppActive from './helpers/isFilesAppActive' export default { @@ -36,7 +36,10 @@ export default { // we don't have details in the trashbin, yet. // return hardcoded `actions-item` in all cases once we have them. await this.openSidebarWithPanel( - isLocationCommonActive(this.$router, 'files-common-trash') ? null : 'actions-item' + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ? null + : 'actions-item' ) } } diff --git a/packages/web-app-files/src/mixins/actions/showDetails.js b/packages/web-app-files/src/mixins/actions/showDetails.js index 9e7a2982efb..99a4c8edacb 100644 --- a/packages/web-app-files/src/mixins/actions/showDetails.js +++ b/packages/web-app-files/src/mixins/actions/showDetails.js @@ -1,5 +1,5 @@ import { mapActions } from 'vuex' -import { isLocationCommonActive } from '../../router' +import { isLocationTrashActive } from '../../router' import isFilesAppActive from './helpers/isFilesAppActive' export default { @@ -21,7 +21,10 @@ export default { return false } - if (isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return false } return resources.length > 0 diff --git a/packages/web-app-files/src/mixins/actions/showShares.js b/packages/web-app-files/src/mixins/actions/showShares.js index 31be88138c5..19b58b51982 100644 --- a/packages/web-app-files/src/mixins/actions/showShares.js +++ b/packages/web-app-files/src/mixins/actions/showShares.js @@ -1,5 +1,5 @@ import quickActions, { canShare, openNewCollaboratorsPanel } from '../../quickActions' -import { isLocationCommonActive, isLocationSharesActive } from '../../router' +import { isLocationSharesActive, isLocationTrashActive } from '../../router' import { ShareStatus } from '../../helpers/share' import isFilesAppActive from './helpers/isFilesAppActive' @@ -20,7 +20,10 @@ export default { return false } - if (isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return false } if (resources.length !== 1) { diff --git a/packages/web-app-files/src/mixins/deleteResources.js b/packages/web-app-files/src/mixins/deleteResources.js index 55f9e75eeae..b0c3cbada39 100644 --- a/packages/web-app-files/src/mixins/deleteResources.js +++ b/packages/web-app-files/src/mixins/deleteResources.js @@ -1,8 +1,9 @@ import { mapGetters, mapActions, mapMutations } from 'vuex' import { cloneStateObject } from '../helpers/store' import { isSameResource } from '../helpers/resource' +import { buildWebDavFilesTrashPath, buildWebDavSpacesTrashPath } from '../helpers/resources' import PQueue from 'p-queue' -import { isLocationCommonActive } from '../router' +import { isLocationTrashActive } from '../router' export default { data: () => ({ @@ -16,7 +17,10 @@ export default { ...mapGetters(['user']), $_deleteResources_isInTrashbin() { - return isLocationCommonActive(this.$router, 'files-common-trash') + return ( + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) }, $_deleteResources_resources() { @@ -95,8 +99,12 @@ export default { ...mapMutations(['SET_QUOTA']), $_deleteResources_trashbin_deleteOp(resource) { + const path = isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ? buildWebDavSpacesTrashPath(this.$route.params.storageId) + : buildWebDavFilesTrashPath(this.user.id) + return this.$client.fileTrash - .clearTrashBin(resource.id) + .clearTrashBin(path, resource.id) .then(() => { this.removeFilesFromTrashbin([resource]) const translated = this.$gettext('"%{file}" was deleted successfully') diff --git a/packages/web-app-files/src/mixins/fileActions.js b/packages/web-app-files/src/mixins/fileActions.js index 8b0067ad742..549d9e758d4 100644 --- a/packages/web-app-files/src/mixins/fileActions.js +++ b/packages/web-app-files/src/mixins/fileActions.js @@ -1,7 +1,7 @@ import get from 'lodash-es/get' import { mapGetters, mapActions, mapState } from 'vuex' -import { isAuthenticatedRoute, isLocationCommonActive } from '../router' +import { isAuthenticatedRoute, isLocationTrashActive } from '../router' import { routeToContextQuery } from 'web-pkg/src/composables/appDefaults' import AcceptShare from './actions/acceptShare' import Copy from './actions/copy' @@ -62,7 +62,10 @@ export default { }, $_fileActions_editorActions() { - if (isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return [] } return this.apps.fileEditors @@ -204,7 +207,10 @@ export default { // to open a resource with a specific mimeType // FIXME: filesApp should not know anything about any other app, dont cross the line!!! BAD $_fileActions_loadExternalAppActions(resources) { - if (isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + isLocationTrashActive(this.$router, 'files-trash-personal') || + isLocationTrashActive(this.$router, 'files-trash-spaces-project') + ) { return [] } diff --git a/packages/web-app-files/src/mixins/spaces/actions/deletedFiles.js b/packages/web-app-files/src/mixins/spaces/actions/deletedFiles.js new file mode 100644 index 00000000000..ce1d59403fb --- /dev/null +++ b/packages/web-app-files/src/mixins/spaces/actions/deletedFiles.js @@ -0,0 +1,34 @@ +import { createLocationTrash } from '../../../router' + +export default { + computed: { + $_deletedFiles_items() { + return [ + { + name: 'deletedFiles', + icon: 'delete-bin-5', + label: () => { + return this.$gettext('Deleted files') + }, + handler: this.$_deletedFiles_trigger, + isEnabled: ({ resources }) => { + return resources.length === 1 + }, + componentType: 'oc-button', + class: 'oc-files-actions-delete-trigger' + } + ] + } + }, + methods: { + $_deletedFiles_trigger({ resources }) { + this.$router.push( + createLocationTrash('files-trash-spaces-project', { + params: { + storageId: resources[0].id + } + }) + ) + } + } +} diff --git a/packages/web-app-files/src/router/common.ts b/packages/web-app-files/src/router/common.ts index 457e94c390b..0d7276bc2a2 100644 --- a/packages/web-app-files/src/router/common.ts +++ b/packages/web-app-files/src/router/common.ts @@ -2,19 +2,17 @@ import { RouteComponents } from './router' import { Location, RouteConfig } from 'vue-router' import { createLocation, $gettext, isLocationActiveDirector } from './utils' -type commonTypes = 'files-common-favorites' | 'files-common-search' | 'files-common-trash' +type commonTypes = 'files-common-favorites' | 'files-common-search' export const createLocationCommon = (name: commonTypes, location = {}): Location => createLocation(name, location) export const locationFavorites = createLocationCommon('files-common-favorites') -export const locationTrash = createLocationCommon('files-common-trash') export const locationSearch = createLocationCommon('files-common-search') export const isLocationCommonActive = isLocationActiveDirector( locationFavorites, - locationSearch, - locationTrash + locationSearch ) export const buildRoutes = (components: RouteComponents): RouteConfig[] => [ @@ -35,22 +33,6 @@ export const buildRoutes = (components: RouteComponents): RouteConfig[] => [ } ] }, - { - path: '/trash', - component: components.App, - children: [ - { - name: locationTrash.name, - path: '', - component: components.Trashbin, - meta: { - hideFilelistActions: true, - hasBulkActions: true, - title: $gettext('Deleted files') - } - } - ] - }, { path: '/favorites', component: components.App, diff --git a/packages/web-app-files/src/router/deprecated.ts b/packages/web-app-files/src/router/deprecated.ts index 5be36a7f7c4..037db30e4e7 100644 --- a/packages/web-app-files/src/router/deprecated.ts +++ b/packages/web-app-files/src/router/deprecated.ts @@ -5,6 +5,7 @@ import { createLocationCommon } from './common' import { createLocationOperations } from './operations' import { createLocationPublic } from './public' import { isLocationActive as isLocationActiveNoCompat } from './utils' +import { createLocationTrash } from './trash' /** * all route configs created by buildRoutes are deprecated, @@ -66,7 +67,7 @@ export const buildRoutes = (): RouteConfig[] => }, { path: '/trash-bin', - redirect: (to) => createLocationCommon('files-common-trash', to) + redirect: (to) => createLocationTrash('files-trash-personal', to) }, { path: '/public/list/:item*', @@ -106,7 +107,7 @@ export const isLocationActive = ( 'files-favorites': createLocationCommon('files-common-favorites').name, 'files-shared-with-others': createLocationShares('files-shares-with-others').name, 'files-shared-with-me': createLocationShares('files-shares-with-me').name, - 'files-trashbin ': createLocationCommon('files-common-trash').name, + 'files-trashbin ': createLocationTrash('files-trash-personal').name, 'files-public-list': createLocationPublic('files-public-files').name }[c.name] diff --git a/packages/web-app-files/src/router/index.ts b/packages/web-app-files/src/router/index.ts index 3bb8ae72819..28310a1364b 100644 --- a/packages/web-app-files/src/router/index.ts +++ b/packages/web-app-files/src/router/index.ts @@ -1,13 +1,11 @@ +import { RouteConfig } from 'vue-router' + import { - buildRoutes as buildSpacesRoutes, - isLocationSpacesActive, - createLocationSpaces -} from './spaces' -import { - buildRoutes as buildSharesRoutes, - isLocationSharesActive, - createLocationShares -} from './shares' + buildRoutes as buildCommonRoutes, + isLocationCommonActive, + createLocationCommon +} from './common' +import { buildRoutes as buildDeprecatedRoutes, isLocationActive } from './deprecated' import { buildRoutes as buildOperationsRoutes, createLocationOperations, @@ -18,15 +16,23 @@ import { createLocationPublic, isLocationPublicActive } from './public' +import { RouteComponents } from './router' import { - buildRoutes as buildCommonRoutes, - isLocationCommonActive, - createLocationCommon -} from './common' + buildRoutes as buildSharesRoutes, + isLocationSharesActive, + createLocationShares +} from './shares' +import { + buildRoutes as buildSpacesRoutes, + isLocationSpacesActive, + createLocationSpaces +} from './spaces' +import { + buildRoutes as buildTrashRoutes, + isLocationTrashActive, + createLocationTrash +} from './trash' import { isAuthenticatedRoute, ActiveRouteDirectorFunc } from './utils' -import { buildRoutes as buildDeprecatedRoutes, isLocationActive } from './deprecated' -import { RouteComponents } from './router' -import { RouteConfig } from 'vue-router' const ROOT_ROUTE = { path: '/', @@ -35,11 +41,12 @@ const ROOT_ROUTE = { const buildRoutes = (components: RouteComponents): RouteConfig[] => [ ROOT_ROUTE, + ...buildCommonRoutes(components), ...buildSharesRoutes(components), ...buildPublicRoutes(components), ...buildSpacesRoutes(components), ...buildOperationsRoutes(components), - ...buildCommonRoutes(components), + ...buildTrashRoutes(components), ...buildDeprecatedRoutes() ] @@ -56,6 +63,8 @@ export { isLocationPublicActive, isLocationActive, isAuthenticatedRoute, + isLocationTrashActive, + createLocationTrash, buildRoutes, ActiveRouteDirectorFunc } diff --git a/packages/web-app-files/src/router/router.ts b/packages/web-app-files/src/router/router.ts index dc6fe1c365a..06b93cde9b9 100644 --- a/packages/web-app-files/src/router/router.ts +++ b/packages/web-app-files/src/router/router.ts @@ -22,6 +22,7 @@ export interface RouteComponents { Spaces: { Projects: ComponentOptions Project: ComponentOptions + Trashbin: ComponentOptions } Trashbin: ComponentOptions } diff --git a/packages/web-app-files/src/router/trash.ts b/packages/web-app-files/src/router/trash.ts new file mode 100644 index 00000000000..4c3be4327b1 --- /dev/null +++ b/packages/web-app-files/src/router/trash.ts @@ -0,0 +1,46 @@ +import { RouteComponents } from './router' +import { Location, RouteConfig } from 'vue-router' +import { createLocation, $gettext, isLocationActiveDirector } from './utils' + +type trashTypes = 'files-trash-personal' | 'files-trash-spaces-project' + +export const createLocationTrash = (name: trashTypes, location = {}): Location => + createLocation(name, location) + +export const locationTrashPersonal = createLocationTrash('files-trash-personal') +export const locationTrashProject = createLocationTrash('files-trash-spaces-project') + +export const isLocationTrashActive = isLocationActiveDirector( + locationTrashPersonal, + locationTrashProject +) + +export const buildRoutes = (components: RouteComponents): RouteConfig[] => [ + { + path: '/trash', + redirect: (to) => createLocationTrash('files-trash-personal', to), + component: components.App, + children: [ + { + name: locationTrashPersonal.name, + path: 'personal', + component: components.Trashbin, + meta: { + hideFilelistActions: true, + hasBulkActions: true, + title: $gettext('Deleted files') + } + }, + { + name: locationTrashProject.name, + path: 'spaces/projects/:storageId', + component: components.Spaces.Trashbin, + meta: { + hideFilelistActions: true, + hasBulkActions: true, + title: $gettext('Deleted files') + } + } + ] + } +] diff --git a/packages/web-app-files/src/router/utils.ts b/packages/web-app-files/src/router/utils.ts index b1ea21daa84..15a1ea6674d 100644 --- a/packages/web-app-files/src/router/utils.ts +++ b/packages/web-app-files/src/router/utils.ts @@ -25,6 +25,16 @@ export const isLocationActive = ( ...comparative // ...(comparative.name && { name: resolveRouteName(comparative.name) }) }) + /** + * Href might be '/' or '#/' if router is not able to resolve the proper path. + * This happens if the we don't pass a param which is defined in the route configuration, for example: + * path: user/:id + * + * This implies that the comparative route is not active + **/ + if (comparativeHref === '/' || comparativeHref === '#/') { + return false + } return currentHref.startsWith(comparativeHref) }) .some(Boolean) diff --git a/packages/web-app-files/src/services/folder.ts b/packages/web-app-files/src/services/folder.ts index 237e6bdceb6..1b85e0cedc3 100644 --- a/packages/web-app-files/src/services/folder.ts +++ b/packages/web-app-files/src/services/folder.ts @@ -1,4 +1,4 @@ -import Router, { Route } from 'vue-router' +import Router from 'vue-router' import { useTask } from 'vue-concurrency' import { useRouter, useClientService } from 'web-pkg/src/composables' import { unref } from '@vue/composition-api' diff --git a/packages/web-app-files/src/services/folder/loaderProject.ts b/packages/web-app-files/src/services/folder/loaderProject.ts index 430e1416c38..2a9f03d68b3 100644 --- a/packages/web-app-files/src/services/folder/loaderProject.ts +++ b/packages/web-app-files/src/services/folder/loaderProject.ts @@ -3,9 +3,7 @@ import Router from 'vue-router' import { useTask } from 'vue-concurrency' import { isLocationSpacesActive } from '../../router' import { clientService } from 'web-pkg/src/services' -import { useStore, useRouter } from 'web-pkg/src/composables' import { buildResource, buildSpace, buildWebDavSpacesPath } from '../../helpers/resources' -import { unref } from '@vue/composition-api' export class FolderLoaderProject implements FolderLoader { public isEnabled(router: Router): boolean { diff --git a/packages/web-app-files/src/services/folder/loaderTrashbin.ts b/packages/web-app-files/src/services/folder/loaderTrashbin.ts index 0a2804729e9..1a0ca1001c1 100644 --- a/packages/web-app-files/src/services/folder/loaderTrashbin.ts +++ b/packages/web-app-files/src/services/folder/loaderTrashbin.ts @@ -2,24 +2,36 @@ import { FolderLoader, FolderLoaderTask, TaskContext } from '../folder' import Router from 'vue-router' import { useTask } from 'vue-concurrency' import { DavProperties } from 'web-pkg/src/constants' -import { isLocationCommonActive } from '../../router' -import { buildDeletedResource, buildResource } from '../../helpers/resources' +import { isLocationTrashActive } from '../../router' +import { + buildDeletedResource, + buildResource, + buildWebDavFilesTrashPath, + buildWebDavSpacesTrashPath +} from '../../helpers/resources' export class FolderLoaderTrashbin implements FolderLoader { public isEnabled(router: Router): boolean { - return isLocationCommonActive(router, 'files-common-trash') + return ( + isLocationTrashActive(router, 'files-trash-personal') || + isLocationTrashActive(router, 'files-trash-spaces-project') + ) } public getTask(context: TaskContext): FolderLoaderTask { const { store, - clientService: { owncloudSdk: client } + clientService: { owncloudSdk: client }, + router } = context return useTask(function* (signal1, signal2, ref) { store.commit('Files/CLEAR_CURRENT_FILES_LIST') - const resources = yield client.fileTrash.list('', '1', DavProperties.Trashbin) + const path = isLocationTrashActive(router, 'files-trash-spaces-project') + ? buildWebDavSpacesTrashPath(router.currentRoute.params.storageId) + : buildWebDavFilesTrashPath(store.getters.user.id) + const resources = yield client.fileTrash.list(path, '1', DavProperties.Trashbin) store.commit('Files/LOAD_FILES', { currentFolder: buildResource(resources[0]), diff --git a/packages/web-app-files/src/views/Trashbin.vue b/packages/web-app-files/src/views/Trashbin.vue index 2e87e24fc47..2ab9482f50b 100644 --- a/packages/web-app-files/src/views/Trashbin.vue +++ b/packages/web-app-files/src/views/Trashbin.vue @@ -1,104 +1,9 @@ - + diff --git a/packages/web-app-files/src/views/spaces/Project.vue b/packages/web-app-files/src/views/spaces/Project.vue index 46c5a0dd22f..ec398e04234 100644 --- a/packages/web-app-files/src/views/spaces/Project.vue +++ b/packages/web-app-files/src/views/spaces/Project.vue @@ -163,6 +163,7 @@ import { VisibilityObserver } from 'web-pkg/src/observer' import Mixins from '../../mixins' import Rename from '../../mixins/spaces/actions/rename' import Delete from '../../mixins/spaces/actions/delete' +import DeletedFiles from '../../mixins/spaces/actions/deletedFiles' import Disable from '../../mixins/spaces/actions/disable' import Restore from '../../mixins/spaces/actions/restore' import EditDescription from '../../mixins/spaces/actions/editDescription' @@ -195,6 +196,7 @@ export default { Mixins, Rename, Delete, + DeletedFiles, EditDescription, Disable, ShowDetails, @@ -397,6 +399,7 @@ export default { ...this.$_editReadmeContent_items, ...this.$_uploadImage_items, ...this.$_editQuota_items, + ...this.$_deletedFiles_items, ...this.$_restore_items, ...this.$_delete_items, ...this.$_disable_items, diff --git a/packages/web-app-files/src/views/spaces/Projects.vue b/packages/web-app-files/src/views/spaces/Projects.vue index ee3483fcb1b..f5d6723a6ec 100644 --- a/packages/web-app-files/src/views/spaces/Projects.vue +++ b/packages/web-app-files/src/views/spaces/Projects.vue @@ -171,6 +171,7 @@ import { createLocationSpaces } from '../../router' import { mapMutations, mapActions, mapGetters } from 'vuex' import Rename from '../../mixins/spaces/actions/rename' import Delete from '../../mixins/spaces/actions/delete' +import DeletedFiles from '../../mixins/spaces/actions/deletedFiles' import Disable from '../../mixins/spaces/actions/disable' import Restore from '../../mixins/spaces/actions/restore' import EditDescription from '../../mixins/spaces/actions/editDescription' @@ -189,7 +190,17 @@ export default { QuotaModal, ListLoader }, - mixins: [Rename, Delete, EditDescription, EditQuota, Disable, ShowDetails, Restore, UploadImage], + mixins: [ + Rename, + Delete, + EditDescription, + EditQuota, + DeletedFiles, + Disable, + ShowDetails, + Restore, + UploadImage + ], setup() { const store = useStore() const spaces = computed(() => store.getters['Files/activeFiles'] || []) @@ -294,6 +305,7 @@ export default { ...this.$_editDescription_items, ...this.$_uploadImage_items, ...this.$_editQuota_items, + ...this.$_deletedFiles_items, ...this.$_restore_items, ...this.$_delete_items, ...this.$_disable_items, diff --git a/packages/web-app-files/src/views/spaces/Trashbin.vue b/packages/web-app-files/src/views/spaces/Trashbin.vue new file mode 100644 index 00000000000..aa2b5c74dc6 --- /dev/null +++ b/packages/web-app-files/src/views/spaces/Trashbin.vue @@ -0,0 +1,9 @@ + + + diff --git a/packages/web-app-files/tests/integration/specs/appBar.spec.js b/packages/web-app-files/tests/integration/specs/appBar.spec.js index 5896cfb4ba5..2c9c5da479f 100644 --- a/packages/web-app-files/tests/integration/specs/appBar.spec.js +++ b/packages/web-app-files/tests/integration/specs/appBar.spec.js @@ -39,6 +39,7 @@ describe('AppBar contains set of actions and informations', () => { { setup: () => { return { + isPersonalLocation: true, isSharesLocation: false } }, diff --git a/packages/web-app-files/tests/integration/specs/pagination.spec.js b/packages/web-app-files/tests/integration/specs/pagination.spec.js index 44cb2a95772..b7cd6406fc2 100644 --- a/packages/web-app-files/tests/integration/specs/pagination.spec.js +++ b/packages/web-app-files/tests/integration/specs/pagination.spec.js @@ -26,7 +26,7 @@ const cases = [ ['PublicFiles', '/public/list/link', PublicFiles], ['SharedViaLink', '/list/shared-via-link/', SharedViaLink], ['SharedWithOthers', '/list/shared-with-others/', SharedWithOthers], - ['Trashbin', '/trash-bin/', Trashbin] + ['Trashbin', '/trash/personal', Trashbin] ] describe('User can navigate in files list using pagination', () => { diff --git a/packages/web-app-files/tests/integration/specs/sidebar.spec.js b/packages/web-app-files/tests/integration/specs/sidebar.spec.js index fb14ce74656..0dd788bce40 100644 --- a/packages/web-app-files/tests/integration/specs/sidebar.spec.js +++ b/packages/web-app-files/tests/integration/specs/sidebar.spec.js @@ -80,7 +80,6 @@ const store = { } } } - describe('Files sidebar', () => { test('Links panel displays private link for a shared file', async () => { config.mocks.publicPage = () => false diff --git a/packages/web-app-files/tests/unit/components/SideBar/FileInfo.spec.js b/packages/web-app-files/tests/unit/components/SideBar/FileInfo.spec.js index 231f6b1610a..85c2a0b6e65 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/FileInfo.spec.js +++ b/packages/web-app-files/tests/unit/components/SideBar/FileInfo.spec.js @@ -72,7 +72,7 @@ describe('FileInfo', () => { resetDateMocks() const tooltipStub = jest.fn() - const wrapper = createWrapper(simpleDeletedFile, tooltipStub, 'files-common-trash') + const wrapper = createWrapper(simpleDeletedFile, tooltipStub, 'files-trash-personal') expect(tooltipStub).toHaveBeenCalledTimes(1) expect(formDateFromRFC).toHaveBeenCalledTimes(1) diff --git a/packages/web-app-files/tests/unit/components/Trashbin.spec.js b/packages/web-app-files/tests/unit/components/Trashbin.spec.js new file mode 100644 index 00000000000..dc5a84ea535 --- /dev/null +++ b/packages/web-app-files/tests/unit/components/Trashbin.spec.js @@ -0,0 +1,203 @@ +import { mount } from '@vue/test-utils' +import Trashbin from '@files/src/components/Trashbin.vue' +import { getStore, localVue, createFile } from '@files/tests/unit/components/components.setup.js' + +const stubs = { + 'list-loader': true, + 'no-content-message': true, + 'resource-table': true, + 'context-actions': true, + pagination: true, + 'list-info': true, + 'router-link': true +} + +const listLoaderStub = 'list-loader-stub' +const noContentStub = 'no-content-message-stub' +const filesTableStub = 'resource-table-stub' +const filesTableSelector = '#files-trashbin-table' +const contextActionsStub = 'context-actions-stub' +const listInfoStub = 'list-info-stub' +const paginationStub = 'pagination-stub' + +describe('Trashbin component', () => { + it('should show the list loader when the view is still loading', () => { + const wrapper = getMountedWrapper({ loading: true }) + + expect(wrapper.find(listLoaderStub).exists()).toBeTruthy() + expect(wrapper.find(noContentStub).exists()).toBeFalsy() + expect(wrapper.find(filesTableStub).exists()).toBeFalsy() + }) + + describe('when the view is not loading anymore', () => { + it('should show the no content message component if the paginated resources is empty', () => { + const wrapper = getMountedWrapper() + + expect(wrapper.find(listLoaderStub).exists()).toBeFalsy() + expect(wrapper.find(filesTableStub).exists()).toBeFalsy() + expect(wrapper.find(noContentStub).exists()).toBeTruthy() + }) + + describe('when length of the paginated resources is greater than zero', () => { + const resourceList = [ + createFile({ id: '1234' }), + createFile({ id: '5896' }), + createFile({ id: '9856' }) + ] + const wrapper = getMountedWrapper({ + paginatedResources: resourceList + }) + + it('should not show the no content message component', () => { + expect(wrapper.find(noContentStub).exists()).toBeFalsy() + }) + + it('should load the resource table with correct props', () => { + stubs['resource-table'] = false + const wrapper = getMountedWrapper({ + paginatedResources: resourceList + }) + const filesTable = wrapper.find(filesTableSelector) + + expect(filesTable.exists()).toBeTruthy() + expect(filesTable).toMatchSnapshot() + + stubs['resource-table'] = true + }) + + describe('context menu', () => { + let wrapper + const selectedResources = [resourceList[0], resourceList[1]] + const notSelectedResources = [resourceList[2]] + beforeEach(() => { + stubs['resource-table'] = false + + wrapper = getMountedWrapper({ + selectedFiles: selectedResources, + paginatedResources: resourceList, + paginationPage: 1, + paginationPages: 2 + }) + }) + afterEach(() => { + stubs['resource-table'] = true + }) + it('should show the context actions for every selected resource', () => { + selectedResources.forEach((selectedResource) => { + const fileRow = wrapper.find(`[data-item-id="${selectedResource.id}"]`) + const contextMenu = fileRow.find(contextActionsStub) + expect(contextMenu.exists()).toBeTruthy() + expect(contextMenu.props().items).toMatchObject(selectedResources) + }) + }) + it('should not show the context actions for a resource that is not selected', () => { + notSelectedResources.forEach((notSelectedResource) => { + const fileRow = wrapper.find(`[data-item-id="${notSelectedResource.id}"]`) + const contextMenu = fileRow.find(contextActionsStub) + expect(contextMenu.exists()).toBeFalsy() + }) + }) + }) + + describe('pagination', () => { + it('should be visible if the paginated pages is greater than zero', () => { + stubs['resource-table'] = false + const wrapper = getMountedWrapper({ + paginatedResources: resourceList, + paginationPages: 2, + paginationPage: 1 + }) + + const pagination = wrapper.find(paginationStub) + + expect(pagination.exists()).toBeTruthy() + expect(pagination.props()).toMatchObject({ + pages: 2, + currentPage: 1 + }) + stubs['resource-table'] = true + }) + }) + + describe('list info', () => { + it('should be present if the paginated resources list is not empty', () => { + stubs['resource-table'] = false + + const wrapper = getMountedWrapper({ + paginatedResources: resourceList, + paginationPage: 1, + paginationPages: 2 + }) + + const listInfo = wrapper.find(listInfoStub) + expect(listInfo.exists()).toBeTruthy() + expect(listInfo.props()).toMatchObject({ + files: 3, + folders: 0 + }) + stubs['resource-table'] = true + }) + }) + }) + }) + + function mountOptions({ store, loading, paginatedResources, paginationPages, paginationPage }) { + const $route = { params: { page: 1 } } + const $router = { + afterEach: jest.fn(), + currentRoute: { + query: {} + } + } + return { + localVue, + store: store, + stubs, + mocks: { + $route, + $router + }, + setup: () => ({ + loadResourcesTask: { + isRunning: loading, + perform: jest.fn() + }, + paginatedResources: paginatedResources, + paginationPages: paginationPages, + paginationPage: paginationPage + }) + } + } + + function getMountedWrapper({ + selectedFiles = [], + loading = false, + paginatedResources = [], + paginationPages = 12, + paginationPage = 21 + } = {}) { + const component = { ...Trashbin, created: jest.fn() } + const store = createStore({ + totalFilesCount: { files: paginatedResources.length, folders: 0 }, + selectedFiles + }) + return mount( + component, + mountOptions({ + store, + loading, + paginatedResources, + paginationPages, + paginationPage + }) + ) + } + + function createStore({ totalFilesCount, highlightedFile, selectedFiles } = {}) { + return getStore({ + highlightedFile, + totalFilesCount, + selectedFiles + }) + } +}) diff --git a/packages/web-app-files/tests/unit/components/__snapshots__/Trashbin.spec.js.snap b/packages/web-app-files/tests/unit/components/__snapshots__/Trashbin.spec.js.snap new file mode 100644 index 00000000000..7987bf373aa --- /dev/null +++ b/packages/web-app-files/tests/unit/components/__snapshots__/Trashbin.spec.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Trashbin component when the view is not loading anymore when length of the paginated resources is greater than zero should load the resource table with correct props 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
Name Status + + Actions + +
+
+
file-name-1234 + +
+ file-path +
+
+
+
+
+
+
+
+
+
file-name-5896 + +
+ file-path +
+
+
+
+
+
+
+
+
+
file-name-9856 + +
+ file-path +
+
+
+
+
+
+
+
+`; diff --git a/packages/web-app-files/tests/unit/components/components.setup.js b/packages/web-app-files/tests/unit/components/components.setup.js new file mode 100644 index 00000000000..bfce076ba7a --- /dev/null +++ b/packages/web-app-files/tests/unit/components/components.setup.js @@ -0,0 +1,183 @@ +import { createLocalVue } from '@vue/test-utils' +import Vuex from 'vuex' +import OwnCloud from 'owncloud-sdk' +import { createStore } from 'vuex-extensions' +import DesignSystem from 'owncloud-design-system' +import GetTextPlugin from 'vue-gettext' +import VueCompositionAPI from '@vue/composition-api' +import { clientService } from 'web-pkg/src/services/client' + +export const createFile = ({ id, status = 1, type = 'folder' }) => ({ + id: `file-id-${id}`, + type, + status, + name: `file-name-${id}`, + path: `/file-path/${id}`, + extension: '', + share: { + id: `file-share-id-${id}` + }, + indicators: [] +}) + +export const localVue = createLocalVue() +localVue.prototype.$client = new OwnCloud() +localVue.prototype.$client.init({ baseUrl: 'http://none.de' }) +localVue.prototype.$clientService = clientService +localVue.prototype.$clientService.owncloudSdk = localVue.prototype.$client +localVue.use(Vuex) +localVue.use(DesignSystem) +localVue.use(VueCompositionAPI) + +/* + * TODO: options on GetTextPlugin do not have any effect because of + * packages/web-app-files/src/gettext.js which overwrites every setting. + */ +localVue.use(GetTextPlugin, { + translations: 'does-not-matter.json', + silent: true +}) + +// mock `v-translate` directive +localVue.directive('translate', { + inserted: (el) => {} +}) + +export const getRouter = ({ query = {} }) => ({ + afterEach: jest.fn(), + replace: jest.fn(), + push: jest.fn(), + currentRoute: { + query + } +}) + +export const getStore = function ({ + highlightedFile = null, + disablePreviews = true, + currentPage = null, + activeFiles = [], + pages = null, + sidebarClosed = false, + currentFolder = null, + inProgress = [null], + selectedFiles = [], + totalFilesCount = null, + totalFilesSize = null, + loginBackgroundImg = '', + loginLogo = '', + davProperties = [], + publicLinkPassword = null, + slogan = null, + user = null, + generalThemeName = '', + isOcis = true, + selectedResourcesForMove = null, + locationPickerTargetFolder = null +} = {}) { + return createStore(Vuex.Store, { + state: { + app: { quickActions: {} } + }, + getters: { + configuration: () => ({ + currentTheme: { + loginPage: { + backgroundImg: loginBackgroundImg + }, + logo: { + login: loginLogo + }, + general: { + name: generalThemeName, + slogan: slogan + } + }, + options: { + disablePreviews: disablePreviews + } + }), + getToken: () => '', + isOcis: () => isOcis, + homeFolder: () => '/', + user: () => user + }, + mutations: { + SET_QUOTA: () => {} + }, + actions: { + showMessage: () => {} + }, + modules: { + Files: { + state: { + resource: null, + filesPageLimit: 100, + files: [], + activeFiles: activeFiles, + currentFolder: currentFolder, + currentPage: currentPage, + selectedResourcesForMove: selectedResourcesForMove, + locationPickerTargetFolder: locationPickerTargetFolder + }, + getters: { + totalFilesCount: () => totalFilesCount, + totalFilesSize: () => totalFilesSize, + selectedFiles: () => selectedFiles, + activeFiles: (state) => state.activeFiles, + inProgress: () => inProgress, + highlightedFile: () => highlightedFile, + currentFolder: () => currentFolder, + pages: () => pages, + davProperties: () => davProperties, + publicLinkPassword: () => publicLinkPassword + }, + mutations: { + UPDATE_RESOURCE: (state, resource) => { + state.resource = resource + }, + UPSERT_RESOURCE: (state, resource) => { + state.activeFiles.push(resource) + }, + CLEAR_FILES_SEARCHED: () => {}, + CLEAR_CURRENT_FILES_LIST: () => {}, + LOAD_FILES: () => {}, + SET_FILES_PAGE_LIMIT: () => {}, + SET_CURRENT_FOLDER: () => {}, + REMOVE_FILE: () => {}, + REMOVE_FILE_FROM_SEARCHED: () => {}, + REMOVE_FILE_SELECTION: () => {}, + SET_FILE_SELECTION: () => {} + }, + actions: { + loadIndicators: () => {}, + loadFiles: () => {} + }, + namespaced: true, + modules: { + sidebar: { + state: { + closed: sidebarClosed + }, + namespaced: true + }, + pagination: { + state: { + currentPage, + itemsPerPage: 100 + }, + getters: { + pages: () => pages + }, + mutations: { + SET_ITEMS_PER_PAGE: () => {}, + UPDATE_CURRENT_PAGE: () => {} + }, + namespaced: true + } + } + }, + user: { state: user } + } + }) +} diff --git a/packages/web-app-files/tests/unit/helpers/user/avatarUrl.spec.ts b/packages/web-app-files/tests/unit/helpers/user/avatarUrl.spec.ts index bbd39bdb5e3..18bd4581752 100644 --- a/packages/web-app-files/tests/unit/helpers/user/avatarUrl.spec.ts +++ b/packages/web-app-files/tests/unit/helpers/user/avatarUrl.spec.ts @@ -2,7 +2,6 @@ import { avatarUrl } from '../../../../src/helpers/user' import { ImageDimension } from '../../../../src/constants' import mockAxios from 'jest-mock-axios' import { ClientService } from 'web-pkg/src/services' -import OwnCloud from 'owncloud-sdk' beforeEach(() => { mockAxios.reset() diff --git a/packages/web-app-files/tests/unit/mixins/actions/delete.spec.js b/packages/web-app-files/tests/unit/mixins/actions/delete.spec.js index 8407140ab2c..9a21ec32a07 100644 --- a/packages/web-app-files/tests/unit/mixins/actions/delete.spec.js +++ b/packages/web-app-files/tests/unit/mixins/actions/delete.spec.js @@ -1,7 +1,11 @@ import Vuex from 'vuex' import { mount, createLocalVue } from '@vue/test-utils' import Delete from '@files/src/mixins/actions/delete.js' -import { createLocationSpaces } from '../../../../src/router' +import { + createLocationShares, + createLocationSpaces, + createLocationTrash +} from '../../../../src/router' const localVue = createLocalVue() localVue.use(Vuex) @@ -13,29 +17,55 @@ const Component = { describe('delete', () => { describe('computed property "$_delete_items"', () => { - describe('isEnabled property of returned element', () => { + describe('delete isEnabled property of returned element', () => { it.each([ - { resources: [{ canBeDeleted: () => true }], expectedStatus: true }, - { resources: [{ canBeDeleted: () => false }], expectedStatus: false } + { resources: [{ canBeDeleted: () => true }], invalidLocation: false, expectedStatus: true }, + { resources: [{ canBeDeleted: () => true }], invalidLocation: true, expectedStatus: false }, + { + resources: [{ canBeDeleted: () => false }], + invalidLocation: false, + expectedStatus: false + } ])('should be set correctly', (inputData) => { - const wrapper = getWrapper() + const wrapper = getWrapper({ invalidLocation: inputData.invalidLocation }) const resources = inputData.resources expect(wrapper.vm.$_delete_items[0].isEnabled({ resources })).toBe(inputData.expectedStatus) }) }) + describe('delete-permanent isEnabled property of returned element', () => { + it.each([ + { resources: [{}], deletePermanent: true, invalidLocation: false, expectedStatus: true }, + { resources: [{}], deletePermanent: true, invalidLocation: true, expectedStatus: false }, + { + resources: [], + deletePermanent: true, + invalidLocation: false, + expectedStatus: false + } + ])('should be set correctly', (inputData) => { + const wrapper = getWrapper({ + deletePermanent: true, + invalidLocation: inputData.invalidLocation + }) + const resources = inputData.resources + expect(wrapper.vm.$_delete_items[1].isEnabled({ resources })).toBe(inputData.expectedStatus) + }) + }) }) }) -function getWrapper() { +function getWrapper({ deletePermanent = false, invalidLocation = false } = {}) { return mount(Component, { localVue, mocks: { - $route: createLocationSpaces('files-spaces-personal-home'), $router: { - resolve: () => { - return { - href: 'href' - } + currentRoute: invalidLocation + ? createLocationShares('files-shares-via-link') + : deletePermanent + ? createLocationTrash('files-trash-personal') + : createLocationSpaces('files-spaces-personal-home'), + resolve: (r) => { + return { href: r.name } } } } diff --git a/packages/web-app-files/tests/unit/mixins/actions/emptyTrashBin.spec.js b/packages/web-app-files/tests/unit/mixins/actions/emptyTrashBin.spec.js new file mode 100644 index 00000000000..d62d5e40968 --- /dev/null +++ b/packages/web-app-files/tests/unit/mixins/actions/emptyTrashBin.spec.js @@ -0,0 +1,126 @@ +import Vuex from 'vuex' +import { createStore } from 'vuex-extensions' +import { mount, createLocalVue } from '@vue/test-utils' +import EmptyTashBin from '@files/src/mixins/actions/emptyTrashBin.js' +import { createLocationTrash, createLocationSpaces } from '../../../../src/router' +// eslint-disable-next-line jest/no-mocks-import +import sdkMock from '@/__mocks__/sdk' + +const localVue = createLocalVue() +localVue.use(Vuex) + +const Component = { + render() {}, + mixins: [EmptyTashBin] +} + +describe('emptyTrashBin', () => { + afterEach(() => jest.clearAllMocks()) + + describe('isEnabled property', () => { + it('should be false when resource is given', () => { + const wrapper = getWrapper() + expect(wrapper.vm.$_emptyTrashBin_items[0].isEnabled({ resources: [{}] })).toBe(false) + }) + it('should be true when no resource is given', () => { + const wrapper = getWrapper() + expect(wrapper.vm.$_emptyTrashBin_items[0].isEnabled({ resources: [] })).toBe(true) + }) + it('should be false when location is invalid', () => { + const wrapper = getWrapper({ invalidLocation: true }) + expect(wrapper.vm.$_emptyTrashBin_items[0].isEnabled({ resources: [] })).toBe(false) + }) + }) + + describe('method "$_emptyTrashBin_trigger"', () => { + it('should trigger the empty trash bin modal window', async () => { + const wrapper = getWrapper() + const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') + await wrapper.vm.$_emptyTrashBin_trigger() + + expect(spyCreateModalStub).toHaveBeenCalledTimes(1) + }) + }) + + describe('method "$_emptyTrashBin_emptyTrashBin"', () => { + it('should hide the modal and show message on success', async () => { + const wrapper = getWrapper() + const hideModalStub = jest.spyOn(wrapper.vm, 'hideModal') + const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') + await wrapper.vm.$_emptyTrashBin_emptyTrashBin() + + expect(hideModalStub).toHaveBeenCalledTimes(1) + expect(showMessageStub).toHaveBeenCalledTimes(1) + }) + + it('should show message on error', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}) + + const wrapper = getWrapper({ resolveClearTrashBin: false }) + const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') + await wrapper.vm.$_emptyTrashBin_emptyTrashBin() + + expect(showMessageStub).toHaveBeenCalledTimes(1) + }) + }) +}) + +function getWrapper({ invalidLocation = false, resolveClearTrashBin = true } = {}) { + return mount(Component, { + localVue, + mocks: { + $router: { + currentRoute: invalidLocation + ? createLocationSpaces('files-spaces-personal-home') + : createLocationTrash('files-trash-personal'), + resolve: (r) => { + return { href: r.name } + } + }, + $gettext: jest.fn(), + $pgettext: jest.fn(), + $client: { + ...sdkMock, + fileTrash: { + ...sdkMock.files, + clearTrashBin: jest.fn().mockImplementation(() => { + if (resolveClearTrashBin) { + return Promise.resolve({}) + } + return Promise.reject(new Error('')) + }) + } + } + }, + store: createStore(Vuex.Store, { + actions: { + createModal: jest.fn(), + hideModal: jest.fn(), + showMessage: jest.fn() + }, + getters: { + configuration: () => ({ + server: 'https://example.com' + }), + getToken: () => 'token' + }, + modules: { + user: { + state: { + id: 'alice', + uuid: 1 + } + }, + Files: { + namespaced: true, + mutations: { + REMOVE_FILE: jest.fn() + }, + actions: { + clearTrashBin: jest.fn() + } + } + } + }) + }) +} diff --git a/packages/web-app-files/tests/unit/mixins/actions/restore.spec.js b/packages/web-app-files/tests/unit/mixins/actions/restore.spec.js new file mode 100644 index 00000000000..4ff703ac89d --- /dev/null +++ b/packages/web-app-files/tests/unit/mixins/actions/restore.spec.js @@ -0,0 +1,118 @@ +import Vuex from 'vuex' +import { createStore } from 'vuex-extensions' +import { mount, createLocalVue } from '@vue/test-utils' +import Restore from '@files/src/mixins/actions/restore.js' +import { createLocationTrash, createLocationSpaces } from '../../../../src/router' +// eslint-disable-next-line jest/no-mocks-import +import sdkMock from '@/__mocks__/sdk' + +const localVue = createLocalVue() +localVue.use(Vuex) + +const Component = { + render() {}, + mixins: [Restore] +} + +describe('restore', () => { + afterEach(() => jest.clearAllMocks()) + + describe('isEnabled property', () => { + it('should be false when no resource is given', () => { + const wrapper = getWrapper() + expect(wrapper.vm.$_restore_items[0].isEnabled({ resources: [] })).toBe(false) + }) + it('should be true when resource is given', () => { + const wrapper = getWrapper() + expect(wrapper.vm.$_restore_items[0].isEnabled({ resources: [{}] })).toBe(true) + }) + it('should be false when location is invalid', () => { + const wrapper = getWrapper({ invalidLocation: true }) + expect(wrapper.vm.$_restore_items[0].isEnabled({ resources: [{}] })).toBe(false) + }) + }) + + describe('method "$_restore_trigger"', () => { + it('should show message on success', async () => { + const wrapper = getWrapper() + const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') + const removeFilesFromTrashbinStub = jest.spyOn(wrapper.vm, 'removeFilesFromTrashbin') + await wrapper.vm.$_restore_trigger({ resources: [{ id: '1' }] }) + + expect(showMessageStub).toHaveBeenCalledTimes(1) + expect(removeFilesFromTrashbinStub).toHaveBeenCalledTimes(1) + }) + + it('should show message on error', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}) + + const wrapper = getWrapper({ resolveClearTrashBin: false }) + const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') + const removeFilesFromTrashbinStub = jest.spyOn(wrapper.vm, 'removeFilesFromTrashbin') + await wrapper.vm.$_restore_trigger({ resources: [{ id: '1' }] }) + + expect(showMessageStub).toHaveBeenCalledTimes(1) + expect(removeFilesFromTrashbinStub).toHaveBeenCalledTimes(0) + }) + }) +}) + +function getWrapper({ invalidLocation = false, resolveClearTrashBin: resolveRestore = true } = {}) { + return mount(Component, { + localVue, + mocks: { + $router: { + currentRoute: invalidLocation + ? createLocationSpaces('files-spaces-personal-home') + : createLocationTrash('files-trash-personal'), + resolve: (r) => { + return { href: r.name } + } + }, + $gettext: jest.fn(), + $gettextInterpolate: jest.fn(), + $client: { + ...sdkMock, + fileTrash: { + ...sdkMock.files, + restore: jest.fn().mockImplementation(() => { + if (resolveRestore) { + return Promise.resolve({}) + } + return Promise.reject(new Error('')) + }) + } + } + }, + store: createStore(Vuex.Store, { + actions: { + createModal: jest.fn(), + hideModal: jest.fn(), + showMessage: jest.fn() + }, + getters: { + configuration: () => ({ + server: 'https://example.com' + }), + getToken: () => 'token' + }, + modules: { + user: { + state: { + id: 'alice', + uuid: 1 + } + }, + Files: { + namespaced: true, + mutations: { + REMOVE_FILE: jest.fn() + }, + actions: { + removeFilesFromTrashbin: jest.fn() + } + } + } + }) + }) +} diff --git a/packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.js b/packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.js new file mode 100644 index 00000000000..c44cfe4ed4e --- /dev/null +++ b/packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.js @@ -0,0 +1,90 @@ +import Vuex from 'vuex' +import { createStore } from 'vuex-extensions' +import { mount, createLocalVue } from '@vue/test-utils' +import DeletedFiles from '@files/src/mixins/spaces/actions/deletedFiles.js' +import { createLocationSpaces, createLocationTrash } from '../../../../src/router' +import { buildSpace } from '../../../../src/helpers/resources' + +const localVue = createLocalVue() +localVue.use(Vuex) + +const Component = { + render() {}, + mixins: [DeletedFiles] +} + +describe('delete', () => { + afterEach(() => jest.clearAllMocks()) + + describe('isEnabled property', () => { + it('should be false when not resource given', () => { + const wrapper = getWrapper() + expect(wrapper.vm.$_deletedFiles_items[0].isEnabled({ resources: [] })).toBe(false) + }) + it('should be true when resource is given', () => { + const spaceMock = { + id: '1' + } + const wrapper = getWrapper() + expect( + wrapper.vm.$_deletedFiles_items[0].isEnabled({ resources: [buildSpace(spaceMock)] }) + ).toBe(true) + }) + }) + + describe('method "$_deletedFiles_trigger"', () => { + it('should trigger route change', async () => { + const spaceMock = { + id: '1' + } + + const wrapper = getWrapper() + await wrapper.vm.$_deletedFiles_trigger({ resources: [buildSpace(spaceMock)] }) + + expect(wrapper.vm.$router.push).toHaveBeenCalledWith( + createLocationTrash('files-trash-spaces-project', { + params: { + storageId: spaceMock.id + } + }) + ) + }) + }) +}) + +function getWrapper() { + return mount(Component, { + localVue, + mocks: { + $router: { + currentRoute: createLocationSpaces('files-spaces-projects'), + resolve: (r) => { + return { href: r.name } + }, + push: jest.fn() + }, + $gettext: jest.fn() + }, + store: createStore(Vuex.Store, { + actions: { + createModal: jest.fn(), + hideModal: jest.fn(), + showMessage: jest.fn() + }, + getters: { + configuration: () => ({ + server: 'https://example.com' + }), + getToken: () => 'token' + }, + modules: { + Files: { + namespaced: true, + mutations: { + REMOVE_FILE: jest.fn() + } + } + } + }) + }) +} diff --git a/packages/web-app-files/tests/unit/views/Trashbin.spec.js b/packages/web-app-files/tests/unit/views/Trashbin.spec.js index 07c46a48133..1797f5fbae5 100644 --- a/packages/web-app-files/tests/unit/views/Trashbin.spec.js +++ b/packages/web-app-files/tests/unit/views/Trashbin.spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils' -import Trashbin from '@files/src/views/Trashbin.vue' +import Trashbin from '@files/src/components/Trashbin.vue' import { getStore, localVue, createFile } from '@files/tests/unit/views/views.setup.js' const stubs = { @@ -20,7 +20,7 @@ const contextActionsStub = 'context-actions-stub' const listInfoStub = 'list-info-stub' const paginationStub = 'pagination-stub' -describe('Trashbin View', () => { +describe('Trashbin component', () => { it('should show the list loader when the view is still loading', () => { const wrapper = getMountedWrapper({ loading: true }) diff --git a/packages/web-app-files/tests/unit/views/__snapshots__/Trashbin.spec.js.snap b/packages/web-app-files/tests/unit/views/__snapshots__/Trashbin.spec.js.snap index 01cbe9a68dc..7987bf373aa 100644 --- a/packages/web-app-files/tests/unit/views/__snapshots__/Trashbin.spec.js.snap +++ b/packages/web-app-files/tests/unit/views/__snapshots__/Trashbin.spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Trashbin View when the view is not loading anymore when length of the paginated resources is greater than zero should load the resource table with correct props 1`] = ` +exports[`Trashbin component when the view is not loading anymore when length of the paginated resources is greater than zero should load the resource table with correct props 1`] = ` diff --git a/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Project.spec.js.snap b/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Project.spec.js.snap index a5a841257f6..841f0824df1 100644 --- a/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Project.spec.js.snap +++ b/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Project.spec.js.snap @@ -16,6 +16,12 @@ exports[`Spaces project view space image should show if given 1`] = `
    +
  • + + + Deleted files + +
  • @@ -59,6 +65,12 @@ exports[`Spaces project view space readme should show if given 1`] = `
      +
    • + + + Deleted files + +
    • diff --git a/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.js.snap b/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.js.snap index dbf9b56915d..41eb8264c80 100644 --- a/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.js.snap +++ b/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.js.snap @@ -31,6 +31,9 @@ exports[`Spaces component should list spaces 1`] = `
        +
      • diff --git a/packages/web-runtime/package.json b/packages/web-runtime/package.json index d63851f0ab8..3849372815f 100644 --- a/packages/web-runtime/package.json +++ b/packages/web-runtime/package.json @@ -19,7 +19,7 @@ "marked": "^4.0.12", "oidc-client": "1.11.5", "owncloud-design-system": "^13.0.0-rc.3", - "owncloud-sdk": "~2.1.0-alpha.3", + "owncloud-sdk": "~3.0.0-alpha.1", "p-queue": "^6.1.1", "popper-max-size-modifier": "^0.2.0", "portal-vue": "^2.1.7", diff --git a/packages/web-runtime/src/router/index.js b/packages/web-runtime/src/router/index.js index dca91e3e3d8..658b1cf189a 100644 --- a/packages/web-runtime/src/router/index.js +++ b/packages/web-runtime/src/router/index.js @@ -2,7 +2,7 @@ import get from 'lodash-es/get.js' import Vue from 'vue' import qs from 'qs' // eslint-disable-next-line no-unused-vars -import Router, { Route } from 'vue-router' +import Router from 'vue-router' import LoginPage from '../pages/login.vue' import OidcCallbackPage from '../pages/oidcCallback.vue' import AccessDeniedPage from '../pages/accessDenied.vue' diff --git a/tests/acceptance/pageObjects/trashbinPage.js b/tests/acceptance/pageObjects/trashbinPage.js index 6a976d828f3..b29c9791944 100644 --- a/tests/acceptance/pageObjects/trashbinPage.js +++ b/tests/acceptance/pageObjects/trashbinPage.js @@ -18,7 +18,11 @@ module.exports = { return this.waitForElementVisible('@clearTrashbin') .initAjaxCounters() .click('@clearTrashbin') - .waitForOutstandingAjaxCalls() + .waitForElementVisible('@dialog') + .waitForAnimationToFinish() // wait for transition on the modal to finish + .click('@dialogConfirmBtnEnabled') + .waitForAjaxCallsToStartAndFinish() + .waitForElementNotPresent('@dialog') }, restoreSelected: function () { return this.waitForElementVisible('@restoreSelectedButton') @@ -37,8 +41,14 @@ module.exports = { } }, elements: { + dialog: { + selector: '.oc-modal' + }, + dialogConfirmBtnEnabled: { + selector: '.oc-modal-body-actions-confirm:enabled' + }, clearTrashbin: { - selector: '.oc-files-actions-empty-trash-bin-trigger' + selector: '.oc-files-actions-empty-trash-bin-trigger:not([disabled])' }, restoreSelectedButton: { selector: '.oc-files-actions-restore-trigger' diff --git a/yarn.lock b/yarn.lock index 519240e6ef7..2a78d3e4905 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2608,12 +2608,12 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^5.14.0": - version: 5.14.0 - resolution: "@typescript-eslint/eslint-plugin@npm:5.14.0" + version: 5.15.0 + resolution: "@typescript-eslint/eslint-plugin@npm:5.15.0" dependencies: - "@typescript-eslint/scope-manager": 5.14.0 - "@typescript-eslint/type-utils": 5.14.0 - "@typescript-eslint/utils": 5.14.0 + "@typescript-eslint/scope-manager": 5.15.0 + "@typescript-eslint/type-utils": 5.15.0 + "@typescript-eslint/utils": 5.15.0 debug: ^4.3.2 functional-red-black-tree: ^1.0.1 ignore: ^5.1.8 @@ -2626,42 +2626,42 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 7176b30ebc2144292b38404b28aadc7a32d24d454deb8000e0e2ae71a199b9cff7c67ed6ff723b93278091f19948fdd4ef44029aade3ee6ec6f340acc9104ec6 + checksum: 730ec621b5d87a22ea53cb2a67cae15fd241ccec9cff1bc3bdd24622feb11ea39b186544b845c730afa22112d8922008f17d9116a06b1e2dcd975429209a0c0c languageName: node linkType: hard "@typescript-eslint/parser@npm:^5.14.0": - version: 5.14.0 - resolution: "@typescript-eslint/parser@npm:5.14.0" + version: 5.15.0 + resolution: "@typescript-eslint/parser@npm:5.15.0" dependencies: - "@typescript-eslint/scope-manager": 5.14.0 - "@typescript-eslint/types": 5.14.0 - "@typescript-eslint/typescript-estree": 5.14.0 + "@typescript-eslint/scope-manager": 5.15.0 + "@typescript-eslint/types": 5.15.0 + "@typescript-eslint/typescript-estree": 5.15.0 debug: ^4.3.2 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 2ee433b070d4b46fe2c2e11596fa92f39e60459a75d0e3fafe4f7d2e2f9772847208ce208190a3af0ec6490cb46a517523ac92a82a17fe289cc03ed7e512638a + checksum: 6b3047236349680e3a408aedf7490d304ad14d3f3264190eb058a472caeec07f85e8f298e6e774fd91cb001c9fd65a5fa558a9906afd30744bc05d8764cec250 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.14.0": - version: 5.14.0 - resolution: "@typescript-eslint/scope-manager@npm:5.14.0" +"@typescript-eslint/scope-manager@npm:5.15.0": + version: 5.15.0 + resolution: "@typescript-eslint/scope-manager@npm:5.15.0" dependencies: - "@typescript-eslint/types": 5.14.0 - "@typescript-eslint/visitor-keys": 5.14.0 - checksum: 20f163877218fc1c880e014ef2f858f7f8280a5d9dfc09dce092df9b72f6c5ebf490ede8b8dcf51e55f4d62a248eb5ccd468d37756731f27c8695c2f199f2638 + "@typescript-eslint/types": 5.15.0 + "@typescript-eslint/visitor-keys": 5.15.0 + checksum: 39fa688691c5cc207d44cc1f5a3ba0ecb3c34144505b32c1267df9e9368cc29373acd7e85e27d6fe84a0012417e40745887baeec6719f33b8a5ae4232d0db061 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.14.0": - version: 5.14.0 - resolution: "@typescript-eslint/type-utils@npm:5.14.0" +"@typescript-eslint/type-utils@npm:5.15.0": + version: 5.15.0 + resolution: "@typescript-eslint/type-utils@npm:5.15.0" dependencies: - "@typescript-eslint/utils": 5.14.0 + "@typescript-eslint/utils": 5.15.0 debug: ^4.3.2 tsutils: ^3.21.0 peerDependencies: @@ -2669,23 +2669,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: b5b71f397a87769558473b9283e65f92a1153d4de93cb9e5bf559ca6f7d8a898b4b565141bd2455c9fb6c2266aa5e39465a0337a7484a09a7fdf4bfbf191c7cf + checksum: ecefdec695602b04f5d403c741336c56a8aa4f5fa3cc48a202a7b3f548d4d470f44cec5632a3db819fe1ca27c9980dbd3100b8c93e4fd7e9a3fa4253c03f4c04 languageName: node linkType: hard -"@typescript-eslint/types@npm:5.14.0": - version: 5.14.0 - resolution: "@typescript-eslint/types@npm:5.14.0" - checksum: 1113c0c785a449970c52459dd1c779a80ae7b34b0bae923eace43a5d27279ee47092703d2a6a120c4ee7a78f0157ce8d6e53f951696d7b6197e121aac08ccc07 +"@typescript-eslint/types@npm:5.15.0": + version: 5.15.0 + resolution: "@typescript-eslint/types@npm:5.15.0" + checksum: 749d6eb366cb103924b51bcbe69d1c0fd6f7a00f5be4c01b3d6de3134537db956653db9958cdd8cc32f375bca818ea804f8e07697122943faff06232519529a1 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.14.0": - version: 5.14.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.14.0" +"@typescript-eslint/typescript-estree@npm:5.15.0": + version: 5.15.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.15.0" dependencies: - "@typescript-eslint/types": 5.14.0 - "@typescript-eslint/visitor-keys": 5.14.0 + "@typescript-eslint/types": 5.15.0 + "@typescript-eslint/visitor-keys": 5.15.0 debug: ^4.3.2 globby: ^11.0.4 is-glob: ^4.0.3 @@ -2694,33 +2694,33 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 41816c4cb37538a8206c1c590e9d0d0c30eca8a972001a102bf43002b8b6e53ec1ce451a156ce037ea978d34b297c5f677de7fe7baecd355687629e177dc6809 + checksum: 84fbb5030db5c1ac34527860725a9ea5b104fa1c49072a69306954b4b8516242427e70cb6a657ec2b822789432179a0df7a866e4618a29ee54b4285ca23556c8 languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.14.0, @typescript-eslint/utils@npm:^5.10.0": - version: 5.14.0 - resolution: "@typescript-eslint/utils@npm:5.14.0" +"@typescript-eslint/utils@npm:5.15.0, @typescript-eslint/utils@npm:^5.10.0": + version: 5.15.0 + resolution: "@typescript-eslint/utils@npm:5.15.0" dependencies: "@types/json-schema": ^7.0.9 - "@typescript-eslint/scope-manager": 5.14.0 - "@typescript-eslint/types": 5.14.0 - "@typescript-eslint/typescript-estree": 5.14.0 + "@typescript-eslint/scope-manager": 5.15.0 + "@typescript-eslint/types": 5.15.0 + "@typescript-eslint/typescript-estree": 5.15.0 eslint-scope: ^5.1.1 eslint-utils: ^3.0.0 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: c44b415e61e83f89ae7314f30e9783d813b19cd65b40993cb6dc28696f42ba3e4fa2bb31006ec67fe5414e37a02386616c87aa35e5ff8659e65a06fb3e83c95d + checksum: 406725b3e1282064612c9e69f346ceae5cf8e3fe4ae37295eaa1d594fb1b7ed3abd161c32b96622b00ca56e7b1120ea43b584954cd0cefad904a46d65b20960e languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.14.0": - version: 5.14.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.14.0" +"@typescript-eslint/visitor-keys@npm:5.15.0": + version: 5.15.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.15.0" dependencies: - "@typescript-eslint/types": 5.14.0 + "@typescript-eslint/types": 5.15.0 eslint-visitor-keys: ^3.0.0 - checksum: 3fc6038b330602b0dd2ff4070b11057a19239caa6f5bd956ee75d66f37f1b936704a98c4b1eae5da19a34d72b836a93e675080608138e50eabf72c2c901f648e + checksum: a3f231bf55794547680284aa23ba495efa1e52f864583fe53e1ff8b2c011db070ca48633eb8a333bfc93be0bdbb76ffa98e81bf032fd2737a5e0f0b1b81bbc22 languageName: node linkType: hard @@ -5741,6 +5741,21 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-unused-imports@npm:^2.0.0": + version: 2.0.0 + resolution: "eslint-plugin-unused-imports@npm:2.0.0" + dependencies: + eslint-rule-composer: ^0.3.0 + peerDependencies: + "@typescript-eslint/eslint-plugin": ^5.0.0 + eslint: ^8.0.0 + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + checksum: 8aa1e03e75da2a62a354065e0cb8fe370118c6f8d9720a32fe8c1da937de6adb81a4fed7d0d391d115ac9453b49029fb19f970d180a2cf3dba451fd4c20f0dc4 + languageName: node + linkType: hard + "eslint-plugin-vue@npm:^7.13.0": version: 7.13.0 resolution: "eslint-plugin-vue@npm:7.13.0" @@ -5768,6 +5783,13 @@ __metadata: languageName: node linkType: hard +"eslint-rule-composer@npm:^0.3.0": + version: 0.3.0 + resolution: "eslint-rule-composer@npm:0.3.0" + checksum: c2f57cded8d1c8f82483e0ce28861214347e24fd79fd4144667974cd334d718f4ba05080aaef2399e3bbe36f7d6632865110227e6b176ed6daa2d676df9281b1 + languageName: node + linkType: hard + "eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -9548,9 +9570,9 @@ __metadata: languageName: node linkType: hard -"owncloud-sdk@npm:~2.1.0-alpha.3": - version: 2.1.0-alpha.3 - resolution: "owncloud-sdk@npm:2.1.0-alpha.3" +"owncloud-sdk@npm:~3.0.0-alpha.1": + version: 3.0.0-alpha.1 + resolution: "owncloud-sdk@npm:3.0.0-alpha.1" peerDependencies: axios: ^0.26.0 cross-fetch: ^3.0.6 @@ -9560,7 +9582,7 @@ __metadata: uuid: ^8.2.0 webdav: 4.8.0 xml-js: ^1.6.11 - checksum: 566bbccd59e4cfbd6471942621e2f006c3337a3637b971492d7324913a37f4d3299deb9029546331cb0ec7e065efd6c4f9ba96a8f850c230d64e83dd9fc8c5db + checksum: 0e91758c3e7ba1fa72faf10a1e6a2a094776255dade51bbed42d44a9326918314f433405ebf147ad679b97c90abcdfd44e3170d2dd781ced26fa4937cc9f7a60 languageName: node linkType: hard @@ -11626,6 +11648,7 @@ __metadata: eslint-plugin-node: ^11.1.0 eslint-plugin-prettier: ^4.0.0 eslint-plugin-promise: ^5.2.0 + eslint-plugin-unused-imports: ^2.0.0 eslint-plugin-vue: ^7.13.0 eslint-plugin-vuejs-accessibility: ^0.7.1 focus-trap: ^6.4.0 @@ -13517,7 +13540,7 @@ __metadata: marked: ^4.0.12 oidc-client: 1.11.5 owncloud-design-system: ^13.0.0-rc.3 - owncloud-sdk: ~2.1.0-alpha.3 + owncloud-sdk: ~3.0.0-alpha.1 p-queue: ^6.1.1 popper-max-size-modifier: ^0.2.0 portal-vue: ^2.1.7