aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src/components/NewFileRequestDialog.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src/components/NewFileRequestDialog.vue')
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog.vue468
1 files changed, 468 insertions, 0 deletions
diff --git a/apps/files_sharing/src/components/NewFileRequestDialog.vue b/apps/files_sharing/src/components/NewFileRequestDialog.vue
new file mode 100644
index 00000000000..392f286e104
--- /dev/null
+++ b/apps/files_sharing/src/components/NewFileRequestDialog.vue
@@ -0,0 +1,468 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <NcDialog can-close
+ class="file-request-dialog"
+ data-cy-file-request-dialog
+ :close-on-click-outside="false"
+ :name="currentStep !== STEP.LAST ? t('files_sharing', 'Create a file request') : t('files_sharing', 'File request created')"
+ size="normal"
+ @closing="onCancel">
+ <!-- Header -->
+ <NcNoteCard v-show="currentStep === STEP.FIRST" type="info" class="file-request-dialog__header">
+ <p id="file-request-dialog-description" class="file-request-dialog__description">
+ {{ t('files_sharing', 'Collect files from others even if they do not have an account.') }}
+ {{ t('files_sharing', 'To ensure you can receive files, verify you have enough storage available.') }}
+ </p>
+ </NcNoteCard>
+
+ <!-- Main form -->
+ <form ref="form"
+ class="file-request-dialog__form"
+ aria-describedby="file-request-dialog-description"
+ :aria-label="t('files_sharing', 'File request')"
+ aria-live="polite"
+ data-cy-file-request-dialog-form
+ @submit.prevent.stop="">
+ <FileRequestIntro v-show="currentStep === STEP.FIRST"
+ :context="context"
+ :destination.sync="destination"
+ :disabled="loading"
+ :label.sync="label"
+ :note.sync="note" />
+
+ <FileRequestDatePassword v-show="currentStep === STEP.SECOND"
+ :disabled="loading"
+ :expiration-date.sync="expirationDate"
+ :password.sync="password" />
+
+ <FileRequestFinish v-if="share"
+ v-show="currentStep === STEP.LAST"
+ :emails="emails"
+ :is-share-by-mail-enabled="isShareByMailEnabled"
+ :share="share"
+ @add-email="email => emails.push(email)"
+ @remove-email="onRemoveEmail" />
+ </form>
+
+ <!-- Controls -->
+ <template #actions>
+ <!-- Back -->
+ <NcButton v-show="currentStep === STEP.SECOND"
+ :aria-label="t('files_sharing', 'Previous step')"
+ :disabled="loading"
+ data-cy-file-request-dialog-controls="back"
+ type="tertiary"
+ @click="currentStep = STEP.FIRST">
+ {{ t('files_sharing', 'Previous step') }}
+ </NcButton>
+
+ <!-- Align right -->
+ <span class="dialog__actions-separator" />
+
+ <!-- Cancel the creation -->
+ <NcButton v-if="currentStep !== STEP.LAST"
+ :aria-label="t('files_sharing', 'Cancel')"
+ :disabled="loading"
+ :title="t('files_sharing', 'Cancel the file request creation')"
+ data-cy-file-request-dialog-controls="cancel"
+ type="tertiary"
+ @click="onCancel">
+ {{ t('files_sharing', 'Cancel') }}
+ </NcButton>
+
+ <!-- Cancel email and just close -->
+ <NcButton v-else-if="emails.length !== 0"
+ :aria-label="t('files_sharing', 'Close without sending emails')"
+ :disabled="loading"
+ :title="t('files_sharing', 'Close without sending emails')"
+ data-cy-file-request-dialog-controls="cancel"
+ type="tertiary"
+ @click="onCancel">
+ {{ t('files_sharing', 'Close') }}
+ </NcButton>
+
+ <!-- Next -->
+ <NcButton v-if="currentStep !== STEP.LAST"
+ :aria-label="t('files_sharing', 'Continue')"
+ :disabled="loading"
+ data-cy-file-request-dialog-controls="next"
+ @click="onPageNext">
+ <template #icon>
+ <NcLoadingIcon v-if="loading" />
+ <IconNext v-else :size="20" />
+ </template>
+ {{ t('files_sharing', 'Continue') }}
+ </NcButton>
+
+ <!-- Finish -->
+ <NcButton v-else
+ :aria-label="finishButtonLabel"
+ :disabled="loading"
+ data-cy-file-request-dialog-controls="finish"
+ type="primary"
+ @click="onFinish">
+ <template #icon>
+ <NcLoadingIcon v-if="loading" />
+ <IconCheck v-else :size="20" />
+ </template>
+ {{ finishButtonLabel }}
+ </NcButton>
+ </template>
+ </NcDialog>
+</template>
+
+<script lang="ts">
+import type { AxiosError } from '@nextcloud/axios'
+import type { Folder, Node } from '@nextcloud/files'
+import type { OCSResponse } from '@nextcloud/typings/ocs'
+import type { PropType } from 'vue'
+
+import { defineComponent } from 'vue'
+import { emit } from '@nextcloud/event-bus'
+import { generateOcsUrl } from '@nextcloud/router'
+import { Permission } from '@nextcloud/files'
+import { ShareType } from '@nextcloud/sharing'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { n, t } from '@nextcloud/l10n'
+import axios from '@nextcloud/axios'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+
+import IconCheck from 'vue-material-design-icons/Check.vue'
+import IconNext from 'vue-material-design-icons/ArrowRight.vue'
+
+import Config from '../services/ConfigService'
+import FileRequestDatePassword from './NewFileRequestDialog/NewFileRequestDialogDatePassword.vue'
+import FileRequestFinish from './NewFileRequestDialog/NewFileRequestDialogFinish.vue'
+import FileRequestIntro from './NewFileRequestDialog/NewFileRequestDialogIntro.vue'
+import logger from '../services/logger'
+import Share from '../models/Share.ts'
+
+enum STEP {
+ FIRST = 0,
+ SECOND = 1,
+ LAST = 2,
+}
+
+const sharingConfig = new Config()
+
+export default defineComponent({
+ name: 'NewFileRequestDialog',
+
+ components: {
+ FileRequestDatePassword,
+ FileRequestFinish,
+ FileRequestIntro,
+ IconCheck,
+ IconNext,
+ NcButton,
+ NcDialog,
+ NcLoadingIcon,
+ NcNoteCard,
+ },
+
+ props: {
+ context: {
+ type: Object as PropType<Folder>,
+ required: true,
+ },
+ content: {
+ type: Array as PropType<Node[]>,
+ required: true,
+ },
+ },
+
+ setup() {
+ return {
+ STEP,
+ n,
+ t,
+
+ isShareByMailEnabled: sharingConfig.isMailShareAllowed,
+ }
+ },
+
+ data() {
+ return {
+ currentStep: STEP.FIRST,
+ loading: false,
+
+ destination: this.context.path || '/',
+ label: '',
+ note: '',
+
+ expirationDate: null as Date | null,
+ password: null as string | null,
+
+ share: null as Share | null,
+ emails: [] as string[],
+ }
+ },
+
+ computed: {
+ finishButtonLabel() {
+ if (this.emails.length === 0) {
+ return t('files_sharing', 'Close')
+ }
+ return n('files_sharing', 'Send email and close', 'Send {count} emails and close', this.emails.length, { count: this.emails.length })
+ },
+ },
+
+ methods: {
+ onPageNext() {
+ const form = this.$refs.form as HTMLFormElement
+
+ // Reset custom validity
+ form.querySelectorAll('input').forEach(input => input.setCustomValidity(''))
+
+ // custom destination validation
+ // cannot share root
+ if (this.destination === '/' || this.destination === '') {
+ const destinationInput = form.querySelector('input[name="destination"]') as HTMLInputElement
+ destinationInput?.setCustomValidity(t('files_sharing', 'Please select a folder, you cannot share the root directory.'))
+ form.reportValidity()
+ return
+ }
+
+ // If the form is not valid, show the error message
+ if (!form.checkValidity()) {
+ form.reportValidity()
+ return
+ }
+
+ if (this.currentStep === STEP.FIRST) {
+ this.currentStep = STEP.SECOND
+ return
+ }
+
+ this.createShare()
+ },
+
+ onRemoveEmail(email: string) {
+ const index = this.emails.indexOf(email)
+ this.emails.splice(index, 1)
+ },
+
+ onCancel() {
+ this.$emit('close')
+ },
+
+ async onFinish() {
+ if (this.emails.length === 0 || this.isShareByMailEnabled === false) {
+ showSuccess(t('files_sharing', 'File request created'))
+ this.$emit('close')
+ return
+ }
+
+ if (sharingConfig.isMailShareAllowed && this.emails.length > 0) {
+ await this.setShareEmails()
+ await this.sendEmails()
+ showSuccess(n('files_sharing', 'File request created and email sent', 'File request created and {count} emails sent', this.emails.length, { count: this.emails.length }))
+ } else {
+ showSuccess(t('files_sharing', 'File request created'))
+ }
+
+ this.$emit('close')
+ },
+
+ async createShare() {
+ this.loading = true
+
+ let expireDate = ''
+ if (this.expirationDate) {
+ const year = this.expirationDate.getFullYear()
+ const month = (this.expirationDate.getMonth() + 1).toString().padStart(2, '0')
+ const day = this.expirationDate.getDate().toString().padStart(2, '0')
+
+ // Format must be YYYY-MM-DD
+ expireDate = `${year}-${month}-${day}`
+ }
+ const shareUrl = generateOcsUrl('apps/files_sharing/api/v1/shares')
+ try {
+ const request = await axios.post<OCSResponse>(shareUrl, {
+ // Always create a file request, but without mail share
+ // permissions, only a share link will be created.
+ shareType: sharingConfig.isMailShareAllowed ? ShareType.Email : ShareType.Link,
+ permissions: Permission.CREATE,
+
+ label: this.label,
+ path: this.destination,
+ note: this.note,
+
+ password: this.password || '',
+ expireDate: expireDate || '',
+
+ // Empty string
+ shareWith: '',
+ attributes: JSON.stringify([{
+ value: true,
+ key: 'enabled',
+ scope: 'fileRequest',
+ }]),
+ })
+
+ // If not an ocs request
+ if (!request?.data?.ocs) {
+ throw request
+ }
+
+ const share = new Share(request.data.ocs.data)
+ this.share = share
+
+ logger.info('New file request created', { share })
+ emit('files_sharing:share:created', { share })
+
+ // Move to the last page
+ this.currentStep = STEP.LAST
+ } catch (error) {
+ const errorMessage = (error as AxiosError<OCSResponse>)?.response?.data?.ocs?.meta?.message
+ showError(
+ errorMessage
+ ? t('files_sharing', 'Error creating the share: {errorMessage}', { errorMessage })
+ : t('files_sharing', 'Error creating the share'),
+ )
+ logger.error('Error while creating share', { error, errorMessage })
+ throw error
+ } finally {
+ this.loading = false
+ }
+ },
+
+ async setShareEmails() {
+ this.loading = true
+
+ // This should never happen™
+ if (!this.share || !this.share?.id) {
+ throw new Error('Share ID is missing')
+ }
+
+ const shareUrl = generateOcsUrl('apps/files_sharing/api/v1/shares/{id}', { id: this.share.id })
+ try {
+ // Convert link share to email share
+ const request = await axios.put<OCSResponse>(shareUrl, {
+ attributes: JSON.stringify([{
+ value: this.emails,
+ key: 'emails',
+ scope: 'shareWith',
+ },
+ {
+ value: true,
+ key: 'enabled',
+ scope: 'fileRequest',
+ }]),
+ })
+
+ // If not an ocs request
+ if (!request?.data?.ocs) {
+ throw request
+ }
+ } catch (error) {
+ this.onEmailSendError(error)
+ throw error
+ } finally {
+ this.loading = false
+ }
+ },
+
+ async sendEmails() {
+ this.loading = true
+
+ // This should never happen™
+ if (!this.share || !this.share?.id) {
+ throw new Error('Share ID is missing')
+ }
+
+ const shareUrl = generateOcsUrl('apps/files_sharing/api/v1/shares/{id}/send-email', { id: this.share.id })
+ try {
+ // Convert link share to email share
+ const request = await axios.post<OCSResponse>(shareUrl, {
+ password: this.password || undefined,
+ })
+
+ // If not an ocs request
+ if (!request?.data?.ocs) {
+ throw request
+ }
+ } catch (error) {
+ this.onEmailSendError(error)
+ throw error
+ } finally {
+ this.loading = false
+ }
+ },
+
+ onEmailSendError(error: AxiosError<OCSResponse>) {
+ const errorMessage = error.response?.data?.ocs?.meta?.message
+ showError(
+ errorMessage
+ ? t('files_sharing', 'Error sending emails: {errorMessage}', { errorMessage })
+ : t('files_sharing', 'Error sending emails'),
+ )
+ logger.error('Error while sending emails', { error, errorMessage })
+ },
+ },
+})
+</script>
+
+<style lang="scss">
+.file-request-dialog {
+ --margin: 18px;
+
+ &__header {
+ margin: 0 var(--margin);
+ }
+
+ &__form {
+ position: relative;
+ overflow: auto;
+ padding: var(--margin) var(--margin);
+ // overlap header bottom padding
+ margin-top: calc(-1 * var(--margin));
+ }
+
+ fieldset {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ margin-top: var(--margin);
+
+ legend {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ }
+ }
+
+ // Using a NcNoteCard was a bit much sometimes.
+ // Using a simple paragraph instead does it.
+ &__info {
+ color: var(--color-text-maxcontrast);
+ padding-block: 4px;
+ display: flex;
+ align-items: center;
+ .file-request-dialog__info-icon {
+ margin-inline-end: 8px;
+ }
+ }
+
+ .dialog__actions {
+ width: auto;
+ margin-inline: 12px;
+ span.dialog__actions-separator {
+ margin-inline-start: auto;
+ }
+ }
+
+ .input-field__helper-text-message {
+ // reduce helper text standing out
+ color: var(--color-text-maxcontrast);
+ }
+}
+</style>