From b8fd829aed6503b32c0a4ad33e0cadba6c959617 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 2 Mar 2022 10:15:57 +0100 Subject: [PATCH 1/8] Implement people sharing for spaces --- .../SideBar/Details/SpaceDetails.vue | 90 +++++------ .../SideBar/Shares/Collaborators/ListItem.vue | 35 ++++- .../components/SideBar/Shares/FileShares.vue | 3 +- .../InviteCollaborator/AutocompleteItem.vue | 6 +- .../InviteCollaboratorForm.vue | 34 ++++- .../InviteCollaborator/RecipientContainer.vue | 9 +- .../SideBar/Shares/RoleDropdown.vue | 29 +++- .../components/SideBar/Shares/SpaceShares.vue | 135 +++++++++++++++++ packages/web-app-files/src/fileSideBars.js | 10 ++ .../web-app-files/src/helpers/resources.js | 45 +++++- .../web-app-files/src/helpers/share/role.ts | 49 ++++++ .../web-app-files/src/helpers/share/type.ts | 7 +- packages/web-app-files/src/store/actions.js | 131 +++++++++++++++- packages/web-app-files/src/store/mutations.js | 21 ++- .../src/views/spaces/Project.vue | 141 ++++++++++++------ .../src/views/spaces/Projects.vue | 90 ++++++----- 16 files changed, 667 insertions(+), 168 deletions(-) create mode 100644 packages/web-app-files/src/components/SideBar/Shares/SpaceShares.vue diff --git a/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue b/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue index 0ae941e8674..50981adeedc 100644 --- a/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue +++ b/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue @@ -13,7 +13,14 @@ />
- + + +
@@ -30,7 +37,7 @@ - + @@ -47,11 +54,10 @@ import { ref } from '@vue/composition-api' import Mixins from '../../../mixins' import MixinResources from '../../../mixins/resources' -import { mapGetters } from 'vuex' +import { mapActions, 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 { spaceManager } from '../../../helpers/share' import SpaceQuota from '../../SpaceQuota.vue' export default { @@ -63,13 +69,7 @@ export default { 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) { @@ -89,24 +89,14 @@ export default { 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 } + return { loadImageTask, spaceImage } }, computed: { + ...mapGetters('Files', [ + 'highlightedFile', + 'currentFileOutgoingCollaborators', + 'currentFileOutgoingLinks' + ]), ...mapGetters(['user']), space() { @@ -118,46 +108,31 @@ export default { 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.currentFileOutgoingCollaborators + .filter((share) => share.role.name === spaceManager.name) + .map((share) => { + if (share.collaborator.name === userId) { return this.$gettextInterpolate(this.$gettext('%{displayName} (me)'), { - displayName: owner.displayName + displayName: share.collaborator.displayName }) } - return owner.displayName + return share.collaborator.displayName }) .join(', ') }, hasPeopleShares() { - return false // @TODO + return this.peopleShareCount > 1 }, hasLinkShares() { - return false // @TODO + return this.linkShareCount > 1 }, peopleShareCount() { - return 0 // @TODO + return this.currentFileOutgoingCollaborators.length }, linkShareCount() { - return 0 // @TODO + return this.currentFileOutgoingLinks.length }, shareLabel() { let peopleString, linksString @@ -205,7 +180,16 @@ export default { }, mounted() { this.loadImageTask.perform(this) - this.loadOwnersTask.perform(this) + }, + methods: { + ...mapActions('Files/sidebar', { + setSidebarPanel: 'setActivePanel', + closeSidebar: 'close' + }), + + expandPeoplePanel() { + this.setSidebarPanel('space-share-item') + } } } diff --git a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue index b93c1aabe9a..bcfabad2809 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue @@ -1,11 +1,13 @@ + + diff --git a/packages/web-app-files/src/fileSideBars.js b/packages/web-app-files/src/fileSideBars.js index 63ab3881037..6587dcd31f8 100644 --- a/packages/web-app-files/src/fileSideBars.js +++ b/packages/web-app-files/src/fileSideBars.js @@ -7,6 +7,7 @@ import FileLinks from './components/SideBar/Links/FileLinks.vue' 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, isLocationSpacesActive } from './router' export default [ @@ -112,5 +113,14 @@ export default [ get enabled() { return highlightedFile?.type === 'space' } + }), + ({ highlightedFile }) => ({ + app: 'space-share-item', + component: SpaceShares, + icon: 'group', + iconFillType: 'line', + get enabled() { + return highlightedFile?.type === 'space' + } }) ] diff --git a/packages/web-app-files/src/helpers/resources.js b/packages/web-app-files/src/helpers/resources.js index d4845efb797..5eb3d547c68 100644 --- a/packages/web-app-files/src/helpers/resources.js +++ b/packages/web-app-files/src/helpers/resources.js @@ -4,7 +4,15 @@ import { DateTime } from 'luxon' import { getIndicators } from './statusIndicators' import { $gettext } from '../gettext' import { DavPermission, DavProperty } from 'web-pkg/src/constants' -import { PeopleShareRoles, SharePermissions, ShareStatus, ShareTypes } from './share' +import { + PeopleShareRoles, + SharePermissions, + ShareStatus, + ShareTypes, + spaceEditor, + spaceManager, + spaceViewer +} from './share' function _getFileExtension(name) { const extension = path.extname(name) @@ -324,9 +332,44 @@ export function buildShare(s, file, allowSharePermission) { if (parseInt(s.share_type) === ShareTypes.link.value) { return _buildLink(s) } + if (parseInt(s.share_type) === ShareTypes.space.value) { + return buildSpaceShare(s, file) + } + return buildCollaboratorShare(s, file, allowSharePermission) } +export function buildSpaceShare(s, spaceId) { + let permissions, role + + switch (s.role) { + case spaceManager.inlineLabel: + permissions = 31 + role = spaceManager + break + case spaceEditor.inlineLabel: + permissions = 15 + role = spaceEditor + break + case spaceViewer.inlineLabel: + permissions = 1 + role = spaceViewer + break + } + + return { + shareType: ShareTypes.space.value, + id: spaceId, + collaborator: { + name: s.onPremisesSamAccountName, + displayName: s.displayName, + additionalInfo: null + }, + permissions, + role + } +} + function _buildLink(link) { let description = '' diff --git a/packages/web-app-files/src/helpers/share/role.ts b/packages/web-app-files/src/helpers/share/role.ts index bd55034878e..4c52f658d7b 100644 --- a/packages/web-app-files/src/helpers/share/role.ts +++ b/packages/web-app-files/src/helpers/share/role.ts @@ -98,6 +98,20 @@ export class PeopleShareRole extends ShareRole { } } +export class SpaceShareRole extends ShareRole { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public description(allowSharing: boolean): string { + switch (this.name) { + case spaceViewer.name: + return $gettext('Download and preview') + case spaceEditor.name: + return $gettext('Upload, edit, delete, download and preview') + case spaceManager.name: + return $gettext('Upload, edit, delete, download, preview and share') + } + } +} + export class LinkShareRole extends ShareRole { public description(allowSharing: boolean): string { return linkRoleDescriptions[this.bitmask(allowSharing)] @@ -193,6 +207,41 @@ export const linkRoleUploaderFolder = new LinkShareRole( $gettext('uploader'), [SharePermissions.create] ) +export const spaceViewer = new SpaceShareRole( + 'spaceViewer', + false, + $gettext('Viewer'), + $gettext('viewer'), + [SharePermissions.read] +) +export const spaceEditor = new SpaceShareRole( + 'spaceEditor', + false, + $gettext('Editor'), + $gettext('editor'), + [SharePermissions.read, SharePermissions.update, SharePermissions.create, SharePermissions.delete] +) +export const spaceManager = new SpaceShareRole( + 'spaceManager', + false, + $gettext('Manager'), + $gettext('manager'), + [ + SharePermissions.read, + SharePermissions.update, + SharePermissions.create, + SharePermissions.delete, + SharePermissions.share + ] +) + +export abstract class SpacePeopleShareRoles { + static readonly all = [spaceViewer, spaceEditor, spaceManager] + + static getByBitmask(bitmask: number): ShareRole { + return this.all.find((r) => r.bitmask(true) === bitmask) + } +} export abstract class PeopleShareRoles { static readonly all = [ diff --git a/packages/web-app-files/src/helpers/share/type.ts b/packages/web-app-files/src/helpers/share/type.ts index 34114f95628..47c928d7489 100644 --- a/packages/web-app-files/src/helpers/share/type.ts +++ b/packages/web-app-files/src/helpers/share/type.ts @@ -35,12 +35,13 @@ export abstract class ShareTypes { static readonly link = new ShareType('link', 3, $gettext('Link')) static readonly guest = new ShareType('guest', 4, $gettext('Guest')) static readonly remote = new ShareType('remote', 6, $gettext('Federated')) + static readonly space = new ShareType('space', 7, $gettext('User')) - static readonly individuals = [this.user, this.guest, this.remote] + static readonly individuals = [this.user, this.guest, this.remote, this.space] static readonly collectives = [this.group] static readonly unauthenticated = [this.link] - static readonly authenticated = [this.user, this.group, this.guest, this.remote] - static readonly all = [this.user, this.group, this.link, this.guest, this.remote] + static readonly authenticated = [this.user, this.group, this.guest, this.remote, this.space] + static readonly all = [this.user, this.group, this.link, this.guest, this.remote, this.space] static isIndividual(type: ShareType): boolean { return this.individuals.includes(type) diff --git a/packages/web-app-files/src/store/actions.js b/packages/web-app-files/src/store/actions.js index 37d0160a99a..94fb0971f6e 100644 --- a/packages/web-app-files/src/store/actions.js +++ b/packages/web-app-files/src/store/actions.js @@ -2,12 +2,17 @@ import PQueue from 'p-queue' import { getParentPaths } from '../helpers/path' import { dirname } from 'path' -import { buildResource, buildShare, buildCollaboratorShare } from '../helpers/resources' +import { + buildResource, + buildShare, + buildCollaboratorShare, + buildSpaceShare +} from '../helpers/resources' import { $gettext, $gettextInterpolate } from '../gettext' import { loadPreview } from '../helpers/resource' import { avatarUrl } from '../helpers/user' import { has } from 'lodash-es' -import { ShareTypes } from '../helpers/share' +import { ShareTypes, SpacePeopleShareRoles } from '../helpers/share' export default { updateFileProgress({ commit }, progress) { @@ -144,11 +149,47 @@ export default { value: computeShareTypes(state.currentFileOutgoingShares) }) }, - loadCurrentFileOutgoingShares(context, { client, path }) { + loadCurrentFileOutgoingShares(context, { client, path, space }) { context.commit('CURRENT_FILE_OUTGOING_SHARES_SET', []) context.commit('CURRENT_FILE_OUTGOING_SHARES_ERROR', null) context.commit('CURRENT_FILE_OUTGOING_SHARES_LOADING', true) + if (space) { + const promises = [] + const spaceShares = [] + + for (const permission of space.spacePermissions) { + for (const { + user: { id } + } of permission.grantedTo) { + promises.push( + client.users.getUser(id).then((resolved) => { + spaceShares.push( + buildSpaceShare( + { + ...resolved.data, + role: permission.roles[0] + }, + space.id + ) + ) + }) + ) + } + } + + return Promise.all(promises) + .then(() => { + context.commit('CURRENT_FILE_OUTGOING_SHARES_SET', spaceShares) + context.dispatch('updateCurrentFileShareTypes') + context.commit('CURRENT_FILE_OUTGOING_SHARES_LOADING', false) + }) + .catch((error) => { + context.commit('CURRENT_FILE_OUTGOING_SHARES_ERROR', error.message) + context.commit('CURRENT_FILE_OUTGOING_SHARES_LOADING', false) + }) + } + // see https://owncloud.dev/owncloud-sdk/Shares.html client.shares .getShares(path, { reshares: true }) @@ -211,6 +252,29 @@ export default { }) } + if (share.shareType === ShareTypes.space.value) { + return new Promise((resolve, reject) => { + client.shares + .shareSpaceWithUser('', share.collaborator.name, share.id, { + permissions + }) + .then(() => { + const role = SpacePeopleShareRoles.getByBitmask(permissions) + const shareObj = { + role: role.inlineLabel, + onPremisesSamAccountName: share.collaborator.name, + displayName: share.collaborator.displayName + } + const updatedShare = buildSpaceShare(shareObj, share.id) + commit('CURRENT_FILE_OUTGOING_SHARES_UPDATE', updatedShare) + resolve(updatedShare) + }) + .catch((e) => { + reject(e) + }) + }) + } + return new Promise((resolve, reject) => { client.shares .updateShare(share.id, params) @@ -228,7 +292,10 @@ export default { }) }) }, - addShare(context, { client, path, shareWith, shareType, permissions, expirationDate }) { + addShare( + context, + { client, path, shareWith, shareType, permissions, expirationDate, spaceId, displayName } + ) { if (shareType === ShareTypes.group.value) { client.shares .shareFileWithGroup(path, shareWith, { @@ -261,6 +328,41 @@ export default { return } + if (shareType === ShareTypes.space.value) { + client.shares + .shareSpaceWithUser(path, shareWith, spaceId, { + permissions + }) + .then(() => { + const role = SpacePeopleShareRoles.getByBitmask(permissions) + const shareObj = { + role: role.inlineLabel, + onPremisesSamAccountName: shareWith, + displayName + } + + context.commit('CURRENT_FILE_OUTGOING_SHARES_ADD', buildSpaceShare(shareObj, spaceId)) + context.commit('CURRENT_FILE_OUTGOING_SHARES_LOADING', true) + + // FIXME + return Promise.all([]).then(() => { + context.commit('CURRENT_FILE_OUTGOING_SHARES_LOADING', false) + }) + }) + .catch((e) => { + context.dispatch( + 'showMessage', + { + title: $gettext('Error while sharing.'), + desc: e, + status: 'danger' + }, + { root: true } + ) + }) + return + } + const remoteShare = shareType === ShareTypes.remote.value client.shares .shareFileWithUser(path, shareWith, { @@ -293,12 +395,27 @@ export default { }) }, deleteShare(context, { client, share, resource }) { + const additionalParams = {} + if (share.shareType === ShareTypes.space.value) { + additionalParams.shareWith = share.collaborator.name + } + client.shares - .deleteShare(share.id) + .deleteShare(share.id, additionalParams) .then(() => { context.commit('CURRENT_FILE_OUTGOING_SHARES_REMOVE', share) - context.dispatch('updateCurrentFileShareTypes') - context.dispatch('loadIndicators', { client, currentFolder: resource.path }) + + if (share.shareType !== ShareTypes.space.value) { + context.dispatch('updateCurrentFileShareTypes') + context.dispatch('loadIndicators', { client, currentFolder: resource.path }) + } else { + context.commit('CURRENT_FILE_OUTGOING_SHARES_LOADING', true) + + // FIXME + return Promise.all([]).then(() => { + context.commit('CURRENT_FILE_OUTGOING_SHARES_LOADING', false) + }) + } }) .catch((e) => { console.error(e) diff --git a/packages/web-app-files/src/store/mutations.js b/packages/web-app-files/src/store/mutations.js index 42929b18445..5c03d85323a 100644 --- a/packages/web-app-files/src/store/mutations.js +++ b/packages/web-app-files/src/store/mutations.js @@ -4,6 +4,7 @@ import { DateTime } from 'luxon' import { set, has } from 'lodash-es' import { getIndicators } from '../helpers/statusIndicators' import { renameResource } from '../helpers/resources' +import { ShareTypes } from '../helpers/share' export default { UPDATE_FILE_PROGRESS(state, file) { @@ -121,14 +122,28 @@ export default { state.currentFileOutgoingShares.push(share) }, CURRENT_FILE_OUTGOING_SHARES_REMOVE(state, share) { + if (share.shareType === ShareTypes.space.value) { + state.currentFileOutgoingShares = state.currentFileOutgoingShares.filter( + (s) => share.id === s.id && share.collaborator.name !== s.collaborator.name + ) + return + } state.currentFileOutgoingShares = state.currentFileOutgoingShares.filter( (s) => share.id !== s.id ) }, CURRENT_FILE_OUTGOING_SHARES_UPDATE(state, share) { - const fileIndex = state.currentFileOutgoingShares.findIndex((s) => { - return s.id === share.id - }) + let fileIndex + if (share.shareType === ShareTypes.space.value) { + fileIndex = state.currentFileOutgoingShares.findIndex((s) => { + return share.id === s.id && share.collaborator.name === s.collaborator.name + }) + } else { + fileIndex = state.currentFileOutgoingShares.findIndex((s) => { + return s.id === share.id + }) + } + if (fileIndex >= 0) { Vue.set(state.currentFileOutgoingShares, fileIndex, share) } else { diff --git a/packages/web-app-files/src/views/spaces/Project.vue b/packages/web-app-files/src/views/spaces/Project.vue index 5698327309b..9eee72e1b1c 100644 --- a/packages/web-app-files/src/views/spaces/Project.vue +++ b/packages/web-app-files/src/views/spaces/Project.vue @@ -26,52 +26,66 @@
-
+

{{ space.name }}

- - - - - -
    -
  • - + + + + + + + + + +
      +
    • - - {{ action.label() }} - -
    • -
    -
    + + + {{ action.label() }} + +
  • +
+
+

{{ space.description }}

@@ -261,9 +275,18 @@ export default { }) }) + const loadSharesTask = useTask(function* (signal, ref) { + yield ref.loadCurrentFileOutgoingShares({ + client: graphClient, + path: ref.space.id, + space: ref.space + }) + }) + return { space, loadResourcesTask, + loadSharesTask, resourceTargetLocation: createLocationSpaces('files-spaces-project'), paginatedResources, paginationPages, @@ -290,7 +313,8 @@ export default { 'selectedFiles', 'currentFolder', 'totalFilesCount', - 'totalFilesSize' + 'totalFilesSize', + 'currentFileOutgoingCollaborators' ]), selected: { @@ -319,6 +343,17 @@ export default { displayThumbnails() { return !this.configuration.options.disablePreviews }, + peopleCountString() { + const translated = this.$ngettext( + '%{count} invited person', + '%{count} invited people', + this.currentFileOutgoingCollaborators.length + ) + + return this.$gettextInterpolate(translated, { + count: this.currentFileOutgoingCollaborators.length + }) + }, quotaModalIsOpen() { return this.$data.$_editQuota_modalOpen }, @@ -388,6 +423,7 @@ export default { }, async mounted() { await this.loadResourcesTask.perform(this, false, this.$route.params.item || '') + this.loadSharesTask.perform(this) if (this.markdownResizeObserver) { this.markdownResizeObserver.unobserve(this.$refs.markdownContainer) @@ -413,7 +449,11 @@ export default { } }, methods: { - ...mapActions('Files', ['loadIndicators', 'loadPreview']), + ...mapActions('Files', ['loadIndicators', 'loadPreview', 'loadCurrentFileOutgoingShares']), + ...mapActions('Files/sidebar', { + openSidebarWithPanel: 'openWithPanel', + closeSidebar: 'close' + }), ...mapMutations('Files', [ 'SET_CURRENT_FOLDER', 'LOAD_FILES', @@ -485,6 +525,11 @@ export default { closeQuotaModal() { this.$_editQuota_closeModal() }, + async openSidebarSharePanel() { + await this.closeSidebar() + this.SET_FILE_SELECTION([this.space]) + this.openSidebarWithPanel('space-share-item') + }, closeReadmeContentModal() { this.$_editReadmeContent_closeModal() } @@ -510,6 +555,10 @@ export default { font-size: 1.5rem; } + &-people-count { + white-space: nowrap; + } + .markdown-container.collapsed { max-height: 150px; overflow: hidden; diff --git a/packages/web-app-files/src/views/spaces/Projects.vue b/packages/web-app-files/src/views/spaces/Projects.vue index f0114c0a4c3..dae2db1fe72 100644 --- a/packages/web-app-files/src/views/spaces/Projects.vue +++ b/packages/web-app-files/src/views/spaces/Projects.vue @@ -52,7 +52,8 @@ oc-grid-row-large oc-text-center oc-child-width-1-3@m - oc-child-width-1-5@l + oc-child-width-1-4@l + oc-child-width-1-5@xl " >
  • @@ -97,40 +98,52 @@
  • - - - - -
      -
    • - + + + +
    +
    + + + + +
      +
    • - - {{ action.label() }} - -
    • -
    -
    + + + {{ action.label() }} + + + + +

    Date: Wed, 2 Mar 2022 10:17:20 +0100 Subject: [PATCH 2/8] Add and adjust unit tests --- __fixtures__/collaborators.js | 23 +- .../SideBar/Details/SpaceDetails.spec.js | 19 +- .../__snapshots__/SpaceDetails.spec.js.snap | 4 +- .../Shares/Collaborators/ListItem.spec.js | 23 +- .../SideBar/Shares/FileShares.spec.js | 2 +- .../RecipientContainer.spec.js.snap | 7 +- .../SideBar/Shares/SpaceShares.spec.js | 246 ++++++++++++++++++ .../__snapshots__/FileShares.spec.js.snap | 4 +- .../__snapshots__/SpaceShares.spec.js.snap | 60 +++++ .../tests/unit/store/actions.spec.js | 146 +++++++++++ .../tests/unit/store/mutations.spec.js | 46 ++++ .../tests/unit/views/spaces/Project.spec.js | 19 +- .../spaces/__snapshots__/Project.spec.js.snap | 186 ++++++------- .../__snapshots__/Projects.spec.js.snap | 46 ++-- 14 files changed, 700 insertions(+), 131 deletions(-) create mode 100644 packages/web-app-files/tests/unit/components/SideBar/Shares/SpaceShares.spec.js create mode 100644 packages/web-app-files/tests/unit/components/SideBar/Shares/__snapshots__/SpaceShares.spec.js.snap create mode 100644 packages/web-app-files/tests/unit/store/actions.spec.js diff --git a/__fixtures__/collaborators.js b/__fixtures__/collaborators.js index 51cd46f0cce..e82820df067 100644 --- a/__fixtures__/collaborators.js +++ b/__fixtures__/collaborators.js @@ -1,6 +1,7 @@ import { peopleRoleEditorFile, - peopleRoleViewerFile + peopleRoleViewerFile, + spaceEditor } from '../packages/web-app-files/src/helpers/share' export default [ @@ -142,5 +143,25 @@ export default [ role: peopleRoleViewerFile, path: "/Neuer Ordner-'singe'", key: "collaborator-51a8aafe-cd40-4d0a-8566-87a1149b7fea" + }, + { + shareType: 7, + id: "f5c28709-b921-4ec8-b39a-4c243709b514", + collaborator: { + name: "einstein", + displayName: "Albert Einstein", + additionalInfo: "einstein@example.org" + }, + owner: { + name: "admin", + displayName: "Admin", + additionalInfo: "admin@example.org" + }, + fileOwner: { + name: "admin", + displayName: "Admin", + additionalInfo: "admin@example.org" + }, + role: spaceEditor } ] 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 index b9b359279fd..c558189929f 100644 --- 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 @@ -5,6 +5,7 @@ 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' +import { ShareTypes, spaceManager } from '../../../../../src/helpers/share' const localVue = createLocalVue() localVue.use(Vuex) @@ -27,6 +28,18 @@ const spaceMock = { } } +const spaceShare = { + id: '1', + shareType: ShareTypes.space.value, + collaborator: { + onPremisesSamAccountName: 'Alice', + displayName: 'alice' + }, + role: { + name: spaceManager.name + } +} + const formDateFromJSDate = jest.fn().mockImplementation(() => 'ABSOLUTE_TIME') const formDateFromHTTP = jest.fn().mockImplementation(() => 'ABSOLUTE_TIME') const refreshShareDetailsTree = jest.fn() @@ -38,7 +51,7 @@ beforeEach(() => { }) describe('Details SideBar Panel', () => { - it('displayes the details side panel', () => { + it('displays the details side panel', () => { const wrapper = createWrapper(spaceMock) expect(wrapper).toMatchSnapshot() }) @@ -76,7 +89,9 @@ function createWrapper(spaceResource) { getters: { highlightedFile: function () { return spaceResource - } + }, + currentFileOutgoingCollaborators: () => [spaceShare], + currentFileOutgoingLinks: () => [] } } } 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 index a63d31fd829..8324e5f0acd 100644 --- 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 @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Details SideBar Panel displayes the details side panel 1`] = ` +exports[`Details SideBar Panel displays the details side panel 1`] = `

    @@ -15,7 +15,7 @@ exports[`Details SideBar Panel displayes the details side panel 1`] = ` Manager - + alice Quota diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/Collaborators/ListItem.spec.js b/packages/web-app-files/tests/unit/components/SideBar/Shares/Collaborators/ListItem.spec.js index de607e403cf..c57fc166737 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Shares/Collaborators/ListItem.spec.js +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/Collaborators/ListItem.spec.js @@ -4,6 +4,7 @@ import ListItem from '../../../../../../src/components/SideBar/Shares/Collaborat import stubs from '../../../../../../../../tests/unit/stubs' import GetTextPlugin from 'vue-gettext' import { peopleRoleViewerFolder, ShareTypes } from '../../../../../../src/helpers/share' +import Users from '@/__fixtures__/users' const localVue = createLocalVue() localVue.use(Vuex) @@ -39,15 +40,16 @@ describe('Collaborator ListItem component', () => { }) }) describe('non-user share types', () => { - it.each(ShareTypes.all.filter((shareType) => shareType !== ShareTypes.user))( - 'should display an oc-avatar-item for any non-user share types', - (shareType) => { - const wrapper = createWrapper({ shareType: shareType.value }) - expect(wrapper.find(selectors.userAvatarImage).exists()).toBeFalsy() - expect(wrapper.find(selectors.notUserAvatar).exists()).toBeTruthy() - expect(wrapper.find(selectors.notUserAvatar).attributes().name).toEqual(shareType.key) - } - ) + it.each( + ShareTypes.all.filter( + (shareType) => ![ShareTypes.user, ShareTypes.space].includes(shareType) + ) + )('should display an oc-avatar-item for any non-user share types', (shareType) => { + const wrapper = createWrapper({ shareType: shareType.value }) + expect(wrapper.find(selectors.userAvatarImage).exists()).toBeFalsy() + expect(wrapper.find(selectors.notUserAvatar).exists()).toBeTruthy() + expect(wrapper.find(selectors.notUserAvatar).attributes().name).toEqual(shareType.key) + }) }) }) describe('share info', () => { @@ -102,6 +104,9 @@ function createWrapper({ } = {}) { return mount(ListItem, { store: new Vuex.Store({ + state: { + user: Users.alice + }, getters: { isOcis: () => false }, diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/FileShares.spec.js b/packages/web-app-files/tests/unit/components/SideBar/Shares/FileShares.spec.js index 75e27c1f3b5..4bd7b562703 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Shares/FileShares.spec.js +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/FileShares.spec.js @@ -14,7 +14,7 @@ localVue.use(GetTextPlugin, { silent: true }) -const user = Users.admin +const user = Users.alice const collaborators = [Collaborators[0], Collaborators[1]] const selectors = { diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/InviteCollaborator/__snapshots__/RecipientContainer.spec.js.snap b/packages/web-app-files/tests/unit/components/SideBar/Shares/InviteCollaborator/__snapshots__/RecipientContainer.spec.js.snap index 3f1c1c3aa8d..d84bb63e827 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Shares/InviteCollaborator/__snapshots__/RecipientContainer.spec.js.snap +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/InviteCollaborator/__snapshots__/RecipientContainer.spec.js.snap @@ -1,11 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`InviteCollaborator RecipientContainer displays an avatar image if capability is present 1`] = ` - +

    Albert Einstein

    -
    +
    `; exports[`InviteCollaborator RecipientContainer renders a recipient with a deselect button different recipients for different shareTypes 1`] = `

    Albert Einstein

    `; @@ -15,3 +14,5 @@ exports[`InviteCollaborator RecipientContainer renders a recipient with a desele exports[`InviteCollaborator RecipientContainer renders a recipient with a deselect button different recipients for different shareTypes 3`] = `

    guest-user

    `; exports[`InviteCollaborator RecipientContainer renders a recipient with a deselect button different recipients for different shareTypes 4`] = `

    remote-user

    `; + +exports[`InviteCollaborator RecipientContainer renders a recipient with a deselect button different recipients for different shareTypes 5`] = `

    Albert Einstein

    `; diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/SpaceShares.spec.js b/packages/web-app-files/tests/unit/components/SideBar/Shares/SpaceShares.spec.js new file mode 100644 index 00000000000..059ecce367f --- /dev/null +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/SpaceShares.spec.js @@ -0,0 +1,246 @@ +import SpaceShares from '@files/src/components/SideBar/Shares/SpaceShares.vue' +import { createLocalVue, mount, shallowMount } from '@vue/test-utils' +import GetTextPlugin from 'vue-gettext' +import Vuex from 'vuex' +import DesignSystem from 'owncloud-design-system' +import Users from '@/__fixtures__/users' +import { ShareTypes, spaceManager, spaceViewer } from '../../../../../src/helpers/share' +import VueCompositionAPI from '@vue/composition-api/dist/vue-composition-api' + +const localVue = createLocalVue() +localVue.use(DesignSystem) +localVue.use(Vuex) +localVue.use(VueCompositionAPI) +localVue.use(GetTextPlugin, { + translations: 'does-not-matter.json', + silent: true +}) + +const user = Users.alice +const spaceMock = { + type: 'space', + name: ' space', + id: '1', + mdate: 'Wed, 21 Oct 2015 07:28:00 GMT', + spaceQuota: { + used: 100, + total: 1000 + } +} + +const outgoingShares = [ + { + id: '1', + shareType: ShareTypes.space.value, + collaborator: { + name: user.id, + displayName: user.displayname + }, + role: { + name: spaceManager.name + } + }, + { + id: '2', + shareType: ShareTypes.space.value, + collaborator: { + onPremisesSamAccountName: 'Einstein', + displayName: 'einstein' + }, + role: { + name: spaceViewer.name + } + }, + { + id: '3', + shareType: ShareTypes.space.value, + collaborator: { + onPremisesSamAccountName: 'Marie', + displayName: 'marie' + }, + role: { + name: spaceManager.name + } + } +] + +describe('SpaceShares', () => { + it('renders loading spinner while loading', () => { + const wrapper = getShallowMountedWrapper( + { + user + }, + true + ) + expect(wrapper).toMatchSnapshot() + }) + describe('if currentUser can share', () => { + it('initially renders add people dialog', () => { + const wrapper = getShallowMountedWrapper({ + user, + outgoingCollaborators: [outgoingShares[0]] + }) + expect(wrapper).toMatchSnapshot() + }) + }) + + describe('if currentUser can not share', () => { + it('other shares are listed, but creating/editing shares is not possible', () => { + const wrapper = getShallowMountedWrapper({ + user, + outgoingCollaborators: [outgoingShares[1]] + }) + expect(wrapper).toMatchSnapshot() + }) + }) + + describe('if currentUser is manager', () => { + it('allows role edit of the current user if another user is manager', () => { + const wrapper = getShallowMountedWrapper({ + user, + outgoingCollaborators: outgoingShares + }) + expect(wrapper).toMatchSnapshot() + }) + it('does not allow role edit of the current user if they are the only manager', () => { + const wrapper = getShallowMountedWrapper({ + user, + outgoingCollaborators: [outgoingShares[0], outgoingShares[1]] + }) + expect(wrapper).toMatchSnapshot() + }) + }) + + describe('if currentUser can delete', () => { + it('reacts on delete events by collaborator list items', async () => { + const wrapper = getMountedWrapper({ + user, + outgoingCollaborators: outgoingShares + }) + + const spyOnCollaboratorDelete = jest.spyOn(wrapper.vm, 'deleteShare') + wrapper + .find(`div[data-testid="collaborator-user-item-${outgoingShares[0].collaborator.name}"]`) + .vm.$emit('onDelete') + await wrapper.vm.$nextTick() + expect(spyOnCollaboratorDelete).toHaveBeenCalledTimes(1) + }) + }) +}) + +const storeOptions = (data, isInLoadingState) => { + const { user, outgoingCollaborators = [] } = data + + return { + state: { + user + }, + modules: { + Files: { + namespaced: true, + getters: { + highlightedFile: () => spaceMock, + currentFileOutgoingCollaborators: () => outgoingCollaborators, + currentFileOutgoingSharesLoading: () => isInLoadingState, + sharesTreeLoading: () => false + }, + actions: { + loadCurrentFileOutgoingShares: jest.fn(), + deleteShare: jest.fn() + }, + mutations: { + SET_HIGHLIGHTED_FILE(state, file) { + state.highlightedFile = spaceMock + } + } + } + }, + getters: { + isOcis: () => true, + user: () => user, + capabilities: () => { + return { + files_sharing: { + user: { + expire_date: { + enabled: true, + days: 10 + } + }, + group: { + expire_date: { + enabled: true, + days: 10 + } + } + } + } + } + } + } +} + +function getShallowMountedWrapper(data, loading = false) { + return shallowMount(getComponent(loading), { + localVue, + store: createStore(data, loading), + stubs: { + 'oc-button': true, + 'oc-icon': true, + 'oc-spinner': true + }, + provide: { + displayedItem: { + value: spaceMock + } + } + }) +} + +function getMountedWrapper(data, loading = false) { + return mount(getComponent(loading), { + localVue, + store: createStore(data, loading), + stubs: { + 'oc-button': true, + 'oc-icon': true, + 'oc-spinner': true, + 'avatar-image': true, + 'role-dropdown': true, + 'edit-dropdown': true + }, + mocks: { + $router: { + go: jest.fn(), + push: jest.fn(), + currentRoute: { + name: 'some-route', + query: { page: 1 } + }, + resolve: (r) => ({ href: r.name }) + } + }, + provide: { + displayedItem: { + value: spaceMock + } + } + }) +} + +function createStore(data, loading) { + return new Vuex.Store(storeOptions(data, loading)) +} + +function getComponent(loading) { + return { + ...SpaceShares, + setup: () => ({ + graphClient: {}, + loadSharesTask: { + isRunning: loading, + perform: jest.fn() + } + }) + } +} diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/__snapshots__/FileShares.spec.js.snap b/packages/web-app-files/tests/unit/components/SideBar/Shares/__snapshots__/FileShares.spec.js.snap index 20889013bfe..59d0ff5d5af 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Shares/__snapshots__/FileShares.spec.js.snap +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/__snapshots__/FileShares.spec.js.snap @@ -27,10 +27,10 @@ exports[`FileShares if there are collaborators present renders sharedWithLabel a
    • - +
    • - +
    diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/__snapshots__/SpaceShares.spec.js.snap b/packages/web-app-files/tests/unit/components/SideBar/Shares/__snapshots__/SpaceShares.spec.js.snap new file mode 100644 index 00000000000..92aeb3f81d5 --- /dev/null +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/__snapshots__/SpaceShares.spec.js.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SpaceShares if currentUser can not share other shares are listed, but creating/editing shares is not possible 1`] = ` +
    + +
      +
    • + +
    • +
    +
    +`; + +exports[`SpaceShares if currentUser can share initially renders add people dialog 1`] = ` +
    + +
      +
    • + +
    • +
    +
    +`; + +exports[`SpaceShares if currentUser is manager allows role edit of the current user if another user is manager 1`] = ` +
    + +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +`; + +exports[`SpaceShares if currentUser is manager does not allow role edit of the current user if they are the only manager 1`] = ` +
    + +
      +
    • + +
    • +
    • + +
    • +
    +
    +`; + +exports[`SpaceShares renders loading spinner while loading 1`] = ` +
    + +
    +`; diff --git a/packages/web-app-files/tests/unit/store/actions.spec.js b/packages/web-app-files/tests/unit/store/actions.spec.js new file mode 100644 index 00000000000..89de538a58c --- /dev/null +++ b/packages/web-app-files/tests/unit/store/actions.spec.js @@ -0,0 +1,146 @@ +import actions from '../../../src/store/actions' +import { ShareTypes, spaceManager } from '../../../src/helpers/share' + +const stateMock = { + commit: jest.fn(), + dispatch: jest.fn(), + getters: { + highlightedFile: { isFolder: false } + }, + rootGetters: { + isOcis: true + } +} + +const clientMock = { + users: { + getUser: () => { + return Promise.resolve({}) + } + }, + shares: { + getShares: () => { + return Promise.resolve([{ shareInfo: { share_type: ShareTypes.user.value, permissions: 1 } }]) + }, + updateShare: () => { + return Promise.resolve({ shareInfo: { share_type: ShareTypes.user.value, permissions: 1 } }) + }, + deleteShare: () => { + return Promise.resolve({}) + }, + shareSpaceWithUser: () => { + return Promise.resolve({}) + }, + shareFileWithUser: () => { + return Promise.resolve({ shareInfo: { share_type: ShareTypes.user.value, permissions: 1 } }) + } + } +} + +const shareMock = { + id: '1', + shareType: ShareTypes.user.value +} + +const spaceMock = { + type: 'space', + name: ' space', + id: '1', + mdate: 'Wed, 21 Oct 2015 07:28:00 GMT', + spacePermissions: [{ grantedTo: [{ user: { id: 1 } }], roles: ['manager'] }], + spaceQuota: { + used: 100, + total: 1000 + } +} + +const spaceShareMock = { + id: '1', + shareType: ShareTypes.space.value, + collaborator: { + onPremisesSamAccountName: 'Alice', + displayName: 'alice' + }, + role: { + name: spaceManager.name + } +} + +describe('vuex store actions', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('loadCurrentFileOutgoingShares', () => { + it.each([ + { space: spaceMock, expectedCommitCalls: 5, expectedDispatchCalls: 1 }, + { space: null, expectedCommitCalls: 5, expectedDispatchCalls: 1 } + ])('succeeds using action %s', async (dataSet) => { + const commitSpy = jest.spyOn(stateMock, 'commit') + const dispatchSpy = jest.spyOn(stateMock, 'dispatch') + + await actions.loadCurrentFileOutgoingShares(stateMock, { + client: clientMock, + path: 'path', + space: dataSet.space + }) + + expect(commitSpy).toBeCalledTimes(dataSet.expectedCommitCalls) + expect(dispatchSpy).toBeCalledTimes(dataSet.expectedDispatchCalls) + }) + }) + + describe('changeShare', () => { + it.each([{ share: spaceShareMock }, { share: shareMock }])( + 'succeeds using action %s', + async (dataSet) => { + const commitSpy = jest.spyOn(stateMock, 'commit') + + await actions.changeShare(stateMock, { + client: clientMock, + share: dataSet.share, + permissions: 1, + expirationDate: null + }) + + expect(commitSpy).toBeCalledTimes(1) + } + ) + }) + + describe('addShare', () => { + it.each([ + { shareType: spaceShareMock.shareType, spaceId: spaceMock.id, expectedCommitCalls: 2 }, + { shareType: shareMock.shareType, spaceId: null, expectedCommitCalls: 1 } + ])('succeeds using action %s', async (dataSet) => { + const commitSpy = jest.spyOn(stateMock, 'commit') + + await actions.addShare(stateMock, { + client: clientMock, + shareType: dataSet.shareType, + spaceId: dataSet.spaceId, + permissions: 1, + expirationDate: null + }) + + expect(commitSpy).toBeCalledTimes(dataSet.expectedCommitCalls) + }) + }) + + describe('deleteShare', () => { + it.each([ + { share: spaceShareMock, spaceId: spaceMock.id, expectedCommitCalls: 2 }, + { share: shareMock, spaceId: null, expectedCommitCalls: 1 } + ])('succeeds using action %s', async (dataSet) => { + const commitSpy = jest.spyOn(stateMock, 'commit') + + await actions.deleteShare(stateMock, { + client: clientMock, + share: dataSet.share, + resource: {} + }) + + expect(commitSpy).toBeCalledTimes(dataSet.expectedCommitCalls) + }) + }) +}) diff --git a/packages/web-app-files/tests/unit/store/mutations.spec.js b/packages/web-app-files/tests/unit/store/mutations.spec.js index beb13c7ab31..d0d7c92bcdb 100644 --- a/packages/web-app-files/tests/unit/store/mutations.spec.js +++ b/packages/web-app-files/tests/unit/store/mutations.spec.js @@ -1,5 +1,6 @@ import mutations from '../../../src/store/mutations' import { cloneDeep } from 'lodash-es' +import { ShareTypes } from '../../../src/helpers/share' const stateFixture = { files: [ @@ -83,4 +84,49 @@ describe('vuex store mutations', () => { expect(state.areHiddenFilesShown).toEqual(false) }) + + describe('CURRENT_FILE_OUTGOING_SHARES_REMOVE', () => { + it('removes an outgoing user share', () => { + const shareToRemove = { id: 1, shareType: ShareTypes.user.value } + const state = { currentFileOutgoingShares: [shareToRemove] } + mutations.CURRENT_FILE_OUTGOING_SHARES_REMOVE(state, shareToRemove) + + expect(state.currentFileOutgoingShares.length).toEqual(0) + }) + it('removes an outgoing space share', () => { + const shareToRemove = { + id: 1, + shareType: ShareTypes.space.value, + collaborator: { name: 'admin' } + } + const state = { currentFileOutgoingShares: [shareToRemove] } + mutations.CURRENT_FILE_OUTGOING_SHARES_REMOVE(state, shareToRemove) + + expect(state.currentFileOutgoingShares.length).toEqual(0) + }) + }) + + describe('CURRENT_FILE_OUTGOING_SHARES_UPDATE', () => { + it('updates an outgoing user share', () => { + const share = { id: 1, shareType: ShareTypes.user.value, permissions: 1 } + const state = { currentFileOutgoingShares: [share] } + const updatedShare = { ...share, permissions: 31 } + mutations.CURRENT_FILE_OUTGOING_SHARES_UPDATE(state, updatedShare) + + expect(state.currentFileOutgoingShares[0]).toEqual(updatedShare) + }) + it('updates an outgoing space share', () => { + const share = { + id: 1, + shareType: ShareTypes.space.value, + permissions: 1, + collaborator: { name: 'admin' } + } + const state = { currentFileOutgoingShares: [share] } + const updatedShare = { ...share, permissions: 31 } + mutations.CURRENT_FILE_OUTGOING_SHARES_UPDATE(state, updatedShare) + + expect(state.currentFileOutgoingShares[0]).toEqual(updatedShare) + }) + }) }) diff --git a/packages/web-app-files/tests/unit/views/spaces/Project.spec.js b/packages/web-app-files/tests/unit/views/spaces/Project.spec.js index 585964f6583..e4044e34dd1 100644 --- a/packages/web-app-files/tests/unit/views/spaces/Project.spec.js +++ b/packages/web-app-files/tests/unit/views/spaces/Project.spec.js @@ -6,6 +6,7 @@ import Files from '@/__fixtures__/files' import mockAxios from 'jest-mock-axios' import SpaceProject from '../../../../src/views/spaces/Project.vue' import Vuex from 'vuex' +import { ShareTypes, spaceManager } from '../../../../src/helpers/share' localVue.use(GetTextPlugin, { translations: 'does-not-matter.json', @@ -58,6 +59,18 @@ const spaceMocks = { } } +const spaceShare = { + id: '1', + shareType: ShareTypes.space.value, + collaborator: { + onPremisesSamAccountName: 'Alice', + displayName: 'alice' + }, + role: { + name: spaceManager.name + } +} + describe('Spaces project view', () => { it('should not show anything if space can not be found', async () => { mockAxios.request.mockImplementationOnce(() => { @@ -230,14 +243,16 @@ function getMountedWrapper(spaceResources = [], spaceItem = null, imageContent = LOAD_FILES: jest.fn() }, actions: { - loadIndicators: jest.fn() + loadIndicators: jest.fn(), + loadCurrentFileOutgoingShares: jest.fn() }, getters: { activeFiles: () => spaceResources, totalFilesCount: () => ({ files: spaceResources.length, folders: 0 }), selectedFiles: () => [], totalFilesSize: () => 10, - pages: () => 1 + pages: () => 1, + currentFileOutgoingCollaborators: () => [spaceShare] } } } 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 d2ec3a7e51a..6a3ae8a31c8 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 @@ -10,51 +10,56 @@ exports[`Spaces project view space image should show if given 1`] = `
    -
    +

    space

    - - - - -
      -
    • - - - Rename - -
    • -
    • - - - Change subtitle - -
    • -
    • - - - Edit description - -
    • -
    • - - - Upload new space image - -
    • -
    • - - - Disable - -
    • -
    • - - - Details - -
    • -
    -
    +
    + + 1 invited person + + + + + +
      +
    • + + + Rename + +
    • +
    • + + + Change subtitle + +
    • +
    • + + + Edit description + +
    • +
    • + + + Upload new space image + +
    • +
    • + + + Disable + +
    • +
    • + + + Details + +
    • +
    +
    +
    @@ -80,51 +85,56 @@ exports[`Spaces project view space readme should show if given 1`] = `
    -
    +

    space

    - - - - -
      -
    • - - - Rename - -
    • -
    • - - - Change subtitle - -
    • -
    • - - - Edit description - -
    • -
    • - - - Upload new space image - -
    • -
    • - - - Disable - -
    • -
    • - - - Details - -
    • -
    -
    +
    + + 1 invited person + + + + + +
      +
    • + + + Rename + +
    • +
    • + + + Change subtitle + +
    • +
    • + + + Edit description + +
    • +
    • + + + Upload new space image + +
    • +
    • + + + Disable + +
    • +
    • + + + Details + +
    • +
    +
    +
    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 1838eab175c..09fd0051fb8 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 @@ -13,7 +13,8 @@ exports[`Spaces component should list spaces 1`] = ` oc-grid-row-large oc-text-center oc-child-width-1-3@m - oc-child-width-1-5@l + oc-child-width-1-4@l + oc-child-width-1-5@xl ">
  • @@ -23,26 +24,29 @@ exports[`Spaces component should list spaces 1`] = `
    -
    -
    -
    -
      -
    • -
    • -
    • -
    • -
    • -
    +
    +
    +
    +
    +
    +
      +
    • +
    • +
    • +
    • +
    • +
    +
    From 8c891b385696d1ba800c0ad1f7162173715f0041 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 2 Mar 2022 10:17:33 +0100 Subject: [PATCH 3/8] Add changelog item --- .../unreleased/enhancement-spaces-people-sharing | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 changelog/unreleased/enhancement-spaces-people-sharing diff --git a/changelog/unreleased/enhancement-spaces-people-sharing b/changelog/unreleased/enhancement-spaces-people-sharing new file mode 100644 index 00000000000..8901c33f485 --- /dev/null +++ b/changelog/unreleased/enhancement-spaces-people-sharing @@ -0,0 +1,11 @@ +Enhancement: Implement people sharing for spaces + +Spaces can now be shared with other people. This change specifically includes: + +* listing all members who have access to a space (possible for all space members) +* adding members to a space and giving them dedicated roles (possible for managers only) +* editing the role of members (possible for managers only) +* removing members from a space (possible for managers only) + +https://github.com/owncloud/web/pull/6455 +https://github.com/owncloud/web/issues/6283 From de908c803eb425c5585b51528bec9b50f4a9642d Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 2 Mar 2022 14:40:53 +0100 Subject: [PATCH 4/8] Use uuid instead of index for the role dropdown class --- .../components/SideBar/Shares/Collaborators/ListItem.vue | 6 ------ .../src/components/SideBar/Shares/FileShares.vue | 3 +-- .../src/components/SideBar/Shares/RoleDropdown.vue | 8 ++------ 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue index bcfabad2809..a8039aaf835 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue @@ -67,7 +67,6 @@ :existing-permissions="share.customPermissions" :existing-role="share.role" :allow-share-permission="!isOcis || isSpace" - :index="index" class="files-collaborators-collaborator-role" @optionChange="shareRoleChanged" /> @@ -110,11 +109,6 @@ export default { modifiable: { type: Boolean, default: false - }, - index: { - type: Number, - required: false, - default: 0 } }, computed: { diff --git a/packages/web-app-files/src/components/SideBar/Shares/FileShares.vue b/packages/web-app-files/src/components/SideBar/Shares/FileShares.vue index c6f7a05d633..9698f3fab2b 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/FileShares.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/FileShares.vue @@ -34,11 +34,10 @@ class="oc-list oc-list-divider oc-overflow-hidden oc-m-rm" :aria-label="$gettext('Share receivers')" > -
  • +
  • diff --git a/packages/web-app-files/src/components/SideBar/Shares/RoleDropdown.vue b/packages/web-app-files/src/components/SideBar/Shares/RoleDropdown.vue index 0c9f03a606e..64574a57ddb 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/RoleDropdown.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/RoleDropdown.vue @@ -83,6 +83,7 @@ import { ShareRole, SpacePeopleShareRoles } from '../../../helpers/share' +import * as uuid from 'uuid' export default { name: 'RoleDropdown', @@ -110,11 +111,6 @@ export default { allowSharePermission: { type: Boolean, required: true - }, - index: { - type: Number, - required: false, - default: 0 } }, data() { @@ -126,7 +122,7 @@ export default { computed: { roleButtonId() { if (this.shareId) { - return `files-collaborators-role-button-${this.shareId}-${this.index}` + return `files-collaborators-role-button-${this.shareId}-${uuid.v4()}` } return 'files-collaborators-role-button-new' }, From 0db26466a24d24f60aca53fe2d51a0d397cb72d1 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 2 Mar 2022 14:41:14 +0100 Subject: [PATCH 5/8] Redesign the project view a bit --- .../src/views/spaces/Project.vue | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/web-app-files/src/views/spaces/Project.vue b/packages/web-app-files/src/views/spaces/Project.vue index 9eee72e1b1c..aa72d1d47bd 100644 --- a/packages/web-app-files/src/views/spaces/Project.vue +++ b/packages/web-app-files/src/views/spaces/Project.vue @@ -10,37 +10,20 @@ :space="space" > -
    -
    -
    - -
    +
    +
    +
    -
    +
    -

    {{ space.name }}

    - - - - +

    {{ space.name }}

    + + + +

    {{ space.description }}

    @@ -345,8 +340,8 @@ export default { }, peopleCountString() { const translated = this.$ngettext( - '%{count} invited person', - '%{count} invited people', + '%{count} member', + '%{count} members', this.currentFileOutgoingCollaborators.length ) @@ -541,9 +536,9 @@ export default { .space-overview { &-image { border-radius: 10px; - max-height: 250px; - object-fit: cover; width: 100%; + aspect-ratio: 16 / 9; + object-fit: cover; } &-image.expanded { From bad70e71813bdf2cf38e1e40926c355fa31c4fc1 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 2 Mar 2022 14:41:38 +0100 Subject: [PATCH 6/8] Rename 'people' to 'members' for spaces --- .../SideBar/Details/SpaceDetails.vue | 110 +++++++++--------- .../components/SideBar/Shares/SpaceShares.vue | 4 +- .../src/views/spaces/Projects.vue | 4 +- .../__snapshots__/FileShares.spec.js.snap | 4 +- .../__snapshots__/SpaceShares.spec.js.snap | 16 +-- .../__snapshots__/Projects.spec.js.snap | 2 +- 6 files changed, 68 insertions(+), 72 deletions(-) diff --git a/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue b/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue index 50981adeedc..03ee2ceed31 100644 --- a/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue +++ b/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue @@ -12,17 +12,32 @@ class="space-default-image oc-px-m oc-py-m" />
    -
    - - - - - +
    +
    + + + + + + + +
    +
    + + +
    @@ -122,60 +137,41 @@ export default { }) .join(', ') }, - hasPeopleShares() { - return this.peopleShareCount > 1 + hasMemberShares() { + return this.memberShareCount > 1 }, hasLinkShares() { return this.linkShareCount > 1 }, - peopleShareCount() { + memberShareCount() { return this.currentFileOutgoingCollaborators.length }, linkShareCount() { return this.currentFileOutgoingLinks.length }, - 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 '' + memberShareLabel() { + return this.$gettextInterpolate( + this.$ngettext( + 'This space has %{memberShareCount} member.', + 'This space has %{memberShareCount} members.', + this.memberShareCount + ), + { + memberShareCount: this.memberShareCount + } + ) + }, + linkShareLabel() { + return this.$gettextInterpolate( + this.$ngettext( + '%{linkShareCount} link giving access.', + '%{linkShareCount} links giving access.', + this.linkShareCount + ), + { + linkShareCount: this.linkShareCount + } + ) } }, mounted() { @@ -187,7 +183,7 @@ export default { closeSidebar: 'close' }), - expandPeoplePanel() { + expandMemberPanel() { this.setSidebarPanel('space-share-item') } } diff --git a/packages/web-app-files/src/components/SideBar/Shares/SpaceShares.vue b/packages/web-app-files/src/components/SideBar/Shares/SpaceShares.vue index cf681ef71af..1011aaadedf 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/SpaceShares.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/SpaceShares.vue @@ -1,6 +1,6 @@