summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorfenn-cs <fenn25.fn@gmail.com>2023-07-19 02:11:27 +0100
committerLouis Chemineau <louis@chmn.me>2023-08-30 18:12:49 +0200
commit8b42fb033fdcd3775b4850de6faf6091c8dcc716 (patch)
tree7ca9ccb33d95090ae4a34e24ef650d0eede8732d /apps
parent191e20d7f48338ca336fd0091301653251fc0667 (diff)
downloadnextcloud-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.vue407
-rw-r--r--apps/files_sharing/src/components/SharingEntryLink.vue178
-rw-r--r--apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue186
-rw-r--r--apps/files_sharing/src/components/SharingEntrySimple.vue4
-rw-r--r--apps/files_sharing/src/components/SharingInput.vue10
-rw-r--r--apps/files_sharing/src/lib/SharePermissionsToolBox.js1
-rw-r--r--apps/files_sharing/src/mixins/ShareDetails.js43
-rw-r--r--apps/files_sharing/src/mixins/ShareRequests.js23
-rw-r--r--apps/files_sharing/src/mixins/SharesMixin.js28
-rw-r--r--apps/files_sharing/src/models/Share.js2
-rw-r--r--apps/files_sharing/src/utils/SharedWithMe.js10
-rw-r--r--apps/files_sharing/src/views/SharingDetailsTab.vue1053
-rw-r--r--apps/files_sharing/src/views/SharingLinkList.vue6
-rw-r--r--apps/files_sharing/src/views/SharingList.vue22
-rw-r--r--apps/files_sharing/src/views/SharingTab.vue49
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;
}