mirror of
https://github.com/nextcloud/server.git
synced 2026-06-26 00:55:54 -04:00
470 lines
12 KiB
Vue
470 lines
12 KiB
Vue
<!--
|
|
- SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
|
- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
-->
|
|
|
|
<template>
|
|
<NcDialog
|
|
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"
|
|
variant="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"
|
|
variant="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"
|
|
variant="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"
|
|
variant="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 axios from '@nextcloud/axios'
|
|
import { showError, showSuccess } from '@nextcloud/dialogs'
|
|
import { emit } from '@nextcloud/event-bus'
|
|
import { Permission } from '@nextcloud/files'
|
|
import { n, t } from '@nextcloud/l10n'
|
|
import { generateOcsUrl } from '@nextcloud/router'
|
|
import { ShareType } from '@nextcloud/sharing'
|
|
import { defineComponent } from 'vue'
|
|
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 IconNext from 'vue-material-design-icons/ArrowRight.vue'
|
|
import IconCheck from 'vue-material-design-icons/Check.vue'
|
|
import FileRequestDatePassword from './NewFileRequestDialog/NewFileRequestDialogDatePassword.vue'
|
|
import FileRequestFinish from './NewFileRequestDialog/NewFileRequestDialogFinish.vue'
|
|
import FileRequestIntro from './NewFileRequestDialog/NewFileRequestDialogIntro.vue'
|
|
import Share from '../models/Share.ts'
|
|
import Config from '../services/ConfigService.ts'
|
|
import logger from '../services/logger.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>
|