aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src/components')
-rw-r--r--apps/files_sharing/src/components/FileListFilterAccount.vue91
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog.vue12
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue40
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue10
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue6
-rw-r--r--apps/files_sharing/src/components/SelectShareFolderDialogue.vue2
-rw-r--r--apps/files_sharing/src/components/ShareExpiryTime.vue91
-rw-r--r--apps/files_sharing/src/components/SharingEntry.vue22
-rw-r--r--apps/files_sharing/src/components/SharingEntryInherited.vue8
-rw-r--r--apps/files_sharing/src/components/SharingEntryInternal.vue2
-rw-r--r--apps/files_sharing/src/components/SharingEntryLink.vue155
-rw-r--r--apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue19
-rw-r--r--apps/files_sharing/src/components/SharingEntrySimple.vue2
-rw-r--r--apps/files_sharing/src/components/SharingInput.vue74
14 files changed, 323 insertions, 211 deletions
diff --git a/apps/files_sharing/src/components/FileListFilterAccount.vue b/apps/files_sharing/src/components/FileListFilterAccount.vue
index 89cedbf1ed8..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,20 +36,17 @@
</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/dist/Components/NcActionButton.js'
-import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActionInput from '@nextcloud/vue/components/NcActionInput'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
interface IUserSelectData {
id: string
@@ -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 7ce8c576a56..392f286e104 100644
--- a/apps/files_sharing/src/components/NewFileRequestDialog.vue
+++ b/apps/files_sharing/src/components/NewFileRequestDialog.vue
@@ -130,10 +130,10 @@ import { showError, showSuccess } from '@nextcloud/dialogs'
import { n, t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import IconCheck from 'vue-material-design-icons/Check.vue'
import IconNext from 'vue-material-design-icons/ArrowRight.vue'
@@ -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 091679ae5c4..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)" />
@@ -85,11 +85,11 @@
import { defineComponent, type PropType } from 'vue'
import { t } from '@nextcloud/l10n'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
+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'
@@ -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 5f84bb7dac0..499fd773edc 100644
--- a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue
+++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogFinish.vue
@@ -67,11 +67,11 @@ import { generateUrl, getBaseUrl } from '@nextcloud/router'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { n, t } from '@nextcloud/l10n'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
-import NcChip from '@nextcloud/vue/dist/Components/NcChip.js'
+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'
diff --git a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue
index 805b13fdf95..5ac60c37e29 100644
--- a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue
+++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue
@@ -78,10 +78,10 @@ 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/dist/Components/NcTextArea.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcTextArea from '@nextcloud/vue/components/NcTextArea'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
export default defineComponent({
name: 'NewFileRequestDialogIntro',
diff --git a/apps/files_sharing/src/components/SelectShareFolderDialogue.vue b/apps/files_sharing/src/components/SelectShareFolderDialogue.vue
index ec29aff9b8a..959fecaa4a4 100644
--- a/apps/files_sharing/src/components/SelectShareFolderDialogue.vue
+++ b/apps/files_sharing/src/components/SelectShareFolderDialogue.vue
@@ -29,7 +29,7 @@ import path from 'path'
import { generateUrl } from '@nextcloud/router'
import { getFilePickerBuilder, showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
const defaultDirectory = loadState('files_sharing', 'default_share_folder', '/')
const directory = loadState('files_sharing', 'share_folder', defaultDirectory)
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 ae58855c9b1..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
@@ -44,11 +46,12 @@
<script>
import { ShareType } from '@nextcloud/sharing'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+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/SharingEntryInherited.vue b/apps/files_sharing/src/components/SharingEntryInherited.vue
index af2cccbb0df..e7dfffd5776 100644
--- a/apps/files_sharing/src/components/SharingEntryInherited.vue
+++ b/apps/files_sharing/src/components/SharingEntryInherited.vue
@@ -31,10 +31,10 @@
<script>
import { generateUrl } from '@nextcloud/router'
import { basename } from '@nextcloud/paths'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
-import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActionLink from '@nextcloud/vue/components/NcActionLink'
+import NcActionText from '@nextcloud/vue/components/NcActionText'
// eslint-disable-next-line no-unused-vars
import Share from '../models/Share.js'
diff --git a/apps/files_sharing/src/components/SharingEntryInternal.vue b/apps/files_sharing/src/components/SharingEntryInternal.vue
index 71931aab465..2ad1256fa82 100644
--- a/apps/files_sharing/src/components/SharingEntryInternal.vue
+++ b/apps/files_sharing/src/components/SharingEntryInternal.vue
@@ -29,7 +29,7 @@
<script>
import { generateUrl } from '@nextcloud/router'
import { showSuccess } from '@nextcloud/dialogs'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import ClipboardIcon from 'vue-material-design-icons/ContentCopy.vue'
diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue
index 747fa31aac9..6a456fa0a15 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"
@@ -86,12 +92,13 @@
:checked.sync="defaultExpirationDateEnabled"
:disabled="pendingEnforcedExpirationDate || saving"
class="share-link-expiration-date-checkbox"
- @change="onDefaultExpirationDateEnabledChange">
- {{ config.enforcePasswordForPublicLink ? t('files_sharing', 'Enable link expiration (enforced)') : t('files_sharing', 'Enable link expiration') }}
+ @update:model-value="onExpirationDateToggleUpdate">
+ {{ config.isDefaultExpireDateEnforced ? t('files_sharing', 'Enable link expiration (enforced)') : t('files_sharing', 'Enable link expiration') }}
</NcActionCheckbox>
<!-- expiration date -->
<NcActionInput v-if="(pendingDefaultExpirationDate || pendingEnforcedExpirationDate) && defaultExpirationDateEnabled"
+ data-cy-files-sharing-expiration-date-input
class="share-link-expire-date"
:label="pendingEnforcedExpirationDate ? t('files_sharing', 'Enter expiration date (enforced)') : t('files_sharing', 'Enter expiration date')"
:disabled="saving"
@@ -101,13 +108,15 @@
type="date"
:min="dateTomorrow"
:max="maxExpirationDateEnforced"
- @input="onExpirationChange /* let's not submit when picked, the user might want to still edit or copy the password */">
+ @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>
@@ -215,42 +224,43 @@
</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 NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js'
-import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
-import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
-import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js'
-import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox'
+import NcActionInput from '@nextcloud/vue/components/NcActionInput'
+import NcActionLink from '@nextcloud/vue/components/NcActionLink'
+import NcActionText from '@nextcloud/vue/components/NcActionText'
+import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
+import NcActions from '@nextcloud/vue/components/NcActions'
+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',
@@ -277,6 +287,7 @@ export default {
CloseIcon,
PlusIcon,
SharingEntryQuickShareSelect,
+ ShareExpiryTime,
},
mixins: [SharesMixin, ShareDetails],
@@ -304,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,
@@ -321,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) {
@@ -328,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() === '') {
@@ -382,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) {
@@ -597,6 +590,9 @@ export default {
},
mounted() {
this.defaultExpirationDateEnabled = this.config.defaultExpirationDate instanceof Date
+ if (this.share && this.isNewShare) {
+ this.share.expireDate = this.defaultExpirationDateEnabled ? this.formatDateToString(this.config.defaultExpirationDate) : ''
+ }
},
methods: {
@@ -618,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
@@ -633,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
@@ -641,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
@@ -651,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)
})
@@ -669,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
@@ -715,7 +712,7 @@ export default {
path,
shareType: ShareType.Link,
password: share.password,
- expireDate: share.expireDate,
+ expireDate: share.expireDate ?? '',
attributes: JSON.stringify(this.fileInfo.shareAttributes),
// we do not allow setting the publicUpload
// before the share creation.
@@ -843,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')
}
},
@@ -858,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')
@@ -871,10 +868,20 @@ export default {
this.onPasswordSubmit()
this.onNoteSubmit()
},
- onDefaultExpirationDateEnabledChange(enabled) {
+
+ /**
+ * @param enabled True if expiration is enabled
+ */
+ onExpirationDateToggleUpdate(enabled) {
this.share.expireDate = enabled ? this.formatDateToString(this.config.defaultExpirationDate) : ''
},
+ expirationDateChanged(event) {
+ const value = event?.target?.value
+ const isValid = !!value && !isNaN(new Date(value).getTime())
+ this.defaultExpirationDateEnabled = isValid
+ },
+
/**
* Cancel the share creation
* Used in the pending popover
@@ -922,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 565bee1d821..102eea63cb6 100644
--- a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue
+++ b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue
@@ -29,13 +29,14 @@
<script>
import { ShareType } from '@nextcloud/sharing'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import DropdownIcon from 'vue-material-design-icons/TriangleSmallDown.vue'
import SharesMixin from '../mixins/SharesMixin.js'
import ShareDetails from '../mixins/ShareDetails.js'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.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'
@@ -145,7 +146,17 @@ export default {
created() {
this.selectedOption = this.preSelectedOption
},
-
+ mounted() {
+ subscribe('update:share', (share) => {
+ if (share.id === this.share.id) {
+ this.share.permissions = share.permissions
+ this.selectedOption = this.preSelectedOption
+ }
+ })
+ },
+ unmounted() {
+ unsubscribe('update:share')
+ },
methods: {
selectOption(optionLabel) {
this.selectedOption = optionLabel
diff --git a/apps/files_sharing/src/components/SharingEntrySimple.vue b/apps/files_sharing/src/components/SharingEntrySimple.vue
index a5e4034cfb1..a00333ba0ce 100644
--- a/apps/files_sharing/src/components/SharingEntrySimple.vue
+++ b/apps/files_sharing/src/components/SharingEntrySimple.vue
@@ -23,7 +23,7 @@
</template>
<script>
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
+import NcActions from '@nextcloud/vue/components/NcActions'
export default {
name: 'SharingEntrySimple',
diff --git a/apps/files_sharing/src/components/SharingInput.vue b/apps/files_sharing/src/components/SharingInput.vue
index 67fdceae336..46bacef0c6c 100644
--- a/apps/files_sharing/src/components/SharingInput.vue
+++ b/apps/files_sharing/src/components/SharingInput.vue
@@ -5,13 +5,13 @@
<template>
<div class="sharing-search">
- <label class="hidden-visually" for="sharing-search-input">
+ <label class="hidden-visually" :for="shareInputId">
{{ isExternal ? t('files_sharing', 'Enter external recipients')
: t('files_sharing', 'Search for internal recipients') }}
</label>
<NcSelect ref="select"
v-model="value"
- input-id="sharing-search-input"
+ :input-id="shareInputId"
class="sharing-search__input"
:disabled="!canReshare"
:loading="loading"
@@ -36,7 +36,7 @@ import { getCurrentUser } from '@nextcloud/auth'
import { getCapabilities } from '@nextcloud/capabilities'
import axios from '@nextcloud/axios'
import debounce from 'debounce'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
import Config from '../services/ConfigService.ts'
import Share from '../models/Share.ts'
@@ -87,6 +87,12 @@ export default {
},
},
+ setup() {
+ return {
+ shareInputId: `share-input-${Math.random().toString(36).slice(2, 7)}`,
+ }
+ },
+
data() {
return {
config: new Config(),
@@ -149,7 +155,10 @@ export default {
},
mounted() {
- this.getRecommendations()
+ if (!this.isExternal) {
+ // We can only recommend users, groups etc for internal shares
+ this.getRecommendations()
+ }
},
methods: {
@@ -183,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,
@@ -198,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
@@ -223,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)
@@ -354,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
@@ -444,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 ?? ''
}
@@ -461,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),