diff options
Diffstat (limited to 'apps/files_sharing/src')
-rw-r--r-- | apps/files_sharing/src/components/ShareExpiryTime.vue | 2 | ||||
-rw-r--r-- | apps/files_sharing/src/components/SharingEntryLink.vue | 38 | ||||
-rw-r--r-- | apps/files_sharing/src/components/SharingInput.vue | 11 | ||||
-rw-r--r-- | apps/files_sharing/src/files_filters/AccountFilter.ts | 6 | ||||
-rw-r--r-- | apps/files_sharing/src/files_views/publicFileDrop.ts | 7 | ||||
-rw-r--r-- | apps/files_sharing/src/public-file-request.ts | 57 | ||||
-rw-r--r-- | apps/files_sharing/src/public-nickname-handler.ts | 86 | ||||
-rw-r--r-- | apps/files_sharing/src/services/ConfigService.ts | 9 | ||||
-rw-r--r-- | apps/files_sharing/src/services/GuestNameValidity.ts | 45 | ||||
-rw-r--r-- | apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue | 93 | ||||
-rw-r--r-- | apps/files_sharing/src/views/PublicAuthPrompt.vue | 123 | ||||
-rw-r--r-- | apps/files_sharing/src/views/SharingDetailsTab.vue | 29 | ||||
-rw-r--r-- | apps/files_sharing/src/views/SharingTab.vue | 53 |
13 files changed, 320 insertions, 239 deletions
diff --git a/apps/files_sharing/src/components/ShareExpiryTime.vue b/apps/files_sharing/src/components/ShareExpiryTime.vue index b789bc92db5..939142616e9 100644 --- a/apps/files_sharing/src/components/ShareExpiryTime.vue +++ b/apps/files_sharing/src/components/ShareExpiryTime.vue @@ -9,7 +9,7 @@ <NcButton v-if="expiryTime" class="hint-icon" type="tertiary" - :aria-label="t('files_sharing', 'Share expiration: ') + new Date(expiryTime).toLocaleString()"> + :aria-label="t('files_sharing', 'Share expiration: {date}', { date: new Date(expiryTime).toLocaleString() })"> <template #icon> <ClockIcon :size="20" /> </template> diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue index 781626a1ec9..702b876306f 100644 --- a/apps/files_sharing/src/components/SharingEntryLink.vue +++ b/apps/files_sharing/src/components/SharingEntryLink.vue @@ -223,13 +223,14 @@ </template> <script> +import { showError, showSuccess } from '@nextcloud/dialogs' import { emit } from '@nextcloud/event-bus' +import { t } from '@nextcloud/l10n' +import moment from '@nextcloud/moment' import { generateUrl, getBaseUrl } from '@nextcloud/router' -import { showError, showSuccess } from '@nextcloud/dialogs' import { ShareType } from '@nextcloud/sharing' -import VueQrcode from '@chenfengyuan/vue-qrcode' -import moment from '@nextcloud/moment' +import VueQrcode from '@chenfengyuan/vue-qrcode' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox' import NcActionInput from '@nextcloud/vue/components/NcActionInput' @@ -258,7 +259,7 @@ import GeneratePassword from '../utils/GeneratePassword.ts' 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 '../services/logger.ts' export default { name: 'SharingEntryLink', @@ -313,10 +314,6 @@ export default { ExternalLegacyLinkActions: OCA.Sharing.ExternalLinkActions.state, ExternalShareActions: OCA.Sharing.ExternalShareActions.state, - logger: getLoggerBuilder() - .setApp('files_sharing') - .detectUser() - .build(), // tracks whether modal should be opened or not showQRCode: false, @@ -330,6 +327,8 @@ export default { * @return {string} */ title() { + const l10nOptions = { escape: false /* no escape as this string is already escaped by Vue */ } + // if we have a valid existing share (not pending) if (this.share && this.share.id) { if (!this.isShareOwner && this.share.ownerDisplayName) { @@ -337,26 +336,26 @@ export default { return t('files_sharing', '{shareWith} by {initiator}', { shareWith: this.share.shareWith, initiator: this.share.ownerDisplayName, - }) + }, l10nOptions) } return t('files_sharing', 'Shared via link by {initiator}', { initiator: this.share.ownerDisplayName, - }) + }, l10nOptions) } if (this.share.label && this.share.label.trim() !== '') { if (this.isEmailShareType) { if (this.isFileRequest) { return t('files_sharing', 'File request ({label})', { label: this.share.label.trim(), - }) + }, l10nOptions) } return t('files_sharing', 'Mail share ({label})', { label: this.share.label.trim(), - }) + }, l10nOptions) } return t('files_sharing', 'Share link ({label})', { label: this.share.label.trim(), - }) + }, l10nOptions) } if (this.isEmailShareType) { if (!this.share.shareWith || this.share.shareWith.trim() === '') { @@ -391,6 +390,7 @@ export default { } return null }, + passwordExpirationTime() { if (this.share.passwordExpirationTime === null) { return null @@ -613,7 +613,7 @@ export default { * @param {boolean} shareReviewComplete if the share was reviewed */ async onNewLinkShare(shareReviewComplete = false) { - this.logger.debug('onNewLinkShare called (with this.share)', this.share) + logger.debug('onNewLinkShare called (with this.share)', this.share) // do not run again if already loading if (this.loading) { return @@ -628,7 +628,7 @@ export default { shareDefaults.expiration = this.formatDateToString(this.config.defaultExpirationDate) } - this.logger.debug('Missing required properties?', this.enforcedPropertiesMissing) + logger.debug('Missing required properties?', this.enforcedPropertiesMissing) // Do not push yet if we need a password or an expiration date: show pending menu // A share would require a review for example is default expiration date is set but not enforced, this allows // the user to review the share and remove the expiration date if they don't want it @@ -636,7 +636,7 @@ export default { this.pending = true this.shareCreationComplete = false - this.logger.info('Share policy requires a review or has mandated properties (password, expirationDate)...') + logger.info('Share policy requires a review or has mandated properties (password, expirationDate)...') // ELSE, show the pending popovermenu // if password default or enforced, pre-fill with random one @@ -664,13 +664,13 @@ export default { // if the share is valid, create it on the server if (this.checkShare(this.share)) { try { - this.logger.info('Sending existing share to server', this.share) + logger.info('Sending existing share to server', this.share) await this.pushNewLinkShare(this.share, true) this.shareCreationComplete = true - this.logger.info('Share created on server', this.share) + logger.info('Share created on server', this.share) } catch (e) { this.pending = false - this.logger.error('Error creating share', e) + logger.error('Error creating share', e) return false } return true diff --git a/apps/files_sharing/src/components/SharingInput.vue b/apps/files_sharing/src/components/SharingInput.vue index 49a39915e5e..b886ba95a17 100644 --- a/apps/files_sharing/src/components/SharingInput.vue +++ b/apps/files_sharing/src/components/SharingInput.vue @@ -194,11 +194,11 @@ export default { let shareType = [] - if (this.isExternal) { - shareType.push(ShareType.Remote) - shareType.push(ShareType.RemoteGroup) + const remoteTypes = [ShareType.Remote, ShareType.RemoteGroup] + + if (this.isExternal && !this.config.showFederatedSharesAsInternal) { + shareType.push(...remoteTypes) } else { - // Merge shareType array shareType = shareType.concat([ ShareType.User, ShareType.Group, @@ -209,6 +209,9 @@ export default { ShareType.ScienceMesh, ]) + if (this.config.showFederatedSharesAsInternal) { + shareType.push(...remoteTypes) + } } if (getCapabilities().files_sharing.public.enabled === true && this.isExternal) { diff --git a/apps/files_sharing/src/files_filters/AccountFilter.ts b/apps/files_sharing/src/files_filters/AccountFilter.ts index 56bcef07200..4f185d9fd9c 100644 --- a/apps/files_sharing/src/files_filters/AccountFilter.ts +++ b/apps/files_sharing/src/files_filters/AccountFilter.ts @@ -10,6 +10,7 @@ import { ShareType } from '@nextcloud/sharing' import Vue from 'vue' import FileListFilterAccount from '../components/FileListFilterAccount.vue' +import { isPublicShare } from '@nextcloud/sharing/public' export interface IAccountData { uid: string @@ -152,5 +153,10 @@ class AccountFilter extends FileListFilter { * Register the file list filter by owner or sharees */ export function registerAccountFilter() { + if (isPublicShare()) { + // We do not show the filter on public pages - it makes no sense + return + } + registerFileListFilter(new AccountFilter()) } diff --git a/apps/files_sharing/src/files_views/publicFileDrop.ts b/apps/files_sharing/src/files_views/publicFileDrop.ts index 0d782d48fc7..65756e83c74 100644 --- a/apps/files_sharing/src/files_views/publicFileDrop.ts +++ b/apps/files_sharing/src/files_views/publicFileDrop.ts @@ -4,7 +4,8 @@ */ import type { VueConstructor } from 'vue' -import { Folder, Permission, View, davRemoteURL, davRootPath, getNavigation } from '@nextcloud/files' +import { Folder, Permission, View, getNavigation } from '@nextcloud/files' +import { defaultRemoteURL, defaultRootPath } from '@nextcloud/files/dav' import { loadState } from '@nextcloud/initial-state' import { translate as t } from '@nextcloud/l10n' import svgCloudUpload from '@mdi/svg/svg/cloud-upload.svg?raw' @@ -45,8 +46,8 @@ export default () => { // Fake a writeonly folder as root folder: new Folder({ id: 0, - source: `${davRemoteURL}${davRootPath}`, - root: davRootPath, + source: `${defaultRemoteURL}${defaultRootPath}`, + root: defaultRootPath, owner: null, permissions: Permission.CREATE, }), diff --git a/apps/files_sharing/src/public-file-request.ts b/apps/files_sharing/src/public-file-request.ts deleted file mode 100644 index 1d640c5ea5e..00000000000 --- a/apps/files_sharing/src/public-file-request.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { defineAsyncComponent } from 'vue' -import { getBuilder } from '@nextcloud/browser-storage' -import { getGuestNickname, setGuestNickname } from '@nextcloud/auth' -import { getUploader } from '@nextcloud/upload' -import { spawnDialog } from '@nextcloud/dialogs' - -import logger from './services/logger' - -const storage = getBuilder('files_sharing').build() - -/** - * Setup file-request nickname header for the uploader - * @param nickname The nickname - */ -function registerFileRequestHeader(nickname: string) { - const uploader = getUploader() - uploader.setCustomHeader('X-NC-Nickname', encodeURIComponent(nickname)) - logger.debug('Nickname header registered for uploader', { headers: uploader.customHeaders }) -} - -/** - * Callback when a nickname was chosen - * @param nickname The chosen nickname - */ -function onSetNickname(nickname: string): void { - // Set the nickname - setGuestNickname(nickname) - // Set the dialog as shown - storage.setItem('public-auth-prompt-shown', 'true') - // Register header for uploader - registerFileRequestHeader(nickname) -} - -window.addEventListener('DOMContentLoaded', () => { - const nickname = getGuestNickname() ?? '' - const dialogShown = storage.getItem('public-auth-prompt-shown') !== null - - // If we don't have a nickname or the public auth prompt hasn't been shown yet, show it - // We still show the prompt if the user has a nickname to double check - if (!nickname || !dialogShown) { - spawnDialog( - defineAsyncComponent(() => import('./views/PublicAuthPrompt.vue')), - { - nickname, - }, - onSetNickname as (...rest: unknown[]) => void, - ) - } else { - logger.debug('Public auth prompt already shown.', { nickname }) - registerFileRequestHeader(nickname) - } -}) diff --git a/apps/files_sharing/src/public-nickname-handler.ts b/apps/files_sharing/src/public-nickname-handler.ts new file mode 100644 index 00000000000..02bdc641aaf --- /dev/null +++ b/apps/files_sharing/src/public-nickname-handler.ts @@ -0,0 +1,86 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getBuilder } from '@nextcloud/browser-storage' +import { getGuestNickname, type NextcloudUser } from '@nextcloud/auth' +import { getUploader } from '@nextcloud/upload' +import { loadState } from '@nextcloud/initial-state' +import { showGuestUserPrompt } from '@nextcloud/dialogs' +import { t } from '@nextcloud/l10n' + +import logger from './services/logger' +import { subscribe } from '@nextcloud/event-bus' + +const storage = getBuilder('files_sharing').build() + +// Setup file-request nickname header for the uploader +const registerFileRequestHeader = (nickname: string) => { + const uploader = getUploader() + uploader.setCustomHeader('X-NC-Nickname', encodeURIComponent(nickname)) + logger.debug('Nickname header registered for uploader', { headers: uploader.customHeaders }) +} + +// Callback when a nickname was chosen +const onUserInfoChanged = (guest: NextcloudUser) => { + logger.debug('User info changed', { guest }) + registerFileRequestHeader(guest.displayName ?? '') +} + +// Monitor nickname changes +subscribe('user:info:changed', onUserInfoChanged) + +window.addEventListener('DOMContentLoaded', () => { + const nickname = getGuestNickname() ?? '' + const dialogShown = storage.getItem('public-auth-prompt-shown') !== null + + // Check if a nickname is mandatory + const isFileRequest = loadState('files_sharing', 'isFileRequest', false) + + const owner = loadState('files_sharing', 'owner', '') + const ownerDisplayName = loadState('files_sharing', 'ownerDisplayName', '') + const label = loadState('files_sharing', 'label', '') + const filename = loadState('files_sharing', 'filename', '') + + // If the owner provided a custom label, use it instead of the filename + const folder = label || filename + + const options = { + nickname, + notice: t('files_sharing', 'To upload files to {folder}, you need to provide your name first.', { folder }), + subtitle: undefined as string | undefined, + title: t('files_sharing', 'Upload files to {folder}', { folder }), + } + + // If the guest already has a nickname, we just make them double check + if (nickname) { + options.notice = t('files_sharing', 'Please confirm your name to upload files to {folder}', { folder }) + } + + // If the account owner set their name as public, + // we show it in the subtitle + if (owner) { + options.subtitle = t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName }) + } + + // If this is a file request, then we need a nickname + if (isFileRequest) { + // If we don't have a nickname or the public auth prompt hasn't been shown yet, show it + // We still show the prompt if the user has a nickname to double check + if (!nickname || !dialogShown) { + logger.debug('Showing public auth prompt.', { nickname }) + showGuestUserPrompt(options) + } + return + } + + if (!dialogShown && !nickname) { + logger.debug('Public auth prompt not shown yet but nickname is not mandatory.', { nickname }) + return + } + + // Else, we just register the nickname header if any. + logger.debug('Public auth prompt already shown.', { nickname }) + registerFileRequestHeader(nickname) +}) diff --git a/apps/files_sharing/src/services/ConfigService.ts b/apps/files_sharing/src/services/ConfigService.ts index 09fdca13598..2114e2d1bae 100644 --- a/apps/files_sharing/src/services/ConfigService.ts +++ b/apps/files_sharing/src/services/ConfigService.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { getCapabilities } from '@nextcloud/capabilities' +import { loadState } from '@nextcloud/initial-state' type PasswordPolicyCapabilities = { enforceNonCommonPassword: boolean @@ -306,4 +307,12 @@ export default class Config { return this._capabilities?.files_sharing?.public?.custom_tokens } + /** + * Show federated shares as internal shares + * @return {boolean} + */ + get showFederatedSharesAsInternal(): boolean { + return loadState('files_sharing', 'showFederatedSharesAsInternal', false) + } + } diff --git a/apps/files_sharing/src/services/GuestNameValidity.ts b/apps/files_sharing/src/services/GuestNameValidity.ts new file mode 100644 index 00000000000..0557c5253ca --- /dev/null +++ b/apps/files_sharing/src/services/GuestNameValidity.ts @@ -0,0 +1,45 @@ +/*! + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { InvalidFilenameError, InvalidFilenameErrorReason, validateFilename } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' + +/** + * Get the validity of a filename (empty if valid). + * This can be used for `setCustomValidity` on input elements + * @param name The filename + * @param escape Escape the matched string in the error (only set when used in HTML) + */ +export function getGuestNameValidity(name: string, escape = false): string { + if (name.trim() === '') { + return t('files', 'Names must not be empty.') + } + + if (name.startsWith('.')) { + return t('files', 'Names must not start with a dot.') + } + + try { + validateFilename(name) + return '' + } catch (error) { + if (!(error instanceof InvalidFilenameError)) { + throw error + } + + switch (error.reason) { + case InvalidFilenameErrorReason.Character: + return t('files', '"{char}" is not allowed inside a name.', { char: error.segment }, undefined, { escape }) + case InvalidFilenameErrorReason.ReservedName: + return t('files', '"{segment}" is a reserved name and not allowed.', { segment: error.segment }, undefined, { escape: false }) + case InvalidFilenameErrorReason.Extension: + if (error.segment.match(/\.[a-z]/i)) { + return t('files', '"{extension}" is not an allowed name.', { extension: error.segment }, undefined, { escape: false }) + } + return t('files', 'Names must not end with "{extension}".', { extension: error.segment }, undefined, { escape: false }) + default: + return t('files', 'Invalid name.') + } + } +} diff --git a/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue index 5571e5e9f5d..33fec9af028 100644 --- a/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue +++ b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue @@ -5,13 +5,29 @@ <template> <NcEmptyContent class="file-drop-empty-content" data-cy-files-sharing-file-drop - :name="t('files_sharing', 'File drop')"> + :name="name"> <template #icon> <NcIconSvgWrapper :svg="svgCloudUpload" /> </template> <template #description> - {{ t('files_sharing', 'Upload files to {foldername}.', { foldername }) }} - {{ disclaimer === '' ? '' : t('files_sharing', 'By uploading files, you agree to the terms of service.') }} + <p> + {{ shareNote || t('files_sharing', 'Upload files to {foldername}.', { foldername }) }} + </p> + <p v-if="disclaimer"> + {{ t('files_sharing', 'By uploading files, you agree to the terms of service.') }} + </p> + <NcNoteCard v-if="getSortedUploads().length" + class="file-drop-empty-content__note-card" + type="success"> + <h2 id="file-drop-empty-content__heading"> + {{ t('files_sharing', 'Successfully uploaded files') }} + </h2> + <ul aria-labelledby="file-drop-empty-content__heading" class="file-drop-empty-content__list"> + <li v-for="file in getSortedUploads()" :key="file"> + {{ file }} + </li> + </ul> + </NcNoteCard> </template> <template #action> <template v-if="disclaimer"> @@ -34,16 +50,24 @@ </NcEmptyContent> </template> +<script lang="ts"> +/* eslint-disable import/first */ + +// We need this on module level rather than on the instance as view will be refreshed by the files app after uploading +const uploads = new Set<string>() +</script> + <script setup lang="ts"> import { loadState } from '@nextcloud/initial-state' import { translate as t } from '@nextcloud/l10n' -import { getUploader, UploadPicker } from '@nextcloud/upload' +import { getUploader, UploadPicker, UploadStatus } from '@nextcloud/upload' import { ref } from 'vue' import NcButton from '@nextcloud/vue/components/NcButton' import NcDialog from '@nextcloud/vue/components/NcDialog' import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' import svgCloudUpload from '@mdi/svg/svg/cloud-upload.svg?raw' defineProps<{ @@ -51,17 +75,62 @@ defineProps<{ }>() const disclaimer = loadState<string>('files_sharing', 'disclaimer', '') +const shareLabel = loadState<string>('files_sharing', 'label', '') +const shareNote = loadState<string>('files_sharing', 'note', '') + +const name = shareLabel || t('files_sharing', 'File drop') + const showDialog = ref(false) const uploadDestination = getUploader().destination -</script> -<style scoped> -:deep(.terms-of-service-dialog) { - min-height: min(100px, 20vh); +getUploader() + .addNotifier((upload) => { + if (upload.status === UploadStatus.FINISHED && upload.file.name) { + // if a upload is finished and is not a meta upload (name is set) + // then we add the upload to the list of finished uploads to be shown to the user + uploads.add(upload.file.name) + } + }) + +/** + * Get the previous uploads as sorted list + */ +function getSortedUploads() { + return [...uploads].sort((a, b) => a.localeCompare(b)) } -/* TODO fix in library */ -.file-drop-empty-content :deep(.empty-content__action) { - display: flex; - gap: var(--default-grid-baseline); +</script> + +<style scoped lang="scss"> +.file-drop-empty-content { + margin: auto; + max-width: max(50vw, 300px); + + .file-drop-empty-content__note-card { + width: fit-content; + margin-inline: auto; + } + + #file-drop-empty-content__heading { + margin-block: 0 10px; + font-weight: bold; + font-size: 20px; + } + + .file-drop-empty-content__list { + list-style: inside; + max-height: min(350px, 33vh); + overflow-y: scroll; + padding-inline-end: calc(2 * var(--default-grid-baseline)); + } + + :deep(.terms-of-service-dialog) { + min-height: min(100px, 20vh); + } + + /* TODO fix in library */ + :deep(.empty-content__action) { + display: flex; + gap: var(--default-grid-baseline); + } } </style> diff --git a/apps/files_sharing/src/views/PublicAuthPrompt.vue b/apps/files_sharing/src/views/PublicAuthPrompt.vue deleted file mode 100644 index 39f5adc4650..00000000000 --- a/apps/files_sharing/src/views/PublicAuthPrompt.vue +++ /dev/null @@ -1,123 +0,0 @@ -<!-- - - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - - SPDX-License-Identifier: AGPL-3.0-or-later ---> - -<template> - <NcDialog :buttons="dialogButtons" - class="public-auth-prompt" - data-cy-public-auth-prompt-dialog - is-form - :can-close="false" - :name="dialogName" - @submit="$emit('close', name)"> - <p v-if="owner" class="public-auth-prompt__subtitle"> - {{ t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName }) }} - </p> - - <!-- Header --> - <NcNoteCard class="public-auth-prompt__header" - :text="t('files_sharing', 'To upload files, you need to provide your name first.')" - type="info" /> - - <!-- Form --> - <NcTextField ref="input" - class="public-auth-prompt__input" - data-cy-public-auth-prompt-dialog-name - :label="t('files_sharing', 'Name')" - :placeholder="t('files_sharing', 'Enter your name')" - minlength="2" - name="name" - required - :value.sync="name" /> - </NcDialog> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue' -import { t } from '@nextcloud/l10n' - -import NcDialog from '@nextcloud/vue/components/NcDialog' -import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' -import NcTextField from '@nextcloud/vue/components/NcTextField' -import { loadState } from '@nextcloud/initial-state' - -export default defineComponent({ - name: 'PublicAuthPrompt', - - components: { - NcDialog, - NcNoteCard, - NcTextField, - }, - - props: { - /** - * Preselected nickname - * @default '' No name preselected by default - */ - nickname: { - type: String, - default: '', - }, - }, - - setup() { - return { - t, - - owner: loadState('files_sharing', 'owner', ''), - ownerDisplayName: loadState('files_sharing', 'ownerDisplayName', ''), - label: loadState('files_sharing', 'label', ''), - note: loadState('files_sharing', 'note', ''), - filename: loadState('files_sharing', 'filename', ''), - } - }, - - data() { - return { - name: '', - } - }, - - computed: { - dialogName() { - return this.t('files_sharing', 'Upload files to {folder}', { folder: this.label || this.filename }) - }, - dialogButtons() { - return [{ - label: t('files_sharing', 'Submit name'), - type: 'primary', - nativeType: 'submit', - }] - }, - }, - - watch: { - /** Reset name to pre-selected nickname (e.g. Talk / Collabora ) */ - nickname: { - handler() { - this.name = this.nickname - }, - immediate: true, - }, - }, -}) -</script> -<style scoped lang="scss"> -.public-auth-prompt { - &__subtitle { - // Smaller than dialog title - font-size: 1.25em; - margin-block: 0 calc(3 * var(--default-grid-baseline)); - } - - &__header { - margin-block: 0 calc(3 * var(--default-grid-baseline)); - } - - &__input { - margin-block: calc(4 * var(--default-grid-baseline)) calc(2 * var(--default-grid-baseline)); - } -} -</style> diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue index 5b778223c8b..f1fb78e548b 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.vue +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -226,19 +226,6 @@ {{ t('files_sharing', 'Delete') }} </NcCheckboxRadioSwitch> </section> - <div class="sharingTabDetailsView__delete"> - <NcButton v-if="!isNewShare" - :aria-label="t('files_sharing', 'Delete share')" - :disabled="false" - :readonly="false" - type="tertiary" - @click.prevent="removeShare"> - <template #icon> - <CloseIcon :size="16" /> - </template> - {{ t('files_sharing', 'Delete share') }} - </NcButton> - </div> </section> </div> </div> @@ -249,6 +236,19 @@ @click="cancel"> {{ t('files_sharing', 'Cancel') }} </NcButton> + <div class="sharingTabDetailsView__delete"> + <NcButton v-if="!isNewShare" + :aria-label="t('files_sharing', 'Delete share')" + :disabled="false" + :readonly="false" + variant="tertiary" + @click.prevent="removeShare"> + <template #icon> + <CloseIcon :size="20" /> + </template> + {{ t('files_sharing', 'Delete share') }} + </NcButton> + </div> <NcButton type="primary" data-cy-files-sharing-share-editor-action="save" :disabled="creating" @@ -905,8 +905,9 @@ export default { this.advancedSectionAccordionExpanded = true } - if (this.share.note) { + if (this.isValidShareAttribute(this.share.note)) { this.writeNoteToRecipientIsChecked = true + this.advancedSectionAccordionExpanded = true } }, diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue index e9e068a7c1d..82a11dea2e0 100644 --- a/apps/files_sharing/src/views/SharingTab.vue +++ b/apps/files_sharing/src/views/SharingTab.vue @@ -50,7 +50,7 @@ :link-shares="linkShares" :reshare="reshare" :shares="shares" - :placeholder="t('files_sharing', 'Share with accounts and teams')" + :placeholder="internalShareInputPlaceholder" @open-sharing-details="toggleShareDetailsView" /> <!-- other shares list --> @@ -90,7 +90,7 @@ :file-info="fileInfo" :link-shares="linkShares" :is-external="true" - :placeholder="t('files_sharing', 'Email, federated cloud id')" + :placeholder="externalShareInputPlaceholder" :reshare="reshare" :shares="shares" @open-sharing-details="toggleShareDetailsView" /> @@ -100,7 +100,7 @@ :file-info="fileInfo" @open-sharing-details="toggleShareDetailsView" /> <!-- link shares list --> - <SharingLinkList v-if="!loading" + <SharingLinkList v-if="!loading && isLinkSharingAllowed" ref="linkShareList" :can-reshare="canReshare" :file-info="fileInfo" @@ -157,6 +157,7 @@ <script> import { getCurrentUser } from '@nextcloud/auth' +import { getCapabilities } from '@nextcloud/capabilities' import { orderBy } from '@nextcloud/files' import { loadState } from '@nextcloud/initial-state' import { generateOcsUrl } from '@nextcloud/router' @@ -242,13 +243,45 @@ export default { * @return {boolean} */ isSharedWithMe() { - return Object.keys(this.sharedWithMe).length > 0 + return this.sharedWithMe !== null + && this.sharedWithMe !== undefined + }, + + /** + * Is link sharing allowed for the current user? + * + * @return {boolean} + */ + isLinkSharingAllowed() { + const currentUser = getCurrentUser() + if (!currentUser) { + return false + } + + const capabilities = getCapabilities() + const publicSharing = capabilities.files_sharing?.public || {} + return publicSharing.enabled === true }, canReshare() { return !!(this.fileInfo.permissions & OC.PERMISSION_SHARE) || !!(this.reshare && this.reshare.hasSharePermission && this.config.isResharingAllowed) }, + + internalShareInputPlaceholder() { + return this.config.showFederatedSharesAsInternal + ? t('files_sharing', 'Share with accounts, teams, federated cloud IDs') + : t('files_sharing', 'Share with accounts and teams') + }, + + externalShareInputPlaceholder() { + if (!this.isLinkSharingAllowed) { + return t('files_sharing', 'Federated cloud ID') + } + return this.config.showFederatedSharesAsInternal + ? t('files_sharing', 'Email') + : t('files_sharing', 'Email, federated cloud ID') + }, }, methods: { @@ -369,7 +402,11 @@ export default { if ([ShareType.Link, ShareType.Email].includes(share.type)) { this.linkShares.push(share) } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) { - this.externalShares.push(share) + if (this.config.showFederatedSharesAsInternal) { + this.shares.push(share) + } else { + this.externalShares.push(share) + } } else { this.shares.push(share) } @@ -439,7 +476,11 @@ export default { if (share.type === ShareType.Email) { this.linkShares.unshift(share) } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) { - this.externalShares.unshift(share) + if (this.config.showFederatedSharesAsInternal) { + this.shares.unshift(share) + } else { + this.externalShares.unshift(share) + } } else { this.shares.unshift(share) } |