diff options
author | fenn-cs <fenn25.fn@gmail.com> | 2023-07-19 02:11:27 +0100 |
---|---|---|
committer | Louis Chemineau <louis@chmn.me> | 2023-08-30 18:12:49 +0200 |
commit | 8b42fb033fdcd3775b4850de6faf6091c8dcc716 (patch) | |
tree | 7ca9ccb33d95090ae4a34e24ef650d0eede8732d /apps | |
parent | 191e20d7f48338ca336fd0091301653251fc0667 (diff) | |
download | nextcloud-server-8b42fb033fdcd3775b4850de6faf6091c8dcc716.tar.gz nextcloud-server-8b42fb033fdcd3775b4850de6faf6091c8dcc716.zip |
Improve sharing flow
This commit introduces the following changes:
- Does not create new share once user is selected for internal shares
- Adds a `SharingDetails` view for share configurations
- Adds a quick share select to enable fast changes in share permisions.
Resolves: https://github.com/nextcloud/server/issues/26691
Signed-off-by: fenn-cs <fenn25.fn@gmail.com>
Signed-off-by: Louis Chemineau <louis@chmn.me>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files_sharing/src/components/SharingEntry.vue | 407 | ||||
-rw-r--r-- | apps/files_sharing/src/components/SharingEntryLink.vue | 178 | ||||
-rw-r--r-- | apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue | 186 | ||||
-rw-r--r-- | apps/files_sharing/src/components/SharingEntrySimple.vue | 4 | ||||
-rw-r--r-- | apps/files_sharing/src/components/SharingInput.vue | 10 | ||||
-rw-r--r-- | apps/files_sharing/src/lib/SharePermissionsToolBox.js | 1 | ||||
-rw-r--r-- | apps/files_sharing/src/mixins/ShareDetails.js | 43 | ||||
-rw-r--r-- | apps/files_sharing/src/mixins/ShareRequests.js | 23 | ||||
-rw-r--r-- | apps/files_sharing/src/mixins/SharesMixin.js | 28 | ||||
-rw-r--r-- | apps/files_sharing/src/models/Share.js | 2 | ||||
-rw-r--r-- | apps/files_sharing/src/utils/SharedWithMe.js | 10 | ||||
-rw-r--r-- | apps/files_sharing/src/views/SharingDetailsTab.vue | 1053 | ||||
-rw-r--r-- | apps/files_sharing/src/views/SharingLinkList.vue | 6 | ||||
-rw-r--r-- | apps/files_sharing/src/views/SharingList.vue | 22 | ||||
-rw-r--r-- | apps/files_sharing/src/views/SharingTab.vue | 49 |
15 files changed, 1468 insertions, 554 deletions
diff --git a/apps/files_sharing/src/components/SharingEntry.vue b/apps/files_sharing/src/components/SharingEntry.vue index 46b65c695ee..7399617a79c 100644 --- a/apps/files_sharing/src/components/SharingEntry.vue +++ b/apps/files_sharing/src/components/SharingEntry.vue @@ -29,147 +29,64 @@ :menu-position="'left'" :url="share.shareWithAvatar" /> - <component :is="share.shareWithLink ? 'a' : 'div'" - :title="tooltip" - :aria-label="tooltip" - :href="share.shareWithLink" - class="sharing-entry__desc"> - <span>{{ title }}<span v-if="!isUnique" class="sharing-entry__desc-unique"> ({{ share.shareWithDisplayNameUnique }})</span></span> - <p v-if="hasStatus"> - <span>{{ share.status.icon || '' }}</span> - <span>{{ share.status.message || '' }}</span> - </p> - </component> - <NcActions menu-align="right" - class="sharing-entry__actions" - @close="onMenuClose"> - <template v-if="share.canEdit"> - <!-- edit permission --> - <NcActionCheckbox ref="canEdit" - :checked.sync="canEdit" - :value="permissionsEdit" - :disabled="saving || !canSetEdit"> - {{ t('files_sharing', 'Allow editing') }} - </NcActionCheckbox> - - <!-- create permission --> - <NcActionCheckbox v-if="isFolder" - ref="canCreate" - :checked.sync="canCreate" - :value="permissionsCreate" - :disabled="saving || !canSetCreate"> - {{ t('files_sharing', 'Allow creating') }} - </NcActionCheckbox> - - <!-- delete permission --> - <NcActionCheckbox v-if="isFolder" - ref="canDelete" - :checked.sync="canDelete" - :value="permissionsDelete" - :disabled="saving || !canSetDelete"> - {{ t('files_sharing', 'Allow deleting') }} - </NcActionCheckbox> - - <!-- reshare permission --> - <NcActionCheckbox v-if="config.isResharingAllowed" - ref="canReshare" - :checked.sync="canReshare" - :value="permissionsShare" - :disabled="saving || !canSetReshare"> - {{ t('files_sharing', 'Allow resharing') }} - </NcActionCheckbox> - - <NcActionCheckbox v-if="isSetDownloadButtonVisible" - ref="canDownload" - :checked.sync="canDownload" - :disabled="saving || !canSetDownload"> - {{ allowDownloadText }} - </NcActionCheckbox> - - <!-- expiration date --> - <NcActionCheckbox :checked.sync="hasExpirationDate" - :disabled="config.isDefaultInternalExpireDateEnforced || saving" - @uncheck="onExpirationDisable"> - {{ config.isDefaultInternalExpireDateEnforced - ? t('files_sharing', 'Expiration date enforced') - : t('files_sharing', 'Set expiration date') }} - </NcActionCheckbox> - <NcActionInput v-if="hasExpirationDate" - ref="expireDate" - :is-native-picker="true" - :hide-label="true" - :class="{ error: errors.expireDate}" - :disabled="saving" - :value="new Date(share.expireDate)" - type="date" - :min="dateTomorrow" - :max="dateMaxEnforced" - @input="onExpirationChange"> - {{ t('files_sharing', 'Enter a date') }} - </NcActionInput> - - <!-- note --> - <template v-if="canHaveNote"> - <NcActionCheckbox :checked.sync="hasNote" - :disabled="saving" - @uncheck="queueUpdate('note')"> - {{ t('files_sharing', 'Note to recipient') }} - </NcActionCheckbox> - <NcActionTextEditable v-if="hasNote" - ref="note" - :class="{ error: errors.note}" - :disabled="saving" - :value="share.newNote || share.note" - icon="icon-edit" - @update:value="onNoteChange" - @submit="onNoteSubmit" /> - </template> + <div class="sharing-entry__summary" @click.prevent="toggleQuickShareSelect"> + <component :is="share.shareWithLink ? 'a' : 'div'" + :title="tooltip" + :aria-label="tooltip" + :href="share.shareWithLink" + class="sharing-entry__desc"> + <span>{{ title }}<span v-if="!isUnique" class="sharing-entry__desc-unique"> ({{ + share.shareWithDisplayNameUnique }})</span></span> + <p v-if="hasStatus"> + <span>{{ share.status.icon || '' }}</span> + <span>{{ share.status.message || '' }}</span> + </p> + </component> + <QuickShareSelect :share="share" + :file-info="fileInfo" + :toggle="showDropdown" + @open-sharing-details="openShareDetailsForCustomSettings(share)" /> + </div> + <NcButton class="sharing-entry__action" + :aria-label="t('files_sharing', 'Open Sharing Details')" + type="tertiary-no-background" + @click="openSharingDetails(share)"> + <template #icon> + <DotsHorizontalIcon :size="20" /> </template> - - <NcActionButton v-if="share.canDelete" - icon="icon-close" - :disabled="saving" - @click.prevent="onDelete"> - {{ t('files_sharing', 'Unshare') }} - </NcActionButton> - </NcActions> + </NcButton> </li> </template> <script> +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' -import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js' -import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js' -import NcActionTextEditable from '@nextcloud/vue/dist/Components/NcActionTextEditable.js' +import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue' + +import QuickShareSelect from './SharingEntryQuickShareSelect.vue' import SharesMixin from '../mixins/SharesMixin.js' +import ShareDetails from '../mixins/ShareDetails.js' export default { name: 'SharingEntry', components: { - NcActions, - NcActionButton, - NcActionCheckbox, - NcActionInput, - NcActionTextEditable, + NcButton, NcAvatar, + DotsHorizontalIcon, + NcSelect, + QuickShareSelect, }, - mixins: [SharesMixin], + mixins: [SharesMixin, ShareDetails], data() { return { - permissionsEdit: OC.PERMISSION_UPDATE, - permissionsCreate: OC.PERMISSION_CREATE, - permissionsDelete: OC.PERMISSION_DELETE, - permissionsRead: OC.PERMISSION_READ, - permissionsShare: OC.PERMISSION_SHARE, + showDropdown: false, } }, - computed: { title() { let title = this.share.shareWithDisplayName @@ -186,7 +103,6 @@ export default { } return title }, - tooltip() { if (this.share.owner !== this.share.uidFileOwner) { const data = { @@ -206,182 +122,6 @@ export default { return null }, - canHaveNote() { - return !this.isRemote - }, - - isRemote() { - return this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE - || this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP - }, - - /** - * Can the sharer set whether the sharee can edit the file ? - * - * @return {boolean} - */ - canSetEdit() { - // If the owner revoked the permission after the resharer granted it - // the share still has the permission, and the resharer is still - // allowed to revoke it too (but not to grant it again). - return (this.fileInfo.sharePermissions & OC.PERMISSION_UPDATE) || this.canEdit - }, - - /** - * Can the sharer set whether the sharee can create the file ? - * - * @return {boolean} - */ - canSetCreate() { - // If the owner revoked the permission after the resharer granted it - // the share still has the permission, and the resharer is still - // allowed to revoke it too (but not to grant it again). - return (this.fileInfo.sharePermissions & OC.PERMISSION_CREATE) || this.canCreate - }, - - /** - * Can the sharer set whether the sharee can delete the file ? - * - * @return {boolean} - */ - canSetDelete() { - // If the owner revoked the permission after the resharer granted it - // the share still has the permission, and the resharer is still - // allowed to revoke it too (but not to grant it again). - return (this.fileInfo.sharePermissions & OC.PERMISSION_DELETE) || this.canDelete - }, - - /** - * Can the sharer set whether the sharee can reshare the file ? - * - * @return {boolean} - */ - canSetReshare() { - // If the owner revoked the permission after the resharer granted it - // the share still has the permission, and the resharer is still - // allowed to revoke it too (but not to grant it again). - return (this.fileInfo.sharePermissions & OC.PERMISSION_SHARE) || this.canReshare - }, - - /** - * Can the sharer set whether the sharee can download the file ? - * - * @return {boolean} - */ - canSetDownload() { - // If the owner revoked the permission after the resharer granted it - // the share still has the permission, and the resharer is still - // allowed to revoke it too (but not to grant it again). - return (this.fileInfo.canDownload() || this.canDownload) - }, - - /** - * Can the sharee edit the shared file ? - */ - canEdit: { - get() { - return this.share.hasUpdatePermission - }, - set(checked) { - this.updatePermissions({ isEditChecked: checked }) - }, - }, - - /** - * Can the sharee create the shared file ? - */ - canCreate: { - get() { - return this.share.hasCreatePermission - }, - set(checked) { - this.updatePermissions({ isCreateChecked: checked }) - }, - }, - - /** - * Can the sharee delete the shared file ? - */ - canDelete: { - get() { - return this.share.hasDeletePermission - }, - set(checked) { - this.updatePermissions({ isDeleteChecked: checked }) - }, - }, - - /** - * Can the sharee reshare the file ? - */ - canReshare: { - get() { - return this.share.hasSharePermission - }, - set(checked) { - this.updatePermissions({ isReshareChecked: checked }) - }, - }, - - /** - * Can the sharee download files or only view them ? - */ - canDownload: { - get() { - return this.share.hasDownloadPermission - }, - set(checked) { - this.updatePermissions({ isDownloadChecked: checked }) - }, - }, - - /** - * Is this share readable - * Needed for some federated shares that might have been added from file drop links - */ - hasRead: { - get() { - return this.share.hasReadPermission - }, - }, - - /** - * Is the current share a folder ? - * - * @return {boolean} - */ - isFolder() { - return this.fileInfo.type === 'dir' - }, - - /** - * Does the current share have an expiration date - * - * @return {boolean} - */ - hasExpirationDate: { - get() { - return this.config.isDefaultInternalExpireDateEnforced || !!this.share.expireDate - }, - set(enabled) { - const defaultExpirationDate = this.config.defaultInternalExpirationDate - || new Date(new Date().setDate(new Date().getDate() + 1)) - this.share.expireDate = enabled - ? this.formatDateToString(defaultExpirationDate) - : '' - console.debug('Expiration date status', enabled, this.share.expireDate) - }, - }, - - dateMaxEnforced() { - if (!this.isRemote && this.config.isDefaultInternalExpireDateEnforced) { - return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultInternalExpireDate)) - } else if (this.config.isDefaultRemoteExpireDateEnforced) { - return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultRemoteExpireDate)) - } - return null - }, - /** * @return {boolean} */ @@ -392,70 +132,18 @@ export default { return (typeof this.share.status === 'object' && !Array.isArray(this.share.status)) }, - - /** - * @return {string} - */ - allowDownloadText() { - return t('files_sharing', 'Allow download') - }, - - /** - * @return {boolean} - */ - isSetDownloadButtonVisible() { - // TODO: Implement download permission for circle shares instead of hiding the option. - // https://github.com/nextcloud/server/issues/39161 - if (this.share && this.share.type === this.SHARE_TYPES.SHARE_TYPE_CIRCLE) { - return false - } - - const allowedMimetypes = [ - // Office documents - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.ms-powerpoint', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.oasis.opendocument.text', - 'application/vnd.oasis.opendocument.spreadsheet', - 'application/vnd.oasis.opendocument.presentation', - ] - - return this.isFolder || allowedMimetypes.includes(this.fileInfo.mimetype) - }, }, methods: { - updatePermissions({ - isEditChecked = this.canEdit, - isCreateChecked = this.canCreate, - isDeleteChecked = this.canDelete, - isReshareChecked = this.canReshare, - isDownloadChecked = this.canDownload, - } = {}) { - // calc permissions if checked - const permissions = 0 - | (this.hasRead ? this.permissionsRead : 0) - | (isCreateChecked ? this.permissionsCreate : 0) - | (isDeleteChecked ? this.permissionsDelete : 0) - | (isEditChecked ? this.permissionsEdit : 0) - | (isReshareChecked ? this.permissionsShare : 0) - - this.share.permissions = permissions - if (this.share.hasDownloadPermission !== isDownloadChecked) { - this.share.hasDownloadPermission = isDownloadChecked - } - this.queueUpdate('permissions', 'attributes') - }, - /** * Save potential changed data on menu close */ onMenuClose() { this.onNoteSubmit() }, + toggleQuickShareSelect() { + this.showDropdown = !this.showDropdown + }, }, } </script> @@ -465,21 +153,34 @@ export default { display: flex; align-items: center; height: 44px; + &__desc { display: flex; flex-direction: column; justify-content: space-between; - padding: 8px; + padding-bottom: 0; line-height: 1.2em; + p { color: var(--color-text-maxcontrast); } + &-unique { color: var(--color-text-maxcontrast); } } + &__actions { margin-left: auto; } + + &__summary { + padding: 8px; + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + } + } </style> diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue index 06c9cb70851..7cf0804b841 100644 --- a/apps/files_sharing/src/components/SharingEntryLink.vue +++ b/apps/files_sharing/src/components/SharingEntryLink.vue @@ -25,13 +25,18 @@ <NcAvatar :is-no-user="true" :icon-class="isEmailShareType ? 'avatar-link-share icon-mail-white' : 'avatar-link-share icon-public-white'" class="sharing-entry__avatar" /> - <div class="sharing-entry__desc"> + <div class="sharing-entry__desc" @click.prevent="toggleQuickShareSelect"> <span class="sharing-entry__title" :title="title"> {{ title }} </span> <p v-if="subtitle"> {{ subtitle }} </p> + <QuickShareSelect v-if="share && share.permissions !== undefined" + :share="share" + :file-info="fileInfo" + :toggle="showDropdown" + @open-sharing-details="openShareDetailsForCustomSettings(share)" /> </div> <!-- clipboard --> @@ -123,110 +128,13 @@ @close="onMenuClose"> <template v-if="share"> <template v-if="share.canEdit && canReshare"> - <!-- Custom Label --> - <NcActionInput ref="label" - :class="{ error: errors.label }" - :disabled="saving" - :label="t('files_sharing', 'Share label')" - :value="share.newLabel !== undefined ? share.newLabel : share.label" - icon="icon-edit" - maxlength="255" - @update:value="onLabelChange" - @submit="onLabelSubmit" /> - - <SharePermissionsEditor :can-reshare="canReshare" - :share.sync="share" - :file-info="fileInfo" /> - - <NcActionSeparator /> - - <NcActionCheckbox :checked.sync="share.hideDownload" - :disabled="saving || canChangeHideDownload" - @change="queueUpdate('hideDownload')"> - {{ t('files_sharing', 'Hide download') }} - </NcActionCheckbox> - - <!-- password --> - <NcActionCheckbox :checked.sync="isPasswordProtected" - :disabled="config.enforcePasswordForPublicLink || saving" - class="share-link-password-checkbox" - @uncheck="onPasswordDisable"> - {{ config.enforcePasswordForPublicLink - ? t('files_sharing', 'Password protection (enforced)') - : t('files_sharing', 'Password protect') }} - </NcActionCheckbox> - - <NcActionInput v-if="isPasswordProtected" - ref="password" - class="share-link-password" - :class="{ error: errors.password}" - :disabled="saving" - :show-trailing-button="hasUnsavedPassword" - :required="config.enforcePasswordForPublicLink" - :value="hasUnsavedPassword ? share.newPassword : '***************'" - icon="icon-password" - autocomplete="new-password" - :type="hasUnsavedPassword ? 'text': 'password'" - @update:value="onPasswordChange" - @submit="onPasswordSubmit"> - {{ t('files_sharing', 'Enter a password') }} - </NcActionInput> - <NcActionText v-if="isEmailShareType && passwordExpirationTime" icon="icon-info"> - {{ t('files_sharing', 'Password expires {passwordExpirationTime}', {passwordExpirationTime}) }} - </NcActionText> - <NcActionText v-else-if="isEmailShareType && passwordExpirationTime !== null" icon="icon-error"> - {{ t('files_sharing', 'Password expired') }} - </NcActionText> - - <!-- password protected by Talk --> - <NcActionCheckbox v-if="isPasswordProtectedByTalkAvailable" - :checked.sync="isPasswordProtectedByTalk" - :disabled="!canTogglePasswordProtectedByTalkAvailable || saving" - class="share-link-password-talk-checkbox" - @change="onPasswordProtectedByTalkChange"> - {{ t('files_sharing', 'Video verification') }} - </NcActionCheckbox> - - <!-- expiration date --> - <NcActionCheckbox :checked.sync="hasExpirationDate" - :disabled="config.isDefaultExpireDateEnforced || saving" - class="share-link-expire-date-checkbox" - @uncheck="onExpirationDisable"> - {{ config.isDefaultExpireDateEnforced - ? t('files_sharing', 'Expiration date (enforced)') - : t('files_sharing', 'Set expiration date') }} - </NcActionCheckbox> - <NcActionInput v-if="hasExpirationDate" - ref="expireDate" - :is-native-picker="true" - :hide-label="true" - class="share-link-expire-date" - :class="{ error: errors.expireDate}" - :disabled="saving" - :value="new Date(share.expireDate)" - type="date" - :min="dateTomorrow" - :max="dateMaxEnforced" - @input="onExpirationChange"> - {{ t('files_sharing', 'Enter a date') }} - </NcActionInput> - - <!-- note --> - <NcActionCheckbox :checked.sync="hasNote" - :disabled="saving" - @uncheck="queueUpdate('note')"> - {{ t('files_sharing', 'Note to recipient') }} - </NcActionCheckbox> - - <NcActionTextEditable v-if="hasNote" - ref="note" - :class="{ error: errors.note}" - :disabled="saving" - :placeholder="t('files_sharing', 'Enter a note for the share recipient')" - :value="share.newNote || share.note" - icon="icon-edit" - @update:value="onNoteChange" - @submit="onNoteSubmit" /> + <NcActionButton :disabled="saving" + @click.prevent="openSharingDetails"> + <template #icon> + <Tune /> + </template> + {{ t('files_sharing', 'Customize link') }} + </NcActionButton> </template> <NcActionSeparator /> @@ -248,18 +156,19 @@ {{ name }} </NcActionLink> - <NcActionButton v-if="share.canDelete" - icon="icon-close" - :disabled="saving" - @click.prevent="onDelete"> - {{ t('files_sharing', 'Unshare') }} - </NcActionButton> <NcActionButton v-if="!isEmailShareType && canReshare" class="new-share-link" icon="icon-add" @click.prevent.stop="onNewLinkShare"> {{ t('files_sharing', 'Add another link') }} </NcActionButton> + + <NcActionButton v-if="share.canDelete" + icon="icon-close" + :disabled="saving" + @click.prevent="onDelete"> + {{ t('files_sharing', 'Unshare') }} + </NcActionButton> </template> <!-- Create new share --> @@ -283,39 +192,40 @@ import { Type as ShareTypes } from '@nextcloud/sharing' import Vue from 'vue' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js' import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js' import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js' import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js' import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js' -import NcActionTextEditable from '@nextcloud/vue/dist/Components/NcActionTextEditable.js' import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' +import Tune from 'vue-material-design-icons/Tune.vue' + +import QuickShareSelect from './SharingEntryQuickShareSelect.vue' + import ExternalShareAction from './ExternalShareAction.vue' -import SharePermissionsEditor from './SharePermissionsEditor.vue' import GeneratePassword from '../utils/GeneratePassword.js' import Share from '../models/Share.js' import SharesMixin from '../mixins/SharesMixin.js' +import ShareDetails from '../mixins/ShareDetails.js' export default { name: 'SharingEntryLink', components: { + ExternalShareAction, NcActions, NcActionButton, - NcActionCheckbox, NcActionInput, NcActionLink, NcActionText, - NcActionTextEditable, NcActionSeparator, NcAvatar, - ExternalShareAction, - SharePermissionsEditor, + Tune, + QuickShareSelect, }, - mixins: [SharesMixin], + mixins: [SharesMixin, ShareDetails], props: { canReshare: { @@ -330,6 +240,7 @@ export default { data() { return { + showDropdown: false, copySuccess: true, copied: false, @@ -593,7 +504,6 @@ export default { canChangeHideDownload() { const hasDisabledDownload = (shareAttribute) => shareAttribute.key === 'download' && shareAttribute.scope === 'permissions' && shareAttribute.enabled === false - return this.fileInfo.shareAttributes.some(hasDisabledDownload) }, }, @@ -671,7 +581,7 @@ export default { * accordingly * * @param {Share} share the new share - * @param {boolean} [update=false] do we update the current share ? + * @param {boolean} [update] do we update the current share ? */ async pushNewLinkShare(share, update) { try { @@ -748,26 +658,6 @@ export default { this.loading = false } }, - - /** - * Label changed, let's save it to a different key - * - * @param {string} label the share label - */ - onLabelChange(label) { - this.$set(this.share, 'newLabel', label.trim()) - }, - - /** - * When the note change, we trim, save and dispatch - */ - onLabelSubmit() { - if (typeof this.share.newLabel === 'string') { - this.share.label = this.share.newLabel - this.$delete(this.share, 'newLabel') - this.queueUpdate('label') - } - }, async copyLink() { try { await navigator.clipboard.writeText(this.shareLink) @@ -870,6 +760,10 @@ export default { // YET. We can safely delete the share :) this.$emit('remove:share', this.share) }, + + toggleQuickShareSelect() { + this.showDropdown = !this.showDropdown + }, }, } </script> @@ -879,13 +773,13 @@ export default { display: flex; align-items: center; min-height: 44px; + &__desc { display: flex; flex-direction: column; justify-content: space-between; padding: 8px; line-height: 1.2em; - overflow: hidden; p { color: var(--color-text-maxcontrast); diff --git a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue new file mode 100644 index 00000000000..8128b3925ee --- /dev/null +++ b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue @@ -0,0 +1,186 @@ +<template> + <div :class="{ 'active': showDropdown, 'share-select': true }"> + <span class="trigger-text" @click="toggleDropdown"> + {{ selectedOption }} + <DropdownIcon :size="15" /> + </span> + <div v-if="showDropdown" class="share-select-dropdown-container"> + <div v-for="option in options" + :key="option" + :class="{ 'dropdown-item': true, 'selected': option === selectedOption }" + @click="selectOption(option)"> + {{ option }} + </div> + </div> + </div> +</template> + +<script> +import DropdownIcon from 'vue-material-design-icons/TriangleSmallDown.vue' +import SharesMixin from '../mixins/SharesMixin.js' +import ShareDetails from '../mixins/ShareDetails.js' +import ShareTypes from '../mixins/ShareTypes.js' + +import { + BUNDLED_PERMISSIONS, + ATOMIC_PERMISSIONS, +} from '../lib/SharePermissionsToolBox.js' + +export default { + components: { + DropdownIcon, + }, + mixins: [SharesMixin, ShareDetails, ShareTypes], + props: { + share: { + type: Object, + required: true, + }, + toggle: { + type: Boolean, + default: false, + }, + }, + data() { + return { + selectedOption: '', + showDropdown: this.toggle, + } + }, + computed: { + canViewText() { + return t('files_sharing', 'View only') + }, + canEditText() { + return t('files_sharing', 'Can edit') + }, + fileDropText() { + return t('files_sharing', 'File drop') + }, + customPermissionsText() { + return t('files_sharing', 'Custom permissions') + }, + preSelectedOption() { + // We remove the share permission for the comparison as it is not relevant for bundled permissions. + if ((this.share.permissions & ~ATOMIC_PERMISSIONS.SHARE) === BUNDLED_PERMISSIONS.READ_ONLY) { + return this.canViewText + } else if (this.share.permissions === BUNDLED_PERMISSIONS.ALL || this.share.permissions === BUNDLED_PERMISSIONS.ALL_FILE) { + return this.canEditText + } else if ((this.share.permissions & ~ATOMIC_PERMISSIONS.SHARE) === BUNDLED_PERMISSIONS.FILE_DROP) { + return this.fileDropText + } + + return this.customPermissionsText + + }, + options() { + const options = [this.canViewText, this.canEditText] + if (this.supportsFileDrop) { + options.push(this.fileDropText) + } + options.push(this.customPermissionsText) + + return options + }, + supportsFileDrop() { + if (this.isFolder) { + const shareType = this.share.type ?? this.share.shareType + return [this.SHARE_TYPES.SHARE_TYPE_LINK, this.SHARE_TYPES.SHARE_TYPE_EMAIL].includes(shareType) + } + return false + }, + dropDownPermissionValue() { + switch (this.selectedOption) { + case this.canEditText: + return this.isFolder ? BUNDLED_PERMISSIONS.ALL : BUNDLED_PERMISSIONS.ALL_FILE + case this.fileDropText: + return BUNDLED_PERMISSIONS.FILE_DROP + case this.customPermissionsText: + return 'custom' + case this.canViewText: + default: + return BUNDLED_PERMISSIONS.READ_ONLY + } + }, + }, + watch: { + toggle(toggleValue) { + this.showDropdown = toggleValue + }, + }, + mounted() { + this.initializeComponent() + }, + methods: { + toggleDropdown() { + this.showDropdown = !this.showDropdown + }, + selectOption(option) { + this.selectedOption = option + if (option === this.customPermissionsText) { + this.$emit('open-sharing-details') + } else { + this.share.permissions = this.dropDownPermissionValue + this.queueUpdate('permissions') + } + this.showDropdown = false + }, + initializeComponent() { + this.selectedOption = this.preSelectedOption + }, + }, + +} +</script> + +<style lang="scss" scoped> +.share-select { + position: relative; + cursor: pointer; + + .trigger-text { + display: flex; + flex-direction: row; + align-items: center; + font-size: 12.5px; + gap: 2px; + color: var(--color-primary-element); + } + + .share-select-dropdown-container { + position: absolute; + top: 100%; + left: 0; + background-color: var(--color-main-background); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + padding: 4px 0; + z-index: 1; + + .dropdown-item { + padding: 8px; + font-size: 12px; + + &:hover { + background-color: #f2f2f2; + } + + &.selected { + background-color: #f0f0f0; + } + } + } + + /* Optional: Add a transition effect for smoother dropdown animation */ + .share-select-dropdown-container { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; + } + + &.active .share-select-dropdown-container { + max-height: 200px; + /* Adjust the value to your desired height */ + } +} +</style> diff --git a/apps/files_sharing/src/components/SharingEntrySimple.vue b/apps/files_sharing/src/components/SharingEntrySimple.vue index daff947fe80..5e858de990b 100644 --- a/apps/files_sharing/src/components/SharingEntrySimple.vue +++ b/apps/files_sharing/src/components/SharingEntrySimple.vue @@ -29,8 +29,8 @@ {{ subtitle }} </p> </div> - <NcActions ref="actionsComponent" - v-if="$slots['default']" + <NcActions v-if="$slots['default']" + ref="actionsComponent" class="sharing-entry__actions" menu-align="right" :aria-expanded="ariaExpandedValue"> diff --git a/apps/files_sharing/src/components/SharingInput.vue b/apps/files_sharing/src/components/SharingInput.vue index 8b740c1bac3..c5ed27477b6 100644 --- a/apps/files_sharing/src/components/SharingInput.vue +++ b/apps/files_sharing/src/components/SharingInput.vue @@ -24,6 +24,7 @@ <div class="sharing-search"> <label for="sharing-search-input">{{ t('files_sharing', 'Search for share recipients') }}</label> <NcSelect ref="select" + v-model="value" input-id="sharing-search-input" class="sharing-search__input" :disabled="!canReshare" @@ -33,10 +34,9 @@ :clear-search-on-blur="() => false" :user-select="true" :options="options" - v-model="value" @open="handleOpen" @search="asyncFind" - @option:selected="addShare"> + @option:selected="openSharingDetails"> <template #no-options="{ search }"> {{ search ? noResultText : t('files_sharing', 'No recommendations. Start typing.') }} </template> @@ -57,6 +57,7 @@ import GeneratePassword from '../utils/GeneratePassword.js' import Share from '../models/Share.js' import ShareRequests from '../mixins/ShareRequests.js' import ShareTypes from '../mixins/ShareTypes.js' +import ShareDetails from '../mixins/ShareDetails.js' export default { name: 'SharingInput', @@ -65,7 +66,7 @@ export default { NcSelect, }, - mixins: [ShareTypes, ShareRequests], + mixins: [ShareTypes, ShareRequests, ShareDetails], props: { shares: { @@ -176,7 +177,7 @@ export default { * Get suggestions * * @param {string} search the search query - * @param {boolean} [lookup=false] search on lookup server + * @param {boolean} [lookup] search on lookup server */ async getSuggestions(search, lookup = false) { this.loading = true @@ -452,7 +453,6 @@ export default { } return { - id: `${result.value.shareType}-${result.value.shareWith}`, shareWith: result.value.shareWith, shareType: result.value.shareType, user: result.uuid || result.value.shareWith, diff --git a/apps/files_sharing/src/lib/SharePermissionsToolBox.js b/apps/files_sharing/src/lib/SharePermissionsToolBox.js index f5806df70bf..d86f8827b2c 100644 --- a/apps/files_sharing/src/lib/SharePermissionsToolBox.js +++ b/apps/files_sharing/src/lib/SharePermissionsToolBox.js @@ -34,6 +34,7 @@ export const BUNDLED_PERMISSIONS = { UPLOAD_AND_UPDATE: ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE, FILE_DROP: ATOMIC_PERMISSIONS.CREATE, ALL: ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.DELETE | ATOMIC_PERMISSIONS.SHARE, + ALL_FILE: ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.SHARE, } /** diff --git a/apps/files_sharing/src/mixins/ShareDetails.js b/apps/files_sharing/src/mixins/ShareDetails.js new file mode 100644 index 00000000000..53ec8bfe16a --- /dev/null +++ b/apps/files_sharing/src/mixins/ShareDetails.js @@ -0,0 +1,43 @@ +import Share from '../models/Share.js' + +export default { + methods: { + openSharingDetails(share) { + const shareRequestObject = { + fileInfo: this.fileInfo, + share: this.mapShareRequestToShareObject(share), + } + this.$emit('open-sharing-details', shareRequestObject) + }, + openShareDetailsForCustomSettings(share) { + share.setCustomPermissions = true + this.openSharingDetails(share) + }, + mapShareRequestToShareObject(shareRequestObject) { + + if (shareRequestObject.id) { + return shareRequestObject + } + + const share = { + attributes: [ + { + enabled: true, + key: 'download', + scope: 'permissions', + }, + ], + share_type: shareRequestObject.shareType, + share_with: shareRequestObject.shareWith, + is_no_user: shareRequestObject.isNoUser, + user: shareRequestObject.shareWith, + share_with_displayname: shareRequestObject.displayName, + subtitle: shareRequestObject.subtitle, + permissions: shareRequestObject.permissions, + expiration: '', + } + + return new Share(share) + }, + }, +} diff --git a/apps/files_sharing/src/mixins/ShareRequests.js b/apps/files_sharing/src/mixins/ShareRequests.js index e1d246b7400..5f301108f6f 100644 --- a/apps/files_sharing/src/mixins/ShareRequests.js +++ b/apps/files_sharing/src/mixins/ShareRequests.js @@ -42,19 +42,20 @@ export default { * @param {string} data.path path to the file/folder which should be shared * @param {number} data.shareType 0 = user; 1 = group; 3 = public link; 6 = federated cloud share * @param {string} data.shareWith user/group id with which the file should be shared (optional for shareType > 1) - * @param {boolean} [data.publicUpload=false] allow public upload to a public shared folder + * @param {boolean} [data.publicUpload] allow public upload to a public shared folder * @param {string} [data.password] password to protect public link Share with - * @param {number} [data.permissions=31] 1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1) - * @param {boolean} [data.sendPasswordByTalk=false] send the password via a talk conversation - * @param {string} [data.expireDate=''] expire the shareautomatically after - * @param {string} [data.label=''] custom label - * @param {string} [data.attributes=null] Share attributes encoded as json + * @param {number} [data.permissions] 1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1) + * @param {boolean} [data.sendPasswordByTalk] send the password via a talk conversation + * @param {string} [data.expireDate] expire the shareautomatically after + * @param {string} [data.label] custom label + * @param {string} [data.attributes] Share attributes encoded as json + * @param data.note * @return {Share} the new share * @throws {Error} */ - async createShare({ path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label, attributes }) { + async createShare({ path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label, note, attributes }) { try { - const request = await axios.post(shareUrl, { path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label, attributes }) + const request = await axios.post(shareUrl, { path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label, note, attributes }) if (!request?.data?.ocs) { throw request } @@ -66,7 +67,7 @@ export default { const errorMessage = error?.response?.data?.ocs?.meta?.message OC.Notification.showTemporary( errorMessage ? t('files_sharing', 'Error creating the share: {errorMessage}', { errorMessage }) : t('files_sharing', 'Error creating the share'), - { type: 'error' } + { type: 'error' }, ) throw error } @@ -91,7 +92,7 @@ export default { const errorMessage = error?.response?.data?.ocs?.meta?.message OC.Notification.showTemporary( errorMessage ? t('files_sharing', 'Error deleting the share: {errorMessage}', { errorMessage }) : t('files_sharing', 'Error deleting the share'), - { type: 'error' } + { type: 'error' }, ) throw error } @@ -118,7 +119,7 @@ export default { const errorMessage = error?.response?.data?.ocs?.meta?.message OC.Notification.showTemporary( errorMessage ? t('files_sharing', 'Error updating the share: {errorMessage}', { errorMessage }) : t('files_sharing', 'Error updating the share'), - { type: 'error' } + { type: 'error' }, ) } const message = error.response.data.ocs.meta.message diff --git a/apps/files_sharing/src/mixins/SharesMixin.js b/apps/files_sharing/src/mixins/SharesMixin.js index a29e1a91b02..aba1462248a 100644 --- a/apps/files_sharing/src/mixins/SharesMixin.js +++ b/apps/files_sharing/src/mixins/SharesMixin.js @@ -36,13 +36,17 @@ import SharesRequests from './ShareRequests.js' import ShareTypes from './ShareTypes.js' import Config from '../services/ConfigService.js' +import { + BUNDLED_PERMISSIONS, +} from '../lib/SharePermissionsToolBox.js' + export default { mixins: [SharesRequests, ShareTypes], props: { fileInfo: { type: Object, - default: () => {}, + default: () => { }, required: true, }, share: { @@ -121,11 +125,24 @@ export default { monthFormat: 'MMM', } }, - + isFolder() { + return this.fileInfo.type === 'dir' + }, + isPublicShare() { + const shareType = this.share.shareType ?? this.share.type + return [this.SHARE_TYPES.SHARE_TYPE_LINK, this.SHARE_TYPES.SHARE_TYPE_EMAIL].includes(shareType) + }, isShareOwner() { return this.share && this.share.owner === getCurrentUser().uid }, - + hasCustomPermissions() { + const bundledPermissions = [ + BUNDLED_PERMISSIONS.ALL, + BUNDLED_PERMISSIONS.READ_ONLY, + BUNDLED_PERMISSIONS.FILE_DROP, + ] + return !bundledPermissions.includes(this.share.permissions) + }, }, methods: { @@ -180,8 +197,7 @@ export default { * @param {Date} date */ onExpirationChange(date) { - this.share.expireDate = this.formatDateToString(date) - this.queueUpdate('expireDate') + this.share.expireDate = this.formatDateToString(new Date(date)) }, /** @@ -192,7 +208,6 @@ export default { */ onExpirationDisable() { this.share.expireDate = '' - this.queueUpdate('expireDate') }, /** @@ -335,7 +350,6 @@ export default { } } }, - /** * Debounce queueUpdate to avoid requests spamming * more importantly for text data diff --git a/apps/files_sharing/src/models/Share.js b/apps/files_sharing/src/models/Share.js index 9b1535184a0..5504c63b345 100644 --- a/apps/files_sharing/src/models/Share.js +++ b/apps/files_sharing/src/models/Share.js @@ -579,7 +579,7 @@ export default class Share { for (const i in this._share.attributes) { const attr = this._share.attributes[i] if (attr.scope === attrUpdate.scope && attr.key === attrUpdate.key) { - this._share.attributes[i] = attrUpdate + this._share.attributes.splice(i, 1, attrUpdate) return } } diff --git a/apps/files_sharing/src/utils/SharedWithMe.js b/apps/files_sharing/src/utils/SharedWithMe.js index bd39c765221..34de3f017ef 100644 --- a/apps/files_sharing/src/utils/SharedWithMe.js +++ b/apps/files_sharing/src/utils/SharedWithMe.js @@ -33,7 +33,7 @@ const shareWithTitle = function(share) { owner: share.ownerDisplayName, }, undefined, - { escape: false } + { escape: false }, ) } else if (share.type === ShareTypes.SHARE_TYPE_CIRCLE) { return t( @@ -44,7 +44,7 @@ const shareWithTitle = function(share) { owner: share.ownerDisplayName, }, undefined, - { escape: false } + { escape: false }, ) } else if (share.type === ShareTypes.SHARE_TYPE_ROOM) { if (share.shareWithDisplayName) { @@ -56,7 +56,7 @@ const shareWithTitle = function(share) { owner: share.ownerDisplayName, }, undefined, - { escape: false } + { escape: false }, ) } else { return t( @@ -66,7 +66,7 @@ const shareWithTitle = function(share) { owner: share.ownerDisplayName, }, undefined, - { escape: false } + { escape: false }, ) } } else { @@ -75,7 +75,7 @@ const shareWithTitle = function(share) { 'Shared with you by {owner}', { owner: share.ownerDisplayName }, undefined, - { escape: false } + { escape: false }, ) } } diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue new file mode 100644 index 00000000000..dbc8c1508e4 --- /dev/null +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -0,0 +1,1053 @@ +<template> + <div class="sharingTabDetailsView"> + <div class="sharingTabDetailsView__header"> + <span> + <NcAvatar v-if="isUserShare" + class="sharing-entry__avatar" + :is-no-user="share.shareType !== SHARE_TYPES.SHARE_TYPE_USER" + :user="share.shareWith" + :display-name="share.shareWithDisplayName" + :menu-position="'left'" + :url="share.shareWithAvatar" /> + <component :is="getShareTypeIcon(share.type)" :size="32" /> + </span> + <span> + <h1>{{ title }}</h1> + </span> + </div> + <div class="sharingTabDetailsView__quick-permissions"> + <div> + <NcCheckboxRadioSwitch :button-variant="true" + :checked.sync="sharingPermission" + :value="bundledPermissions.READ_ONLY.toString()" + name="sharing_permission_radio" + type="radio" + button-variant-grouped="vertical" + @update:checked="toggleCustomPermissions"> + {{ t('files_sharing', 'View only') }} + <template #icon> + <ViewIcon :size="20" /> + </template> + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch :button-variant="true" + :checked.sync="sharingPermission" + :value="bundledPermissions.ALL.toString()" + name="sharing_permission_radio" + type="radio" + button-variant-grouped="vertical" + @update:checked="toggleCustomPermissions"> + {{ t('files_sharing', 'Allow upload and editing') }} + <template #icon> + <EditIcon :size="20" /> + </template> + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch v-if="allowsFileDrop" + :button-variant="true" + :checked.sync="sharingPermission" + :value="bundledPermissions.FILE_DROP.toString()" + name="sharing_permission_radio" + type="radio" + button-variant-grouped="vertical" + @update:checked="toggleCustomPermissions"> + {{ t('files_sharing', 'File drop') }} + <small>{{ t('files_sharing', 'Upload only') }}</small> + <template #icon> + <UploadIcon :size="20" /> + </template> + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch :button-variant="true" + :checked.sync="sharingPermission" + :value="'custom'" + name="sharing_permission_radio" + type="radio" + button-variant-grouped="vertical" + @update:checked="toggleCustomPermissions"> + {{ t('files_sharing', 'Custom permissions') }} + <small>{{ t('files_sharing', customPermissionsList) }}</small> + <template #icon> + <DotsHorizontalIcon :size="20" /> + </template> + </NcCheckboxRadioSwitch> + </div> + </div> + <div class="sharingTabDetailsView__advanced-control"> + <NcButton type="tertiary" + alignment="end-reverse" + @click="advancedSectionAccordionExpanded = !advancedSectionAccordionExpanded"> + {{ t('files_sharing', 'Advanced settings') }} + <template #icon> + <MenuDownIcon /> + </template> + </NcButton> + </div> + <div v-if="advancedSectionAccordionExpanded" class="sharingTabDetailsView__advanced"> + <section> + <NcInputField v-if="isPublicShare" + :value.sync="share.label" + type="text" + :label="t('file_sharing', 'Share label')" /> + <template v-if="isPublicShare"> + <NcCheckboxRadioSwitch :checked.sync="isPasswordProtected" :disabled="isPasswordEnforced"> + {{ t('file_sharing', 'Set password') }} + </NcCheckboxRadioSwitch> + <NcInputField v-if="isPasswordProtected" + :type="hasUnsavedPassword ? 'text' : 'password'" + :value="hasUnsavedPassword ? share.newPassword : '***************'" + :error="passwordError" + :required="isPasswordEnforced" + :label="t('file_sharing', 'Password')" + @update:value="onPasswordChange" /> + + <!-- Migrate icons and remote -> icon="icon-info"--> + <span v-if="isEmailShareType && passwordExpirationTime" icon="icon-info"> + {{ t('files_sharing', 'Password expires {passwordExpirationTime}', { passwordExpirationTime }) }} + </span> + <span v-else-if="isEmailShareType && passwordExpirationTime !== null" icon="icon-error"> + {{ t('files_sharing', 'Password expired') }} + </span> + </template> + <NcCheckboxRadioSwitch :checked.sync="hasExpirationDate" :disabled="isExpiryDateEnforced"> + {{ isExpiryDateEnforced + ? t('files_sharing', 'Expiration date (enforced)') + : t('files_sharing', 'Set expiration date') }} + </NcCheckboxRadioSwitch> + <NcDateTimePickerNative v-if="hasExpirationDate" + id="share-date-picker" + :value="new Date(share.expireDate)" + :min="dateTomorrow" + :max="dateMaxEnforced" + :hide-label="true" + :disabled="isExpiryDateEnforced" + :placeholder="t('file_sharing', 'Expiration date')" + type="date" + @input="onExpirationChange" /> + <NcCheckboxRadioSwitch v-if="isPublicShare" + :disabled="canChangeHideDownload" + :checked.sync="share.hideDownload" + @update:checked="queueUpdate('hideDownload')"> + {{ t('file_sharing', 'Hide download') }} + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch v-if="canTogglePasswordProtectedByTalkAvailable" + :checked.sync="isPasswordProtectedByTalk" + @update:checked="onPasswordProtectedByTalkChange"> + {{ t('file_sharing', 'Video verification') }} + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch :checked.sync="writeNoteToRecipientIsChecked"> + {{ t('file_sharing', 'Note to recipient') }} + </NcCheckboxRadioSwitch> + <template v-if="writeNoteToRecipientIsChecked"> + <textarea :value="share.note" @input="share.note = $event.target.value" /> + </template> + <NcCheckboxRadioSwitch :checked.sync="setCustomPermissions"> + {{ t('file_sharing', 'Custom permissions') }} + </NcCheckboxRadioSwitch> + <section v-if="setCustomPermissions" class="custom-permissions-group"> + <NcCheckboxRadioSwitch :disabled="!allowsFileDrop && share.type === SHARE_TYPES.SHARE_TYPE_LINK" :checked.sync="hasRead"> + {{ t('file_sharing', 'Read') }} + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch v-if="isFolder" :disabled="!canSetCreate" :checked.sync="canCreate"> + {{ t('file_sharing', 'Create') }} + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch :disabled="!canSetEdit" :checked.sync="canEdit"> + {{ t('file_sharing', 'Update') }} + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch v-if="config.isResharingAllowed && share.type !== SHARE_TYPES.SHARE_TYPE_LINK" :disabled="!canSetReshare" :checked.sync="canReshare"> + {{ t('file_sharing', 'Share') }} + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch v-if="!isPublicShare" :disabled="!canSetDownload" :checked.sync="canDownload"> + {{ t('file_sharing', 'Download') }} + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch :disabled="!canSetDelete" :checked.sync="canDelete"> + {{ t('file_sharing', 'Delete') }} + </NcCheckboxRadioSwitch> + </section> + </section> + </div> + + <div class="sharingTabDetailsView__footer"> + <NcButton v-if="!isNewShare" + :aria-label="t('files_sharing', 'Delete share')" + :disabled="false" + :readonly="false" + type="tertiary" + @click.prevent="removeShare"> + <template #icon> + <CloseIcon :size="16" /> + </template> + {{ t('files_sharing', 'Delete share') }} + </NcButton> + <div class="button-group"> + <NcButton @click="$emit('close-sharing-details')"> + {{ t('file_sharing', 'Cancel') }} + </NcButton> + <NcButton type="primary" @click="saveShare"> + {{ shareButtonText }} + </NcButton> + </div> + </div> + </div> +</template> + +<script> +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' +import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' +import NcDatetimePicker from '@nextcloud/vue/dist/Components/NcDatetimePicker.js' +import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' +import CircleIcon from 'vue-material-design-icons/CircleOutline.vue' +import CloseIcon from 'vue-material-design-icons/Close.vue' +import EditIcon from 'vue-material-design-icons/Pencil.vue' +import EmailIcon from 'vue-material-design-icons/Email.vue' +import LinkIcon from 'vue-material-design-icons/Link.vue' +import GroupIcon from 'vue-material-design-icons/AccountGroup.vue' +import ShareIcon from 'vue-material-design-icons/ShareCircle.vue' +import UserIcon from 'vue-material-design-icons/AccountCircleOutline.vue' +import ViewIcon from 'vue-material-design-icons/Eye.vue' +import UploadIcon from 'vue-material-design-icons/Upload.vue' +import MenuDownIcon from 'vue-material-design-icons/MenuDown.vue' +import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue' + +import GeneratePassword from '../utils/GeneratePassword.js' +import Share from '../models/Share.js' +import ShareRequests from '../mixins/ShareRequests.js' +import ShareTypes from '../mixins/ShareTypes.js' +import SharesMixin from '../mixins/SharesMixin.js' + +import { + ATOMIC_PERMISSIONS, + BUNDLED_PERMISSIONS, + hasPermissions, +} from '../lib/SharePermissionsToolBox.js' + +export default { + name: 'SharingDetailsTab', + components: { + NcAvatar, + NcButton, + NcInputField, + NcDatetimePicker, + NcDateTimePickerNative, + NcCheckboxRadioSwitch, + CloseIcon, + CircleIcon, + EditIcon, + LinkIcon, + GroupIcon, + ShareIcon, + UserIcon, + UploadIcon, + ViewIcon, + MenuDownIcon, + DotsHorizontalIcon, + }, + mixins: [ShareTypes, ShareRequests, SharesMixin], + props: { + shareRequestValue: { + type: Object, + required: false, + }, + fileInfo: { + type: Object, + required: true, + }, + share: { + type: Object, + required: true, + }, + }, + data() { + return { + writeNoteToRecipientIsChecked: false, + sharingPermission: BUNDLED_PERMISSIONS.ALL.toString(), + revertSharingPermission: null, + setCustomPermissions: false, + passwordError: false, + advancedSectionAccordionExpanded: false, + bundledPermissions: BUNDLED_PERMISSIONS, + isFirstComponentLoad: true, + test: false, + } + }, + + computed: { + title() { + let title = t('files_sharing', 'Share with ') + if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_USER) { + title = title + this.share.shareWithDisplayName + } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_LINK) { + title = t('files_sharing', 'Share link') + } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GROUP) { + title += ` (${t('files_sharing', 'group')})` + } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_ROOM) { + title += ` (${t('files_sharing', 'conversation')})` + } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE) { + title += ` (${t('files_sharing', 'remote')})` + } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP) { + title += ` (${t('files_sharing', 'remote group')})` + } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GUEST) { + title += ` (${t('files_sharing', 'guest')})` + } + + return title + }, + /** + * Can the sharee edit the shared file ? + */ + canEdit: { + get() { + return this.share.hasUpdatePermission + }, + set(checked) { + this.updateAtomicPermissions({ isEditChecked: checked }) + }, + }, + /** + * Can the sharee create the shared file ? + */ + canCreate: { + get() { + return this.share.hasCreatePermission + }, + set(checked) { + this.updateAtomicPermissions({ isCreateChecked: checked }) + }, + }, + /** + * Can the sharee delete the shared file ? + */ + canDelete: { + get() { + return this.share.hasDeletePermission + }, + set(checked) { + this.updateAtomicPermissions({ isDeleteChecked: checked }) + }, + }, + /** + * Can the sharee reshare the file ? + */ + canReshare: { + get() { + return this.share.hasSharePermission + }, + set(checked) { + this.updateAtomicPermissions({ isReshareChecked: checked }) + }, + }, + /** + * Can the sharee download files or only view them ? + */ + canDownload: { + get() { + return this.share.hasDownloadPermission + }, + set(checked) { + this.updateAtomicPermissions({ isDownloadChecked: checked }) + }, + }, + /** + * Is this share readable + * Needed for some federated shares that might have been added from file drop links + */ + hasRead: { + get() { + return this.share.hasReadPermission + }, + set(checked) { + this.updateAtomicPermissions({ isReadChecked: checked }) + }, + }, + /** + * Does the current share have an expiration date + * + * @return {boolean} + */ + hasExpirationDate: { + get() { + return !!this.share.expireDate || this.config.isDefaultInternalExpireDateEnforced + }, + set(enabled) { + this.share.expireDate = enabled + ? this.formatDateToString(this.defaultExpiryDate) + : '' + }, + }, + /** + * Is the current share password protected ? + * + * @return {boolean} + */ + isPasswordProtected: { + get() { + return this.config.enforcePasswordForPublicLink + || !!this.share.password + }, + async set(enabled) { + // TODO: directly save after generation to make sure the share is always protected + this.share.password = enabled ? await GeneratePassword() : '' + this.$set(this.share, 'newPassword', this.share.password) + }, + }, + /** + * Is the current share a folder ? + * + * @return {boolean} + */ + isFolder() { + return this.fileInfo.type === 'dir' + }, + dateMaxEnforced() { + if (!this.isRemote && this.config.isDefaultInternalExpireDateEnforced) { + return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultInternalExpireDate)) + } else if (this.config.isDefaultRemoteExpireDateEnforced) { + return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultRemoteExpireDate)) + } + return null + }, + /** + * @return {boolean} + */ + isSetDownloadButtonVisible() { + // TODO: Implement download permission for circle shares instead of hiding the option. + // https://github.com/nextcloud/server/issues/39161 + if (this.share && this.share.type === this.SHARE_TYPES.SHARE_TYPE_CIRCLE) { + return false + } + + const allowedMimetypes = [ + // Office documents + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.oasis.opendocument.text', + 'application/vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.presentation', + ] + + return this.isFolder || allowedMimetypes.includes(this.fileInfo.mimetype) + }, + isPasswordEnforced() { + return this.isPublicShare && this.config.enforcePasswordForPublicLink + }, + isExpiryDateEnforced() { + return this.config.isDefaultInternalExpireDateEnforced + }, + defaultExpiryDate() { + if ((this.isGroupShare || this.isUserShare) && this.config.isDefaultInternalExpireDateEnabled) { + return new Date(this.config.defaultInternalExpirationDate) + } else if (this.isRemoteShare && this.config.isDefaultRemoteExpireDateEnabled) { + return new Date(this.config.defaultRemoteExpireDateEnabled) + } else if (this.isPublicShare && this.config.isDefaultExpireDateEnabled) { + return new Date(this.config.defaultExpirationDate) + } + return new Date(new Date().setDate(new Date().getDate() + 1)) + }, + isUserShare() { + return this.share.type === this.SHARE_TYPES.SHARE_TYPE_USER + }, + isGroupShare() { + return this.share.type === this.SHARE_TYPES.SHARE_TYPE_GROUP + }, + isRemoteShare() { + return this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP || this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE + }, + isNewShare() { + return this.share.id === null || this.share.id === undefined + }, + allowsFileDrop() { + if (this.isFolder) { + if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_LINK || this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) { + return true + } + } + return false + }, + hasFileDropPermissions() { + return this.share.permissions === this.bundledPermissions.FILE_DROP + }, + shareButtonText() { + if (this.isNewShare) { + return t('file_sharing', 'Save share') + } + return t('file_sharing', 'Update share') + + }, + /** + * Can the sharer set whether the sharee can edit the file ? + * + * @return {boolean} + */ + canSetEdit() { + // If the owner revoked the permission after the resharer granted it + // the share still has the permission, and the resharer is still + // allowed to revoke it too (but not to grant it again). + return (this.fileInfo.sharePermissions & OC.PERMISSION_UPDATE) || this.canEdit + }, + + /** + * Can the sharer set whether the sharee can create the file ? + * + * @return {boolean} + */ + canSetCreate() { + // If the owner revoked the permission after the resharer granted it + // the share still has the permission, and the resharer is still + // allowed to revoke it too (but not to grant it again). + return (this.fileInfo.sharePermissions & OC.PERMISSION_CREATE) || this.canCreate + }, + + /** + * Can the sharer set whether the sharee can delete the file ? + * + * @return {boolean} + */ + canSetDelete() { + // If the owner revoked the permission after the resharer granted it + // the share still has the permission, and the resharer is still + // allowed to revoke it too (but not to grant it again). + return (this.fileInfo.sharePermissions & OC.PERMISSION_DELETE) || this.canDelete + }, + /** + * Can the sharer set whether the sharee can reshare the file ? + * + * @return {boolean} + */ + canSetReshare() { + // If the owner revoked the permission after the resharer granted it + // the share still has the permission, and the resharer is still + // allowed to revoke it too (but not to grant it again). + return (this.fileInfo.sharePermissions & OC.PERMISSION_SHARE) || this.canReshare + }, + /** + * Can the sharer set whether the sharee can download the file ? + * + * @return {boolean} + */ + canSetDownload() { + // If the owner revoked the permission after the resharer granted it + // the share still has the permission, and the resharer is still + // allowed to revoke it too (but not to grant it again). + return (this.fileInfo.canDownload() || this.canDownload) + }, + // if newPassword exists, but is empty, it means + // the user deleted the original password + hasUnsavedPassword() { + return this.share.newPassword !== undefined + }, + passwordExpirationTime() { + if (this.share.passwordExpirationTime === null) { + return null + } + + const expirationTime = moment(this.share.passwordExpirationTime) + + if (expirationTime.diff(moment()) < 0) { + return false + } + + return expirationTime.fromNow() + }, + + /** + * Is Talk enabled? + * + * @return {boolean} + */ + isTalkEnabled() { + return OC.appswebroots.spreed !== undefined + }, + + /** + * Is it possible to protect the password by Talk? + * + * @return {boolean} + */ + isPasswordProtectedByTalkAvailable() { + return this.isPasswordProtected && this.isTalkEnabled + }, + /** + * Is the current share password protected by Talk? + * + * @return {boolean} + */ + isPasswordProtectedByTalk: { + get() { + return this.share.sendPasswordByTalk + }, + async set(enabled) { + this.share.sendPasswordByTalk = enabled + }, + }, + /** + * Is the current share an email share ? + * + * @return {boolean} + */ + isEmailShareType() { + return this.share + ? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL + : false + }, + canTogglePasswordProtectedByTalkAvailable() { + if (!this.isPublicShare || !this.isPasswordProtected) { + // Makes no sense + return false + } else if (this.isEmailShareType && !this.hasUnsavedPassword) { + // For email shares we need a new password in order to enable or + // disable + return false + } + + // Anything else should be fine + return true + }, + canChangeHideDownload() { + const hasDisabledDownload = (shareAttribute) => shareAttribute.key === 'download' && shareAttribute.scope === 'permissions' && shareAttribute.enabled === false + return this.fileInfo.shareAttributes.some(hasDisabledDownload) + }, + customPermissionsList() { + const perms = [] + if (hasPermissions(this.share.permissions, ATOMIC_PERMISSIONS.READ)) { + perms.push('read') + } + if (hasPermissions(this.share.permissions, ATOMIC_PERMISSIONS.CREATE)) { + perms.push('create') + } + if (hasPermissions(this.share.permissions, ATOMIC_PERMISSIONS.UPDATE)) { + perms.push('update') + } + if (hasPermissions(this.share.permissions, ATOMIC_PERMISSIONS.DELETE)) { + perms.push('delete') + } + if (hasPermissions(this.share.permissions, ATOMIC_PERMISSIONS.SHARE)) { + perms.push('share') + } + if (this.share.hasDownloadPermission) { + perms.push('download') + } + const capitalizeFirstAndJoin = array => array.map((item, index) => index === 0 ? item[0].toUpperCase() + item.substring(1) : item).join(', ') + + return capitalizeFirstAndJoin(perms) + + }, + }, + watch: { + setCustomPermissions(isChecked) { + if (isChecked) { + this.sharingPermission = 'custom' + } else { + this.sharingPermission = this.revertSharingPermission + } + }, + }, + beforeMount() { + this.initializePermissions() + this.initializeAttributes() + console.debug('shareSentIn', this.share) + console.debug('config', this.config) + }, + + methods: { + updateAtomicPermissions({ + isReadChecked = this.hasRead, + isEditChecked = this.canEdit, + isCreateChecked = this.canCreate, + isDeleteChecked = this.canDelete, + isReshareChecked = this.canReshare, + isDownloadChecked = this.canDownload, + } = {}) { + // calc permissions if checked + const permissions = 0 + | (isReadChecked ? ATOMIC_PERMISSIONS.READ : 0) + | (isCreateChecked ? ATOMIC_PERMISSIONS.CREATE : 0) + | (isDeleteChecked ? ATOMIC_PERMISSIONS.DELETE : 0) + | (isEditChecked ? ATOMIC_PERMISSIONS.UPDATE : 0) + | (isReshareChecked ? ATOMIC_PERMISSIONS.SHARE : 0) + this.share.permissions = permissions + if (this.share.hasDownloadPermission !== isDownloadChecked) { + this.$set(this.share, 'hasDownloadPermission', isDownloadChecked) + } + }, + + toggleCustomPermissions(selectedPermission) { + if (this.sharingPermission === 'custom') { + this.advancedSectionAccordionExpanded = true + this.setCustomPermissions = true + } else { + this.advancedSectionAccordionExpanded = false + this.revertSharingPermission = selectedPermission + this.setCustomPermissions = false + } + }, + initializeAttributes() { + + if (this.isNewShare) return + + let hasAdvancedAttributes = false + if (this.isValidShareAttribute(this.share.note)) { + this.writeNoteToRecipientIsChecked = true + hasAdvancedAttributes = true + } + + if (this.isValidShareAttribute(this.share.password)) { + hasAdvancedAttributes = true + } + + if (this.isValidShareAttribute(this.share.expireDate)) { + hasAdvancedAttributes = true + } + + if (this.isValidShareAttribute(this.share.label)) { + hasAdvancedAttributes = true + } + + if (hasAdvancedAttributes) { + this.advancedSectionAccordionExpanded = true + } + + }, + initializePermissions() { + if (this.share.share_type) { + this.share.type = this.share.share_type + } + // shareType 0 (USER_SHARE) would evaluate to zero + // Hence the use of hasOwnProperty + if ('shareType' in this.share) { + this.share.type = this.share.shareType + } + if (this.isNewShare) { + if (this.isPublicShare) { + this.sharingPermission = BUNDLED_PERMISSIONS.READ_ONLY.toString() + } else { + this.sharingPermission = BUNDLED_PERMISSIONS.ALL.toString() + } + + } else { + if (this.hasCustomPermissions || this.share.setCustomPermissions) { + this.sharingPermission = 'custom' + this.advancedSectionAccordionExpanded = true + this.setCustomPermissions = true + } else { + this.sharingPermission = this.share.permissions.toString() + } + } + }, + async saveShare() { + const permissionsAndAttributes = ['permissions', 'attributes', 'note', 'expireDate'] + const publicShareAttributes = ['label', 'password', 'hideDownload'] + if (this.isPublicShare) { + permissionsAndAttributes.push(...publicShareAttributes) + } + const sharePermissionsSet = parseInt(this.sharingPermission) + if (this.setCustomPermissions) { + this.updateAtomicPermissions() + } else { + this.share.permissions = sharePermissionsSet + } + + if (!this.isFolder && this.share.permissions === BUNDLED_PERMISSIONS.ALL) { + // It's not possible to create an existing file. + this.share.permissions = BUNDLED_PERMISSIONS.ALL_FILE + } + if (!this.writeNoteToRecipientIsChecked) { + this.share.note = '' + } + + if (this.isPasswordProtected) { + if (this.isValidShareAttribute(this.share.newPassword)) { + this.share.password = this.share.newPassword + this.$delete(this.share, 'newPassword') + } else { + if (this.isPasswordEnforced) { + this.passwordError = true + return + } + } + } else { + this.share.password = '' + } + + if (!this.hasExpirationDate) { + this.share.expireDate = '' + } + + if (this.isNewShare) { + const incomingShare = { + permissions: this.share.permissions, + shareType: this.share.type, + shareWith: this.share.shareWith, + attributes: this.share.attributes, + note: this.share.note, + } + + if (this.hasExpirationDate) { + incomingShare.expireDate = this.share.expireDate + } + + if (this.isPasswordProtected) { + incomingShare.password = this.share.password + } + + const share = await this.addShare(incomingShare, this.fileInfo, this.config) + this.share = share + this.$emit('add:share', this.share) + } else { + this.queueUpdate(...permissionsAndAttributes) + } + + this.$emit('close-sharing-details') + }, + /** + * Process the new share request + * + * @param {object} value the multiselect option + * @param {object} fileInfo file data + * @param {Config} config instance configs + */ + async addShare(value, fileInfo, config) { + // Clear the displayed selection + this.value = null + + // handle externalResults from OCA.Sharing.ShareSearch + if (value.handler) { + const share = await value.handler(this) + this.$emit('add:share', new Share(share)) + return true + } + + // this.loading = true // Are we adding loaders the new share flow? + console.debug('Adding a new share from the input for', value) + try { + const path = (fileInfo.path + '/' + fileInfo.name).replace('//', '/') + const share = await this.createShare({ + path, + shareType: value.shareType, + shareWith: value.shareWith, + permissions: value.permissions, + attributes: JSON.stringify(fileInfo.shareAttributes), + ...(value.note ? { note: value.note } : {}), + ...(value.password ? { password: value.password } : {}), + ...(value.expireDate ? { expireDate: value.expireDate } : {}), + }) + return share + } catch (error) { + console.error('Error while adding new share', error) + } finally { + // this.loading = false // No loader here yet + } + }, + async removeShare() { + await this.onDelete() + this.$emit('close-sharing-details') + }, + /** + * Update newPassword values + * of share. If password is set but not newPassword + * then the user did not changed the password + * If both co-exists, the password have changed and + * we show it in plain text. + * Then on submit (or menu close), we sync it. + * + * @param {string} password the changed password + */ + onPasswordChange(password) { + this.passwordError = !this.isValidShareAttribute(password) + this.$set(this.share, 'newPassword', password) + }, + /** + * Update the password along with "sendPasswordByTalk". + * + * If the password was modified the new password is sent; otherwise + * updating a mail share would fail, as in that case it is required that + * a new password is set when enabling or disabling + * "sendPasswordByTalk". + */ + onPasswordProtectedByTalkChange() { + if (this.hasUnsavedPassword) { + this.share.password = this.share.newPassword.trim() + } + + this.queueUpdate('sendPasswordByTalk', 'password') + }, + isValidShareAttribute(value) { + if ([null, undefined].includes(value)) { + return false + } + + if (!(value.trim().length > 0)) { + return false + } + + return true + }, + getShareTypeIcon(type) { + switch (type) { + case this.SHARE_TYPES.SHARE_TYPE_LINK: + return LinkIcon + case this.SHARE_TYPES.SHARE_TYPE_GUEST: + return UserIcon + case this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP: + case this.SHARE_TYPES.SHARE_TYPE_GROUP: + return GroupIcon + case this.SHARE_TYPES.SHARE_TYPE_EMAIL: + return EmailIcon + case this.SHARE_TYPES.SHARE_TYPE_CIRCLE: + return CircleIcon + case this.SHARE_TYPES.SHARE_TYPE_ROOM: + return ShareIcon + case this.SHARE_TYPES.SHARE_TYPE_DECK: + return ShareIcon + case this.SHARE_TYPES.SHARE_TYPE_SCIENCEMESH: + return ShareIcon + default: + return null // Or a default icon component if needed + } + }, + }, +} +</script> + +<style lang="scss" scoped> +.sharingTabDetailsView { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 96%; + margin: 0 auto; + + &__header { + display: flex; + align-items: center; + box-sizing: border-box; + margin: 0.2em; + + span { + display: flex; + align-items: center; + + h1 { + font-size: 15px; + padding-left: 0.3em; + } + + } + } + + &__quick-permissions { + display: flex; + justify-content: center; + margin-bottom: 0.2em; + width: 100%; + margin: 0 auto; + border-radius: 0; + + div { + width: 100%; + + span { + width: 100%; + + span:nth-child(1) { + align-items: center; + justify-content: center; + color: var(--color-primary-element); + padding: 0.1em; + } + + ::v-deep label { + + span { + display: flex; + flex-direction: column; + } + } + } + + } + } + + &__advanced-control { + width: 100%; + + button { + margin-top: 0.5em; + } + + } + + &__advanced { + width: 100%; + margin-bottom: 0.5em; + text-align: left; + padding-left: 0; + + section { + + textarea, + div.mx-datepicker { + width: 100%; + } + + textarea { + height: 80px; + } + + /* + The following style is applied out of the component's scope + to remove padding from the label.checkbox-radio-switch__label, + which is used to group radio checkbox items. The use of ::v-deep + ensures that the padding is modified without being affected by + the component's scoping. + Without this achieving left alignment for the checkboxes would not + be possible. + */ + span { + ::v-deep label { + padding-left: 0 !important; + background-color: initial !important; + border: none !important; + } + } + + section.custom-permissions-group { + padding-left: 1.5em; + } + } + } + + &__footer { + width: 100%; + display: flex; + position: sticky; + bottom: 0; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + + >button:first-child { + font-size: 12px; + color: rgb(223, 7, 7); + background-color: #f5f5f5; + } + + .button-group { + display: flex; + justify-content: space-between; + width: 100%; + margin-top: 16px; + + button { + margin-left: 16px; + + &:first-child { + margin-left: 0; + } + } + } + } +} +</style> diff --git a/apps/files_sharing/src/views/SharingLinkList.vue b/apps/files_sharing/src/views/SharingLinkList.vue index c3f1425cb70..899424be1d8 100644 --- a/apps/files_sharing/src/views/SharingLinkList.vue +++ b/apps/files_sharing/src/views/SharingLinkList.vue @@ -39,7 +39,8 @@ :file-info="fileInfo" @add:share="addShare(...arguments)" @update:share="awaitForShare(...arguments)" - @remove:share="removeShare" /> + @remove:share="removeShare" + @open-sharing-details="openSharingDetails(share)" /> </template> </ul> </template> @@ -49,6 +50,7 @@ import Share from '../models/Share.js' import ShareTypes from '../mixins/ShareTypes.js' import SharingEntryLink from '../components/SharingEntryLink.vue' +import ShareDetails from '../mixins/ShareDetails.js' export default { name: 'SharingLinkList', @@ -57,7 +59,7 @@ export default { SharingEntryLink, }, - mixins: [ShareTypes], + mixins: [ShareTypes, ShareDetails], props: { fileInfo: { diff --git a/apps/files_sharing/src/views/SharingList.vue b/apps/files_sharing/src/views/SharingList.vue index 05dc87d9b07..6da48c2eece 100644 --- a/apps/files_sharing/src/views/SharingList.vue +++ b/apps/files_sharing/src/views/SharingList.vue @@ -27,15 +27,15 @@ :file-info="fileInfo" :share="share" :is-unique="isUnique(share)" - @remove:share="removeShare" /> + @open-sharing-details="openSharingDetails(share)" /> </ul> </template> <script> // eslint-disable-next-line no-unused-vars -import Share from '../models/Share.js' import SharingEntry from '../components/SharingEntry.vue' import ShareTypes from '../mixins/ShareTypes.js' +import ShareDetails from '../mixins/ShareDetails.js' export default { name: 'SharingList', @@ -44,12 +44,12 @@ export default { SharingEntry, }, - mixins: [ShareTypes], + mixins: [ShareTypes, ShareDetails], props: { fileInfo: { type: Object, - default: () => {}, + default: () => { }, required: true, }, shares: { @@ -58,7 +58,6 @@ export default { required: true, }, }, - computed: { hasShares() { return this.shares.length === 0 @@ -71,18 +70,5 @@ export default { } }, }, - - methods: { - /** - * Remove a share from the shares list - * - * @param {Share} share the share to remove - */ - removeShare(share) { - const index = this.shares.findIndex(item => item === share) - // eslint-disable-next-line vue/no-mutating-props - this.shares.splice(index, 1) - }, - }, } </script> diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue index bfaf8a766ee..e5d26156750 100644 --- a/apps/files_sharing/src/views/SharingTab.vue +++ b/apps/files_sharing/src/views/SharingTab.vue @@ -29,7 +29,7 @@ </div> <!-- shares content --> - <div v-else class="sharingTab__content"> + <div v-if="!showSharingDetailsView" class="sharingTab__content"> <!-- shared with me information --> <SharingEntrySimple v-if="isSharedWithMe" v-bind="sharedWithMe" class="sharing-entry__reshare"> <template #avatar> @@ -46,20 +46,22 @@ :link-shares="linkShares" :reshare="reshare" :shares="shares" - @add:share="addShare" /> + @open-sharing-details="toggleShareDetailsView" /> <!-- link shares list --> <SharingLinkList v-if="!loading" ref="linkShareList" :can-reshare="canReshare" :file-info="fileInfo" - :shares="linkShares" /> + :shares="linkShares" + @open-sharing-details="toggleShareDetailsView" /> <!-- other shares list --> <SharingList v-if="!loading" ref="shareList" :shares="shares" - :file-info="fileInfo" /> + :file-info="fileInfo" + @open-sharing-details="toggleShareDetailsView" /> <!-- inherited shares --> <SharingInherited v-if="canReshare && !loading" :file-info="fileInfo" /> @@ -74,6 +76,15 @@ :name="fileInfo.name" /> </div> + <!-- share details --> + <div v-else> + <SharingDetailsTab :file-info="shareDetailsData.fileInfo" + :share="shareDetailsData.share" + @close-sharing-details="toggleShareDetailsView" + @add:share="addShare" + @remove:share="removeShare" /> + </div> + <!-- additional entries, use it with cautious --> <div v-for="(section, index) in sections" :ref="'section-' + index" @@ -102,6 +113,7 @@ import SharingInput from '../components/SharingInput.vue' import SharingInherited from './SharingInherited.vue' import SharingLinkList from './SharingLinkList.vue' import SharingList from './SharingList.vue' +import SharingDetailsTab from './SharingDetailsTab.vue' export default { name: 'SharingTab', @@ -115,6 +127,7 @@ export default { SharingInput, SharingLinkList, SharingList, + SharingDetailsTab, }, mixins: [ShareTypes], @@ -122,7 +135,7 @@ export default { data() { return { config: new Config(), - + deleteEvent: null, error: '', expirationInterval: null, loading: true, @@ -137,6 +150,8 @@ export default { sections: OCA.Sharing.ShareTabSections.getSections(), projectsEnabled: loadState('core', 'projects_enabled', false), + showSharingDetailsView: false, + shareDetailsData: {}, } }, @@ -225,6 +240,8 @@ export default { this.sharedWithMe = {} this.shares = [] this.linkShares = [] + this.showSharingDetailsView = false + this.shareDetailsData = {} }, /** @@ -307,7 +324,7 @@ export default { 'Shared with you by {owner}', { owner: this.fileInfo.shareOwner }, undefined, - { escape: false } + { escape: false }, ), user: this.fileInfo.shareOwnerId, } @@ -321,7 +338,7 @@ export default { * @param {Share} share the share to add to the array * @param {Function} [resolve] a function to run after the share is added and its component initialized */ - addShare(share, resolve = () => {}) { + addShare(share, resolve = () => { }) { // only catching share type MAIL as link shares are added differently // meaning: not from the ShareInput if (share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) { @@ -331,7 +348,16 @@ export default { } this.awaitForShare(share, resolve) }, - + /** + * Remove a share from the shares list + * + * @param {Share} share the share to remove + */ + removeShare(share) { + const index = this.shares.findIndex(item => item.id === share.id) + // eslint-disable-next-line vue/no-mutating-props + this.shares.splice(index, 1) + }, /** * Await for next tick and render after the list updated * Then resolve with the matched vue component of the @@ -355,6 +381,12 @@ export default { } }) }, + toggleShareDetailsView(eventData) { + if (eventData) { + this.shareDetailsData = eventData + } + this.showSharingDetailsView = !this.showSharingDetailsView + }, }, } </script> @@ -368,6 +400,7 @@ export default { &__content { padding: 0 6px; } + &__additionalContent { margin: 44px 0; } |