aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src/components/NewFileRequestDialog
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src/components/NewFileRequestDialog')
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue258
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue236
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue166
3 files changed, 660 insertions, 0 deletions
diff --git a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue
new file mode 100644
index 00000000000..7e6d56e8794
--- /dev/null
+++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue
@@ -0,0 +1,258 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <div>
+ <!-- Password and expiration summary -->
+ <NcNoteCard v-if="passwordAndExpirationSummary" type="success">
+ {{ passwordAndExpirationSummary }}
+ </NcNoteCard>
+
+ <!-- Expiration date -->
+ <fieldset class="file-request-dialog__expiration" data-cy-file-request-dialog-fieldset="expiration">
+ <!-- Enable expiration -->
+ <legend>{{ t('files_sharing', 'When should the request expire?') }}</legend>
+ <NcCheckboxRadioSwitch v-show="!isExpirationDateEnforced"
+ :checked="isExpirationDateEnforced || expirationDate !== null"
+ :disabled="disabled || isExpirationDateEnforced"
+ @update:checked="onToggleDeadline">
+ {{ t('files_sharing', 'Set a submission expiration date') }}
+ </NcCheckboxRadioSwitch>
+
+ <!-- Date picker -->
+ <NcDateTimePickerNative v-if="expirationDate !== null"
+ id="file-request-dialog-expirationDate"
+ :disabled="disabled"
+ :hide-label="true"
+ :label="t('files_sharing', 'Expiration date')"
+ :max="maxDate"
+ :min="minDate"
+ :placeholder="t('files_sharing', 'Select a date')"
+ :required="defaultExpireDateEnforced"
+ :value="expirationDate"
+ name="expirationDate"
+ type="date"
+ @input="$emit('update:expirationDate', $event)" />
+
+ <p v-if="defaultExpireDateEnforced" class="file-request-dialog__info">
+ <IconInfo :size="18" class="file-request-dialog__info-icon" />
+ {{ t('files_sharing', 'Your administrator has enforced a {count} days expiration policy.', { count: defaultExpireDate }) }}
+ </p>
+ </fieldset>
+
+ <!-- Password -->
+ <fieldset class="file-request-dialog__password" data-cy-file-request-dialog-fieldset="password">
+ <!-- Enable password -->
+ <legend>{{ t('files_sharing', 'What password should be used for the request?') }}</legend>
+ <NcCheckboxRadioSwitch v-show="!isPasswordEnforced"
+ :checked="isPasswordEnforced || password !== null"
+ :disabled="disabled || isPasswordEnforced"
+ @update:checked="onTogglePassword">
+ {{ t('files_sharing', 'Set a password') }}
+ </NcCheckboxRadioSwitch>
+
+ <div v-if="password !== null" class="file-request-dialog__password-field">
+ <NcPasswordField ref="passwordField"
+ :check-password-strength="true"
+ :disabled="disabled"
+ :label="t('files_sharing', 'Password')"
+ :placeholder="t('files_sharing', 'Enter a valid password')"
+ :required="enforcePasswordForPublicLink"
+ :value="password"
+ name="password"
+ @update:value="$emit('update:password', $event)" />
+ <NcButton :aria-label="t('files_sharing', 'Generate a new password')"
+ :title="t('files_sharing', 'Generate a new password')"
+ type="tertiary-no-background"
+ @click="onGeneratePassword">
+ <template #icon>
+ <IconPasswordGen :size="20" />
+ </template>
+ </NcButton>
+ </div>
+
+ <p v-if="enforcePasswordForPublicLink" class="file-request-dialog__info">
+ <IconInfo :size="18" class="file-request-dialog__info-icon" />
+ {{ t('files_sharing', 'Your administrator has enforced a password protection.') }}
+ </p>
+ </fieldset>
+ </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, type PropType } from 'vue'
+import { t } from '@nextcloud/l10n'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
+
+import IconInfo from 'vue-material-design-icons/Information.vue'
+import IconPasswordGen from 'vue-material-design-icons/AutoFix.vue'
+
+import Config from '../../services/ConfigService'
+import GeneratePassword from '../../utils/GeneratePassword'
+
+const sharingConfig = new Config()
+
+export default defineComponent({
+ name: 'NewFileRequestDialogDatePassword',
+
+ components: {
+ IconInfo,
+ IconPasswordGen,
+ NcButton,
+ NcCheckboxRadioSwitch,
+ NcDateTimePickerNative,
+ NcNoteCard,
+ NcPasswordField,
+ },
+
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ expirationDate: {
+ type: Date as PropType<Date | null>,
+ required: false,
+ default: null,
+ },
+ password: {
+ type: String as PropType<string | null>,
+ required: false,
+ default: null,
+ },
+ },
+
+ emits: [
+ 'update:expirationDate',
+ 'update:password',
+ ],
+
+ setup() {
+ return {
+ t,
+
+ // Default expiration date if defaultExpireDateEnabled is true
+ defaultExpireDate: sharingConfig.defaultExpireDate,
+ // Default expiration date is enabled for public links (can be disabled)
+ defaultExpireDateEnabled: sharingConfig.isDefaultExpireDateEnabled,
+ // Default expiration date is enforced for public links (can't be disabled)
+ defaultExpireDateEnforced: sharingConfig.isDefaultExpireDateEnforced,
+
+ // Default password protection is enabled for public links (can be disabled)
+ enableLinkPasswordByDefault: sharingConfig.enableLinkPasswordByDefault,
+ // Password protection is enforced for public links (can't be disabled)
+ enforcePasswordForPublicLink: sharingConfig.enforcePasswordForPublicLink,
+ }
+ },
+
+ data() {
+ return {
+ maxDate: null as Date | null,
+ minDate: new Date(new Date().setDate(new Date().getDate() + 1)),
+ }
+ },
+
+ computed: {
+ passwordAndExpirationSummary(): string {
+ if (this.expirationDate && this.password) {
+ return t('files_sharing', 'The request will expire on {date} at midnight and will be password protected.', {
+ date: this.expirationDate.toLocaleDateString(),
+ })
+ }
+
+ if (this.expirationDate) {
+ return t('files_sharing', 'The request will expire on {date} at midnight.', {
+ date: this.expirationDate.toLocaleDateString(),
+ })
+ }
+
+ if (this.password) {
+ return t('files_sharing', 'The request will be password protected.')
+ }
+
+ return ''
+ },
+
+ isExpirationDateEnforced(): boolean {
+ // Both fields needs to be enabled in the settings
+ return this.defaultExpireDateEnabled
+ && this.defaultExpireDateEnforced
+ },
+
+ isPasswordEnforced(): boolean {
+ // Both fields needs to be enabled in the settings
+ return this.enableLinkPasswordByDefault
+ && this.enforcePasswordForPublicLink
+ },
+ },
+
+ mounted() {
+ // If defined, we set the default expiration date
+ if (this.defaultExpireDate) {
+ this.$emit('update:expirationDate', sharingConfig.defaultExpirationDate)
+ }
+
+ // If enforced, we cannot set a date before the default expiration days (see admin settings)
+ if (this.isExpirationDateEnforced) {
+ this.maxDate = sharingConfig.defaultExpirationDate
+ }
+
+ // If enabled by default, we generate a valid password
+ if (this.isPasswordEnforced) {
+ this.generatePassword()
+ }
+ },
+
+ methods: {
+ onToggleDeadline(checked: boolean) {
+ this.$emit('update:expirationDate', checked ? (this.maxDate || this.minDate) : null)
+ },
+
+ async onTogglePassword(checked: boolean) {
+ if (checked) {
+ this.generatePassword()
+ return
+ }
+ this.$emit('update:password', null)
+ },
+
+ async onGeneratePassword() {
+ await this.generatePassword()
+ this.showPassword()
+ },
+
+ async generatePassword() {
+ await GeneratePassword().then(password => {
+ this.$emit('update:password', password)
+ })
+ },
+
+ showPassword() {
+ // @ts-expect-error isPasswordHidden is private
+ this.$refs.passwordField.isPasswordHidden = false
+ },
+ },
+})
+</script>
+
+<style scoped lang="scss">
+.file-request-dialog__password-field {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ // Compensate label gab with legend
+ margin-top: 12px;
+ > div {
+ // Force margin to 0 as we handle it above
+ margin: 0;
+ }
+}
+</style>
diff --git a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue
new file mode 100644
index 00000000000..7826aab581e
--- /dev/null
+++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue
@@ -0,0 +1,236 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <div>
+ <!-- Request note -->
+ <NcNoteCard type="success">
+ {{ t('files_sharing', 'You can now share the link below to allow people to upload files to your directory.') }}
+ </NcNoteCard>
+
+ <!-- Copy share link -->
+ <NcInputField ref="clipboard"
+ :value="shareLink"
+ :label="t('files_sharing', 'Share link')"
+ :readonly="true"
+ :show-trailing-button="true"
+ :trailing-button-label="t('files_sharing', 'Copy')"
+ data-cy-file-request-dialog-fieldset="link"
+ @click="copyShareLink"
+ @trailing-button-click="copyShareLink">
+ <template #trailing-button-icon>
+ <IconCheck v-if="isCopied" :size="20" />
+ <IconClipboard v-else :size="20" />
+ </template>
+ </NcInputField>
+
+ <template v-if="isShareByMailEnabled">
+ <!-- Email share-->
+ <NcTextField :value.sync="email"
+ :label="t('files_sharing', 'Send link via email')"
+ :placeholder="t('files_sharing', 'Enter an email address or paste a list')"
+ data-cy-file-request-dialog-fieldset="email"
+ type="email"
+ @keypress.enter.stop="addNewEmail"
+ @paste.stop.prevent="onPasteEmails"
+ @focusout.native="addNewEmail" />
+
+ <!-- Email list -->
+ <div v-if="emails.length > 0" class="file-request-dialog__emails">
+ <NcChip v-for="mail in emails"
+ :key="mail"
+ :aria-label-close="t('files_sharing', 'Remove email')"
+ :text="mail"
+ @close="$emit('remove-email', mail)">
+ <template #icon>
+ <NcAvatar :disable-menu="true"
+ :disable-tooltip="true"
+ :display-name="mail"
+ :is-no-user="true"
+ :show-user-status="false"
+ :size="24" />
+ </template>
+ </NcChip>
+ </div>
+ </template>
+ </div>
+</template>
+
+<script lang="ts">
+import type { PropType } from 'vue'
+import Share from '../../models/Share.ts'
+
+import { defineComponent } from 'vue'
+import { generateUrl, getBaseUrl } from '@nextcloud/router'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { n, t } from '@nextcloud/l10n'
+
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcInputField from '@nextcloud/vue/components/NcInputField'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+import NcChip from '@nextcloud/vue/components/NcChip'
+
+import IconCheck from 'vue-material-design-icons/Check.vue'
+import IconClipboard from 'vue-material-design-icons/ClipboardText.vue'
+
+export default defineComponent({
+ name: 'NewFileRequestDialogFinish',
+
+ components: {
+ IconCheck,
+ IconClipboard,
+ NcAvatar,
+ NcInputField,
+ NcNoteCard,
+ NcTextField,
+ NcChip,
+ },
+
+ props: {
+ share: {
+ type: Object as PropType<Share>,
+ required: true,
+ },
+ emails: {
+ type: Array as PropType<string[]>,
+ required: true,
+ },
+ isShareByMailEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ },
+
+ emits: ['add-email', 'remove-email'],
+
+ setup() {
+ return {
+ n, t,
+ }
+ },
+
+ data() {
+ return {
+ isCopied: false,
+ email: '',
+ }
+ },
+
+ computed: {
+ shareLink() {
+ return generateUrl('/s/{token}', { token: this.share.token }, { baseURL: getBaseUrl() })
+ },
+ },
+
+ methods: {
+ async copyShareLink(event: MouseEvent) {
+ if (this.isCopied) {
+ this.isCopied = false
+ return
+ }
+
+ if (!navigator.clipboard) {
+ // Clipboard API not available
+ window.prompt(t('files_sharing', 'Automatically copying failed, please copy the share link manually'), this.shareLink)
+ return
+ }
+
+ await navigator.clipboard.writeText(this.shareLink)
+
+ showSuccess(t('files_sharing', 'Link copied'))
+ this.isCopied = true
+ event.target?.select?.()
+
+ setTimeout(() => {
+ this.isCopied = false
+ }, 3000)
+ },
+
+ addNewEmail(e: KeyboardEvent) {
+ if (this.email.trim() === '') {
+ return
+ }
+
+ if (e.target instanceof HTMLInputElement) {
+ // Reset the custom validity
+ e.target.setCustomValidity('')
+
+ // Check if the field is valid
+ if (e.target.checkValidity() === false) {
+ e.target.reportValidity()
+ return
+ }
+
+ // The email is already in the list
+ if (this.emails.includes(this.email.trim())) {
+ e.target.setCustomValidity(t('files_sharing', 'Email already added'))
+ e.target.reportValidity()
+ return
+ }
+
+ // Check if the email is valid
+ if (!this.isValidEmail(this.email.trim())) {
+ e.target.setCustomValidity(t('files_sharing', 'Invalid email address'))
+ e.target.reportValidity()
+ return
+ }
+
+ this.$emit('add-email', this.email.trim())
+ this.email = ''
+ }
+ },
+
+ // Handle dumping a list of emails
+ onPasteEmails(e: ClipboardEvent) {
+ const clipboardData = e.clipboardData
+ if (!clipboardData) {
+ return
+ }
+
+ const pastedText = clipboardData.getData('text')
+ const emails = pastedText.split(/[\s,;]+/).filter(Boolean).map((email) => email.trim())
+
+ const duplicateEmails = emails.filter((email) => this.emails.includes(email))
+ const validEmails = emails.filter((email) => this.isValidEmail(email) && !duplicateEmails.includes(email))
+ const invalidEmails = emails.filter((email) => !this.isValidEmail(email))
+ validEmails.forEach((email) => this.$emit('add-email', email))
+
+ // Warn about invalid emails
+ if (invalidEmails.length > 0) {
+ showError(n('files_sharing', 'The following email address is not valid: {emails}', 'The following email addresses are not valid: {emails}', invalidEmails.length, { emails: invalidEmails.join(', ') }))
+ }
+
+ // Warn about duplicate emails
+ if (duplicateEmails.length > 0) {
+ showError(n('files_sharing', '{count} email address already added', '{count} email addresses already added', duplicateEmails.length, { count: duplicateEmails.length }))
+ }
+
+ if (validEmails.length > 0) {
+ showSuccess(n('files_sharing', '{count} email address added', '{count} email addresses added', validEmails.length, { count: validEmails.length }))
+ }
+
+ this.email = ''
+ },
+
+ // No need to have a fancy regex, just check for an @
+ isValidEmail(email: string): boolean {
+ return email.includes('@')
+ },
+ },
+})
+</script>
+<style scoped>
+.input-field,
+.file-request-dialog__emails {
+ margin-top: var(--margin);
+}
+
+.file-request-dialog__emails {
+ display: flex;
+ gap: var(--default-grid-baseline);
+ flex-wrap: wrap;
+}
+</style>
diff --git a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue
new file mode 100644
index 00000000000..5ac60c37e29
--- /dev/null
+++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue
@@ -0,0 +1,166 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <div>
+ <!-- Request label -->
+ <fieldset class="file-request-dialog__label" data-cy-file-request-dialog-fieldset="label">
+ <legend>
+ {{ t('files_sharing', 'What are you requesting?') }}
+ </legend>
+ <NcTextField :value="label"
+ :disabled="disabled"
+ :label="t('files_sharing', 'Request subject')"
+ :placeholder="t('files_sharing', 'Birthday party photos, History assignment…')"
+ :required="false"
+ name="label"
+ @update:value="$emit('update:label', $event)" />
+ </fieldset>
+
+ <!-- Request destination -->
+ <fieldset class="file-request-dialog__destination" data-cy-file-request-dialog-fieldset="destination">
+ <legend>
+ {{ t('files_sharing', 'Where should these files go?') }}
+ </legend>
+ <NcTextField :value="destination"
+ :disabled="disabled"
+ :label="t('files_sharing', 'Upload destination')"
+ :minlength="2/* cannot share root */"
+ :placeholder="t('files_sharing', 'Select a destination')"
+ :readonly="false /* cannot validate a readonly input */"
+ :required="true /* cannot be empty */"
+ :show-trailing-button="destination !== context.path"
+ :trailing-button-icon="'undo'"
+ :trailing-button-label="t('files_sharing', 'Revert to default')"
+ name="destination"
+ @click="onPickDestination"
+ @keypress.prevent.stop="/* prevent typing in the input, we use the picker */"
+ @paste.prevent.stop="/* prevent pasting in the input, we use the picker */"
+ @trailing-button-click="$emit('update:destination', '')">
+ <IconFolder :size="18" />
+ </NcTextField>
+
+ <p class="file-request-dialog__info">
+ <IconLock :size="18" class="file-request-dialog__info-icon" />
+ {{ t('files_sharing', 'The uploaded files are visible only to you unless you choose to share them.') }}
+ </p>
+ </fieldset>
+
+ <!-- Request note -->
+ <fieldset class="file-request-dialog__note" data-cy-file-request-dialog-fieldset="note">
+ <legend>
+ {{ t('files_sharing', 'Add a note') }}
+ </legend>
+ <NcTextArea :value="note"
+ :disabled="disabled"
+ :label="t('files_sharing', 'Note for recipient')"
+ :placeholder="t('files_sharing', 'Add a note to help people understand what you are requesting.')"
+ :required="false"
+ name="note"
+ @update:value="$emit('update:note', $event)" />
+
+ <p class="file-request-dialog__info">
+ <IconInfo :size="18" class="file-request-dialog__info-icon" />
+ {{ t('files_sharing', 'You can add links, date or any other information that will help the recipient understand what you are requesting.') }}
+ </p>
+ </fieldset>
+ </div>
+</template>
+
+<script lang="ts">
+import type { PropType } from 'vue'
+import type { Folder, Node } from '@nextcloud/files'
+
+import { defineComponent } from 'vue'
+import { getFilePickerBuilder } from '@nextcloud/dialogs'
+import { t } from '@nextcloud/l10n'
+
+import IconFolder from 'vue-material-design-icons/Folder.vue'
+import IconInfo from 'vue-material-design-icons/InformationOutline.vue'
+import IconLock from 'vue-material-design-icons/Lock.vue'
+import NcTextArea from '@nextcloud/vue/components/NcTextArea'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+
+export default defineComponent({
+ name: 'NewFileRequestDialogIntro',
+
+ components: {
+ IconFolder,
+ IconInfo,
+ IconLock,
+ NcTextArea,
+ NcTextField,
+ },
+
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ context: {
+ type: Object as PropType<Folder>,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ destination: {
+ type: String,
+ required: true,
+ },
+ note: {
+ type: String,
+ required: true,
+ },
+ },
+
+ emits: [
+ 'update:destination',
+ 'update:label',
+ 'update:note',
+ ],
+
+ setup() {
+ return {
+ t,
+ }
+ },
+
+ methods: {
+ onPickDestination() {
+ const filepicker = getFilePickerBuilder(t('files_sharing', 'Select a destination'))
+ .addMimeTypeFilter('httpd/unix-directory')
+ .allowDirectories(true)
+ .addButton({
+ label: t('files_sharing', 'Select'),
+ callback: this.onPickedDestination,
+ })
+ .setFilter(node => node.path !== '/')
+ .startAt(this.destination)
+ .build()
+ try {
+ filepicker.pick()
+ } catch (e) {
+ // ignore cancel
+ }
+ },
+
+ onPickedDestination(nodes: Node[]) {
+ const node = nodes[0]
+ if (node) {
+ this.$emit('update:destination', node.path)
+ }
+ },
+ },
+})
+</script>
+<style scoped>
+.file-request-dialog__note :deep(textarea) {
+ width: 100% !important;
+ min-height: 80px;
+}
+</style>