aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src/components/SharingEntryLink.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src/components/SharingEntryLink.vue')
-rw-r--r--apps/files_sharing/src/components/SharingEntryLink.vue885
1 files changed, 480 insertions, 405 deletions
diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue
index ee7e8d4b930..6865af1b864 100644
--- a/apps/files_sharing/src/components/SharingEntryLink.vue
+++ b/apps/files_sharing/src/components/SharingEntryLink.vue
@@ -1,261 +1,163 @@
<!--
- - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <li :class="{'sharing-entry--share': share}" class="sharing-entry sharing-entry__link">
- <Avatar :is-no-user="true"
+ <li :class="{ 'sharing-entry--share': share }"
+ class="sharing-entry sharing-entry__link">
+ <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">
- <h5 :title="title">
- {{ title }}
- </h5>
- <p v-if="subtitle">
- {{ subtitle }}
- </p>
- </div>
- <!-- clipboard -->
- <Actions v-if="share && !isEmailShareType && share.token"
- ref="copyButton"
- class="sharing-entry__copy">
- <ActionLink :href="shareLink"
- target="_blank"
- :icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
- @click.stop.prevent="copyLink">
- {{ clipboardTooltip }}
- </ActionLink>
- </Actions>
+ <div class="sharing-entry__summary">
+ <div class="sharing-entry__desc">
+ <span class="sharing-entry__title" :title="title">
+ {{ title }}
+ </span>
+ <p v-if="subtitle">
+ {{ subtitle }}
+ </p>
+ <SharingEntryQuickShareSelect v-if="share && share.permissions !== undefined"
+ :share="share"
+ :file-info="fileInfo"
+ @open-sharing-details="openShareDetailsForCustomSettings(share)" />
+ </div>
+
+ <div class="sharing-entry__actions">
+ <ShareExpiryTime v-if="share && share.expireDate" :share="share" />
+
+ <!-- clipboard -->
+ <div>
+ <NcActions v-if="share && (!isEmailShareType || isFileRequest) && share.token" ref="copyButton" class="sharing-entry__copy">
+ <NcActionButton :aria-label="copyLinkTooltip"
+ :title="copyLinkTooltip"
+ :href="shareLink"
+ @click.prevent="copyLink">
+ <template #icon>
+ <CheckIcon v-if="copied && copySuccess"
+ :size="20"
+ class="icon-checkmark-color" />
+ <ClipboardIcon v-else :size="20" />
+ </template>
+ </NcActionButton>
+ </NcActions>
+ </div>
+ </div>
+ </div>
<!-- pending actions -->
- <Actions v-if="!pending && (pendingPassword || pendingExpirationDate)"
+ <NcActions v-if="!pending && pendingDataIsMissing"
class="sharing-entry__actions"
+ :aria-label="actionsTooltip"
menu-align="right"
:open.sync="open"
- @close="onNewLinkShare">
+ @close="onCancel">
<!-- pending data menu -->
- <ActionText v-if="errors.pending"
- icon="icon-error"
- :class="{ error: errors.pending}">
+ <NcActionText v-if="errors.pending"
+ class="error">
+ <template #icon>
+ <ErrorIcon :size="20" />
+ </template>
{{ errors.pending }}
- </ActionText>
- <ActionText v-else icon="icon-info">
+ </NcActionText>
+ <NcActionText v-else icon="icon-info">
{{ t('files_sharing', 'Please enter the following required information before creating the share') }}
- </ActionText>
+ </NcActionText>
<!-- password -->
- <ActionText v-if="pendingPassword" icon="icon-password">
- {{ t('files_sharing', 'Password protection (enforced)') }}
- </ActionText>
- <ActionCheckbox v-else-if="config.enableLinkPasswordByDefault"
+ <NcActionCheckbox v-if="pendingPassword"
:checked.sync="isPasswordProtected"
:disabled="config.enforcePasswordForPublicLink || saving"
class="share-link-password-checkbox"
@uncheck="onPasswordDisable">
- {{ t('files_sharing', 'Password protection') }}
- </ActionCheckbox>
- <ActionInput v-if="pendingPassword || share.password"
- v-tooltip.auto="{
- content: errors.password,
- show: errors.password,
- trigger: 'manual',
- defaultContainer: '#app-sidebar'
- }"
+ {{ config.enforcePasswordForPublicLink ? t('files_sharing', 'Password protection (enforced)') : t('files_sharing', 'Password protection') }}
+ </NcActionCheckbox>
+
+ <NcActionInput v-if="pendingEnforcedPassword || isPasswordProtected"
class="share-link-password"
- :value.sync="share.password"
+ :label="t('files_sharing', 'Enter a password')"
+ :value.sync="share.newPassword"
:disabled="saving"
:required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink"
:minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength"
- icon=""
autocomplete="new-password"
- @submit="onNewLinkShare">
- {{ t('files_sharing', 'Enter a password') }}
- </ActionInput>
+ @submit="onNewLinkShare(true)">
+ <template #icon>
+ <LockIcon :size="20" />
+ </template>
+ </NcActionInput>
+
+ <NcActionCheckbox v-if="pendingDefaultExpirationDate"
+ :checked.sync="defaultExpirationDateEnabled"
+ :disabled="pendingEnforcedExpirationDate || saving"
+ class="share-link-expiration-date-checkbox"
+ @update:model-value="onExpirationDateToggleUpdate">
+ {{ config.isDefaultExpireDateEnforced ? t('files_sharing', 'Enable link expiration (enforced)') : t('files_sharing', 'Enable link expiration') }}
+ </NcActionCheckbox>
<!-- expiration date -->
- <ActionText v-if="pendingExpirationDate" icon="icon-calendar-dark">
- {{ t('files_sharing', 'Expiration date (enforced)') }}
- </ActionText>
- <ActionInput v-if="pendingExpirationDate"
- v-model="share.expireDate"
- v-tooltip.auto="{
- content: errors.expireDate,
- show: errors.expireDate,
- trigger: 'manual',
- defaultContainer: '#app-sidebar'
- }"
+ <NcActionInput v-if="(pendingDefaultExpirationDate || pendingEnforcedExpirationDate) && defaultExpirationDateEnabled"
+ data-cy-files-sharing-expiration-date-input
class="share-link-expire-date"
+ :label="pendingEnforcedExpirationDate ? t('files_sharing', 'Enter expiration date (enforced)') : t('files_sharing', 'Enter expiration date')"
:disabled="saving"
-
- :lang="lang"
- icon=""
+ :is-native-picker="true"
+ :hide-label="true"
+ :value="new Date(share.expireDate)"
type="date"
- value-type="format"
- :disabled-date="disabledDate">
- <!-- let's not submit when picked, the user
- might want to still edit or copy the password -->
- {{ t('files_sharing', 'Enter a date') }}
- </ActionInput>
-
- <ActionButton icon="icon-checkmark" @click.prevent.stop="onNewLinkShare">
+ :min="dateTomorrow"
+ :max="maxExpirationDateEnforced"
+ @update:model-value="onExpirationChange"
+ @change="expirationDateChanged">
+ <template #icon>
+ <IconCalendarBlank :size="20" />
+ </template>
+ </NcActionInput>
+
+ <NcActionButton :disabled="pendingEnforcedPassword && !share.newPassword"
+ @click.prevent.stop="onNewLinkShare(true)">
+ <template #icon>
+ <CheckIcon :size="20" />
+ </template>
{{ t('files_sharing', 'Create share') }}
- </ActionButton>
- <ActionButton icon="icon-close" @click.prevent.stop="onCancel">
+ </NcActionButton>
+ <NcActionButton @click.prevent.stop="onCancel">
+ <template #icon>
+ <CloseIcon :size="20" />
+ </template>
{{ t('files_sharing', 'Cancel') }}
- </ActionButton>
- </Actions>
+ </NcActionButton>
+ </NcActions>
<!-- actions -->
- <Actions v-else-if="!loading"
+ <NcActions v-else-if="!loading"
class="sharing-entry__actions"
+ :aria-label="actionsTooltip"
menu-align="right"
:open.sync="open"
@close="onMenuClose">
<template v-if="share">
<template v-if="share.canEdit && canReshare">
- <!-- Custom Label -->
- <ActionInput ref="label"
- v-tooltip.auto="{
- content: errors.label,
- show: errors.label,
- trigger: 'manual',
- defaultContainer: '.app-sidebar'
- }"
- :class="{ error: errors.label }"
- :disabled="saving"
- :aria-label="t('files_sharing', 'Share label')"
- :value="share.newLabel !== undefined ? share.newLabel : share.label"
- icon="icon-edit"
- maxlength="255"
- @update:value="onLabelChange"
- @submit="onLabelSubmit">
- {{ t('files_sharing', 'Share label') }}
- </ActionInput>
-
- <SharePermissionsEditor :can-reshare="canReshare"
- :share.sync="share"
- :file-info="fileInfo" />
-
- <ActionSeparator />
-
- <ActionCheckbox :checked.sync="share.hideDownload"
- :disabled="saving"
- @change="queueUpdate('hideDownload')">
- {{ t('files_sharing', 'Hide download') }}
- </ActionCheckbox>
-
- <!-- password -->
- <ActionCheckbox :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') }}
- </ActionCheckbox>
- <ActionInput v-if="isPasswordProtected"
- ref="password"
- v-tooltip.auto="{
- content: errors.password,
- show: errors.password,
- trigger: 'manual',
- defaultContainer: '#app-sidebar'
- }"
- class="share-link-password"
- :class="{ error: errors.password}"
- :disabled="saving"
- :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') }}
- </ActionInput>
-
- <!-- password protected by Talk -->
- <ActionCheckbox v-if="isPasswordProtectedByTalkAvailable"
- :checked.sync="isPasswordProtectedByTalk"
- :disabled="!canTogglePasswordProtectedByTalkAvailable || saving"
- class="share-link-password-talk-checkbox"
- @change="onPasswordProtectedByTalkChange">
- {{ t('files_sharing', 'Video verification') }}
- </ActionCheckbox>
-
- <!-- expiration date -->
- <ActionCheckbox :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') }}
- </ActionCheckbox>
- <ActionInput v-if="hasExpirationDate"
- ref="expireDate"
- v-tooltip.auto="{
- content: errors.expireDate,
- show: errors.expireDate,
- trigger: 'manual',
- defaultContainer: '#app-sidebar'
- }"
- class="share-link-expire-date"
- :class="{ error: errors.expireDate}"
- :disabled="saving"
- :lang="lang"
- :value="share.expireDate"
- value-type="format"
- icon="icon-calendar-dark"
- type="date"
- :disabled-date="disabledDate"
- @update:value="onExpirationChange">
- {{ t('files_sharing', 'Enter a date') }}
- </ActionInput>
-
- <!-- note -->
- <ActionCheckbox :checked.sync="hasNote"
- :disabled="saving"
- @uncheck="queueUpdate('note')">
- {{ t('files_sharing', 'Note to recipient') }}
- </ActionCheckbox>
- <ActionTextEditable v-if="hasNote"
- ref="note"
- v-tooltip.auto="{
- content: errors.note,
- show: errors.note,
- trigger: 'manual',
- defaultContainer: '#app-sidebar'
- }"
- :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"
+ :close-after-click="true"
+ @click.prevent="openSharingDetails">
+ <template #icon>
+ <Tune :size="20" />
+ </template>
+ {{ t('files_sharing', 'Customize link') }}
+ </NcActionButton>
</template>
- <ActionSeparator />
+ <NcActionButton :close-after-click="true"
+ @click.prevent="showQRCode = true">
+ <template #icon>
+ <IconQr :size="20" />
+ </template>
+ {{ t('files_sharing', 'Generate QR code') }}
+ </NcActionButton>
+
+ <NcActionSeparator />
<!-- external actions -->
<ExternalShareAction v-for="action in externalLinkActions"
@@ -266,104 +168,156 @@
:share="share" />
<!-- external legacy sharing via url (social...) -->
- <ActionLink v-for="({icon, url, name}, index) in externalLegacyLinkActions"
- :key="index"
+ <NcActionLink v-for="({ icon, url, name }, actionIndex) in externalLegacyLinkActions"
+ :key="actionIndex"
:href="url(shareLink)"
:icon="icon"
target="_blank">
{{ name }}
- </ActionLink>
+ </NcActionLink>
- <ActionButton v-if="share.canDelete"
- icon="icon-close"
- :disabled="saving"
- @click.prevent="onDelete">
- {{ t('files_sharing', 'Unshare') }}
- </ActionButton>
- <ActionButton v-if="!isEmailShareType && canReshare"
+ <NcActionButton v-if="!isEmailShareType && canReshare"
class="new-share-link"
- icon="icon-add"
@click.prevent.stop="onNewLinkShare">
+ <template #icon>
+ <PlusIcon :size="20" />
+ </template>
{{ t('files_sharing', 'Add another link') }}
- </ActionButton>
+ </NcActionButton>
+
+ <NcActionButton v-if="share.canDelete"
+ :disabled="saving"
+ @click.prevent="onDelete">
+ <template #icon>
+ <CloseIcon :size="20" />
+ </template>
+ {{ t('files_sharing', 'Unshare') }}
+ </NcActionButton>
</template>
<!-- Create new share -->
- <ActionButton v-else-if="canReshare"
+ <NcActionButton v-else-if="canReshare"
class="new-share-link"
+ :title="t('files_sharing', 'Create a new share link')"
+ :aria-label="t('files_sharing', 'Create a new share link')"
:icon="loading ? 'icon-loading-small' : 'icon-add'"
- @click.prevent.stop="onNewLinkShare">
- {{ t('files_sharing', 'Create a new share link') }}
- </ActionButton>
- </Actions>
+ @click.prevent.stop="onNewLinkShare" />
+ </NcActions>
<!-- loading indicator to replace the menu -->
<div v-else class="icon-loading-small sharing-entry__loading" />
+
+ <!-- Modal to open whenever we have a QR code -->
+ <NcDialog v-if="showQRCode"
+ size="normal"
+ :open.sync="showQRCode"
+ :name="title"
+ :close-on-click-outside="true"
+ @close="showQRCode = false">
+ <div class="qr-code-dialog">
+ <VueQrcode tag="img"
+ :value="shareLink"
+ class="qr-code-dialog__img" />
+ </div>
+ </NcDialog>
</li>
</template>
<script>
-import { generateUrl } from '@nextcloud/router'
-import { Type as ShareTypes } from '@nextcloud/sharing'
-import Vue from 'vue'
-
-import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
-import ActionCheckbox from '@nextcloud/vue/dist/Components/ActionCheckbox'
-import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
-import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
-import ActionText from '@nextcloud/vue/dist/Components/ActionText'
-import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
-import ActionTextEditable from '@nextcloud/vue/dist/Components/ActionTextEditable'
-import Actions from '@nextcloud/vue/dist/Components/Actions'
-import Avatar from '@nextcloud/vue/dist/Components/Avatar'
-import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
-
-import ExternalShareAction from './ExternalShareAction'
-import SharePermissionsEditor from './SharePermissionsEditor'
-import GeneratePassword from '../utils/GeneratePassword'
-import Share from '../models/Share'
-import SharesMixin from '../mixins/SharesMixin'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { emit } from '@nextcloud/event-bus'
+import { t } from '@nextcloud/l10n'
+import moment from '@nextcloud/moment'
+import { generateUrl, getBaseUrl } from '@nextcloud/router'
+import { ShareType } from '@nextcloud/sharing'
+
+import VueQrcode from '@chenfengyuan/vue-qrcode'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox'
+import NcActionInput from '@nextcloud/vue/components/NcActionInput'
+import NcActionLink from '@nextcloud/vue/components/NcActionLink'
+import NcActionText from '@nextcloud/vue/components/NcActionText'
+import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+
+import Tune from 'vue-material-design-icons/Tune.vue'
+import IconCalendarBlank from 'vue-material-design-icons/CalendarBlankOutline.vue'
+import IconQr from 'vue-material-design-icons/Qrcode.vue'
+import ErrorIcon from 'vue-material-design-icons/Exclamation.vue'
+import LockIcon from 'vue-material-design-icons/LockOutline.vue'
+import CheckIcon from 'vue-material-design-icons/CheckBold.vue'
+import ClipboardIcon from 'vue-material-design-icons/ContentCopy.vue'
+import CloseIcon from 'vue-material-design-icons/Close.vue'
+import PlusIcon from 'vue-material-design-icons/Plus.vue'
+
+import SharingEntryQuickShareSelect from './SharingEntryQuickShareSelect.vue'
+import ShareExpiryTime from './ShareExpiryTime.vue'
+
+import ExternalShareAction from './ExternalShareAction.vue'
+import GeneratePassword from '../utils/GeneratePassword.ts'
+import Share from '../models/Share.ts'
+import SharesMixin from '../mixins/SharesMixin.js'
+import ShareDetails from '../mixins/ShareDetails.js'
+import logger from '../services/logger.ts'
export default {
name: 'SharingEntryLink',
components: {
- Actions,
- ActionButton,
- ActionCheckbox,
- ActionInput,
- ActionLink,
- ActionText,
- ActionTextEditable,
- ActionSeparator,
- Avatar,
ExternalShareAction,
- SharePermissionsEditor,
- },
-
- directives: {
- Tooltip,
+ NcActions,
+ NcActionButton,
+ NcActionCheckbox,
+ NcActionInput,
+ NcActionLink,
+ NcActionText,
+ NcActionSeparator,
+ NcAvatar,
+ NcDialog,
+ VueQrcode,
+ Tune,
+ IconCalendarBlank,
+ IconQr,
+ ErrorIcon,
+ LockIcon,
+ CheckIcon,
+ ClipboardIcon,
+ CloseIcon,
+ PlusIcon,
+ SharingEntryQuickShareSelect,
+ ShareExpiryTime,
},
- mixins: [SharesMixin],
+ mixins: [SharesMixin, ShareDetails],
props: {
canReshare: {
type: Boolean,
default: true,
},
+ index: {
+ type: Number,
+ default: null,
+ },
},
data() {
return {
+ shareCreationComplete: false,
copySuccess: true,
copied: false,
+ defaultExpirationDateEnabled: false,
// Are we waiting for password/expiration date
pending: false,
ExternalLegacyLinkActions: OCA.Sharing.ExternalLinkActions.state,
ExternalShareActions: OCA.Sharing.ExternalShareActions.state,
+
+ // tracks whether modal should be opened or not
+ showQRCode: false,
}
},
@@ -374,6 +328,8 @@ export default {
* @return {string}
*/
title() {
+ const l10nOptions = { escape: false /* no escape as this string is already escaped by Vue */ }
+
// if we have a valid existing share (not pending)
if (this.share && this.share.id) {
if (!this.isShareOwner && this.share.ownerDisplayName) {
@@ -381,27 +337,46 @@ export default {
return t('files_sharing', '{shareWith} by {initiator}', {
shareWith: this.share.shareWith,
initiator: this.share.ownerDisplayName,
- })
+ }, l10nOptions)
}
return t('files_sharing', 'Shared via link by {initiator}', {
initiator: this.share.ownerDisplayName,
- })
+ }, l10nOptions)
}
if (this.share.label && this.share.label.trim() !== '') {
if (this.isEmailShareType) {
+ if (this.isFileRequest) {
+ return t('files_sharing', 'File request ({label})', {
+ label: this.share.label.trim(),
+ }, l10nOptions)
+ }
return t('files_sharing', 'Mail share ({label})', {
label: this.share.label.trim(),
- })
+ }, l10nOptions)
}
return t('files_sharing', 'Share link ({label})', {
label: this.share.label.trim(),
- })
+ }, l10nOptions)
}
if (this.isEmailShareType) {
+ if (!this.share.shareWith || this.share.shareWith.trim() === '') {
+ return this.isFileRequest
+ ? t('files_sharing', 'File request')
+ : t('files_sharing', 'Mail share')
+ }
return this.share.shareWith
}
+
+ if (this.index === null) {
+ return t('files_sharing', 'Share link')
+ }
+ }
+
+ if (this.index >= 1) {
+ return t('files_sharing', 'Share link ({index})', { index: this.index })
}
- return t('files_sharing', 'Share link')
+
+ return t('files_sharing', 'Create public link')
},
/**
@@ -417,48 +392,18 @@ export default {
return null
},
- /**
- * Does the current share have an expiration date
- *
- * @return {boolean}
- */
- hasExpirationDate: {
- get() {
- return this.config.isDefaultExpireDateEnforced
- || !!this.share.expireDate
- },
- set(enabled) {
- let dateString = moment(this.config.defaultExpirationDateString)
- if (!dateString.isValid()) {
- dateString = moment()
- }
- this.share.state.expiration = enabled
- ? dateString.format('YYYY-MM-DD')
- : ''
- console.debug('Expiration date status', enabled, this.share.expireDate)
- },
- },
+ passwordExpirationTime() {
+ if (this.share.passwordExpirationTime === null) {
+ return null
+ }
- dateMaxEnforced() {
- return this.config.isDefaultExpireDateEnforced
- && moment().add(1 + this.config.defaultExpireDate, 'days')
- },
+ const expirationTime = moment(this.share.passwordExpirationTime)
- /**
- * 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
- Vue.set(this.share, 'password', enabled ? await GeneratePassword() : '')
- Vue.set(this.share, 'newPassword', this.share.password)
- },
+ if (expirationTime.diff(moment()) < 0) {
+ return false
+ }
+
+ return expirationTime.fromNow()
},
/**
@@ -500,7 +445,7 @@ export default {
*/
isEmailShareType() {
return this.share
- ? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL
+ ? this.share.type === ShareType.Email
: false
},
@@ -525,13 +470,50 @@ export default {
*
* @return {boolean}
*/
+ pendingDataIsMissing() {
+ return this.pendingPassword || this.pendingEnforcedPassword || this.pendingDefaultExpirationDate || this.pendingEnforcedExpirationDate
+ },
pendingPassword() {
- return this.config.enforcePasswordForPublicLink && this.share && !this.share.id
+ return this.config.enableLinkPasswordByDefault && this.isPendingShare
+ },
+ pendingEnforcedPassword() {
+ return this.config.enforcePasswordForPublicLink && this.isPendingShare
+ },
+ pendingEnforcedExpirationDate() {
+ return this.config.isDefaultExpireDateEnforced && this.isPendingShare
},
- pendingExpirationDate() {
- return this.config.isDefaultExpireDateEnforced && this.share && !this.share.id
+ pendingDefaultExpirationDate() {
+ return (this.config.defaultExpirationDate instanceof Date || !isNaN(new Date(this.config.defaultExpirationDate).getTime())) && this.isPendingShare
},
+ isPendingShare() {
+ return !!(this.share && !this.share.id)
+ },
+ sharePolicyHasEnforcedProperties() {
+ return this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced
+ },
+
+ enforcedPropertiesMissing() {
+ // Ensure share exist and the share policy has required properties
+ if (!this.sharePolicyHasEnforcedProperties) {
+ return false
+ }
+
+ if (!this.share) {
+ // if no share, we can't tell if properties are missing or not so we assume properties are missing
+ return true
+ }
+
+ // If share has ID, then this is an incoming link share created from the existing link share
+ // Hence assume required properties
+ if (this.share.id) {
+ return true
+ }
+ // Check if either password or expiration date is missing and enforced
+ const isPasswordMissing = this.config.enforcePasswordForPublicLink && !this.share.password
+ const isExpireDateMissing = this.config.isDefaultExpireDateEnforced && !this.share.expireDate
+ return isPasswordMissing || isExpireDateMissing
+ },
// if newPassword exists, but is empty, it means
// the user deleted the original password
hasUnsavedPassword() {
@@ -544,21 +526,31 @@ export default {
* @return {string}
*/
shareLink() {
- return window.location.protocol + '//' + window.location.host + generateUrl('/s/') + this.share.token
+ return generateUrl('/s/{token}', { token: this.share.token }, { baseURL: getBaseUrl() })
},
/**
- * Clipboard v-tooltip message
+ * Tooltip message for actions button
*
* @return {string}
*/
- clipboardTooltip() {
+ actionsTooltip() {
+ return t('files_sharing', 'Actions for "{title}"', { title: this.title })
+ },
+
+ /**
+ * Tooltip message for copy button
+ *
+ * @return {string}
+ */
+ copyLinkTooltip() {
if (this.copied) {
- return this.copySuccess
- ? t('files_sharing', 'Link copied')
- : t('files_sharing', 'Cannot copy, please copy the link manually')
+ if (this.copySuccess) {
+ return ''
+ }
+ return t('files_sharing', 'Cannot copy, please copy the link manually')
}
- return t('files_sharing', 'Copy to clipboard')
+ return t('files_sharing', 'Copy public link of "{title}"', { title: this.title })
},
/**
@@ -577,64 +569,85 @@ export default {
* @return {Array}
*/
externalLinkActions() {
+ const filterValidAction = (action) => (action.shareType.includes(ShareType.Link) || action.shareType.includes(ShareType.Email)) && !action.advanced
// filter only the registered actions for said link
return this.ExternalShareActions.actions
- .filter(action => action.shareType.includes(ShareTypes.SHARE_TYPE_LINK)
- || action.shareType.includes(ShareTypes.SHARE_TYPE_EMAIL))
+ .filter(filterValidAction)
},
isPasswordPolicyEnabled() {
return typeof this.config.passwordPolicy === 'object'
},
+
+ canChangeHideDownload() {
+ const hasDisabledDownload = (shareAttribute) => shareAttribute.scope === 'permissions' && shareAttribute.key === 'download' && shareAttribute.value === false
+ return this.fileInfo.shareAttributes.some(hasDisabledDownload)
+ },
+
+ isFileRequest() {
+ return this.share.isFileRequest
+ },
+ },
+ mounted() {
+ this.defaultExpirationDateEnabled = this.config.defaultExpirationDate instanceof Date
+ if (this.share && this.isNewShare) {
+ this.share.expireDate = this.defaultExpirationDateEnabled ? this.formatDateToString(this.config.defaultExpirationDate) : ''
+ }
},
methods: {
/**
+ * Check if the share requires review
+ *
+ * @param {boolean} shareReviewComplete if the share was reviewed
+ * @return {boolean}
+ */
+ shareRequiresReview(shareReviewComplete) {
+ // If a user clicks 'Create share' it means they have reviewed the share
+ if (shareReviewComplete) {
+ return false
+ }
+ return this.defaultExpirationDateEnabled || this.config.enableLinkPasswordByDefault
+ },
+ /**
* Create a new share link and append it to the list
+ * @param {boolean} shareReviewComplete if the share was reviewed
*/
- async onNewLinkShare() {
+ async onNewLinkShare(shareReviewComplete = false) {
+ logger.debug('onNewLinkShare called (with this.share)', this.share)
// do not run again if already loading
if (this.loading) {
return
}
const shareDefaults = {
- share_type: ShareTypes.SHARE_TYPE_LINK,
+ share_type: ShareType.Link,
}
if (this.config.isDefaultExpireDateEnforced) {
// default is empty string if not set
// expiration is the share object key, not expireDate
- shareDefaults.expiration = this.config.defaultExpirationDateString
- }
- if (this.config.enableLinkPasswordByDefault) {
- shareDefaults.password = await GeneratePassword()
+ shareDefaults.expiration = this.formatDateToString(this.config.defaultExpirationDate)
}
- // do not push yet if we need a password or an expiration date: show pending menu
- if (this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced) {
+ logger.debug('Missing required properties?', this.enforcedPropertiesMissing)
+ // Do not push yet if we need a password or an expiration date: show pending menu
+ // A share would require a review for example is default expiration date is set but not enforced, this allows
+ // the user to review the share and remove the expiration date if they don't want it
+ if ((this.sharePolicyHasEnforcedProperties && this.enforcedPropertiesMissing) || this.shareRequiresReview(shareReviewComplete === true)) {
this.pending = true
+ this.shareCreationComplete = false
- // if a share already exists, pushing it
- if (this.share && !this.share.id) {
- // if the share is valid, create it on the server
- if (this.checkShare(this.share)) {
- await this.pushNewLinkShare(this.share, true)
- return true
- } else {
- this.open = true
- OC.Notification.showTemporary(t('files_sharing', 'Error, please enter proper password and/or expiration date'))
- return false
- }
- }
+ logger.info('Share policy requires a review or has mandated properties (password, expirationDate)...')
// ELSE, show the pending popovermenu
- // if password enforced, pre-fill with random one
- if (this.config.enforcePasswordForPublicLink) {
- shareDefaults.password = await GeneratePassword()
+ // if password default or enforced, pre-fill with random one
+ if (this.config.enableLinkPasswordByDefault || this.config.enforcePasswordForPublicLink) {
+ shareDefaults.password = await GeneratePassword(true)
}
// create share & close menu
const share = new Share(shareDefaults)
+ share.newPassword = share.password
const component = await new Promise(resolve => {
this.$emit('add:share', share, resolve)
})
@@ -645,10 +658,34 @@ export default {
this.pending = false
component.open = true
- // Nothing is enforced, creating share directly
+ // Nothing is enforced, creating share directly
} else {
+
+ // if a share already exists, pushing it
+ if (this.share && !this.share.id) {
+ // if the share is valid, create it on the server
+ if (this.checkShare(this.share)) {
+ try {
+ logger.info('Sending existing share to server', this.share)
+ await this.pushNewLinkShare(this.share, true)
+ this.shareCreationComplete = true
+ logger.info('Share created on server', this.share)
+ } catch (e) {
+ this.pending = false
+ logger.error('Error creating share', e)
+ return false
+ }
+ return true
+ } else {
+ this.open = true
+ showError(t('files_sharing', 'Error, please enter proper password and/or expiration date'))
+ return false
+ }
+ }
+
const share = new Share(shareDefaults)
await this.pushNewLinkShare(share)
+ this.shareCreationComplete = true
}
},
@@ -658,7 +695,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 {
@@ -671,22 +708,25 @@ export default {
this.errors = {}
const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
- const newShare = await this.createShare({
+ const options = {
path,
- shareType: ShareTypes.SHARE_TYPE_LINK,
+ shareType: ShareType.Link,
password: share.password,
- expireDate: share.expireDate,
+ expireDate: share.expireDate ?? '',
+ attributes: JSON.stringify(this.fileInfo.shareAttributes),
// we do not allow setting the publicUpload
// before the share creation.
// Todo: We also need to fix the createShare method in
- // lib/Controller/ShareAPIController.php to allow file drop
+ // lib/Controller/ShareAPIController.php to allow file requests
// (currently not supported on create, only update)
- })
+ }
- this.open = false
+ console.debug('Creating link share with options', options)
+ const newShare = await this.createShare(options)
+ this.open = false
+ this.shareCreationComplete = true
console.debug('Link share created', newShare)
-
// if share already exists, copy link directly on next tick
let component
if (update) {
@@ -702,6 +742,9 @@ export default {
})
}
+ await this.getNode()
+ emit('files:node:updated', this.node)
+
// Execute the copy link method
// freshly created share component
// ! somehow does not works on firefox !
@@ -710,9 +753,16 @@ export default {
// otherwise the user needs to copy/paste the password before finishing the share.
component.copyLink()
}
+ showSuccess(t('files_sharing', 'Link share created'))
+
+ } catch (data) {
+ const message = data?.response?.data?.ocs?.meta?.message
+ if (!message) {
+ showError(t('files_sharing', 'Error while creating the share'))
+ console.error(data)
+ return
+ }
- } catch ({ response }) {
- const message = response.data.ocs.meta.message
if (message.match(/password/i)) {
this.onSyncError('password', message)
} else if (message.match(/date/i)) {
@@ -720,33 +770,17 @@ export default {
} else {
this.onSyncError('pending', message)
}
+ throw data
+
} finally {
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')
+ this.shareCreationComplete = true
}
},
async copyLink() {
try {
- await this.$copyText(this.shareLink)
+ await navigator.clipboard.writeText(this.shareLink)
+ showSuccess(t('files_sharing', 'Link copied'))
// focus and show the tooltip
this.$refs.copyButton.$el.focus()
this.copySuccess = true
@@ -796,7 +830,7 @@ export default {
},
/**
- * Menu have been closed or password has been submited.
+ * Menu have been closed or password has been submitted.
* The only property that does not get
* synced automatically is the password
* So let's check if we have an unsaved
@@ -806,7 +840,7 @@ export default {
*/
onPasswordSubmit() {
if (this.hasUnsavedPassword) {
- this.share.password = this.share.newPassword.trim()
+ this.share.newPassword = this.share.newPassword.trim()
this.queueUpdate('password')
}
},
@@ -821,7 +855,7 @@ export default {
*/
onPasswordProtectedByTalkChange() {
if (this.hasUnsavedPassword) {
- this.share.password = this.share.newPassword.trim()
+ this.share.newPassword = this.share.newPassword.trim()
}
this.queueUpdate('sendPasswordByTalk', 'password')
@@ -836,6 +870,19 @@ export default {
},
/**
+ * @param enabled True if expiration is enabled
+ */
+ onExpirationDateToggleUpdate(enabled) {
+ this.share.expireDate = enabled ? this.formatDateToString(this.config.defaultExpirationDate) : ''
+ },
+
+ expirationDateChanged(event) {
+ const value = event?.target?.value
+ const isValid = !!value && !isNaN(new Date(value).getTime())
+ this.defaultExpirationDateEnabled = isValid
+ },
+
+ /**
* Cancel the share creation
* Used in the pending popover
*/
@@ -843,10 +890,11 @@ export default {
// this.share already exists at this point,
// but is incomplete as not pushed to server
// YET. We can safely delete the share :)
- this.$emit('remove:share', this.share)
+ if (!this.shareCreationComplete) {
+ this.$emit('remove:share', this.share)
+ }
},
},
-
}
</script>
@@ -855,23 +903,37 @@ export default {
display: flex;
align-items: center;
min-height: 44px;
- &__desc {
+
+ &__summary {
+ padding: 8px;
+ padding-inline-start: 10px;
display: flex;
- flex-direction: column;
justify-content: space-between;
- padding: 8px;
- line-height: 1.2em;
- overflow: hidden;
+ flex: 1 0;
+ min-width: 0;
+ }
+
+ &__desc {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.2em;
+
+ p {
+ color: var(--color-text-maxcontrast);
+ }
- h5 {
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
+ &__title {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
}
- p {
- color: var(--color-text-maxcontrast);
+
+ &__actions {
+ display: flex;
+ align-items: center;
+ margin-inline-start: auto;
}
- }
&:not(.sharing-entry--share) &__actions {
.new-share-link {
@@ -879,8 +941,8 @@ export default {
}
}
- ::v-deep .avatar-link-share {
- background-color: var(--color-primary);
+ :deep(.avatar-link-share) {
+ background-color: var(--color-primary-element);
}
.sharing-entry__action--public-upload {
@@ -892,21 +954,34 @@ export default {
height: 44px;
margin: 0;
padding: 14px;
- margin-left: auto;
+ margin-inline-start: auto;
}
// put menus to the left
// but only the first one
.action-item {
- margin-left: auto;
- ~ .action-item,
- ~ .sharing-entry__loading {
- margin-left: 0;
+
+ ~.action-item,
+ ~.sharing-entry__loading {
+ margin-inline-start: 0;
}
}
.icon-checkmark-color {
opacity: 1;
+ color: var(--color-success);
+ }
+}
+
+// styling for the qr-code container
+.qr-code-dialog {
+ display: flex;
+ width: 100%;
+ justify-content: center;
+
+ &__img {
+ width: 100%;
+ height: auto;
}
}
</style>