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>
This commit is contained in:
fenn-cs 2023-07-19 02:11:27 +01:00 committed by Louis Chemineau
parent 191e20d7f4
commit 8b42fb033f
20 changed files with 1474 additions and 562 deletions

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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">

View file

@ -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,

View file

@ -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,
}
/**

View file

@ -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)
},
},
}

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -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 },
)
}
}

File diff suppressed because it is too large Load diff

View file

@ -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: {

View file

@ -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>

View file

@ -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;
}

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

View file

@ -372,8 +372,6 @@ object-assign
/*! For license information please see NcActionText.js.LICENSE.txt */
/*! For license information please see NcActionTextEditable.js.LICENSE.txt */
/*! For license information please see NcActions.js.LICENSE.txt */
/*! For license information please see NcAppContent.js.LICENSE.txt */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long