diff options
author | nfebe <fenn25.fn@gmail.com> | 2025-02-14 22:56:44 +0100 |
---|---|---|
committer | nfebe <fenn25.fn@gmail.com> | 2025-04-01 10:45:46 +0100 |
commit | b6f1376cf3ddf2cb7150be989e81a083643dc399 (patch) | |
tree | 57cb9462a0bdc7d4b6505f78ab632a5e85f6618f | |
parent | 1cfd3cd2499f3ac93cd00c6c6277541200d17992 (diff) | |
download | nextcloud-server-enh/share-sidebar.tar.gz nextcloud-server-enh/share-sidebar.zip |
feat: New create link share buttonenh/share-sidebar
Signed-off-by: nfebe <fenn25.fn@gmail.com>
-rw-r--r-- | apps/files_sharing/src/components/PendingActions.vue | 235 | ||||
-rw-r--r-- | apps/files_sharing/src/components/SharingEntryLink.vue | 241 | ||||
-rw-r--r-- | apps/files_sharing/src/logger.ts | 11 | ||||
-rw-r--r-- | apps/files_sharing/src/mixins/PendingActionsHandlersMixin.ts | 239 | ||||
-rw-r--r-- | apps/files_sharing/src/views/SharingTab.vue | 26 |
5 files changed, 545 insertions, 207 deletions
diff --git a/apps/files_sharing/src/components/PendingActions.vue b/apps/files_sharing/src/components/PendingActions.vue new file mode 100644 index 00000000000..2aa54d0a033 --- /dev/null +++ b/apps/files_sharing/src/components/PendingActions.vue @@ -0,0 +1,235 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcActions class="sharing-entry__actions" + :aria-label="actionsTooltip" + menu-align="right" + :open="localOpen" + @close="onCancel"> + <!-- pending data menu --> + <NcActionText v-if="errors.pending" + class="error"> + <template #icon> + <ErrorIcon :size="20" /> + </template> + {{ errors.pending }} + </NcActionText> + <NcActionText v-else icon="icon-info"> + {{ t('files_sharing', 'Please enter the following required information before creating the share') }} + </NcActionText> + + <!-- password --> + <NcActionCheckbox v-if="pendingPassword" + :checked="localIsPasswordProtected" + :disabled="config.enforcePasswordForPublicLink || saving" + class="share-link-password-checkbox" + @update:checked="onPasswordProtectedChange"> + {{ config.enforcePasswordForPublicLink ? t('files_sharing', 'Password protection (enforced)') : t('files_sharing', 'Password protection') }} + </NcActionCheckbox> + + <NcActionInput v-if="pendingEnforcedPassword || share.password" + class="share-link-password" + :label="t('files_sharing', 'Enter a password')" + :value="share.password" + :disabled="saving" + :required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink" + :minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength" + autocomplete="new-password" + @submit="onNewLinkShare(true)"> + <template #icon> + <LockIcon :size="20" /> + </template> + </NcActionInput> + + <NcActionCheckbox v-if="pendingDefaultExpirationDate" + :checked="localDefaultExpirationDateEnabled" + :disabled="pendingEnforcedExpirationDate || saving" + class="share-link-expiration-date-checkbox" + @update:checked="onExpirationDateToggleChange"> + {{ config.isDefaultExpireDateEnforced ? t('files_sharing', 'Enable link expiration (enforced)') : t('files_sharing', 'Enable link expiration') }} + </NcActionCheckbox> + + <!-- expiration date --> + <NcActionInput v-if="(pendingDefaultExpirationDate || pendingEnforcedExpirationDate) && localDefaultExpirationDateEnabled" + 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" + :is-native-picker="true" + :hide-label="true" + :value="new Date(share.expireDate)" + type="date" + :min="dateTomorrow" + :max="maxExpirationDateEnforced" + @change="expirationDateChanged($event)"> + <template #icon> + <IconCalendarBlank :size="20" /> + </template> + </NcActionInput> + + <NcActionButton @click.prevent.stop="onNewLinkShare(true)"> + <template #icon> + <CheckIcon :size="20" /> + </template> + {{ t('files_sharing', 'Create share') }} + </NcActionButton> + <NcActionButton @click.prevent.stop="onCancel"> + <template #icon> + <CloseIcon :size="20" /> + </template> + {{ t('files_sharing', 'Cancel') }} + </NcActionButton> + </NcActions> +</template> + +<script> +import { NcActions, NcActionButton, NcActionCheckbox, NcActionInput, NcActionText } from '@nextcloud/vue' +import ErrorIcon from 'vue-material-design-icons/Exclamation.vue' +import LockIcon from 'vue-material-design-icons/Lock.vue' +import CheckIcon from 'vue-material-design-icons/CheckBold.vue' +import CloseIcon from 'vue-material-design-icons/Close.vue' +import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue' + +export default { + name: 'PendingActions', + + components: { + NcActions, + NcActionButton, + NcActionCheckbox, + NcActionInput, + NcActionText, + ErrorIcon, + LockIcon, + CheckIcon, + CloseIcon, + IconCalendarBlank, + }, + + props: { + open: { + type: Boolean, + required: true, + }, + share: { + type: Object, + required: true, + }, + config: { + type: Object, + required: true, + }, + errors: { + type: Object, + required: true, + }, + pendingPassword: { + type: Boolean, + required: true, + }, + pendingEnforcedPassword: { + type: Boolean, + required: true, + }, + pendingDefaultExpirationDate: { + type: Boolean, + required: true, + }, + pendingEnforcedExpirationDate: { + type: Boolean, + required: true, + }, + defaultExpirationDateEnabled: { + type: Boolean, + required: true, + }, + saving: { + type: Boolean, + required: true, + }, + isPasswordPolicyEnabled: { + type: Boolean, + required: true, + }, + dateTomorrow: { + type: Date, + required: true, + }, + maxExpirationDateEnforced: { + type: String, + default: '', + }, + actionsTooltip: { + type: String, + required: true, + }, + isPasswordProtected: { + type: Boolean, + required: true, + }, + }, + + data() { + return { + // Local state for checkboxes + localIsPasswordProtected: this.isPasswordProtected, + localDefaultExpirationDateEnabled: this.defaultExpirationDateEnabled, + // Local copy of the open prop + localOpen: this.open, + } + }, + + watch: { + // Sync changes from parent to local state + isPasswordProtected(newVal) { + this.localIsPasswordProtected = newVal + }, + defaultExpirationDateEnabled(newVal) { + this.localDefaultExpirationDateEnabled = newVal + }, + open(newVal) { + this.localOpen = newVal + }, + // Sync changes from localOpen to parent + localOpen(newVal) { + this.$emit('update:open', newVal) + }, + }, + + methods: { + onNewLinkShare(shareReviewComplete) { + this.$emit('new-link-share', shareReviewComplete) + }, + onCancel() { + this.$emit('cancel') + }, + onPasswordDisable() { + this.$emit('password-disable') + }, + onExpirationDateToggleChange(enabled) { + this.localDefaultExpirationDateEnabled = enabled + this.$emit('update:defaultExpirationDateEnabled', enabled) + }, + onPasswordProtectedChange(enabled) { + this.localIsPasswordProtected = enabled + this.$emit('update:isPasswordProtected', enabled) + }, + expirationDateChanged(event) { + this.$emit('expiration-date-changed', event) + }, + }, +} +</script> + +<style lang="scss" scoped> +.sharing-entry__actions { + .action-item { + ~.action-item { + margin-inline-start: 0 + } + } +} +</style> diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue index 9427bd78967..21e934401db 100644 --- a/apps/files_sharing/src/components/SharingEntryLink.vue +++ b/apps/files_sharing/src/components/SharingEntryLink.vue @@ -9,7 +9,6 @@ <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__summary"> <div class="sharing-entry__desc"> <span class="sharing-entry__title" :title="title"> @@ -47,86 +46,28 @@ </div> <!-- pending actions --> - <NcActions v-if="!pending && pendingDataIsMissing" - class="sharing-entry__actions" - :aria-label="actionsTooltip" - menu-align="right" + <PendingActions v-if="!pending && pendingDataIsMissing" :open.sync="open" - @close="onCancel"> - <!-- pending data menu --> - <NcActionText v-if="errors.pending" - class="error"> - <template #icon> - <ErrorIcon :size="20" /> - </template> - {{ errors.pending }} - </NcActionText> - <NcActionText v-else icon="icon-info"> - {{ t('files_sharing', 'Please enter the following required information before creating the share') }} - </NcActionText> - - <!-- password --> - <NcActionCheckbox v-if="pendingPassword" - :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 protection') }} - </NcActionCheckbox> - - <NcActionInput v-if="pendingEnforcedPassword || share.password" - class="share-link-password" - :label="t('files_sharing', 'Enter a password')" - :value.sync="share.password" - :disabled="saving" - :required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink" - :minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength" - autocomplete="new-password" - @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 --> - <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" - :is-native-picker="true" - :hide-label="true" - :value="new Date(share.expireDate)" - type="date" - :min="dateTomorrow" - :max="maxExpirationDateEnforced" - @change="expirationDateChanged($event)"> - <template #icon> - <IconCalendarBlank :size="20" /> - </template> - </NcActionInput> - - <NcActionButton @click.prevent.stop="onNewLinkShare(true)"> - <template #icon> - <CheckIcon :size="20" /> - </template> - {{ t('files_sharing', 'Create share') }} - </NcActionButton> - <NcActionButton @click.prevent.stop="onCancel"> - <template #icon> - <CloseIcon :size="20" /> - </template> - {{ t('files_sharing', 'Cancel') }} - </NcActionButton> - </NcActions> + :share="share" + :config="config" + :errors="errors" + :pending-password="pendingPassword" + :pending-enforced-password="pendingEnforcedPassword" + :pending-default-expiration-date="pendingDefaultExpirationDate" + :pending-enforced-expiration-date="pendingEnforcedExpirationDate" + :default-expiration-date-enabled="defaultExpirationDateEnabled" + :saving="saving" + :is-password-policy-enabled="isPasswordPolicyEnabled" + :date-tomorrow="dateTomorrow" + :max-expiration-date-enforced="maxExpirationDateEnforced" + :actions-tooltip="actionsTooltip" + :is-password-protected="isPasswordProtected" + @new-link-share="onNewLinkShare" + @cancel="onCancel" + @password-disable="onPasswordDisable" + @update:isPasswordProtected="onPasswordProtectedChange" + @update:defaultExpirationDateEnabled="onExpirationDateToggleChange" + @expiration-date-changed="expirationDateChanged" /> <!-- actions --> <NcActions v-else-if="!loading" @@ -192,14 +133,6 @@ {{ t('files_sharing', 'Unshare') }} </NcActionButton> </template> - - <!-- Create new share --> - <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" /> </NcActions> <!-- loading indicator to replace the menu --> @@ -222,7 +155,6 @@ </template> <script> -import { emit } from '@nextcloud/event-bus' import { generateUrl, getBaseUrl } from '@nextcloud/router' import { showError, showSuccess } from '@nextcloud/dialogs' import { ShareType } from '@nextcloud/sharing' @@ -230,21 +162,15 @@ import VueQrcode from '@chenfengyuan/vue-qrcode' import moment from '@nextcloud/moment' import Vue from 'vue' -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 NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' +import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js' +import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js' +import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' +import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' +import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' import Tune from 'vue-material-design-icons/Tune.vue' -import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.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/Lock.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' @@ -255,10 +181,11 @@ import ShareExpiryTime from './ShareExpiryTime.vue' import ExternalShareAction from './ExternalShareAction.vue' import GeneratePassword from '../utils/GeneratePassword.ts' +import PendingActions from './PendingActions.vue' import Share from '../models/Share.ts' import SharesMixin from '../mixins/SharesMixin.js' import ShareDetails from '../mixins/ShareDetails.js' -import { getLoggerBuilder } from '@nextcloud/logger' +import logger from '../logger.ts' export default { name: 'SharingEntryLink', @@ -267,23 +194,18 @@ export default { ExternalShareAction, NcActions, NcActionButton, - NcActionCheckbox, - NcActionInput, NcActionLink, - NcActionText, NcActionSeparator, NcAvatar, NcDialog, VueQrcode, Tune, - IconCalendarBlank, IconQr, - ErrorIcon, - LockIcon, CheckIcon, ClipboardIcon, CloseIcon, PlusIcon, + PendingActions, SharingEntryQuickShareSelect, ShareExpiryTime, }, @@ -313,10 +235,7 @@ export default { ExternalLegacyLinkActions: OCA.Sharing.ExternalLinkActions.state, ExternalShareActions: OCA.Sharing.ExternalShareActions.state, - logger: getLoggerBuilder() - .setApp('files_sharing') - .detectUser() - .build(), + logger, // tracks whether modal should be opened or not showQRCode: false, @@ -703,95 +622,6 @@ export default { this.shareCreationComplete = true } }, - - /** - * Push a new link share to the server - * And update or append to the list - * accordingly - * - * @param {Share} share the new share - * @param {boolean} [update] do we update the current share ? - */ - async pushNewLinkShare(share, update) { - try { - // do nothing if we're already pending creation - if (this.loading) { - return true - } - - this.loading = true - this.errors = {} - - const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/') - const options = { - path, - shareType: ShareType.Link, - password: share.password, - 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 requests - // (currently not supported on create, only update) - } - - 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) { - component = await new Promise(resolve => { - this.$emit('update:share', newShare, resolve) - }) - } else { - // adding new share to the array and copying link to clipboard - // using promise so that we can copy link in the same click function - // and avoid firefox copy permissions issue - component = await new Promise(resolve => { - this.$emit('add:share', newShare, resolve) - }) - } - - await this.getNode() - emit('files:node:updated', this.node) - - // Execute the copy link method - // freshly created share component - // ! somehow does not works on firefox ! - if (!this.config.enforcePasswordForPublicLink) { - // Only copy the link when the password was not forced, - // 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 - } - - if (message.match(/password/i)) { - this.onSyncError('password', message) - } else if (message.match(/date/i)) { - this.onSyncError('expireDate', message) - } else { - this.onSyncError('pending', message) - } - throw data - - } finally { - this.loading = false - this.shareCreationComplete = true - } - }, async copyLink() { try { await navigator.clipboard.writeText(this.shareLink) @@ -883,11 +713,10 @@ export default { this.onPasswordSubmit() this.onNoteSubmit() }, - - /** - * @param enabled True if expiration is enabled - */ - onExpirationDateToggleUpdate(enabled) { + onPasswordProtectedChange(enabled) { + this.isPasswordProtected = enabled + }, + onExpirationDateToggleChange(enabled) { this.share.expireDate = enabled ? this.formatDateToString(this.config.defaultExpirationDate) : '' }, diff --git a/apps/files_sharing/src/logger.ts b/apps/files_sharing/src/logger.ts new file mode 100644 index 00000000000..31490d814e8 --- /dev/null +++ b/apps/files_sharing/src/logger.ts @@ -0,0 +1,11 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getLoggerBuilder } from '@nextcloud/logger' + +export default getLoggerBuilder() + .setApp('files_sharing') + .detectUser() + .build()
\ No newline at end of file diff --git a/apps/files_sharing/src/mixins/PendingActionsHandlersMixin.ts b/apps/files_sharing/src/mixins/PendingActionsHandlersMixin.ts new file mode 100644 index 00000000000..99c16ca4005 --- /dev/null +++ b/apps/files_sharing/src/mixins/PendingActionsHandlersMixin.ts @@ -0,0 +1,239 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { emit } from '@nextcloud/event-bus' +import { ShareType } from '@nextcloud/sharing' +import GeneratePassword from '../utils/GeneratePassword' +import Share from '../models/Share' +import { showError, showSuccess } from '@nextcloud/dialogs' +import { t } from '@nextcloud/l10n' +import SharesMixin from '../mixins/SharesMixin.js' + +export default { + computed: { + /** + * Whether the share policy has enforced properties. + * @return {boolean} + */ + sharePolicyHasEnforcedProperties() { + return this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced + }, + + /** + * Whether required properties are missing. + * @return {boolean} + */ + enforcedPropertiesMissing() { + if (!this.sharePolicyHasEnforcedProperties) return false + if (!this.share) return true + if (this.share.id) return true + + const isPasswordMissing = this.config.enforcePasswordForPublicLink && !this.share.password + const isExpireDateMissing = this.config.isDefaultExpireDateEnforced && !this.share.expireDate + + return isPasswordMissing || isExpireDateMissing + }, + }, + + mixins: [SharesMixin], + + 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 + }, + /** + * Handle the creation of a new link share. + * @param {boolean} shareReviewComplete Whether the share has been reviewed. + */ + async onNewLinkShare(shareReviewComplete = false) { + this.logger.debug('onNewLinkShare called (with this.share)', this.share) + if (this.loading) return + + const shareDefaults = { + share_type: ShareType.Link, + } + + if (this.config.isDefaultExpireDateEnforced) { + shareDefaults.expiration = this.formatDateToString(this.config.defaultExpirationDate) + } + + if ( + (this.sharePolicyHasEnforcedProperties && this.enforcedPropertiesMissing) + || this.shareRequiresReview(shareReviewComplete) + ) { + this.pending = true + this.shareCreationComplete = false + + if (this.config.enableLinkPasswordByDefault || this.config.enforcePasswordForPublicLink) { + shareDefaults.password = await GeneratePassword(true) + } + + const share = new Share(shareDefaults) + const component = await new Promise((resolve) => { + this.$emit('add:share', share, resolve) + }) + + this.open = false + this.pending = false + component.open = true + } else { + if (this.share && !this.share.id) { + if (this.checkShare(this.share)) { + try { + await this.pushNewLinkShare(this.share, true) + this.shareCreationComplete = true + } catch (e) { + this.pending = false + this.logger.error('Error creating share', e) + return + } + return + } else { + this.open = true + showError(t('files_sharing', 'Error, please enter proper password and/or expiration date')) + return + } + } + + const share = new Share(shareDefaults) + await this.pushNewLinkShare(share) + this.shareCreationComplete = true + } + }, + + /** + * Push a new link share to the server + * And update or append to the list + * accordingly + * + * @param {Share} share the new share + * @param {boolean} [update] do we update the current share ? + */ + async pushNewLinkShare(share, update) { + try { + // do nothing if we're already pending creation + if (this.loading) { + return true + } + + this.loading = true + this.errors = {} + + const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/') + const options = { + path, + shareType: ShareType.Link, + password: share.password, + 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 requests + // (currently not supported on create, only update) + } + + 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) { + component = await new Promise(resolve => { + this.$emit('update:share', newShare, resolve) + }) + } else { + // adding new share to the array and copying link to clipboard + // using promise so that we can copy link in the same click function + // and avoid firefox copy permissions issue + component = await new Promise(resolve => { + this.$emit('add:share', newShare, resolve) + }) + } + + await this.getNode() + emit('files:node:updated', this.node) + + // Execute the copy link method + // freshly created share component + // ! somehow does not works on firefox ! + if (!this.config.enforcePasswordForPublicLink) { + // Only copy the link when the password was not forced, + // 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 + } + + if (message.match(/password/i)) { + this.onSyncError('password', message) + } else if (message.match(/date/i)) { + this.onSyncError('expireDate', message) + } else { + this.onSyncError('pending', message) + } + throw data + + } finally { + this.loading = false + this.shareCreationComplete = true + } + }, + + /** + * Handle password protection change. + * @param {boolean} enabled Whether password protection is enabled. + */ + onPasswordProtectedChange(enabled) { + this.isPasswordProtected = enabled + }, + + /** + * Handle expiration date toggle change. + * @param {boolean} enabled Whether the expiration date is enabled. + */ + onExpirationDateToggleChange(enabled) { + this.share.expireDate = enabled ? this.formatDateToString(this.config.defaultExpirationDate) : '' + }, + + /** + * Handle expiration date change. + * @param {Event} event The change event. + */ + expirationDateChanged(event) { + const date = event.target.value + this.onExpirationChange(date) + this.defaultExpirationDateEnabled = !!date + }, + + /** + * Handle cancel action. + */ + onCancel() { + if (!this.shareCreationComplete) { + this.$emit('remove:share', this.share) + } + }, + }, +} diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue index 4da22434a94..9815f401ea2 100644 --- a/apps/files_sharing/src/views/SharingTab.vue +++ b/apps/files_sharing/src/views/SharingTab.vue @@ -101,6 +101,17 @@ :file-info="fileInfo" :shares="linkShares" @open-sharing-details="toggleShareDetailsView" /> + <!-- Create new share --> + <NcButton v-if="canReshare" + class="new-link-share" + :disabled="loading" + @click.prevent.stop="onNewLinkShare"> + {{ t('files_sharing', 'Create a link share') }} + <template #icon> + <LoadingIcon v-if="loading" :size="20" /> + <LinkIcon v-else :size="20" /> + </template> + </NcButton> </section> <section v-if="sections.length > 0 && !showSharingDetailsView"> @@ -181,12 +192,19 @@ import SharingDetailsTab from './SharingDetailsTab.vue' import ShareDetails from '../mixins/ShareDetails.js' +import LinkIcon from 'vue-material-design-icons/Link.vue' +import LoadingIcon from 'vue-material-design-icons/Loading.vue' +import PendingActionsHandlersMixin from '../mixins/PendingActionsHandlersMixin.ts' +import logger from '../logger.ts' + export default { name: 'SharingTab', components: { CollectionList, InfoIcon, + LinkIcon, + LoadingIcon, NcAvatar, NcButton, NcPopover, @@ -198,7 +216,7 @@ export default { SharingList, SharingDetailsTab, }, - mixins: [ShareDetails], + mixins: [ShareDetails, PendingActionsHandlersMixin], data() { return { @@ -215,6 +233,7 @@ export default { sharedWithMe: {}, shares: [], linkShares: [], + logger, sections: OCA.Sharing.ShareTabSections.getSections(), projectsEnabled: loadState('core', 'projects_enabled', false), @@ -534,6 +553,11 @@ export default { } + .new-link-share { + width: 100%; + height: 44px; + } + } & > section:not(:last-child) { |