diff --git a/changelog/unreleased/enhancement-spaces-sidebar b/changelog/unreleased/enhancement-spaces-sidebar new file mode 100644 index 00000000000..fc4a2731600 --- /dev/null +++ b/changelog/unreleased/enhancement-spaces-sidebar @@ -0,0 +1,6 @@ +Enhancement: Implement the right sidebar for spaces + +The right sidebar for a space functions similar to the files sidebar and gives the user basic information and actions for the current space. + +https://github.com/owncloud/web/pull/6437 +https://github.com/owncloud/web/issues/6284 diff --git a/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue b/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue new file mode 100644 index 00000000000..76b1f3f1178 --- /dev/null +++ b/packages/web-app-files/src/components/SideBar/Actions/SpaceActions.vue @@ -0,0 +1,59 @@ +<template> + <oc-list id="oc-spaces-actions-sidebar" class-name="oc-mt-s"> + <action-menu-item + v-for="(action, index) in actions" + :key="`action-${index}`" + :action="action" + :items="resources" + class="oc-py-xs" + /> + </oc-list> +</template> + +<script> +import { mapGetters } from 'vuex' +import ActionMenuItem from '../../ActionMenuItem.vue' +import Rename from '../../../mixins/spaces/actions/rename' +import Delete from '../../../mixins/spaces/actions/delete' +import Disable from '../../../mixins/spaces/actions/disable' +import Restore from '../../../mixins/spaces/actions/restore' +import EditDescription from '../../../mixins/spaces/actions/editDescription' + +export default { + name: 'SpaceActions', + title: ($gettext) => { + return $gettext('Actions') + }, + components: { ActionMenuItem }, + mixins: [Rename, Delete, EditDescription, Disable, Restore], + computed: { + ...mapGetters('Files', ['highlightedFile']), + + resources() { + return [this.highlightedFile] + }, + + actions() { + return [ + ...this.$_rename_items, + ...this.$_editDescription_items, + ...this.$_restore_items, + ...this.$_delete_items, + ...this.$_disable_items + ].filter((item) => item.isEnabled({ resources: this.resources })) + } + } +} +</script> + +<style lang="scss"> +#oc-spaces-actions-sidebar { + > li a, + > li a:hover { + color: var(--oc-color-swatch-passive-default); + display: inline-flex; + gap: 10px; + vertical-align: top; + } +} +</style> diff --git a/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue b/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue new file mode 100644 index 00000000000..1567bcd94d4 --- /dev/null +++ b/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue @@ -0,0 +1,230 @@ +<template> + <div id="oc-space-details-sidebar"> + <div class="oc-space-details-sidebar-image oc-text-center"> + <oc-spinner v-if="loadImageTask.isRunning" /> + <img + v-else-if="spaceImage" + :src="'data:image/jpeg;base64,' + spaceImage" + alt="" + class="oc-mb-m" + /> + <oc-icon + v-else + name="layout-grid" + size="xxlarge" + class="space-default-image oc-px-m oc-py-m" + /> + </div> + <div v-if="hasPeopleShares || hasLinkShares" class="oc-flex oc-flex-middle oc-mb-m"> + <oc-icon v-if="hasPeopleShares" name="group" class="oc-mr-s" /> + <oc-icon v-if="hasLinkShares" name="link" class="oc-mr-s" /> + <span class="oc-text-small" v-text="shareLabel" /> + </div> + <div> + <table class="details-table" :aria-label="detailsTableLabel"> + <tr> + <th scope="col" class="oc-pr-s" v-text="$gettext('Last activity')" /> + <td v-text="lastModifyDate" /> + </tr> + <tr v-if="space.description"> + <th scope="col" class="oc-pr-s" v-text="$gettext('Subtitle')" /> + <td v-text="space.description" /> + </tr> + <tr> + <th scope="col" class="oc-pr-s" v-text="$gettext('Manager')" /> + <td> + <span v-if="!loadOwnersTask.isRunning" v-text="ownerUsernames" /> + </td> + </tr> + <tr> + <th scope="col" class="oc-pr-s" v-text="$gettext('Quota')" /> + <td> + <space-quota :space-quota="space.spaceQuota" /> + </td> + </tr> + </table> + </div> + </div> +</template> +<script> +import { ref } from '@vue/composition-api' +import Mixins from '../../../mixins' +import MixinResources from '../../../mixins/resources' +import { mapGetters } from 'vuex' +import { useTask } from 'vue-concurrency' +import { buildWebDavSpacesPath } from '../../../helpers/resources' +import { useStore } from 'web-pkg/src/composables' +import { clientService } from 'web-pkg/src/services' +import SpaceQuota from '../../SpaceQuota.vue' + +export default { + name: 'SpaceDetails', + components: { SpaceQuota }, + mixins: [Mixins, MixinResources], + inject: ['displayedItem'], + title: ($gettext) => { + return $gettext('Details') + }, + setup() { + const store = useStore() + const spaceImage = ref('') + const owners = ref([]) + const graphClient = clientService.graphAuthenticated( + store.getters.configuration.server, + store.getters.getToken + ) + + const loadImageTask = useTask(function* (signal, ref) { + if (!ref.space?.spaceImageData) { + return + } + + const fileContents = yield ref.$client.files.getFileContents( + buildWebDavSpacesPath(ref.space.id, ref.space.spaceImageData.name), + { responseType: 'arrayBuffer' } + ) + + spaceImage.value = Buffer.from(fileContents).toString('base64') + }) + + const loadOwnersTask = useTask(function* (signal, ref) { + const promises = [] + for (const userId of ref.ownerUserIds) { + promises.push(graphClient.users.getUser(userId)) + } + + if (promises.length > 0) { + yield Promise.all(promises).then((resolvedData) => { + resolvedData.forEach((response) => { + owners.value.push(response.data) + }) + }) + } + }) + + return { loadImageTask, loadOwnersTask, spaceImage, owners } + }, + computed: { + ...mapGetters(['user']), + + space() { + return this.displayedItem.value + }, + detailsTableLabel() { + return this.$gettext('Overview of the information about the selected space') + }, + lastModifyDate() { + return this.formDateFromISO(this.space.mdate) + }, + ownerUserIds() { + const permissions = this.space.spacePermissions?.filter((permission) => + permission.roles.includes('manager') + ) + if (!permissions.length) { + return [] + } + + const userIds = permissions.reduce((acc, item) => { + const ids = item.grantedTo.map((user) => user.user.id) + acc = acc.concat(ids) + return acc + }, []) + + return [...new Set(userIds)] + }, + ownerUsernames() { + const userId = this.user?.id + return this.owners + .map((owner) => { + if (owner.onPremisesSamAccountName === userId) { + return this.$gettextInterpolate(this.$gettext('%{displayName} (me)'), { + displayName: owner.displayName + }) + } + return owner.displayName + }) + .join(', ') + }, + hasPeopleShares() { + return false // @TODO + }, + hasLinkShares() { + return false // @TODO + }, + peopleShareCount() { + return 0 // @TODO + }, + linkShareCount() { + return 0 // @TODO + }, + shareLabel() { + let peopleString, linksString + + if (this.hasPeopleShares) { + peopleString = this.$gettextInterpolate( + this.$ngettext( + 'This space has been shared with %{peopleShareCount} person.', + 'This space has been shared with %{peopleShareCount} people.', + this.peopleShareCount + ), + { + peopleShareCount: this.peopleShareCount + } + ) + } + + if (this.hasLinkShares) { + linksString = this.$gettextInterpolate( + this.$ngettext( + '%{linkShareCount} link giving access.', + '%{linkShareCount} links giving access.', + this.linkShareCount + ), + { + linkShareCount: this.linkShareCount + } + ) + } + + if (peopleString && linksString) { + return `${peopleString} ${linksString}` + } + + if (peopleString) { + return peopleString + } + + if (linksString) { + return linksString + } + + return '' + } + }, + mounted() { + this.loadImageTask.perform(this) + this.loadOwnersTask.perform(this) + } +} +</script> +<style lang="scss" scoped> +.oc-space-details-sidebar { + &-image img { + max-height: 150px; + object-fit: cover; + width: 100%; + } +} + +.details-table { + text-align: left; + + tr { + height: 1.5rem; + } + + th { + font-weight: 600; + } +} +</style> diff --git a/packages/web-app-files/src/components/SideBar/SideBar.vue b/packages/web-app-files/src/components/SideBar/SideBar.vue index e9580086050..b974dae2d0e 100644 --- a/packages/web-app-files/src/components/SideBar/SideBar.vue +++ b/packages/web-app-files/src/components/SideBar/SideBar.vue @@ -52,10 +52,11 @@ </oc-button> </div> <file-info - v-if="isSingleResource" + v-if="isSingleResource && !highlightedFileIsSpace" class="sidebar-panel__file_info" :is-content-displayed="isContentDisplayed" /> + <space-info v-if="highlightedFileIsSpace" class="sidebar-panel__space_info" /> <div class="sidebar-panel__body"> <template v-if="isContentDisplayed"> <component :is="panel.component" class="sidebar-panel__body-content" /> @@ -95,12 +96,13 @@ import { isLocationCommonActive, isLocationSharesActive } from '../../router' import { computed } from '@vue/composition-api' import FileInfo from './FileInfo.vue' +import SpaceInfo from './SpaceInfo.vue' let visibilityObserver let hiddenObserver export default { - components: { FileInfo }, + components: { FileInfo, SpaceInfo }, provide() { return { @@ -181,7 +183,7 @@ export default { return null }, isSingleResource() { - return !this.areMultipleSelected && !this.isRootFolder + return !this.areMultipleSelected && (!this.isRootFolder || this.highlightedFileIsSpace) }, areMultipleSelected() { return this.selectedFiles && this.selectedFiles.length > 1 @@ -194,6 +196,9 @@ export default { }, highlightedFileFavorite() { return this.highlightedFile?.starred + }, + highlightedFileIsSpace() { + return this.highlightedFile?.type === 'space' } }, watch: { @@ -292,7 +297,10 @@ export default { return } - if (isLocationCommonActive(this.$router, 'files-common-trash')) { + if ( + isLocationCommonActive(this.$router, 'files-common-trash') || + this.highlightedFileIsSpace + ) { this.selectedFile = this.highlightedFile return } @@ -391,7 +399,8 @@ export default { } } - &__file_info { + &__file_info, + &__space_info { border-bottom: 1px solid var(--oc-color-border); background-color: var(--oc-color-background-default); padding: 0 10px; diff --git a/packages/web-app-files/src/components/SideBar/SpaceInfo.vue b/packages/web-app-files/src/components/SideBar/SpaceInfo.vue new file mode 100644 index 00000000000..fe2bc529ad1 --- /dev/null +++ b/packages/web-app-files/src/components/SideBar/SpaceInfo.vue @@ -0,0 +1,52 @@ +<template> + <div class="space_info"> + <div class="space_info__body oc-text-overflow oc-flex oc-flex-middle"> + <div class="oc-mr-s"> + <oc-icon + name="layout-grid" + :size="space.description ? 'large' : 'medium'" + class="oc-display-block" + /> + </div> + <div> + <h3 data-testid="space-info-name" v-text="space.name" /> + <span data-testid="space-info-subtitle" v-text="space.description" /> + </div> + </div> + </div> +</template> + +<script> +import Mixins from '../../mixins' + +export default { + name: 'SpaceInfo', + mixins: [Mixins], + inject: ['displayedItem'], + computed: { + space() { + return this.displayedItem.value + } + } +} +</script> + +<style lang="scss"> +.space_info { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + grid-gap: 5px; + + &__body { + text-align: left; + font-size: 0.75rem; + + h3 { + font-size: 0.9rem; + font-weight: 600; + margin: 0; + } + } +} +</style> diff --git a/packages/web-app-files/src/components/SpaceQuota.vue b/packages/web-app-files/src/components/SpaceQuota.vue new file mode 100644 index 00000000000..865b50c81da --- /dev/null +++ b/packages/web-app-files/src/components/SpaceQuota.vue @@ -0,0 +1,58 @@ +<template> + <div class="space-quota"> + <p class="oc-mb-s oc-mt-rm" v-text="spaceStorageDetailsLabel" /> + <oc-progress + :value="parseInt(quotaUsagePercent)" + :max="100" + size="small" + :variation="quotaProgressVariant" + /> + </div> +</template> + +<script> +import filesize from 'filesize' + +export default { + name: 'SpaceQuota', + props: { + spaceQuota: { + type: Object, + required: true + } + }, + computed: { + spaceStorageDetailsLabel() { + return this.$gettextInterpolate( + this.$gettext('%{used} of %{total} used (%{percentage}% used)'), + { + used: this.quotaUsed, + total: this.quotaTotal, + percentage: this.quotaUsagePercent + } + ) + }, + quotaTotal() { + return filesize(this.spaceQuota.total) + }, + quotaUsed() { + return filesize(this.spaceQuota.used) + }, + quotaUsagePercent() { + return ((this.spaceQuota.used / this.spaceQuota.total) * 100).toFixed(1) + }, + quotaProgressVariant() { + switch (this.spaceQuota.state) { + case 'normal': + return 'primary' + case 'nearing': + return 'warning' + case 'critical': + return 'warning' + default: + return 'danger' + } + } + } +} +</script> diff --git a/packages/web-app-files/src/fileSideBars.js b/packages/web-app-files/src/fileSideBars.js index eae8cb8038e..63ab3881037 100644 --- a/packages/web-app-files/src/fileSideBars.js +++ b/packages/web-app-files/src/fileSideBars.js @@ -5,18 +5,20 @@ import FileVersions from './components/SideBar/Versions/FileVersions.vue' import FileShares from './components/SideBar/Shares/FileShares.vue' import FileLinks from './components/SideBar/Links/FileLinks.vue' import NoSelection from './components/SideBar/NoSelection.vue' -import { isLocationCommonActive } from './router' +import SpaceActions from './components/SideBar/Actions/SpaceActions.vue' +import SpaceDetails from './components/SideBar/Details/SpaceDetails.vue' +import { isLocationCommonActive, isLocationSpacesActive } from './router' export default [ // We don't have file details in the trashbin, yet. // Only allow `actions` panel on trashbin route for now. - ({ rootFolder }) => ({ + ({ rootFolder, highlightedFile }) => ({ app: 'no-selection-item', icon: 'questionnaire-line', component: NoSelection, default: () => true, get enabled() { - return rootFolder + return rootFolder && highlightedFile?.type !== 'space' } }), ({ router, multipleSelection, rootFolder }) => ({ @@ -92,5 +94,23 @@ export default [ } return !!capabilities.core && highlightedFile && highlightedFile.type !== 'folder' } + }), + ({ router, highlightedFile }) => ({ + app: 'details-space-item', + icon: 'questionnaire-line', + component: SpaceDetails, + default: isLocationSpacesActive(router, 'files-spaces-projects'), + get enabled() { + return highlightedFile?.type === 'space' + } + }), + ({ highlightedFile }) => ({ + app: 'space-actions-item', + component: SpaceActions, + icon: 'slideshow-3', + iconFillType: 'line', + get enabled() { + return highlightedFile?.type === 'space' + } }) ] diff --git a/packages/web-app-files/src/mixins/spaces/actions/delete.js b/packages/web-app-files/src/mixins/spaces/actions/delete.js index 02dce11ebfc..564cce18c23 100644 --- a/packages/web-app-files/src/mixins/spaces/actions/delete.js +++ b/packages/web-app-files/src/mixins/spaces/actions/delete.js @@ -14,12 +14,12 @@ export default { return this.$gettext('Delete') }, handler: this.$_delete_trigger, - isEnabled: ({ spaces }) => { - if (spaces.length !== 1) { + isEnabled: ({ resources }) => { + if (resources.length !== 1) { return false } - return spaces[0].disabled + return resources[0].disabled }, componentType: 'oc-button', class: 'oc-files-actions-delete-trigger' @@ -37,21 +37,21 @@ export default { ]), ...mapMutations('Files', ['REMOVE_FILE']), - $_delete_trigger({ spaces }) { - if (spaces.length !== 1) { + $_delete_trigger({ resources }) { + if (resources.length !== 1) { return } const modal = { variation: 'danger', - title: this.$gettext('Delete space') + ' ' + spaces[0].name, + title: this.$gettext('Delete space') + ' ' + resources[0].name, cancelText: this.$gettext('Cancel'), confirmText: this.$gettext('Delete'), icon: 'alarm-warning', message: this.$gettext('Are you sure you want to delete this space?'), hasInput: false, onCancel: this.hideModal, - onConfirm: () => this.$_delete_deleteSpace(spaces[0].id) + onConfirm: () => this.$_delete_deleteSpace(resources[0].id) } this.createModal(modal) diff --git a/packages/web-app-files/src/mixins/spaces/actions/disable.js b/packages/web-app-files/src/mixins/spaces/actions/disable.js index 9a7222d8f6a..75a018c6a13 100644 --- a/packages/web-app-files/src/mixins/spaces/actions/disable.js +++ b/packages/web-app-files/src/mixins/spaces/actions/disable.js @@ -14,12 +14,12 @@ export default { return this.$gettext('Disable') }, handler: this.$_disable_trigger, - isEnabled: ({ spaces }) => { - if (spaces.length !== 1) { + isEnabled: ({ resources }) => { + if (resources.length !== 1) { return false } - return !spaces[0].disabled + return !resources[0].disabled }, componentType: 'oc-button', class: 'oc-files-actions-disable-trigger' @@ -37,21 +37,21 @@ export default { ]), ...mapMutations('Files', ['UPDATE_RESOURCE_FIELD']), - $_disable_trigger({ spaces }) { - if (spaces.length !== 1) { + $_disable_trigger({ resources }) { + if (resources.length !== 1) { return } const modal = { variation: 'danger', - title: this.$gettext('Disable space') + ' ' + spaces[0].name, + title: this.$gettext('Disable space') + ' ' + resources[0].name, cancelText: this.$gettext('Cancel'), confirmText: this.$gettext('Disable'), icon: 'alarm-warning', message: this.$gettext('Are you sure you want to disable this space?'), hasInput: false, onCancel: this.hideModal, - onConfirm: () => this.$_disable_disableSpace(spaces[0].id) + onConfirm: () => this.$_disable_disableSpace(resources[0].id) } this.createModal(modal) diff --git a/packages/web-app-files/src/mixins/spaces/actions/editDescription.js b/packages/web-app-files/src/mixins/spaces/actions/editDescription.js index 1e9b7a46ec0..833d8f54b9d 100644 --- a/packages/web-app-files/src/mixins/spaces/actions/editDescription.js +++ b/packages/web-app-files/src/mixins/spaces/actions/editDescription.js @@ -31,22 +31,22 @@ export default { ]), ...mapMutations('Files', ['UPDATE_RESOURCE_FIELD']), - $_editDescription_trigger({ spaces }) { - if (spaces.length !== 1) { + $_editDescription_trigger({ resources }) { + if (resources.length !== 1) { return } const modal = { variation: 'passive', - title: this.$gettext('Change description for space') + ' ' + spaces[0].name, + title: this.$gettext('Change description for space') + ' ' + resources[0].name, cancelText: this.$gettext('Cancel'), confirmText: this.$gettext('Confirm'), hasInput: true, inputLabel: this.$gettext('Space description'), - inputValue: spaces[0].description, + inputValue: resources[0].description, onCancel: this.hideModal, onConfirm: (description) => - this.$_editDescription_editDescriptionSpace(spaces[0].id, description) + this.$_editDescription_editDescriptionSpace(resources[0].id, description) } this.createModal(modal) diff --git a/packages/web-app-files/src/mixins/spaces/actions/rename.js b/packages/web-app-files/src/mixins/spaces/actions/rename.js index 5cf4049b939..f7bde4bd7d0 100644 --- a/packages/web-app-files/src/mixins/spaces/actions/rename.js +++ b/packages/web-app-files/src/mixins/spaces/actions/rename.js @@ -14,7 +14,7 @@ export default { return this.$gettext('Rename') }, handler: this.$_rename_trigger, - isEnabled: ({ spaces }) => spaces.length === 1, + isEnabled: ({ resources }) => resources.length === 1, componentType: 'oc-button', class: 'oc-files-actions-rename-trigger' } @@ -31,21 +31,21 @@ export default { ]), ...mapMutations('Files', ['UPDATE_RESOURCE_FIELD']), - $_rename_trigger({ spaces }) { - if (spaces.length !== 1) { + $_rename_trigger({ resources }) { + if (resources.length !== 1) { return } const modal = { variation: 'passive', - title: this.$gettext('Rename space') + ' ' + spaces[0].name, + title: this.$gettext('Rename space') + ' ' + resources[0].name, cancelText: this.$gettext('Cancel'), confirmText: this.$gettext('Rename'), hasInput: true, inputLabel: this.$gettext('Space name'), - inputValue: spaces[0].name, + inputValue: resources[0].name, onCancel: this.hideModal, - onConfirm: (name) => this.$_rename_renameSpace(spaces[0].id, name), + onConfirm: (name) => this.$_rename_renameSpace(resources[0].id, name), onInput: this.$_rename_checkName } diff --git a/packages/web-app-files/src/mixins/spaces/actions/restore.js b/packages/web-app-files/src/mixins/spaces/actions/restore.js index a8ab0968bc9..7bbd5afd937 100644 --- a/packages/web-app-files/src/mixins/spaces/actions/restore.js +++ b/packages/web-app-files/src/mixins/spaces/actions/restore.js @@ -14,12 +14,12 @@ export default { return this.$gettext('Restore') }, handler: this.$_restore_trigger, - isEnabled: ({ spaces }) => { - if (spaces.length !== 1) { + isEnabled: ({ resources }) => { + if (resources.length !== 1) { return false } - return spaces[0].disabled + return resources[0].disabled }, componentType: 'oc-button', class: 'oc-files-actions-restore-trigger' @@ -37,21 +37,21 @@ export default { ]), ...mapMutations('Files', ['UPDATE_RESOURCE_FIELD']), - $_restore_trigger({ spaces }) { - if (spaces.length !== 1) { + $_restore_trigger({ resources }) { + if (resources.length !== 1) { return } const modal = { variation: 'passive', - title: this.$gettext('Restore space') + ' ' + spaces[0].name, + title: this.$gettext('Restore space') + ' ' + resources[0].name, cancelText: this.$gettext('Cancel'), confirmText: this.$gettext('Restore'), icon: 'alarm-warning', message: this.$gettext('Are you sure you want to restore this space?'), hasInput: false, onCancel: this.hideModal, - onConfirm: () => this.$_restore_restoreSpace(spaces[0].id) + onConfirm: () => this.$_restore_restoreSpace(resources[0].id) } this.createModal(modal) diff --git a/packages/web-app-files/src/mixins/spaces/actions/showDetails.js b/packages/web-app-files/src/mixins/spaces/actions/showDetails.js index d8772747761..f9422f7bc2c 100644 --- a/packages/web-app-files/src/mixins/spaces/actions/showDetails.js +++ b/packages/web-app-files/src/mixins/spaces/actions/showDetails.js @@ -1,4 +1,4 @@ -import { mapActions } from 'vuex' +import { mapActions, mapMutations } from 'vuex' export default { computed: { @@ -10,7 +10,7 @@ export default { iconFillType: 'line', label: () => this.$gettext('Details'), handler: this.$_showDetails_trigger, - isEnabled: () => false, // @TODO As soon as we have the details + isEnabled: ({ resources }) => resources.length === 1, componentType: 'oc-button', class: 'oc-files-actions-show-details-trigger' } @@ -18,10 +18,16 @@ export default { } }, methods: { - ...mapActions('Files/sidebar', { openSidebar: 'open' }), + ...mapActions('Files/sidebar', { openSidebar: 'open', closeSidebar: 'close' }), + ...mapMutations('Files', ['SET_FILE_SELECTION']), - async $_showDetails_trigger() { - await this.openSidebar() + $_showDetails_trigger({ resources }) { + if (resources.length !== 1) { + return + } + + this.SET_FILE_SELECTION([resources[0]]) + this.openSidebar() } } } diff --git a/packages/web-app-files/src/router/spaces.ts b/packages/web-app-files/src/router/spaces.ts index 33ffc763d52..f9aac42b612 100644 --- a/packages/web-app-files/src/router/spaces.ts +++ b/packages/web-app-files/src/router/spaces.ts @@ -36,7 +36,7 @@ export const buildRoutes = (components: RouteComponents): RouteConfig[] => [ component: components.Spaces.Projects, meta: { hideFilelistActions: true, - hasBulkActions: true, + hasBulkActions: false, hideViewOptions: true, title: $gettext('Spaces') } diff --git a/packages/web-app-files/src/views/spaces/Projects.vue b/packages/web-app-files/src/views/spaces/Projects.vue index 920b523ce66..55a386b1319 100644 --- a/packages/web-app-files/src/views/spaces/Projects.vue +++ b/packages/web-app-files/src/views/spaces/Projects.vue @@ -103,14 +103,14 @@ <li v-for="(action, actionIndex) in getContextMenuActions(space)" :key="`action-${actionIndex}`" - class="oc-spaces-context-action oc-py-xs oc-px-s" + class="oc-files-context-action oc-px-s" > <oc-button appearance="raw" justify-content="left" - @click="action.handler({ spaces: [space] })" + @click="action.handler({ resources: [space] })" > - <oc-icon :name="action.icon" /> + <oc-icon :name="action.icon" fill-type="line" class="oc-flex" /> {{ action.label() }} </oc-button> </li> @@ -231,17 +231,22 @@ export default { }, methods: { ...mapActions(['createModal', 'hideModal', 'setModalInputErrorMessage']), - ...mapMutations('Files', ['SET_CURRENT_FOLDER', 'LOAD_FILES', 'CLEAR_CURRENT_FILES_LIST']), + ...mapMutations('Files', [ + 'SET_CURRENT_FOLDER', + 'LOAD_FILES', + 'CLEAR_CURRENT_FILES_LIST', + 'SET_FILE_SELECTION' + ]), getContextMenuActions(space) { return [ ...this.$_rename_items, ...this.$_editDescription_items, - ...this.$_showDetails_items, ...this.$_restore_items, ...this.$_delete_items, - ...this.$_disable_items - ].filter((item) => item.isEnabled({ spaces: [space] })) + ...this.$_disable_items, + ...this.$_showDetails_items + ].filter((item) => item.isEnabled({ resources: [space] })) }, getSpaceProjectRoute({ id, name }) { diff --git a/packages/web-app-files/tests/unit/components/SideBar/Details/SpaceDetails.spec.js b/packages/web-app-files/tests/unit/components/SideBar/Details/SpaceDetails.spec.js new file mode 100644 index 00000000000..a1dfa2e7670 --- /dev/null +++ b/packages/web-app-files/tests/unit/components/SideBar/Details/SpaceDetails.spec.js @@ -0,0 +1,102 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils' +import Vuex from 'vuex' +import SpaceDetails from '../../../../../src/components/SideBar/Details/SpaceDetails.vue' +import stubs from '../../../../../../../tests/unit/stubs' +import GetTextPlugin from 'vue-gettext' +import AsyncComputed from 'vue-async-computed' +import VueCompositionAPI from '@vue/composition-api/dist/vue-composition-api' + +const localVue = createLocalVue() +localVue.use(Vuex) +localVue.use(AsyncComputed) +localVue.use(VueCompositionAPI) +localVue.use(GetTextPlugin, { + translations: 'does-not-matter.json', + silent: true +}) +const OcTooltip = jest.fn() + +const spaceMock = { + type: 'space', + name: ' space', + id: '1', + mdate: 'Wed, 21 Oct 2015 07:28:00 GMT', + spaceQuota: { + used: 100, + total: 1000 + } +} + +const formDateFromJSDate = jest.fn().mockImplementation(() => 'ABSOLUTE_TIME') +const formDateFromHTTP = jest.fn().mockImplementation(() => 'ABSOLUTE_TIME') +const refreshShareDetailsTree = jest.fn() +beforeEach(() => { + formDateFromJSDate.mockClear() + formDateFromHTTP.mockClear() + refreshShareDetailsTree.mockReset() +}) + +describe('Details SideBar Panel', () => { + it('displayes the details side panel', () => { + const wrapper = createWrapper(spaceMock) + expect(wrapper).toMatchSnapshot() + }) +}) + +function createWrapper(spaceResource) { + const component = { + ...SpaceDetails, + setup: () => ({ + spaceImage: '', + owners: [], + loadImageTask: { + isRunning: false, + perform: jest.fn() + }, + loadOwnersTask: { + isRunning: false, + perform: jest.fn() + } + }) + } + return shallowMount(component, { + store: new Vuex.Store({ + getters: { + user: function () { + return { id: 'marie' } + } + }, + modules: { + Files: { + namespaced: true, + state: { + sharesTree: {} + }, + getters: { + highlightedFile: function () { + return spaceResource + } + } + } + } + }), + localVue, + stubs: stubs, + directives: { + OcTooltip + }, + mixins: [ + { + methods: { + formDateFromJSDate, + formDateFromHTTP + } + } + ], + provide: { + displayedItem: { + value: spaceResource + } + } + }) +} diff --git a/packages/web-app-files/tests/unit/components/SideBar/Details/__snapshots__/SpaceDetails.spec.js.snap b/packages/web-app-files/tests/unit/components/SideBar/Details/__snapshots__/SpaceDetails.spec.js.snap new file mode 100644 index 00000000000..a63d31fd829 --- /dev/null +++ b/packages/web-app-files/tests/unit/components/SideBar/Details/__snapshots__/SpaceDetails.spec.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Details SideBar Panel displayes the details side panel 1`] = ` +<div id="oc-space-details-sidebar"> + <div class="oc-space-details-sidebar-image oc-text-center"> + <oc-icon-stub name="layout-grid" size="xxlarge" class="space-default-image oc-px-m oc-py-m"></oc-icon-stub> + </div> + <!----> + <div> + <table aria-label="Overview of the information about the selected space" class="details-table"> + <tr> + <th scope="col" class="oc-pr-s">Last activity</th> + <td>Invalid DateTime</td> + </tr> + <!----> + <tr> + <th scope="col" class="oc-pr-s">Manager</th> + <td><span></span></td> + </tr> + <tr> + <th scope="col" class="oc-pr-s">Quota</th> + <td> + <space-quota-stub spacequota="[object Object]"></space-quota-stub> + </td> + </tr> + </table> + </div> +</div> +`; diff --git a/packages/web-app-files/tests/unit/components/SideBar/SpaceInfo.spec.js b/packages/web-app-files/tests/unit/components/SideBar/SpaceInfo.spec.js new file mode 100644 index 00000000000..511a22eb235 --- /dev/null +++ b/packages/web-app-files/tests/unit/components/SideBar/SpaceInfo.spec.js @@ -0,0 +1,106 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils' +import Vuex from 'vuex' +import GetTextPlugin from 'vue-gettext' +import AsyncComputed from 'vue-async-computed' + +import stubs from '@/tests/unit/stubs' + +import SpaceInfo from '@files/src/components/SideBar/SpaceInfo.vue' + +const spaceMock = { + type: 'space', + name: ' space', + id: '1', + mdate: 'Wed, 21 Oct 2015 07:28:00 GMT', + spaceQuota: { + used: 100 + } +} + +const localVue = createLocalVue() +localVue.use(Vuex) +localVue.use(AsyncComputed) +localVue.use(GetTextPlugin, { + translations: 'does-not-matter.json', + silent: true +}) + +const selectors = { + name: '[data-testid="space-info-name"]', + subtitle: '[data-testid="space-info-subtitle"]' +} + +const formDateFromRFC = jest.fn() +const formRelativeDateFromRFC = jest.fn() +const resetDateMocks = () => { + formDateFromRFC.mockReset() + formRelativeDateFromRFC.mockReset() + formDateFromRFC.mockImplementation(() => 'ABSOLUTE_TIME') + formRelativeDateFromRFC.mockImplementation(() => 'RELATIVE_TIME') +} + +describe('SpaceInfo', () => { + it('shows space info', () => { + resetDateMocks() + + const wrapper = createWrapper(spaceMock) + expect(wrapper.find(selectors.name).exists()).toBeTruthy() + expect(wrapper.find(selectors.subtitle).exists()).toBeTruthy() + expect(wrapper).toMatchSnapshot() + }) +}) + +function createWrapper(spaceResource) { + return shallowMount(SpaceInfo, { + store: new Vuex.Store({ + getters: { + user: function () { + return { id: 'marie' } + }, + capabilities: jest.fn(() => ({})) + }, + modules: { + Files: { + namespaced: true, + getters: { + highlightedFile: function () { + return spaceResource + } + } + } + } + }), + localVue, + stubs: { + ...stubs, + 'oc-resource-icon': true, + 'oc-resource-name': true + }, + directives: { + OcTooltip: null + }, + mixins: [ + { + methods: { + formDateFromRFC, + formRelativeDateFromRFC + } + } + ], + mocks: { + $router: { + currentRoute: { + name: 'some-route', + query: { page: 1 } + }, + resolve: (r) => ({ href: r.name }) + }, + publicPage: () => false + }, + provide: { + displayedItem: { + value: spaceResource + } + } + }) +} diff --git a/packages/web-app-files/tests/unit/components/SideBar/__snapshots__/SpaceInfo.spec.js.snap b/packages/web-app-files/tests/unit/components/SideBar/__snapshots__/SpaceInfo.spec.js.snap new file mode 100644 index 00000000000..a76e34a8f09 --- /dev/null +++ b/packages/web-app-files/tests/unit/components/SideBar/__snapshots__/SpaceInfo.spec.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SpaceInfo shows space info 1`] = ` +<div class="space_info"> + <div class="space_info__body oc-text-overflow oc-flex oc-flex-middle"> + <div class="oc-mr-s"> + <oc-icon-stub name="layout-grid" size="medium" class="oc-display-block"></oc-icon-stub> + </div> + <div> + <h3 data-testid="space-info-name"> space</h3> <span data-testid="space-info-subtitle"></span> + </div> + </div> +</div> +`; diff --git a/packages/web-app-files/tests/unit/components/SpaceQuota.spec.js b/packages/web-app-files/tests/unit/components/SpaceQuota.spec.js new file mode 100644 index 00000000000..40ac4ebbc0a --- /dev/null +++ b/packages/web-app-files/tests/unit/components/SpaceQuota.spec.js @@ -0,0 +1,42 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils' +import Vuex from 'vuex' +import DesignSystem from 'owncloud-design-system' +import GetTextPlugin from 'vue-gettext' + +import SpaceQuota from '@files/src/components/SpaceQuota' + +const localVue = createLocalVue() +localVue.use(Vuex) +localVue.use(DesignSystem) +localVue.use(GetTextPlugin, { + translations: 'does-not-matter.json', + silent: true +}) + +describe('SpaceQuota component', () => { + it('renders the space storage quota label', () => { + const wrapper = getWrapper({ total: 10, used: 1, state: 'normal' }) + expect(wrapper.find('.space-quota').exists()).toBeTruthy() + expect(wrapper).toMatchSnapshot() + }) + it.each([ + { state: 'normal', expectedVariation: 'primary' }, + { state: 'nearing', expectedVariation: 'warning' }, + { state: 'critical', expectedVariation: 'warning' }, + { state: 'exceeded', expectedVariation: 'danger' } + ])('renders the progress variant correctly', (dataSet) => { + const wrapper = getWrapper({ total: 10, used: 1, state: dataSet.state }) + const progressBar = wrapper.find('.space-quota oc-progress-stub') + expect(progressBar.exists()).toBeTruthy() + expect(progressBar.props().variation).toBe(dataSet.expectedVariation) + }) +}) + +function getWrapper(spaceQuota) { + return shallowMount(SpaceQuota, { + localVue, + propsData: { + spaceQuota + } + }) +} diff --git a/packages/web-app-files/tests/unit/components/__snapshots__/SpaceQuota.spec.js.snap b/packages/web-app-files/tests/unit/components/__snapshots__/SpaceQuota.spec.js.snap new file mode 100644 index 00000000000..f6d007d7977 --- /dev/null +++ b/packages/web-app-files/tests/unit/components/__snapshots__/SpaceQuota.spec.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SpaceQuota component renders the space storage quota label 1`] = ` +<div class="space-quota"> + <p class="oc-mb-s oc-mt-rm">1 B of 10 B used (10.0% used)</p> + <oc-progress-stub value="10" max="100" size="small" variation="primary"></oc-progress-stub> +</div> +`; diff --git a/packages/web-app-files/tests/unit/mixins/spaces/delete.spec.js b/packages/web-app-files/tests/unit/mixins/spaces/delete.spec.js index 9a2a33eae2a..08657a68a7e 100644 --- a/packages/web-app-files/tests/unit/mixins/spaces/delete.spec.js +++ b/packages/web-app-files/tests/unit/mixins/spaces/delete.spec.js @@ -50,14 +50,40 @@ describe('delete', () => { }) } + describe('isEnabled property', () => { + it('should be false when not resource given', () => { + const wrapper = getWrapper() + expect(wrapper.vm.$_delete_items[0].isEnabled({ resources: [] })).toBe(false) + }) + it('should be false when the space is not disabled', () => { + const wrapper = getWrapper() + expect( + wrapper.vm.$_delete_items[0].isEnabled({ resources: [{ id: 1, disabled: false }] }) + ).toBe(false) + }) + it('should be true when the space is disabled', () => { + const wrapper = getWrapper() + expect( + wrapper.vm.$_delete_items[0].isEnabled({ resources: [{ id: 1, disabled: true }] }) + ).toBe(true) + }) + }) + describe('method "$_delete_trigger"', () => { it('should trigger the delete modal window', async () => { const wrapper = getWrapper() const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') - await wrapper.vm.$_delete_trigger({ spaces: [{ id: 1 }] }) + await wrapper.vm.$_delete_trigger({ resources: [{ id: 1 }] }) expect(spyCreateModalStub).toHaveBeenCalledTimes(1) }) + it('should not trigger the delete modal window without any resource', async () => { + const wrapper = getWrapper() + const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') + await wrapper.vm.$_delete_trigger({ resources: [] }) + + expect(spyCreateModalStub).toHaveBeenCalledTimes(0) + }) }) describe('method "$_delete_deleteSpace"', () => { diff --git a/packages/web-app-files/tests/unit/mixins/spaces/disable.spec.js b/packages/web-app-files/tests/unit/mixins/spaces/disable.spec.js new file mode 100644 index 00000000000..d23eaef8f1c --- /dev/null +++ b/packages/web-app-files/tests/unit/mixins/spaces/disable.spec.js @@ -0,0 +1,115 @@ +import Vuex from 'vuex' +import { createStore } from 'vuex-extensions' +import { mount, createLocalVue } from '@vue/test-utils' +import disable from '@files/src/mixins/spaces/actions/disable.js' +import { createLocationSpaces } from '../../../../src/router' +import mockAxios from 'jest-mock-axios' + +const localVue = createLocalVue() +localVue.use(Vuex) + +describe('disable', () => { + const Component = { + render() {}, + mixins: [disable] + } + + function getWrapper() { + return mount(Component, { + localVue, + mocks: { + $router: { + currentRoute: createLocationSpaces('files-spaces-projects'), + resolve: (r) => { + return { href: r.name } + } + }, + $gettext: jest.fn() + }, + store: createStore(Vuex.Store, { + actions: { + createModal: jest.fn(), + hideModal: jest.fn(), + showMessage: jest.fn(), + setModalInputErrorMessage: jest.fn() + }, + getters: { + configuration: () => ({ + server: 'https://example.com' + }), + getToken: () => 'token' + }, + modules: { + Files: { + namespaced: true, + mutations: { + UPDATE_RESOURCE_FIELD: jest.fn() + } + } + } + }) + }) + } + + describe('isEnabled property', () => { + it('should be false when no resource given', () => { + const wrapper = getWrapper() + expect(wrapper.vm.$_disable_items[0].isEnabled({ resources: [] })).toBe(false) + }) + it('should be true when the space is not disabled', () => { + const wrapper = getWrapper() + expect( + wrapper.vm.$_disable_items[0].isEnabled({ resources: [{ id: 1, disabled: false }] }) + ).toBe(true) + }) + it('should be false when the space is disabled', () => { + const wrapper = getWrapper() + expect( + wrapper.vm.$_disable_items[0].isEnabled({ resources: [{ id: 1, disabled: true }] }) + ).toBe(false) + }) + }) + + describe('method "$_disable_trigger"', () => { + it('should trigger the disable modal window', async () => { + const wrapper = getWrapper() + const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') + await wrapper.vm.$_disable_trigger({ resources: [{ id: 1 }] }) + + expect(spyCreateModalStub).toHaveBeenCalledTimes(1) + }) + it('should not trigger the disable modal window without any resource', async () => { + const wrapper = getWrapper() + const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') + await wrapper.vm.$_disable_trigger({ resources: [] }) + + expect(spyCreateModalStub).toHaveBeenCalledTimes(0) + }) + }) + + describe('method "$_disable_disableSpace"', () => { + it('should hide the modal on success', async () => { + mockAxios.request.mockImplementationOnce(() => { + return Promise.resolve() + }) + + const wrapper = getWrapper() + const hideModalStub = jest.spyOn(wrapper.vm, 'hideModal') + await wrapper.vm.$_disable_disableSpace(1) + + expect(hideModalStub).toHaveBeenCalledTimes(1) + }) + + it('should show message on error', async () => { + mockAxios.request.mockImplementationOnce(() => { + return Promise.reject(new Error()) + }) + + const wrapper = getWrapper() + const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') + await wrapper.vm.$_disable_disableSpace(1) + + expect(showMessageStub).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/web-app-files/tests/unit/mixins/spaces/editDescription.spec.js b/packages/web-app-files/tests/unit/mixins/spaces/editDescription.spec.js new file mode 100644 index 00000000000..37317b9fd3f --- /dev/null +++ b/packages/web-app-files/tests/unit/mixins/spaces/editDescription.spec.js @@ -0,0 +1,93 @@ +import Vuex from 'vuex' +import { createStore } from 'vuex-extensions' +import { mount, createLocalVue } from '@vue/test-utils' +import EditDescription from '@files/src/mixins/spaces/actions/editDescription.js' +import { createLocationSpaces } from '../../../../src/router' +import mockAxios from 'jest-mock-axios' + +const localVue = createLocalVue() +localVue.use(Vuex) + +describe('editDescription', () => { + const Component = { + render() {}, + mixins: [EditDescription] + } + + function getWrapper() { + return mount(Component, { + localVue, + mocks: { + $router: { + currentRoute: createLocationSpaces('files-spaces-projects'), + resolve: (r) => { + return { href: r.name } + } + }, + $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: { + UPDATE_RESOURCE_FIELD: jest.fn() + } + } + } + }) + }) + } + + describe('method "$_editDescription_trigger"', () => { + it('should trigger the editDescription modal window with one resource', async () => { + const wrapper = getWrapper() + const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') + await wrapper.vm.$_editDescription_trigger({ resources: [{ id: 1 }] }) + + expect(spyCreateModalStub).toHaveBeenCalledTimes(1) + }) + it('should not trigger the editDescription modal window with no resource', async () => { + const wrapper = getWrapper() + const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') + await wrapper.vm.$_editDescription_trigger({ resources: [] }) + + expect(spyCreateModalStub).toHaveBeenCalledTimes(0) + }) + }) + + describe('method "$_editDescription_editDescriptionSpace"', () => { + it('should hide the modal on success', async () => { + mockAxios.request.mockImplementationOnce(() => { + return Promise.resolve() + }) + const wrapper = getWrapper() + const hideModalStub = jest.spyOn(wrapper.vm, 'hideModal') + await wrapper.vm.$_editDescription_editDescriptionSpace(1) + + expect(hideModalStub).toHaveBeenCalledTimes(1) + }) + + it('should show message on error', async () => { + mockAxios.request.mockImplementationOnce(() => { + return Promise.reject(new Error()) + }) + const wrapper = getWrapper() + const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') + await wrapper.vm.$_editDescription_editDescriptionSpace(1) + + expect(showMessageStub).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/web-app-files/tests/unit/mixins/spaces/rename.spec.js b/packages/web-app-files/tests/unit/mixins/spaces/rename.spec.js index d052a5e1b26..2b1e4b8822d 100644 --- a/packages/web-app-files/tests/unit/mixins/spaces/rename.spec.js +++ b/packages/web-app-files/tests/unit/mixins/spaces/rename.spec.js @@ -55,10 +55,17 @@ describe('rename', () => { it('should trigger the rename modal window', async () => { const wrapper = getWrapper() const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') - await wrapper.vm.$_rename_trigger({ spaces: [{ id: 1, name: 'renamed space' }] }) + await wrapper.vm.$_rename_trigger({ resources: [{ id: 1, name: 'renamed space' }] }) expect(spyCreateModalStub).toHaveBeenCalledTimes(1) }) + it('should not trigger the rename modal window without any resource', async () => { + const wrapper = getWrapper() + const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') + await wrapper.vm.$_rename_trigger({ resources: [] }) + + expect(spyCreateModalStub).toHaveBeenCalledTimes(0) + }) }) describe('method "$_rename_checkName"', () => { diff --git a/packages/web-app-files/tests/unit/mixins/spaces/restore.spec.js b/packages/web-app-files/tests/unit/mixins/spaces/restore.spec.js new file mode 100644 index 00000000000..484fcaec5d5 --- /dev/null +++ b/packages/web-app-files/tests/unit/mixins/spaces/restore.spec.js @@ -0,0 +1,115 @@ +import Vuex from 'vuex' +import { createStore } from 'vuex-extensions' +import { mount, createLocalVue } from '@vue/test-utils' +import restore from '@files/src/mixins/spaces/actions/restore.js' +import { createLocationSpaces } from '../../../../src/router' +import mockAxios from 'jest-mock-axios' + +const localVue = createLocalVue() +localVue.use(Vuex) + +describe('restore', () => { + const Component = { + render() {}, + mixins: [restore] + } + + function getWrapper() { + return mount(Component, { + localVue, + mocks: { + $router: { + currentRoute: createLocationSpaces('files-spaces-projects'), + resolve: (r) => { + return { href: r.name } + } + }, + $gettext: jest.fn() + }, + store: createStore(Vuex.Store, { + actions: { + createModal: jest.fn(), + hideModal: jest.fn(), + showMessage: jest.fn(), + setModalInputErrorMessage: jest.fn() + }, + getters: { + configuration: () => ({ + server: 'https://example.com' + }), + getToken: () => 'token' + }, + modules: { + Files: { + namespaced: true, + mutations: { + UPDATE_RESOURCE_FIELD: jest.fn() + } + } + } + }) + }) + } + + describe('isEnabled property', () => { + it('should be false when no resource given', () => { + const wrapper = getWrapper() + expect(wrapper.vm.$_restore_items[0].isEnabled({ resources: [] })).toBe(false) + }) + it('should be false when the space is not disabled', () => { + const wrapper = getWrapper() + expect( + wrapper.vm.$_restore_items[0].isEnabled({ resources: [{ id: 1, disabled: false }] }) + ).toBe(false) + }) + it('should be true when the space is disabled', () => { + const wrapper = getWrapper() + expect( + wrapper.vm.$_restore_items[0].isEnabled({ resources: [{ id: 1, disabled: true }] }) + ).toBe(true) + }) + }) + + describe('method "$_restore_trigger"', () => { + it('should trigger the restore modal window', async () => { + const wrapper = getWrapper() + const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') + await wrapper.vm.$_restore_trigger({ resources: [{ id: 1 }] }) + + expect(spyCreateModalStub).toHaveBeenCalledTimes(1) + }) + it('should not trigger the restore modal window without any resource', async () => { + const wrapper = getWrapper() + const spyCreateModalStub = jest.spyOn(wrapper.vm, 'createModal') + await wrapper.vm.$_restore_trigger({ resources: [] }) + + expect(spyCreateModalStub).toHaveBeenCalledTimes(0) + }) + }) + + describe('method "$_restore_restoreSpace"', () => { + it('should hide the modal on success', async () => { + mockAxios.request.mockImplementationOnce(() => { + return Promise.resolve() + }) + + const wrapper = getWrapper() + const hideModalStub = jest.spyOn(wrapper.vm, 'hideModal') + await wrapper.vm.$_restore_restoreSpace(1, 'renamed space') + + expect(hideModalStub).toHaveBeenCalledTimes(1) + }) + + it('should show message on error', async () => { + mockAxios.request.mockImplementationOnce(() => { + return Promise.reject(new Error()) + }) + + const wrapper = getWrapper() + const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') + await wrapper.vm.$_restore_restoreSpace(1) + + expect(showMessageStub).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/web-app-files/tests/unit/mixins/spaces/showDetails.spec.js b/packages/web-app-files/tests/unit/mixins/spaces/showDetails.spec.js new file mode 100644 index 00000000000..2faa986bcb7 --- /dev/null +++ b/packages/web-app-files/tests/unit/mixins/spaces/showDetails.spec.js @@ -0,0 +1,70 @@ +import Vuex from 'vuex' +import { createStore } from 'vuex-extensions' +import { mount, createLocalVue } from '@vue/test-utils' +import ShowDetails from '@files/src/mixins/spaces/actions/showDetails.js' +import { createLocationSpaces } from '../../../../src/router' + +const localVue = createLocalVue() +localVue.use(Vuex) + +describe('showDetails', () => { + const Component = { + render() {}, + mixins: [ShowDetails] + } + + function getWrapper() { + return mount(Component, { + localVue, + mocks: { + $router: { + currentRoute: createLocationSpaces('files-spaces-projects'), + resolve: (r) => { + return { href: r.name } + } + }, + $gettext: jest.fn() + }, + store: createStore(Vuex.Store, { + modules: { + Files: { + namespaced: true, + mutations: { + SET_FILE_SELECTION: jest.fn() + }, + modules: { + sidebar: { + namespaced: true, + actions: { + close: jest.fn(), + open: jest.fn() + } + } + } + } + } + }) + }) + } + + describe('method "$_showDetails_trigger"', () => { + it('should trigger the sidebar for one resource', async () => { + const wrapper = getWrapper() + const setSelectionStub = jest.spyOn(wrapper.vm, 'SET_FILE_SELECTION') + const openSidebarStub = jest.spyOn(wrapper.vm, 'openSidebar') + await wrapper.vm.$_showDetails_trigger({ resources: [{ id: 1 }] }) + + expect(setSelectionStub).toHaveBeenCalledTimes(1) + expect(openSidebarStub).toHaveBeenCalledTimes(1) + }) + it('should not trigger the sidebar without any resource', async () => { + const wrapper = getWrapper() + const setSelectionStub = jest.spyOn(wrapper.vm, 'SET_FILE_SELECTION') + const openSidebarStub = jest.spyOn(wrapper.vm, 'openSidebar') + await wrapper.vm.$_showDetails_trigger({ resources: [] }) + + expect(setSelectionStub).toHaveBeenCalledTimes(0) + expect(openSidebarStub).toHaveBeenCalledTimes(0) + }) + }) +}) 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 94c49e2c834..a02fb2060fd 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 @@ -27,12 +27,15 @@ exports[`Spaces component should list spaces 1`] = ` <div id="space-context-drop-1" class="oc-drop oc-box-shadow-medium oc-rounded" options="[object Object]"> <div class="oc-card oc-card-body oc-rounded oc-background-highlight oc-p-s"> <ul class="oc-list oc-files-context-actions"> - <li class="oc-spaces-context-action oc-py-xs oc-px-s"><button class="oc-button oc-button-m oc-button-justify-content-left oc-button-gap-m oc-button-passive oc-button-passive-raw"><span class="oc-icon oc-icon-m oc-icon-passive"><!----></span> + <li class="oc-files-context-action oc-px-s"><button class="oc-button oc-button-m oc-button-justify-content-left oc-button-gap-m oc-button-passive oc-button-passive-raw"><span class="oc-flex oc-icon oc-icon-m oc-icon-passive"><!----></span> Rename </button></li> - <li class="oc-spaces-context-action oc-py-xs oc-px-s"><button class="oc-button oc-button-m oc-button-justify-content-left oc-button-gap-m oc-button-passive oc-button-passive-raw"><span class="oc-icon oc-icon-m oc-icon-passive"><!----></span> + <li class="oc-files-context-action oc-px-s"><button class="oc-button oc-button-m oc-button-justify-content-left oc-button-gap-m oc-button-passive oc-button-passive-raw"><span class="oc-flex oc-icon oc-icon-m oc-icon-passive"><!----></span> Disable </button></li> + <li class="oc-files-context-action oc-px-s"><button class="oc-button oc-button-m oc-button-justify-content-left oc-button-gap-m oc-button-passive oc-button-passive-raw"><span class="oc-flex oc-icon oc-icon-m oc-icon-passive"><!----></span> + Details + </button></li> </ul> </div> </div> diff --git a/packages/web-client/src/index.ts b/packages/web-client/src/index.ts index eb99d411c9e..7adc7b356f9 100644 --- a/packages/web-client/src/index.ts +++ b/packages/web-client/src/index.ts @@ -4,7 +4,9 @@ import { MeDrivesApi, Drive, DrivesApiFactory, - CollectionOfDrives + CollectionOfDrives, + UserApiFactory, + User } from './generated' export interface Graph { @@ -15,6 +17,9 @@ export interface Graph { updateDrive: (id: string, drive: Drive, options: any) => AxiosPromise<Drive> deleteDrive: (id: string, ifMatch: string, options: any) => AxiosPromise<void> } + users: { + getUser: (userId: string) => AxiosPromise<User> + } } const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => { @@ -24,6 +29,7 @@ const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => { }) const meDrivesApi = new MeDrivesApi(config, config.basePath, axiosClient) + const userApiFactory = UserApiFactory(config, config.basePath, axiosClient) const drivesApiFactory = DrivesApiFactory(config, config.basePath, axiosClient) return { @@ -36,6 +42,9 @@ const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => { drivesApiFactory.updateDrive(id, drive, options), deleteDrive: (id: string, ifMatch: string, options: any): AxiosPromise<void> => drivesApiFactory.deleteDrive(id, ifMatch, options) + }, + users: { + getUser: (userId: string) => userApiFactory.getUser(userId) } } } diff --git a/packages/web-runtime/tests/unit/components/Topbar/__snapshots__/UserMenu.spec.js.snap b/packages/web-runtime/tests/unit/components/Topbar/__snapshots__/UserMenu.spec.js.snap index cf3998fea82..88547f37df8 100644 --- a/packages/web-runtime/tests/unit/components/Topbar/__snapshots__/UserMenu.spec.js.snap +++ b/packages/web-runtime/tests/unit/components/Topbar/__snapshots__/UserMenu.spec.js.snap @@ -29,7 +29,8 @@ exports[`User Menu component when basic quota is set renders a danger quota prog <li class="storage-wrapper oc-pl-s"> <oc-icon-stub name="cloud" filltype="line" accessiblelabel="" type="span" size="medium" variation="passive" color="" class="oc-p-xs"></oc-icon-stub> <div class="storage-wrapper-text"> - <p class="oc-my-rm"><span>Personal storage (91.0% used)</span> <br> <span class="oc-text-small">910 B of 1 kB used</span></p> <progress max="100" aria-valuemax="100" aria-valuenow="91" aria-busy="true" aria-valuemin="0" tabindex="-1" class="oc-progress oc-progress-small oc-progress-danger" value="91"></progress> + <p class="oc-my-rm"><span>Personal storage (91.0% used)</span> <br> <span class="oc-text-small">910 B of 1 kB used</span></p> + <oc-progress-stub value="91" max="100" size="small" variation="danger"></oc-progress-stub> </div> </li> </oc-list-stub> @@ -130,7 +131,8 @@ exports[`User Menu component when quota and no email is set renders a navigation <li class="storage-wrapper oc-pl-s"> <oc-icon-stub name="cloud" filltype="line" accessiblelabel="" type="span" size="medium" variation="passive" color="" class="oc-p-xs"></oc-icon-stub> <div class="storage-wrapper-text"> - <p class="oc-my-rm"><span>Personal storage (30.0% used)</span> <br> <span class="oc-text-small">300 B of 1 kB used</span></p> <progress max="100" aria-valuemax="100" aria-valuenow="30" aria-busy="true" aria-valuemin="0" tabindex="-1" class="oc-progress oc-progress-small oc-progress-primary" value="30"></progress> + <p class="oc-my-rm"><span>Personal storage (30.0% used)</span> <br> <span class="oc-text-small">300 B of 1 kB used</span></p> + <oc-progress-stub value="30" max="100" size="small" variation="primary"></oc-progress-stub> </div> </li> </oc-list-stub> @@ -167,7 +169,8 @@ exports[`User Menu component when quota is above 80% and below 90% renders a war <li class="storage-wrapper oc-pl-s"> <oc-icon-stub name="cloud" filltype="line" accessiblelabel="" type="span" size="medium" variation="passive" color="" class="oc-p-xs"></oc-icon-stub> <div class="storage-wrapper-text"> - <p class="oc-my-rm"><span>Personal storage (81.0% used)</span> <br> <span class="oc-text-small">810 B of 1 kB used</span></p> <progress max="100" aria-valuemax="100" aria-valuenow="81" aria-busy="true" aria-valuemin="0" tabindex="-1" class="oc-progress oc-progress-small oc-progress-warning" value="81"></progress> + <p class="oc-my-rm"><span>Personal storage (81.0% used)</span> <br> <span class="oc-text-small">810 B of 1 kB used</span></p> + <oc-progress-stub value="81" max="100" size="small" variation="warning"></oc-progress-stub> </div> </li> </oc-list-stub> @@ -204,7 +207,8 @@ exports[`User Menu component when quota is below 80% renders a primary quota pro <li class="storage-wrapper oc-pl-s"> <oc-icon-stub name="cloud" filltype="line" accessiblelabel="" type="span" size="medium" variation="passive" color="" class="oc-p-xs"></oc-icon-stub> <div class="storage-wrapper-text"> - <p class="oc-my-rm"><span>Personal storage (30.0% used)</span> <br> <span class="oc-text-small">300 B of 1 kB used</span></p> <progress max="100" aria-valuemax="100" aria-valuenow="30" aria-busy="true" aria-valuemin="0" tabindex="-1" class="oc-progress oc-progress-small oc-progress-primary" value="30"></progress> + <p class="oc-my-rm"><span>Personal storage (30.0% used)</span> <br> <span class="oc-text-small">300 B of 1 kB used</span></p> + <oc-progress-stub value="30" max="100" size="small" variation="primary"></oc-progress-stub> </div> </li> </oc-list-stub> diff --git a/tests/unit/stubs/index.js b/tests/unit/stubs/index.js index bf488c93235..af6f74c8983 100644 --- a/tests/unit/stubs/index.js +++ b/tests/unit/stubs/index.js @@ -15,5 +15,6 @@ export default { 'oc-img': true, 'oc-page-size': true, 'router-link': true, - 'portal-target': true + 'portal-target': true, + 'oc-progress': true }