aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authornfebe <fenn25.fn@gmail.com>2025-02-14 22:56:44 +0100
committernfebe <fenn25.fn@gmail.com>2025-04-01 10:45:46 +0100
commitb6f1376cf3ddf2cb7150be989e81a083643dc399 (patch)
tree57cb9462a0bdc7d4b6505f78ab632a5e85f6618f
parent1cfd3cd2499f3ac93cd00c6c6277541200d17992 (diff)
downloadnextcloud-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.vue235
-rw-r--r--apps/files_sharing/src/components/SharingEntryLink.vue241
-rw-r--r--apps/files_sharing/src/logger.ts11
-rw-r--r--apps/files_sharing/src/mixins/PendingActionsHandlersMixin.ts239
-rw-r--r--apps/files_sharing/src/views/SharingTab.vue26
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) {