diff options
Diffstat (limited to 'apps/files_sharing/src/components')
11 files changed, 248 insertions, 173 deletions
diff --git a/apps/files_sharing/src/components/FileListFilterAccount.vue b/apps/files_sharing/src/components/FileListFilterAccount.vue index f4e4eefb6e2..150516e139b 100644 --- a/apps/files_sharing/src/components/FileListFilterAccount.vue +++ b/apps/files_sharing/src/components/FileListFilterAccount.vue @@ -8,7 +8,7 @@ :filter-name="t('files_sharing', 'People')" @reset-filter="resetFilter"> <template #icon> - <NcIconSvgWrapper :path="mdiAccountMultiple" /> + <NcIconSvgWrapper :path="mdiAccountMultipleOutline" /> </template> <NcActionInput v-if="availableAccounts.length > 1" :label="t('files_sharing', 'Filter accounts')" @@ -36,14 +36,11 @@ </template> <script setup lang="ts"> -import type { IAccountData } from '../filters/AccountFilter.ts' +import type { IAccountData } from '../files_filters/AccountFilter.ts' import { translate as t } from '@nextcloud/l10n' -import { ShareType } from '@nextcloud/sharing' -import { mdiAccountMultiple } from '@mdi/js' -import { useBrowserLocation } from '@vueuse/core' +import { mdiAccountMultipleOutline } from '@mdi/js' import { computed, ref, watch } from 'vue' -import { useNavigation } from '../../../files/src/composables/useNavigation.ts' import FileListFilter from '../../../files/src/components/FileListFilter/FileListFilter.vue' import NcActionButton from '@nextcloud/vue/components/NcActionButton' @@ -61,8 +58,6 @@ const emit = defineEmits<{ (event: 'update:accounts', value: IAccountData[]): void }>() -const { currentView } = useNavigation() -const currentLocation = useBrowserLocation() const accountFilter = ref('') const availableAccounts = ref<IUserSelectData[]>([]) const selectedAccounts = ref<IUserSelectData[]>([]) @@ -106,71 +101,27 @@ watch(selectedAccounts, () => { }) /** - * Update the accounts owning nodes or have nodes shared to them - * @param path The path inside the current view to load for accounts - */ -async function updateAvailableAccounts(path: string = '/') { - availableAccounts.value = [] - if (!currentView.value) { - return - } - - const { contents } = await currentView.value.getContents(path) - const available = new Map<string, IUserSelectData>() - for (const node of contents) { - const owner = node.owner - if (owner && !available.has(owner)) { - available.set(owner, { - id: owner, - user: owner, - displayName: node.attributes['owner-display-name'] ?? node.owner, - }) - } - - const sharees = node.attributes.sharees?.sharee - if (sharees) { - // ensure sharees is an array (if only one share then it is just an object) - for (const sharee of [sharees].flat()) { - // Skip link shares and other without user - if (sharee.id === '') { - continue - } - if (sharee.type !== ShareType.User && sharee.type !== ShareType.Remote) { - continue - } - // Add if not already added - if (!available.has(sharee.id)) { - available.set(sharee.id, { - id: sharee.id, - user: sharee.id, - displayName: sharee['display-name'], - }) - } - } - } - } - availableAccounts.value = [...available.values()] -} - -/** * Reset this filter */ function resetFilter() { selectedAccounts.value = [] accountFilter.value = '' } -defineExpose({ resetFilter, toggleAccount }) -// When the current view changes or the current directory, -// then we need to rebuild the available accounts -watch([currentView, currentLocation], () => { - if (currentView.value) { - // we have no access to the files router here... - const path = (currentLocation.value.search ?? '?dir=/').match(/(?<=&|\?)dir=([^&#]+)/)?.[1] - resetFilter() - updateAvailableAccounts(decodeURIComponent(path ?? '/')) - } -}, { immediate: true }) +/** + * Update list of available accounts in current view. + * + * @param accounts - Accounts to use + */ +function setAvailableAccounts(accounts: IAccountData[]): void { + availableAccounts.value = accounts.map(({ uid, displayName }) => ({ displayName, id: uid, user: uid })) +} + +defineExpose({ + resetFilter, + setAvailableAccounts, + toggleAccount, +}) </script> <style scoped lang="scss"> diff --git a/apps/files_sharing/src/components/NewFileRequestDialog.vue b/apps/files_sharing/src/components/NewFileRequestDialog.vue index 102d1a0fed9..392f286e104 100644 --- a/apps/files_sharing/src/components/NewFileRequestDialog.vue +++ b/apps/files_sharing/src/components/NewFileRequestDialog.vue @@ -296,8 +296,8 @@ export default defineComponent({ path: this.destination, note: this.note, - password: this.password || undefined, - expireDate: expireDate || undefined, + password: this.password || '', + expireDate: expireDate || '', // Empty string shareWith: '', diff --git a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue index 4c14b21e1d5..7e6d56e8794 100644 --- a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue +++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue @@ -14,9 +14,9 @@ <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="!defaultExpireDateEnforced" - :checked="defaultExpireDateEnforced || expirationDate !== null" - :disabled="disabled || defaultExpireDateEnforced" + <NcCheckboxRadioSwitch v-show="!isExpirationDateEnforced" + :checked="isExpirationDateEnforced || expirationDate !== null" + :disabled="disabled || isExpirationDateEnforced" @update:checked="onToggleDeadline"> {{ t('files_sharing', 'Set a submission expiration date') }} </NcCheckboxRadioSwitch> @@ -46,9 +46,9 @@ <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="!enforcePasswordForPublicLink" - :checked="enforcePasswordForPublicLink || password !== null" - :disabled="disabled || enforcePasswordForPublicLink" + <NcCheckboxRadioSwitch v-show="!isPasswordEnforced" + :checked="isPasswordEnforced || password !== null" + :disabled="disabled || isPasswordEnforced" @update:checked="onTogglePassword"> {{ t('files_sharing', 'Set a password') }} </NcCheckboxRadioSwitch> @@ -59,7 +59,7 @@ :disabled="disabled" :label="t('files_sharing', 'Password')" :placeholder="t('files_sharing', 'Enter a valid password')" - :required="false" + :required="enforcePasswordForPublicLink" :value="password" name="password" @update:value="$emit('update:password', $event)" /> @@ -180,6 +180,18 @@ export default defineComponent({ 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() { @@ -189,12 +201,12 @@ export default defineComponent({ } // If enforced, we cannot set a date before the default expiration days (see admin settings) - if (this.defaultExpireDateEnforced) { + if (this.isExpirationDateEnforced) { this.maxDate = sharingConfig.defaultExpirationDate } // If enabled by default, we generate a valid password - if (this.enableLinkPasswordByDefault) { + if (this.isPasswordEnforced) { this.generatePassword() } }, diff --git a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue index 499fd773edc..7826aab581e 100644 --- a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue +++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue @@ -16,7 +16,7 @@ :label="t('files_sharing', 'Share link')" :readonly="true" :show-trailing-button="true" - :trailing-button-label="t('files_sharing', 'Copy to clipboard')" + :trailing-button-label="t('files_sharing', 'Copy')" data-cy-file-request-dialog-fieldset="link" @click="copyShareLink" @trailing-button-click="copyShareLink"> @@ -140,7 +140,7 @@ export default defineComponent({ await navigator.clipboard.writeText(this.shareLink) - showSuccess(t('files_sharing', 'Link copied to clipboard')) + showSuccess(t('files_sharing', 'Link copied')) this.isCopied = true event.target?.select?.() diff --git a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue index 2d4d8eafa2b..5ac60c37e29 100644 --- a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue +++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue @@ -78,7 +78,7 @@ 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/Information.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' diff --git a/apps/files_sharing/src/components/ShareExpiryTime.vue b/apps/files_sharing/src/components/ShareExpiryTime.vue new file mode 100644 index 00000000000..939142616e9 --- /dev/null +++ b/apps/files_sharing/src/components/ShareExpiryTime.vue @@ -0,0 +1,91 @@ +<!-- + - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div class="share-expiry-time"> + <NcPopover popup-role="dialog"> + <template #trigger> + <NcButton v-if="expiryTime" + class="hint-icon" + type="tertiary" + :aria-label="t('files_sharing', 'Share expiration: {date}', { date: new Date(expiryTime).toLocaleString() })"> + <template #icon> + <ClockIcon :size="20" /> + </template> + </NcButton> + </template> + <h3 class="hint-heading"> + {{ t('files_sharing', 'Share Expiration') }} + </h3> + <p v-if="expiryTime" class="hint-body"> + <NcDateTime :timestamp="expiryTime" + :format="timeFormat" + :relative-time="false" /> (<NcDateTime :timestamp="expiryTime" />) + </p> + </NcPopover> + </div> +</template> + +<script> +import NcButton from '@nextcloud/vue/components/NcButton' +import NcPopover from '@nextcloud/vue/components/NcPopover' +import NcDateTime from '@nextcloud/vue/components/NcDateTime' +import ClockIcon from 'vue-material-design-icons/Clock.vue' + +export default { + name: 'ShareExpiryTime', + + components: { + NcButton, + NcPopover, + NcDateTime, + ClockIcon, + }, + + props: { + share: { + type: Object, + required: true, + }, + }, + + computed: { + expiryTime() { + return this.share?.expireDate ? new Date(this.share.expireDate).getTime() : null + }, + timeFormat() { + return { dateStyle: 'full', timeStyle: 'short' } + }, + }, +} +</script> + +<style scoped lang="scss"> +.share-expiry-time { + display: inline-flex; + align-items: center; + justify-content: center; + + .hint-icon { + padding: 0; + margin: 0; + width: 24px; + height: 24px; + } +} + +.hint-heading { + text-align: center; + font-size: 1rem; + margin-top: 8px; + padding-bottom: 8px; + margin-bottom: 0; + border-bottom: 1px solid var(--color-border); +} + +.hint-body { + padding: var(--border-radius-element); + max-width: 300px; +} +</style> diff --git a/apps/files_sharing/src/components/SharingEntry.vue b/apps/files_sharing/src/components/SharingEntry.vue index 3f8f03753d8..342b40ce384 100644 --- a/apps/files_sharing/src/components/SharingEntry.vue +++ b/apps/files_sharing/src/components/SharingEntry.vue @@ -19,8 +19,9 @@ :href="share.shareWithLink" class="sharing-entry__summary__desc"> <span>{{ title }} - <span v-if="!isUnique" class="sharing-entry__summary__desc-unique"> ({{ - share.shareWithDisplayNameUnique }})</span> + <span v-if="!isUnique" class="sharing-entry__summary__desc-unique"> + ({{ share.shareWithDisplayNameUnique }}) + </span> <small v-if="hasStatus && share.status.message">({{ share.status.message }})</small> </span> </component> @@ -28,6 +29,7 @@ :file-info="fileInfo" @open-sharing-details="openShareDetailsForCustomSettings(share)" /> </div> + <ShareExpiryTime v-if="share && share.expireDate" :share="share" /> <NcButton v-if="share.canEdit" class="sharing-entry__action" data-cy-files-sharing-share-actions @@ -49,6 +51,7 @@ import NcSelect from '@nextcloud/vue/components/NcSelect' import NcAvatar from '@nextcloud/vue/components/NcAvatar' import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue' +import ShareExpiryTime from './ShareExpiryTime.vue' import SharingEntryQuickShareSelect from './SharingEntryQuickShareSelect.vue' import SharesMixin from '../mixins/SharesMixin.js' @@ -62,6 +65,7 @@ export default { NcAvatar, DotsHorizontalIcon, NcSelect, + ShareExpiryTime, SharingEntryQuickShareSelect, }, @@ -70,11 +74,15 @@ export default { computed: { title() { let title = this.share.shareWithDisplayName - if (this.share.type === ShareType.Group) { + + const showAsInternal = this.config.showFederatedSharesAsInternal + || (this.share.isTrustedServer && this.config.showFederatedSharesToTrustedServersAsInternal) + + if (this.share.type === ShareType.Group || (this.share.type === ShareType.RemoteGroup && showAsInternal)) { title += ` (${t('files_sharing', 'group')})` } else if (this.share.type === ShareType.Room) { title += ` (${t('files_sharing', 'conversation')})` - } else if (this.share.type === ShareType.Remote) { + } else if (this.share.type === ShareType.Remote && !showAsInternal) { title += ` (${t('files_sharing', 'remote')})` } else if (this.share.type === ShareType.RemoteGroup) { title += ` (${t('files_sharing', 'remote group')})` diff --git a/apps/files_sharing/src/components/SharingEntryInternal.vue b/apps/files_sharing/src/components/SharingEntryInternal.vue index 2ad1256fa82..027d2a3d5c3 100644 --- a/apps/files_sharing/src/components/SharingEntryInternal.vue +++ b/apps/files_sharing/src/components/SharingEntryInternal.vue @@ -83,14 +83,11 @@ export default { } return t('files_sharing', 'Cannot copy, please copy the link manually') } - return t('files_sharing', 'Copy internal link to clipboard') + return t('files_sharing', 'Copy internal link') }, internalLinkSubtitle() { - if (this.fileInfo.type === 'dir') { - return t('files_sharing', 'Only works for people with access to this folder') - } - return t('files_sharing', 'Only works for people with access to this file') + return t('files_sharing', 'For people who already have access') }, }, diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue index e03370bb6e8..6865af1b864 100644 --- a/apps/files_sharing/src/components/SharingEntryLink.vue +++ b/apps/files_sharing/src/components/SharingEntryLink.vue @@ -24,20 +24,26 @@ @open-sharing-details="openShareDetailsForCustomSettings(share)" /> </div> - <!-- clipboard --> - <NcActions v-if="share && (!isEmailShareType || isFileRequest) && share.token" ref="copyButton" class="sharing-entry__copy"> - <NcActionButton :aria-label="copyLinkTooltip" - :title="copyLinkTooltip" - :href="shareLink" - @click.prevent="copyLink"> - <template #icon> - <CheckIcon v-if="copied && copySuccess" - :size="20" - class="icon-checkmark-color" /> - <ClipboardIcon v-else :size="20" /> - </template> - </NcActionButton> - </NcActions> + <div class="sharing-entry__actions"> + <ShareExpiryTime v-if="share && share.expireDate" :share="share" /> + + <!-- clipboard --> + <div> + <NcActions v-if="share && (!isEmailShareType || isFileRequest) && share.token" ref="copyButton" class="sharing-entry__copy"> + <NcActionButton :aria-label="copyLinkTooltip" + :title="copyLinkTooltip" + :href="shareLink" + @click.prevent="copyLink"> + <template #icon> + <CheckIcon v-if="copied && copySuccess" + :size="20" + class="icon-checkmark-color" /> + <ClipboardIcon v-else :size="20" /> + </template> + </NcActionButton> + </NcActions> + </div> + </div> </div> <!-- pending actions --> @@ -68,10 +74,10 @@ {{ config.enforcePasswordForPublicLink ? t('files_sharing', 'Password protection (enforced)') : t('files_sharing', 'Password protection') }} </NcActionCheckbox> - <NcActionInput v-if="pendingEnforcedPassword || share.password" + <NcActionInput v-if="pendingEnforcedPassword || isPasswordProtected" class="share-link-password" :label="t('files_sharing', 'Enter a password')" - :value.sync="share.password" + :value.sync="share.newPassword" :disabled="saving" :required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink" :minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength" @@ -102,13 +108,15 @@ type="date" :min="dateTomorrow" :max="maxExpirationDateEnforced" - @change="expirationDateChanged($event)"> + @update:model-value="onExpirationChange" + @change="expirationDateChanged"> <template #icon> <IconCalendarBlank :size="20" /> </template> </NcActionInput> - <NcActionButton @click.prevent.stop="onNewLinkShare(true)"> + <NcActionButton :disabled="pendingEnforcedPassword && !share.newPassword" + @click.prevent.stop="onNewLinkShare(true)"> <template #icon> <CheckIcon :size="20" /> </template> @@ -216,14 +224,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 Vue from 'vue' +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' @@ -235,23 +243,24 @@ import NcAvatar from '@nextcloud/vue/components/NcAvatar' import NcDialog from '@nextcloud/vue/components/NcDialog' import Tune from 'vue-material-design-icons/Tune.vue' -import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue' +import IconCalendarBlank from 'vue-material-design-icons/CalendarBlankOutline.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 LockIcon from 'vue-material-design-icons/LockOutline.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' import PlusIcon from 'vue-material-design-icons/Plus.vue' import SharingEntryQuickShareSelect from './SharingEntryQuickShareSelect.vue' +import ShareExpiryTime from './ShareExpiryTime.vue' import ExternalShareAction from './ExternalShareAction.vue' 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', @@ -278,6 +287,7 @@ export default { CloseIcon, PlusIcon, SharingEntryQuickShareSelect, + ShareExpiryTime, }, mixins: [SharesMixin, ShareDetails], @@ -305,10 +315,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, @@ -322,6 +328,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) { @@ -329,26 +337,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() === '') { @@ -383,22 +391,6 @@ export default { } return null }, - /** - * Is the current share password protected ? - * - * @return {boolean} - */ - isPasswordProtected: { - get() { - return this.config.enforcePasswordForPublicLink - || !!this.share.password - }, - async set(enabled) { - // TODO: directly save after generation to make sure the share is always protected - Vue.set(this.share, 'password', enabled ? await GeneratePassword(true) : '') - Vue.set(this.share, 'newPassword', this.share.password) - }, - }, passwordExpirationTime() { if (this.share.passwordExpirationTime === null) { @@ -558,7 +550,7 @@ export default { } return t('files_sharing', 'Cannot copy, please copy the link manually') } - return t('files_sharing', 'Copy public link of "{title}" to clipboard', { title: this.title }) + return t('files_sharing', 'Copy public link of "{title}"', { title: this.title }) }, /** @@ -622,7 +614,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 @@ -637,7 +629,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 @@ -645,7 +637,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 @@ -655,6 +647,7 @@ export default { // create share & close menu const share = new Share(shareDefaults) + share.newPassword = share.password const component = await new Promise(resolve => { this.$emit('add:share', share, resolve) }) @@ -673,13 +666,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 @@ -847,7 +840,7 @@ export default { */ onPasswordSubmit() { if (this.hasUnsavedPassword) { - this.share.password = this.share.newPassword.trim() + this.share.newPassword = this.share.newPassword.trim() this.queueUpdate('password') } }, @@ -862,7 +855,7 @@ export default { */ onPasswordProtectedByTalkChange() { if (this.hasUnsavedPassword) { - this.share.password = this.share.newPassword.trim() + this.share.newPassword = this.share.newPassword.trim() } this.queueUpdate('sendPasswordByTalk', 'password') @@ -884,9 +877,9 @@ export default { }, expirationDateChanged(event) { - const date = event.target.value - this.onExpirationChange(date) - this.defaultExpirationDateEnabled = !!date + const value = event?.target?.value + const isValid = !!value && !isNaN(new Date(value).getTime()) + this.defaultExpirationDateEnabled = isValid }, /** @@ -936,6 +929,12 @@ export default { } } + &__actions { + display: flex; + align-items: center; + margin-inline-start: auto; + } + &:not(.sharing-entry--share) &__actions { .new-share-link { border-top: 1px solid var(--color-border); diff --git a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue index 041841201d0..102eea63cb6 100644 --- a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue +++ b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue @@ -36,7 +36,7 @@ import ShareDetails from '../mixins/ShareDetails.js' import NcActions from '@nextcloud/vue/components/NcActions' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import IconEyeOutline from 'vue-material-design-icons/EyeOutline.vue' -import IconPencil from 'vue-material-design-icons/Pencil.vue' +import IconPencil from 'vue-material-design-icons/PencilOutline.vue' import IconFileUpload from 'vue-material-design-icons/FileUpload.vue' import IconTune from 'vue-material-design-icons/Tune.vue' diff --git a/apps/files_sharing/src/components/SharingInput.vue b/apps/files_sharing/src/components/SharingInput.vue index 49a39915e5e..6fb33aba6b2 100644 --- a/apps/files_sharing/src/components/SharingInput.vue +++ b/apps/files_sharing/src/components/SharingInput.vue @@ -192,14 +192,25 @@ export default { lookup = true } - let shareType = [] + const remoteTypes = [ShareType.Remote, ShareType.RemoteGroup] + const shareType = [] + + const showFederatedAsInternal = this.config.showFederatedSharesAsInternal + || this.config.showFederatedSharesToTrustedServersAsInternal + + // For internal users, add remote types if config says to show them as internal + const shouldAddRemoteTypes = (!this.isExternal && showFederatedAsInternal) + // For external users, add them if config *doesn't* say to show them as internal + || (this.isExternal && !showFederatedAsInternal) + // Edge case: federated-to-trusted is a separate "add" trigger for external users + || (this.isExternal && this.config.showFederatedSharesToTrustedServersAsInternal) if (this.isExternal) { - shareType.push(ShareType.Remote) - shareType.push(ShareType.RemoteGroup) + if (getCapabilities().files_sharing.public.enabled === true) { + shareType.push(ShareType.Email) + } } else { - // Merge shareType array - shareType = shareType.concat([ + shareType.push( ShareType.User, ShareType.Group, ShareType.Team, @@ -207,12 +218,11 @@ export default { ShareType.Guest, ShareType.Deck, ShareType.ScienceMesh, - ]) - + ) } - if (getCapabilities().files_sharing.public.enabled === true && this.isExternal) { - shareType.push(ShareType.Email) + if (shouldAddRemoteTypes) { + shareType.push(...remoteTypes) } let request = null @@ -232,13 +242,10 @@ export default { return } - const data = request.data.ocs.data - const exact = request.data.ocs.data.exact - data.exact = [] // removing exact from general results - + const { exact, ...data } = request.data.ocs.data // flatten array of arrays - const rawExactSuggestions = Object.values(exact).reduce((arr, elem) => arr.concat(elem), []) - const rawSuggestions = Object.values(data).reduce((arr, elem) => arr.concat(elem), []) + const rawExactSuggestions = Object.values(exact).flat() + const rawSuggestions = Object.values(data).flat() // remove invalid data and format to user-select layout const exactSuggestions = this.filterOutExistingShares(rawExactSuggestions) @@ -257,7 +264,7 @@ export default { lookupEntry.push({ id: 'global-lookup', isNoUser: true, - displayName: t('files_sharing', 'Search globally'), + displayName: t('files_sharing', 'Search everywhere'), lookup: true, }) } @@ -363,6 +370,11 @@ export default { // filter out existing mail shares if (share.value.shareType === ShareType.Email) { + // When sharing internally, we don't want to suggest email addresses + // that the user previously created shares to + if (!this.isExternal) { + return arr + } const emails = this.linkShares.map(elem => elem.shareWith) if (emails.indexOf(share.value.shareWith.trim()) !== -1) { return arr @@ -453,14 +465,19 @@ export default { */ formatForMultiselect(result) { let subname + let displayName = result.name || result.label + if (result.value.shareType === ShareType.User && this.config.shouldAlwaysShowUnique) { subname = result.shareWithDisplayNameUnique ?? '' - } else if ((result.value.shareType === ShareType.Remote - || result.value.shareType === ShareType.RemoteGroup - ) && result.value.server) { - subname = t('files_sharing', 'on {server}', { server: result.value.server }) } else if (result.value.shareType === ShareType.Email) { subname = result.value.shareWith + } else if (result.value.shareType === ShareType.Remote || result.value.shareType === ShareType.RemoteGroup) { + if (this.config.showFederatedSharesAsInternal) { + subname = result.extra?.email?.value ?? '' + displayName = result.extra?.name?.value ?? displayName + } else if (result.value.server) { + subname = t('files_sharing', 'on {server}', { server: result.value.server }) + } } else { subname = result.shareWithDescription ?? '' } @@ -470,7 +487,7 @@ export default { shareType: result.value.shareType, user: result.uuid || result.value.shareWith, isNoUser: result.value.shareType !== ShareType.User, - displayName: result.name || result.label, + displayName, subname, shareWithDisplayNameUnique: result.shareWithDisplayNameUnique || '', ...this.shareTypeToIcon(result.value.shareType), |