aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src/views
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src/views')
-rw-r--r--apps/files_sharing/src/views/CollaborationView.vue53
-rw-r--r--apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue73
-rw-r--r--apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue136
-rw-r--r--apps/files_sharing/src/views/SharingDetailsTab.vue1310
-rw-r--r--apps/files_sharing/src/views/SharingInherited.vue35
-rw-r--r--apps/files_sharing/src/views/SharingLinkList.vue64
-rw-r--r--apps/files_sharing/src/views/SharingList.vue59
-rw-r--r--apps/files_sharing/src/views/SharingTab.vue478
8 files changed, 1938 insertions, 270 deletions
diff --git a/apps/files_sharing/src/views/CollaborationView.vue b/apps/files_sharing/src/views/CollaborationView.vue
deleted file mode 100644
index a3249f8b5c7..00000000000
--- a/apps/files_sharing/src/views/CollaborationView.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<!--
- - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
-
-<template>
- <CollectionList v-if="fileId"
- :id="fileId"
- type="file"
- :name="filename" />
-</template>
-
-<script>
-import { CollectionList } from 'nextcloud-vue-collections'
-
-export default {
- name: 'CollaborationView',
- components: {
- CollectionList,
- },
- computed: {
- fileId() {
- if (this.$root.model && this.$root.model.id) {
- return '' + this.$root.model.id
- }
- return null
- },
- filename() {
- if (this.$root.model && this.$root.model.name) {
- return '' + this.$root.model.name
- }
- return ''
- },
- },
-}
-</script>
diff --git a/apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue b/apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue
new file mode 100644
index 00000000000..ec6348606fb
--- /dev/null
+++ b/apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue
@@ -0,0 +1,73 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcNoteCard v-if="note.length > 0"
+ class="note-to-recipient"
+ type="info">
+ <p v-if="displayName" class="note-to-recipient__heading">
+ {{ t('files_sharing', 'Note from') }}
+ <NcUserBubble :user="user.id" :display-name="user.displayName" />
+ </p>
+ <p v-else class="note-to-recipient__heading">
+ {{ t('files_sharing', 'Note:') }}
+ </p>
+ <p class="note-to-recipient__text" v-text="note" />
+ </NcNoteCard>
+</template>
+
+<script setup lang="ts">
+import type { Folder } from '@nextcloud/files'
+import { getCurrentUser } from '@nextcloud/auth'
+import { t } from '@nextcloud/l10n'
+import { computed, ref } from 'vue'
+
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcUserBubble from '@nextcloud/vue/components/NcUserBubble'
+
+const folder = ref<Folder>()
+const note = computed<string>(() => folder.value?.attributes.note ?? '')
+const displayName = computed<string>(() => folder.value?.attributes['owner-display-name'] ?? '')
+const user = computed(() => {
+ const id = folder.value?.owner
+ if (id !== getCurrentUser()?.uid) {
+ return {
+ id,
+ displayName: displayName.value,
+ }
+ }
+ return null
+})
+
+/**
+ * Update the current folder
+ * @param newFolder the new folder to show note for
+ */
+function updateFolder(newFolder: Folder) {
+ folder.value = newFolder
+}
+
+defineExpose({ updateFolder })
+</script>
+
+<style scoped>
+.note-to-recipient {
+ margin-inline: var(--row-height)
+}
+
+.note-to-recipient__text {
+ /* respect new lines */
+ white-space: pre-line;
+}
+
+.note-to-recipient__heading {
+ font-weight: bold;
+}
+
+@media screen and (max-width: 512px) {
+ .note-to-recipient {
+ margin-inline: var(--default-grid-baseline);
+ }
+}
+</style>
diff --git a/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue
new file mode 100644
index 00000000000..dac22748d8a
--- /dev/null
+++ b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue
@@ -0,0 +1,136 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcEmptyContent class="file-drop-empty-content"
+ data-cy-files-sharing-file-drop
+ :name="name">
+ <template #icon>
+ <NcIconSvgWrapper :svg="svgCloudUpload" />
+ </template>
+ <template #description>
+ <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">
+ <!-- Terms of service if enabled -->
+ <NcButton type="primary" @click="showDialog = true">
+ {{ t('files_sharing', 'View terms of service') }}
+ </NcButton>
+ <NcDialog close-on-click-outside
+ content-classes="terms-of-service-dialog"
+ :open.sync="showDialog"
+ :name="t('files_sharing', 'Terms of service')"
+ :message="disclaimer" />
+ </template>
+ <UploadPicker allow-folders
+ :content="() => []"
+ no-menu
+ :destination="uploadDestination"
+ multiple />
+ </template>
+ </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, 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-outline.svg?raw'
+
+defineProps<{
+ foldername: string
+}>()
+
+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
+
+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))
+}
+</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/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue
new file mode 100644
index 00000000000..b3a3b95d92e
--- /dev/null
+++ b/apps/files_sharing/src/views/SharingDetailsTab.vue
@@ -0,0 +1,1310 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <div class="sharingTabDetailsView">
+ <div class="sharingTabDetailsView__header">
+ <span>
+ <NcAvatar v-if="isUserShare"
+ class="sharing-entry__avatar"
+ :is-no-user="share.shareType !== ShareType.User"
+ :user="share.shareWith"
+ :display-name="share.shareWithDisplayName"
+ :menu-position="'left'"
+ :url="share.shareWithAvatar" />
+ <component :is="getShareTypeIcon(share.type)" :size="32" />
+ </span>
+ <span>
+ <h1>{{ title }}</h1>
+ </span>
+ </div>
+ <div class="sharingTabDetailsView__wrapper">
+ <div ref="quickPermissions" class="sharingTabDetailsView__quick-permissions">
+ <div>
+ <NcCheckboxRadioSwitch :button-variant="true"
+ data-cy-files-sharing-share-permissions-bundle="read-only"
+ :checked.sync="sharingPermission"
+ :value="bundledPermissions.READ_ONLY.toString()"
+ name="sharing_permission_radio"
+ type="radio"
+ button-variant-grouped="vertical"
+ @update:checked="toggleCustomPermissions">
+ {{ t('files_sharing', 'View only') }}
+ <template #icon>
+ <ViewIcon :size="20" />
+ </template>
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch :button-variant="true"
+ data-cy-files-sharing-share-permissions-bundle="upload-edit"
+ :checked.sync="sharingPermission"
+ :value="allPermissions"
+ name="sharing_permission_radio"
+ type="radio"
+ button-variant-grouped="vertical"
+ @update:checked="toggleCustomPermissions">
+ <template v-if="allowsFileDrop">
+ {{ t('files_sharing', 'Allow upload and editing') }}
+ </template>
+ <template v-else>
+ {{ t('files_sharing', 'Allow editing') }}
+ </template>
+ <template #icon>
+ <EditIcon :size="20" />
+ </template>
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch v-if="allowsFileDrop"
+ data-cy-files-sharing-share-permissions-bundle="file-drop"
+ :button-variant="true"
+ :checked.sync="sharingPermission"
+ :value="bundledPermissions.FILE_DROP.toString()"
+ name="sharing_permission_radio"
+ type="radio"
+ button-variant-grouped="vertical"
+ @update:checked="toggleCustomPermissions">
+ {{ t('files_sharing', 'File request') }}
+ <small class="subline">{{ t('files_sharing', 'Upload only') }}</small>
+ <template #icon>
+ <UploadIcon :size="20" />
+ </template>
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch :button-variant="true"
+ data-cy-files-sharing-share-permissions-bundle="custom"
+ :checked.sync="sharingPermission"
+ :value="'custom'"
+ name="sharing_permission_radio"
+ type="radio"
+ button-variant-grouped="vertical"
+ @update:checked="expandCustomPermissions">
+ {{ t('files_sharing', 'Custom permissions') }}
+ <small class="subline">{{ customPermissionsList }}</small>
+ <template #icon>
+ <DotsHorizontalIcon :size="20" />
+ </template>
+ </NcCheckboxRadioSwitch>
+ </div>
+ </div>
+ <div class="sharingTabDetailsView__advanced-control">
+ <NcButton id="advancedSectionAccordionAdvancedControl"
+ type="tertiary"
+ alignment="end-reverse"
+ aria-controls="advancedSectionAccordionAdvanced"
+ :aria-expanded="advancedControlExpandedValue"
+ @click="advancedSectionAccordionExpanded = !advancedSectionAccordionExpanded">
+ {{ t('files_sharing', 'Advanced settings') }}
+ <template #icon>
+ <MenuDownIcon v-if="!advancedSectionAccordionExpanded" />
+ <MenuUpIcon v-else />
+ </template>
+ </NcButton>
+ </div>
+ <div v-if="advancedSectionAccordionExpanded"
+ id="advancedSectionAccordionAdvanced"
+ class="sharingTabDetailsView__advanced"
+ aria-labelledby="advancedSectionAccordionAdvancedControl"
+ role="region">
+ <section>
+ <NcInputField v-if="isPublicShare"
+ class="sharingTabDetailsView__label"
+ autocomplete="off"
+ :label="t('files_sharing', 'Share label')"
+ :value.sync="share.label" />
+ <NcInputField v-if="config.allowCustomTokens && isPublicShare && !isNewShare"
+ autocomplete="off"
+ :label="t('files_sharing', 'Share link token')"
+ :helper-text="t('files_sharing', 'Set the public share link token to something easy to remember or generate a new token. It is not recommended to use a guessable token for shares which contain sensitive information.')"
+ show-trailing-button
+ :trailing-button-label="loadingToken ? t('files_sharing', 'Generating…') : t('files_sharing', 'Generate new token')"
+ :value.sync="share.token"
+ @trailing-button-click="generateNewToken">
+ <template #trailing-button-icon>
+ <NcLoadingIcon v-if="loadingToken" />
+ <Refresh v-else :size="20" />
+ </template>
+ </NcInputField>
+ <template v-if="isPublicShare">
+ <NcCheckboxRadioSwitch :checked.sync="isPasswordProtected" :disabled="isPasswordEnforced">
+ {{ t('files_sharing', 'Set password') }}
+ </NcCheckboxRadioSwitch>
+ <NcPasswordField v-if="isPasswordProtected"
+ autocomplete="new-password"
+ :value="share.newPassword ?? ''"
+ :error="passwordError"
+ :helper-text="errorPasswordLabel || passwordHint"
+ :required="isPasswordEnforced && isNewShare"
+ :label="t('files_sharing', 'Password')"
+ @update:value="onPasswordChange" />
+
+ <!-- Migrate icons and remote -> icon="icon-info"-->
+ <span v-if="isEmailShareType && passwordExpirationTime" icon="icon-info">
+ {{ t('files_sharing', 'Password expires {passwordExpirationTime}', { passwordExpirationTime }) }}
+ </span>
+ <span v-else-if="isEmailShareType && passwordExpirationTime !== null" icon="icon-error">
+ {{ t('files_sharing', 'Password expired') }}
+ </span>
+ </template>
+ <NcCheckboxRadioSwitch v-if="canTogglePasswordProtectedByTalkAvailable"
+ :checked.sync="isPasswordProtectedByTalk"
+ @update:checked="onPasswordProtectedByTalkChange">
+ {{ t('files_sharing', 'Video verification') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch :checked.sync="hasExpirationDate" :disabled="isExpiryDateEnforced">
+ {{ isExpiryDateEnforced
+ ? t('files_sharing', 'Expiration date (enforced)')
+ : t('files_sharing', 'Set expiration date') }}
+ </NcCheckboxRadioSwitch>
+ <NcDateTimePickerNative v-if="hasExpirationDate"
+ id="share-date-picker"
+ :value="new Date(share.expireDate ?? dateTomorrow)"
+ :min="dateTomorrow"
+ :max="maxExpirationDateEnforced"
+ hide-label
+ :label="t('files_sharing', 'Expiration date')"
+ :placeholder="t('files_sharing', 'Expiration date')"
+ type="date"
+ @input="onExpirationChange" />
+ <NcCheckboxRadioSwitch v-if="isPublicShare"
+ :disabled="canChangeHideDownload"
+ :checked.sync="share.hideDownload"
+ @update:checked="queueUpdate('hideDownload')">
+ {{ t('files_sharing', 'Hide download') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch v-else
+ :disabled="!canSetDownload"
+ :checked.sync="canDownload"
+ data-cy-files-sharing-share-permissions-checkbox="download">
+ {{ t('files_sharing', 'Allow download and sync') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch :checked.sync="writeNoteToRecipientIsChecked">
+ {{ t('files_sharing', 'Note to recipient') }}
+ </NcCheckboxRadioSwitch>
+ <template v-if="writeNoteToRecipientIsChecked">
+ <NcTextArea :label="t('files_sharing', 'Note to recipient')"
+ :placeholder="t('files_sharing', 'Enter a note for the share recipient')"
+ :value.sync="share.note" />
+ </template>
+ <NcCheckboxRadioSwitch v-if="isPublicShare && isFolder"
+ :checked.sync="showInGridView">
+ {{ t('files_sharing', 'Show files in grid view') }}
+ </NcCheckboxRadioSwitch>
+ <ExternalShareAction v-for="action in externalLinkActions"
+ :id="action.id"
+ ref="externalLinkActions"
+ :key="action.id"
+ :action="action"
+ :file-info="fileInfo"
+ :share="share" />
+ <NcCheckboxRadioSwitch :checked.sync="setCustomPermissions">
+ {{ t('files_sharing', 'Custom permissions') }}
+ </NcCheckboxRadioSwitch>
+ <section v-if="setCustomPermissions" class="custom-permissions-group">
+ <NcCheckboxRadioSwitch :disabled="!canRemoveReadPermission"
+ :checked.sync="hasRead"
+ data-cy-files-sharing-share-permissions-checkbox="read">
+ {{ t('files_sharing', 'Read') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch v-if="isFolder"
+ :disabled="!canSetCreate"
+ :checked.sync="canCreate"
+ data-cy-files-sharing-share-permissions-checkbox="create">
+ {{ t('files_sharing', 'Create') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch :disabled="!canSetEdit"
+ :checked.sync="canEdit"
+ data-cy-files-sharing-share-permissions-checkbox="update">
+ {{ t('files_sharing', 'Edit') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch v-if="resharingIsPossible"
+ :disabled="!canSetReshare"
+ :checked.sync="canReshare"
+ data-cy-files-sharing-share-permissions-checkbox="share">
+ {{ t('files_sharing', 'Share') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch :disabled="!canSetDelete"
+ :checked.sync="canDelete"
+ data-cy-files-sharing-share-permissions-checkbox="delete">
+ {{ t('files_sharing', 'Delete') }}
+ </NcCheckboxRadioSwitch>
+ </section>
+ </section>
+ </div>
+ </div>
+
+ <div class="sharingTabDetailsView__footer">
+ <div class="button-group">
+ <NcButton data-cy-files-sharing-share-editor-action="cancel"
+ @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"
+ @click="saveShare">
+ {{ shareButtonText }}
+ <template v-if="creating" #icon>
+ <NcLoadingIcon />
+ </template>
+ </NcButton>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import { emit } from '@nextcloud/event-bus'
+import { getLanguage } from '@nextcloud/l10n'
+import { ShareType } from '@nextcloud/sharing'
+import { showError } from '@nextcloud/dialogs'
+import moment from '@nextcloud/moment'
+
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
+import NcInputField from '@nextcloud/vue/components/NcInputField'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
+import NcTextArea from '@nextcloud/vue/components/NcTextArea'
+
+import CircleIcon from 'vue-material-design-icons/CircleOutline.vue'
+import CloseIcon from 'vue-material-design-icons/Close.vue'
+import EditIcon from 'vue-material-design-icons/PencilOutline.vue'
+import EmailIcon from 'vue-material-design-icons/Email.vue'
+import LinkIcon from 'vue-material-design-icons/Link.vue'
+import GroupIcon from 'vue-material-design-icons/AccountGroup.vue'
+import ShareIcon from 'vue-material-design-icons/ShareCircle.vue'
+import UserIcon from 'vue-material-design-icons/AccountCircleOutline.vue'
+import ViewIcon from 'vue-material-design-icons/Eye.vue'
+import UploadIcon from 'vue-material-design-icons/Upload.vue'
+import MenuDownIcon from 'vue-material-design-icons/MenuDown.vue'
+import MenuUpIcon from 'vue-material-design-icons/MenuUp.vue'
+import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
+import Refresh from 'vue-material-design-icons/Refresh.vue'
+
+import ExternalShareAction from '../components/ExternalShareAction.vue'
+
+import GeneratePassword from '../utils/GeneratePassword.ts'
+import Share from '../models/Share.ts'
+import ShareRequests from '../mixins/ShareRequests.js'
+import SharesMixin from '../mixins/SharesMixin.js'
+import { generateToken } from '../services/TokenService.ts'
+import logger from '../services/logger.ts'
+
+import {
+ ATOMIC_PERMISSIONS,
+ BUNDLED_PERMISSIONS,
+ hasPermissions,
+} from '../lib/SharePermissionsToolBox.js'
+
+export default {
+ name: 'SharingDetailsTab',
+ components: {
+ NcAvatar,
+ NcButton,
+ NcCheckboxRadioSwitch,
+ NcDateTimePickerNative,
+ NcInputField,
+ NcLoadingIcon,
+ NcPasswordField,
+ NcTextArea,
+ CloseIcon,
+ CircleIcon,
+ EditIcon,
+ ExternalShareAction,
+ LinkIcon,
+ GroupIcon,
+ ShareIcon,
+ UserIcon,
+ UploadIcon,
+ ViewIcon,
+ MenuDownIcon,
+ MenuUpIcon,
+ DotsHorizontalIcon,
+ Refresh,
+ },
+ mixins: [ShareRequests, SharesMixin],
+ props: {
+ shareRequestValue: {
+ type: Object,
+ required: false,
+ },
+ fileInfo: {
+ type: Object,
+ required: true,
+ },
+ share: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ writeNoteToRecipientIsChecked: false,
+ sharingPermission: BUNDLED_PERMISSIONS.ALL.toString(),
+ revertSharingPermission: BUNDLED_PERMISSIONS.ALL.toString(),
+ setCustomPermissions: false,
+ passwordError: false,
+ advancedSectionAccordionExpanded: false,
+ bundledPermissions: BUNDLED_PERMISSIONS,
+ isFirstComponentLoad: true,
+ test: false,
+ creating: false,
+ initialToken: this.share.token,
+ loadingToken: false,
+
+ ExternalShareActions: OCA.Sharing.ExternalShareActions.state,
+ }
+ },
+
+ computed: {
+ title() {
+ switch (this.share.type) {
+ case ShareType.User:
+ return t('files_sharing', 'Share with {user}', { user: this.share.shareWithDisplayName })
+ case ShareType.Email:
+ return t('files_sharing', 'Share with email {email}', { email: this.share.shareWith })
+ case ShareType.Link:
+ return t('files_sharing', 'Share link')
+ case ShareType.Group:
+ return t('files_sharing', 'Share with group')
+ case ShareType.Room:
+ return t('files_sharing', 'Share in conversation')
+ case ShareType.Remote: {
+ const [user, server] = this.share.shareWith.split('@')
+ if (this.config.showFederatedSharesAsInternal) {
+ return t('files_sharing', 'Share with {user}', { user })
+ }
+ return t('files_sharing', 'Share with {user} on remote server {server}', { user, server })
+ }
+ case ShareType.RemoteGroup:
+ return t('files_sharing', 'Share with remote group')
+ case ShareType.Guest:
+ return t('files_sharing', 'Share with guest')
+ default: {
+ if (this.share.id) {
+ // Share already exists
+ return t('files_sharing', 'Update share')
+ } else {
+ return t('files_sharing', 'Create share')
+ }
+ }
+ }
+ },
+ allPermissions() {
+ return this.isFolder ? this.bundledPermissions.ALL.toString() : this.bundledPermissions.ALL_FILE.toString()
+ },
+ /**
+ * Can the sharee edit the shared file ?
+ */
+ canEdit: {
+ get() {
+ return this.share.hasUpdatePermission
+ },
+ set(checked) {
+ this.updateAtomicPermissions({ isEditChecked: checked })
+ },
+ },
+ /**
+ * Can the sharee create the shared file ?
+ */
+ canCreate: {
+ get() {
+ return this.share.hasCreatePermission
+ },
+ set(checked) {
+ this.updateAtomicPermissions({ isCreateChecked: checked })
+ },
+ },
+ /**
+ * Can the sharee delete the shared file ?
+ */
+ canDelete: {
+ get() {
+ return this.share.hasDeletePermission
+ },
+ set(checked) {
+ this.updateAtomicPermissions({ isDeleteChecked: checked })
+ },
+ },
+ /**
+ * Can the sharee reshare the file ?
+ */
+ canReshare: {
+ get() {
+ return this.share.hasSharePermission
+ },
+ set(checked) {
+ this.updateAtomicPermissions({ isReshareChecked: checked })
+ },
+ },
+
+ /**
+ * Change the default view for public shares from "list" to "grid"
+ */
+ showInGridView: {
+ get() {
+ return this.getShareAttribute('config', 'grid_view', false)
+ },
+ /** @param {boolean} value If the default view should be changed to "grid" */
+ set(value) {
+ this.setShareAttribute('config', 'grid_view', value)
+ },
+ },
+
+ /**
+ * Can the sharee download files or only view them ?
+ */
+ canDownload: {
+ get() {
+ return this.getShareAttribute('permissions', 'download', true)
+ },
+ set(checked) {
+ this.setShareAttribute('permissions', 'download', checked)
+ },
+ },
+ /**
+ * Is this share readable
+ * Needed for some federated shares that might have been added from file requests links
+ */
+ hasRead: {
+ get() {
+ return this.share.hasReadPermission
+ },
+ set(checked) {
+ this.updateAtomicPermissions({ isReadChecked: checked })
+ },
+ },
+ /**
+ * Does the current share have an expiration date
+ *
+ * @return {boolean}
+ */
+ hasExpirationDate: {
+ get() {
+ return this.isValidShareAttribute(this.share.expireDate)
+ },
+ set(enabled) {
+ this.share.expireDate = enabled
+ ? this.formatDateToString(this.defaultExpiryDate)
+ : ''
+ },
+ },
+ /**
+ * Is the current share a folder ?
+ *
+ * @return {boolean}
+ */
+ isFolder() {
+ return this.fileInfo.type === 'dir'
+ },
+ /**
+ * @return {boolean}
+ */
+ isSetDownloadButtonVisible() {
+ const allowedMimetypes = [
+ // Office documents
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.ms-powerpoint',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'application/vnd.ms-excel',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.oasis.opendocument.text',
+ 'application/vnd.oasis.opendocument.spreadsheet',
+ 'application/vnd.oasis.opendocument.presentation',
+ ]
+
+ return this.isFolder || allowedMimetypes.includes(this.fileInfo.mimetype)
+ },
+ isPasswordEnforced() {
+ return this.isPublicShare && this.config.enforcePasswordForPublicLink
+ },
+ defaultExpiryDate() {
+ if ((this.isGroupShare || this.isUserShare) && this.config.isDefaultInternalExpireDateEnabled) {
+ return new Date(this.config.defaultInternalExpirationDate)
+ } else if (this.isRemoteShare && this.config.isDefaultRemoteExpireDateEnabled) {
+ return new Date(this.config.defaultRemoteExpireDateEnabled)
+ } else if (this.isPublicShare && this.config.isDefaultExpireDateEnabled) {
+ return new Date(this.config.defaultExpirationDate)
+ }
+ return new Date(new Date().setDate(new Date().getDate() + 1))
+ },
+ isUserShare() {
+ return this.share.type === ShareType.User
+ },
+ isGroupShare() {
+ return this.share.type === ShareType.Group
+ },
+ allowsFileDrop() {
+ if (this.isFolder && this.config.isPublicUploadEnabled) {
+ if (this.share.type === ShareType.Link || this.share.type === ShareType.Email) {
+ return true
+ }
+ }
+ return false
+ },
+ hasFileDropPermissions() {
+ return this.share.permissions === this.bundledPermissions.FILE_DROP
+ },
+ shareButtonText() {
+ if (this.isNewShare) {
+ return t('files_sharing', 'Save share')
+ }
+ return t('files_sharing', 'Update share')
+
+ },
+ resharingIsPossible() {
+ return this.config.isResharingAllowed && this.share.type !== ShareType.Link && this.share.type !== ShareType.Email
+ },
+ /**
+ * Can the sharer set whether the sharee can edit the file ?
+ *
+ * @return {boolean}
+ */
+ canSetEdit() {
+ // If the owner revoked the permission after the resharer granted it
+ // the share still has the permission, and the resharer is still
+ // allowed to revoke it too (but not to grant it again).
+ return (this.fileInfo.sharePermissions & OC.PERMISSION_UPDATE) || this.canEdit
+ },
+
+ /**
+ * Can the sharer set whether the sharee can create the file ?
+ *
+ * @return {boolean}
+ */
+ canSetCreate() {
+ // If the owner revoked the permission after the resharer granted it
+ // the share still has the permission, and the resharer is still
+ // allowed to revoke it too (but not to grant it again).
+ return (this.fileInfo.sharePermissions & OC.PERMISSION_CREATE) || this.canCreate
+ },
+
+ /**
+ * Can the sharer set whether the sharee can delete the file ?
+ *
+ * @return {boolean}
+ */
+ canSetDelete() {
+ // If the owner revoked the permission after the resharer granted it
+ // the share still has the permission, and the resharer is still
+ // allowed to revoke it too (but not to grant it again).
+ return (this.fileInfo.sharePermissions & OC.PERMISSION_DELETE) || this.canDelete
+ },
+ /**
+ * Can the sharer set whether the sharee can reshare the file ?
+ *
+ * @return {boolean}
+ */
+ canSetReshare() {
+ // If the owner revoked the permission after the resharer granted it
+ // the share still has the permission, and the resharer is still
+ // allowed to revoke it too (but not to grant it again).
+ return (this.fileInfo.sharePermissions & OC.PERMISSION_SHARE) || this.canReshare
+ },
+ /**
+ * Can the sharer set whether the sharee can download the file ?
+ *
+ * @return {boolean}
+ */
+ canSetDownload() {
+ // If the owner revoked the permission after the resharer granted it
+ // the share still has the permission, and the resharer is still
+ // allowed to revoke it too (but not to grant it again).
+ return (this.fileInfo.canDownload() || this.canDownload)
+ },
+ canRemoveReadPermission() {
+ return this.allowsFileDrop && (
+ this.share.type === ShareType.Link
+ || this.share.type === ShareType.Email
+ )
+ },
+ // if newPassword exists, but is empty, it means
+ // the user deleted the original password
+ hasUnsavedPassword() {
+ return this.share.newPassword !== undefined
+ },
+ passwordExpirationTime() {
+ if (!this.isValidShareAttribute(this.share.passwordExpirationTime)) {
+ return null
+ }
+
+ const expirationTime = moment(this.share.passwordExpirationTime)
+
+ if (expirationTime.diff(moment()) < 0) {
+ return false
+ }
+
+ return expirationTime.fromNow()
+ },
+
+ /**
+ * Is Talk enabled?
+ *
+ * @return {boolean}
+ */
+ isTalkEnabled() {
+ return OC.appswebroots.spreed !== undefined
+ },
+
+ /**
+ * Is it possible to protect the password by Talk?
+ *
+ * @return {boolean}
+ */
+ isPasswordProtectedByTalkAvailable() {
+ return this.isPasswordProtected && this.isTalkEnabled
+ },
+ /**
+ * Is the current share password protected by Talk?
+ *
+ * @return {boolean}
+ */
+ isPasswordProtectedByTalk: {
+ get() {
+ return this.share.sendPasswordByTalk
+ },
+ async set(enabled) {
+ this.share.sendPasswordByTalk = enabled
+ },
+ },
+ /**
+ * Is the current share an email share ?
+ *
+ * @return {boolean}
+ */
+ isEmailShareType() {
+ return this.share
+ ? this.share.type === ShareType.Email
+ : false
+ },
+ canTogglePasswordProtectedByTalkAvailable() {
+ if (!this.isPublicShare || !this.isPasswordProtected) {
+ // Makes no sense
+ return false
+ } else if (this.isEmailShareType && !this.hasUnsavedPassword) {
+ // For email shares we need a new password in order to enable or
+ // disable
+ return false
+ }
+
+ // Is Talk enabled?
+ return OC.appswebroots.spreed !== undefined
+ },
+ canChangeHideDownload() {
+ const hasDisabledDownload = (shareAttribute) => shareAttribute.key === 'download' && shareAttribute.scope === 'permissions' && shareAttribute.value === false
+ return this.fileInfo.shareAttributes.some(hasDisabledDownload)
+ },
+ customPermissionsList() {
+ // Key order will be different, because ATOMIC_PERMISSIONS are numbers
+ const translatedPermissions = {
+ [ATOMIC_PERMISSIONS.READ]: this.t('files_sharing', 'Read'),
+ [ATOMIC_PERMISSIONS.CREATE]: this.t('files_sharing', 'Create'),
+ [ATOMIC_PERMISSIONS.UPDATE]: this.t('files_sharing', 'Edit'),
+ [ATOMIC_PERMISSIONS.SHARE]: this.t('files_sharing', 'Share'),
+ [ATOMIC_PERMISSIONS.DELETE]: this.t('files_sharing', 'Delete'),
+ }
+
+ const permissionsList = [
+ ATOMIC_PERMISSIONS.READ,
+ ...(this.isFolder ? [ATOMIC_PERMISSIONS.CREATE] : []),
+ ATOMIC_PERMISSIONS.UPDATE,
+ ...(this.resharingIsPossible ? [ATOMIC_PERMISSIONS.SHARE] : []),
+ ...(this.isFolder ? [ATOMIC_PERMISSIONS.DELETE] : []),
+ ]
+
+ return permissionsList.filter((permission) => hasPermissions(this.share.permissions, permission))
+ .map((permission, index) => index === 0
+ ? translatedPermissions[permission]
+ : translatedPermissions[permission].toLocaleLowerCase(getLanguage()))
+ .join(', ')
+ },
+ advancedControlExpandedValue() {
+ return this.advancedSectionAccordionExpanded ? 'true' : 'false'
+ },
+ errorPasswordLabel() {
+ if (this.passwordError) {
+ return t('files_sharing', 'Password field cannot be empty')
+ }
+ return undefined
+ },
+
+ passwordHint() {
+ if (this.isNewShare || this.hasUnsavedPassword) {
+ return undefined
+ }
+ return t('files_sharing', 'Replace current password')
+ },
+
+ /**
+ * Additional actions for the menu
+ *
+ * @return {Array}
+ */
+ externalLinkActions() {
+ const filterValidAction = (action) => (action.shareType.includes(ShareType.Link) || action.shareType.includes(ShareType.Email)) && action.advanced
+ // filter only the advanced registered actions for said link
+ return this.ExternalShareActions.actions
+ .filter(filterValidAction)
+ },
+ },
+ watch: {
+ setCustomPermissions(isChecked) {
+ if (isChecked) {
+ this.sharingPermission = 'custom'
+ } else {
+ this.sharingPermission = this.revertSharingPermission
+ }
+ },
+ },
+ beforeMount() {
+ this.initializePermissions()
+ this.initializeAttributes()
+ logger.debug('Share object received', { share: this.share })
+ logger.debug('Configuration object received', { config: this.config })
+ },
+
+ mounted() {
+ this.$refs.quickPermissions?.querySelector('input:checked')?.focus()
+ },
+
+ methods: {
+ /**
+ * Set a share attribute on the current share
+ * @param {string} scope The attribute scope
+ * @param {string} key The attribute key
+ * @param {boolean} value The value
+ */
+ setShareAttribute(scope, key, value) {
+ if (!this.share.attributes) {
+ this.$set(this.share, 'attributes', [])
+ }
+
+ const attribute = this.share.attributes
+ .find((attr) => attr.scope === scope || attr.key === key)
+
+ if (attribute) {
+ attribute.value = value
+ } else {
+ this.share.attributes.push({
+ scope,
+ key,
+ value,
+ })
+ }
+ },
+
+ /**
+ * Get the value of a share attribute
+ * @param {string} scope The attribute scope
+ * @param {string} key The attribute key
+ * @param {undefined|boolean} fallback The fallback to return if not found
+ */
+ getShareAttribute(scope, key, fallback = undefined) {
+ const attribute = this.share.attributes?.find((attr) => attr.scope === scope && attr.key === key)
+ return attribute?.value ?? fallback
+ },
+
+ async generateNewToken() {
+ if (this.loadingToken) {
+ return
+ }
+ this.loadingToken = true
+ try {
+ this.share.token = await generateToken()
+ } catch (error) {
+ showError(t('files_sharing', 'Failed to generate a new token'))
+ }
+ this.loadingToken = false
+ },
+
+ cancel() {
+ this.share.token = this.initialToken
+ this.$emit('close-sharing-details')
+ },
+
+ updateAtomicPermissions({
+ isReadChecked = this.hasRead,
+ isEditChecked = this.canEdit,
+ isCreateChecked = this.canCreate,
+ isDeleteChecked = this.canDelete,
+ isReshareChecked = this.canReshare,
+ } = {}) {
+ // calc permissions if checked
+
+ if (!this.isFolder && (isCreateChecked || isDeleteChecked)) {
+ logger.debug('Ignoring create/delete permissions for file share — only available for folders')
+ isCreateChecked = false
+ isDeleteChecked = false
+ }
+
+ const permissions = 0
+ | (isReadChecked ? ATOMIC_PERMISSIONS.READ : 0)
+ | (isCreateChecked ? ATOMIC_PERMISSIONS.CREATE : 0)
+ | (isDeleteChecked ? ATOMIC_PERMISSIONS.DELETE : 0)
+ | (isEditChecked ? ATOMIC_PERMISSIONS.UPDATE : 0)
+ | (isReshareChecked ? ATOMIC_PERMISSIONS.SHARE : 0)
+ this.share.permissions = permissions
+ },
+ expandCustomPermissions() {
+ if (!this.advancedSectionAccordionExpanded) {
+ this.advancedSectionAccordionExpanded = true
+ }
+ this.toggleCustomPermissions()
+ },
+ toggleCustomPermissions(selectedPermission) {
+ const isCustomPermissions = this.sharingPermission === 'custom'
+ this.revertSharingPermission = !isCustomPermissions ? selectedPermission : 'custom'
+ this.setCustomPermissions = isCustomPermissions
+ },
+ async initializeAttributes() {
+
+ if (this.isNewShare) {
+ if ((this.config.enableLinkPasswordByDefault || this.isPasswordEnforced) && this.isPublicShare) {
+ this.$set(this.share, 'newPassword', await GeneratePassword(true))
+ this.advancedSectionAccordionExpanded = true
+ }
+ /* Set default expiration dates if configured */
+ if (this.isPublicShare && this.config.isDefaultExpireDateEnabled) {
+ this.share.expireDate = this.config.defaultExpirationDate.toDateString()
+ } else if (this.isRemoteShare && this.config.isDefaultRemoteExpireDateEnabled) {
+ this.share.expireDate = this.config.defaultRemoteExpirationDateString.toDateString()
+ } else if (this.config.isDefaultInternalExpireDateEnabled) {
+ this.share.expireDate = this.config.defaultInternalExpirationDate.toDateString()
+ }
+
+ if (this.isValidShareAttribute(this.share.expireDate)) {
+ this.advancedSectionAccordionExpanded = true
+ }
+
+ return
+ }
+
+ // If there is an enforced expiry date, then existing shares created before enforcement
+ // have no expiry date, hence we set it here.
+ if (!this.isValidShareAttribute(this.share.expireDate) && this.isExpiryDateEnforced) {
+ this.hasExpirationDate = true
+ }
+
+ if (
+ this.isValidShareAttribute(this.share.password)
+ || this.isValidShareAttribute(this.share.expireDate)
+ || this.isValidShareAttribute(this.share.label)
+ ) {
+ this.advancedSectionAccordionExpanded = true
+ }
+
+ if (this.isValidShareAttribute(this.share.note)) {
+ this.writeNoteToRecipientIsChecked = true
+ this.advancedSectionAccordionExpanded = true
+ }
+
+ },
+ handleShareType() {
+ if ('shareType' in this.share) {
+ this.share.type = this.share.shareType
+ } else if (this.share.share_type) {
+ this.share.type = this.share.share_type
+ }
+ },
+ handleDefaultPermissions() {
+ if (this.isNewShare) {
+ const defaultPermissions = this.config.defaultPermissions
+ if (defaultPermissions === BUNDLED_PERMISSIONS.READ_ONLY || defaultPermissions === BUNDLED_PERMISSIONS.ALL) {
+ this.sharingPermission = defaultPermissions.toString()
+ } else {
+ this.sharingPermission = 'custom'
+ this.share.permissions = defaultPermissions
+ this.advancedSectionAccordionExpanded = true
+ this.setCustomPermissions = true
+ }
+ }
+ // Read permission required for share creation
+ if (!this.canRemoveReadPermission) {
+ this.hasRead = true
+ }
+ },
+ handleCustomPermissions() {
+ if (!this.isNewShare && (this.hasCustomPermissions || this.share.setCustomPermissions)) {
+ this.sharingPermission = 'custom'
+ this.advancedSectionAccordionExpanded = true
+ this.setCustomPermissions = true
+ } else if (this.share.permissions) {
+ this.sharingPermission = this.share.permissions.toString()
+ }
+ },
+ initializePermissions() {
+ this.handleShareType()
+ this.handleDefaultPermissions()
+ this.handleCustomPermissions()
+ },
+ async saveShare() {
+ const permissionsAndAttributes = ['permissions', 'attributes', 'note', 'expireDate']
+ const publicShareAttributes = ['label', 'password', 'hideDownload']
+ if (this.config.allowCustomTokens) {
+ publicShareAttributes.push('token')
+ }
+ if (this.isPublicShare) {
+ permissionsAndAttributes.push(...publicShareAttributes)
+ }
+ const sharePermissionsSet = parseInt(this.sharingPermission)
+ if (this.setCustomPermissions) {
+ this.updateAtomicPermissions()
+ } else {
+ this.share.permissions = sharePermissionsSet
+ }
+
+ if (!this.isFolder && this.share.permissions === BUNDLED_PERMISSIONS.ALL) {
+ // It's not possible to create an existing file.
+ this.share.permissions = BUNDLED_PERMISSIONS.ALL_FILE
+ }
+ if (!this.writeNoteToRecipientIsChecked) {
+ this.share.note = ''
+ }
+ if (this.isPasswordProtected) {
+ if (this.isPasswordEnforced && this.isNewShare && !this.isValidShareAttribute(this.share.password)) {
+ this.passwordError = true
+ }
+ } else {
+ this.share.password = ''
+ }
+
+ if (!this.hasExpirationDate) {
+ this.share.expireDate = ''
+ }
+
+ if (this.isNewShare) {
+ const incomingShare = {
+ permissions: this.share.permissions,
+ shareType: this.share.type,
+ shareWith: this.share.shareWith,
+ attributes: this.share.attributes,
+ note: this.share.note,
+ fileInfo: this.fileInfo,
+ }
+
+ incomingShare.expireDate = this.hasExpirationDate ? this.share.expireDate : ''
+
+ if (this.isPasswordProtected) {
+ incomingShare.password = this.share.newPassword
+ }
+
+ let share
+ try {
+ this.creating = true
+ share = await this.addShare(incomingShare)
+ } catch (error) {
+ this.creating = false
+ // Error is already handled by ShareRequests mixin
+ return
+ }
+
+ // ugly hack to make code work - we need the id to be set but at the same time we need to keep values we want to update
+ this.share._share.id = share.id
+ await this.queueUpdate(...permissionsAndAttributes)
+ // Also a ugly hack to update the updated permissions
+ for (const prop of permissionsAndAttributes) {
+ if (prop in share && prop in this.share) {
+ try {
+ share[prop] = this.share[prop]
+ } catch {
+ share._share[prop] = this.share[prop]
+ }
+ }
+ }
+
+ this.share = share
+ this.creating = false
+ this.$emit('add:share', this.share)
+ } else {
+ // Let's update after creation as some attrs are only available after creation
+ await this.queueUpdate(...permissionsAndAttributes)
+ this.$emit('update:share', this.share)
+ }
+
+ await this.getNode()
+ emit('files:node:updated', this.node)
+
+ if (this.$refs.externalLinkActions?.length > 0) {
+ await Promise.allSettled(this.$refs.externalLinkActions.map((action) => {
+ if (typeof action.$children.at(0)?.onSave !== 'function') {
+ return Promise.resolve()
+ }
+ return action.$children.at(0)?.onSave?.()
+ }))
+ }
+
+ this.$emit('close-sharing-details')
+ },
+ /**
+ * Process the new share request
+ *
+ * @param {Share} share incoming share object
+ */
+ async addShare(share) {
+ logger.debug('Adding a new share from the input for', { share })
+ const path = this.path
+ try {
+ const resultingShare = await this.createShare({
+ path,
+ shareType: share.shareType,
+ shareWith: share.shareWith,
+ permissions: share.permissions,
+ expireDate: share.expireDate,
+ attributes: JSON.stringify(share.attributes),
+ ...(share.note ? { note: share.note } : {}),
+ ...(share.password ? { password: share.password } : {}),
+ })
+ return resultingShare
+ } catch (error) {
+ logger.error('Error while adding new share', { error })
+ } finally {
+ // this.loading = false // No loader here yet
+ }
+ },
+ async removeShare() {
+ await this.onDelete()
+ await this.getNode()
+ emit('files:node:updated', this.node)
+ this.$emit('close-sharing-details')
+ },
+ /**
+ * Update newPassword values
+ * of share. If password is set but not newPassword
+ * then the user did not changed the password
+ * If both co-exists, the password have changed and
+ * we show it in plain text.
+ * Then on submit (or menu close), we sync it.
+ *
+ * @param {string} password the changed password
+ */
+ onPasswordChange(password) {
+ if (password === '') {
+ this.$delete(this.share, 'newPassword')
+ this.passwordError = this.isNewShare && this.isPasswordEnforced
+ return
+ }
+ this.passwordError = !this.isValidShareAttribute(password)
+ this.$set(this.share, 'newPassword', password)
+ },
+ /**
+ * Update the password along with "sendPasswordByTalk".
+ *
+ * If the password was modified the new password is sent; otherwise
+ * updating a mail share would fail, as in that case it is required that
+ * a new password is set when enabling or disabling
+ * "sendPasswordByTalk".
+ */
+ onPasswordProtectedByTalkChange() {
+ this.queueUpdate('sendPasswordByTalk', 'password')
+ },
+ isValidShareAttribute(value) {
+ if ([null, undefined].includes(value)) {
+ return false
+ }
+
+ if (!(value.trim().length > 0)) {
+ return false
+ }
+
+ return true
+ },
+ getShareTypeIcon(type) {
+ switch (type) {
+ case ShareType.Link:
+ return LinkIcon
+ case ShareType.Guest:
+ return UserIcon
+ case ShareType.RemoteGroup:
+ case ShareType.Group:
+ return GroupIcon
+ case ShareType.Email:
+ return EmailIcon
+ case ShareType.Team:
+ return CircleIcon
+ case ShareType.Room:
+ return ShareIcon
+ case ShareType.Deck:
+ return ShareIcon
+ case ShareType.ScienceMesh:
+ return ShareIcon
+ default:
+ return null // Or a default icon component if needed
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.sharingTabDetailsView {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ margin: 0 auto;
+ position: relative;
+ height: 100%;
+ overflow: hidden;
+
+ &__header {
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ margin: 0.2em;
+
+ span {
+ display: flex;
+ align-items: center;
+
+ h1 {
+ font-size: 15px;
+ padding-inline-start: 0.3em;
+ }
+
+ }
+ }
+
+ &__wrapper {
+ position: relative;
+ overflow: scroll;
+ flex-shrink: 1;
+ padding: 4px;
+ padding-inline-end: 12px;
+ }
+
+ &__quick-permissions {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ margin: 0 auto;
+ border-radius: 0;
+
+ div {
+ width: 100%;
+
+ span {
+ width: 100%;
+
+ span:nth-child(1) {
+ align-items: center;
+ justify-content: center;
+ padding: 0.1em;
+ }
+
+ :deep(label span) {
+ display: flex;
+ flex-direction: column;
+ }
+
+ /* Target component based style in NcCheckboxRadioSwitch slot content*/
+ :deep(span.checkbox-content__text.checkbox-radio-switch__text) {
+ flex-wrap: wrap;
+
+ .subline {
+ display: block;
+ flex-basis: 100%;
+ }
+ }
+ }
+
+ }
+ }
+
+ &__advanced-control {
+ width: 100%;
+
+ button {
+ margin-top: 0.5em;
+ }
+
+ }
+
+ &__advanced {
+ width: 100%;
+ margin-bottom: 0.5em;
+ text-align: start;
+ padding-inline-start: 0;
+
+ section {
+
+ textarea,
+ div.mx-datepicker {
+ width: 100%;
+ }
+
+ textarea {
+ height: 80px;
+ margin: 0;
+ }
+
+ /*
+ The following style is applied out of the component's scope
+ to remove padding from the label.checkbox-radio-switch__label,
+ which is used to group radio checkbox items. The use of ::v-deep
+ ensures that the padding is modified without being affected by
+ the component's scoping.
+ Without this achieving left alignment for the checkboxes would not
+ be possible.
+ */
+ span :deep(label) {
+ padding-inline-start: 0 !important;
+ background-color: initial !important;
+ border: none !important;
+ }
+
+ section.custom-permissions-group {
+ padding-inline-start: 1.5em;
+ }
+ }
+ }
+
+ &__label {
+ padding-block-end: 6px;
+ }
+
+ &__delete {
+ > button:first-child {
+ color: rgb(223, 7, 7);
+ }
+ }
+
+ &__footer {
+ width: 100%;
+ display: flex;
+ position: sticky;
+ bottom: 0;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-start;
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--color-main-background));
+
+ .button-group {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ margin-top: 16px;
+
+ button {
+ margin-inline-start: 16px;
+
+ &:first-child {
+ margin-inline-start: 0;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/apps/files_sharing/src/views/SharingInherited.vue b/apps/files_sharing/src/views/SharingInherited.vue
index b570b47e257..809de522d93 100644
--- a/apps/files_sharing/src/views/SharingInherited.vue
+++ b/apps/files_sharing/src/views/SharingInherited.vue
@@ -1,27 +1,10 @@
<!--
- - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <ul id="sharing-inherited-shares">
+ <ul v-if="shares.length" id="sharing-inherited-shares">
<!-- Main collapsible entry -->
<SharingEntrySimple class="sharing-entry__inherited"
:title="mainTitle"
@@ -47,12 +30,12 @@
<script>
import { generateOcsUrl } from '@nextcloud/router'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import axios from '@nextcloud/axios'
-import Share from '../models/Share'
-import SharingEntryInherited from '../components/SharingEntryInherited'
-import SharingEntrySimple from '../components/SharingEntrySimple'
+import Share from '../models/Share.ts'
+import SharingEntryInherited from '../components/SharingEntryInherited.vue'
+import SharingEntrySimple from '../components/SharingEntrySimple.vue'
export default {
name: 'SharingInherited',
@@ -94,7 +77,7 @@ export default {
},
subTitle() {
return (this.showInheritedShares && this.shares.length === 0)
- ? t('files_sharing', 'No other users with access found')
+ ? t('files_sharing', 'No other accounts with access found')
: ''
},
toggleTooltip() {
diff --git a/apps/files_sharing/src/views/SharingLinkList.vue b/apps/files_sharing/src/views/SharingLinkList.vue
index ee7418c00d5..c3d9a7f83dc 100644
--- a/apps/files_sharing/src/views/SharingLinkList.vue
+++ b/apps/files_sharing/src/views/SharingLinkList.vue
@@ -1,33 +1,12 @@
<!--
- - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <ul v-if="canLinkShare" class="sharing-link-list">
- <!-- If no link shares, show the add link default entry -->
- <SharingEntryLink v-if="!hasLinkShares && canReshare"
- :can-reshare="canReshare"
- :file-info="fileInfo"
- @add:share="addShare" />
-
+ <ul v-if="canLinkShare"
+ :aria-label="t('files_sharing', 'Link shares')"
+ class="sharing-link-list">
<!-- Else we display the list -->
<template v-if="hasShares">
<!-- using shares[index] to work with .sync -->
@@ -39,16 +18,27 @@
:file-info="fileInfo"
@add:share="addShare(...arguments)"
@update:share="awaitForShare(...arguments)"
- @remove:share="removeShare" />
+ @remove:share="removeShare"
+ @open-sharing-details="openSharingDetails(share)" />
</template>
+
+ <!-- If no link shares, show the add link default entry -->
+ <SharingEntryLink v-if="!hasLinkShares && canReshare"
+ :can-reshare="canReshare"
+ :file-info="fileInfo"
+ @add:share="addShare" />
</ul>
</template>
<script>
-// eslint-disable-next-line no-unused-vars
-import Share from '../models/Share'
-import ShareTypes from '../mixins/ShareTypes'
-import SharingEntryLink from '../components/SharingEntryLink'
+import { getCapabilities } from '@nextcloud/capabilities'
+
+import { t } from '@nextcloud/l10n'
+
+import Share from '../models/Share.js'
+import SharingEntryLink from '../components/SharingEntryLink.vue'
+import ShareDetails from '../mixins/ShareDetails.js'
+import { ShareType } from '@nextcloud/sharing'
export default {
name: 'SharingLinkList',
@@ -57,7 +47,7 @@ export default {
SharingEntryLink,
},
- mixins: [ShareTypes],
+ mixins: [ShareDetails],
props: {
fileInfo: {
@@ -78,7 +68,7 @@ export default {
data() {
return {
- canLinkShare: OC.getCapabilities().files_sharing.public.enabled,
+ canLinkShare: getCapabilities().files_sharing.public.enabled,
}
},
@@ -91,7 +81,7 @@ export default {
* @return {Array}
*/
hasLinkShares() {
- return this.shares.filter(share => share.type === this.SHARE_TYPES.SHARE_TYPE_LINK).length > 0
+ return this.shares.filter(share => share.type === ShareType.Link).length > 0
},
/**
@@ -105,6 +95,8 @@ export default {
},
methods: {
+ t,
+
/**
* Add a new share into the link shares list
* and return the newly created share component
@@ -114,7 +106,7 @@ export default {
*/
addShare(share, resolve) {
// eslint-disable-next-line vue/no-mutating-props
- this.shares.unshift(share)
+ this.shares.push(share)
this.awaitForShare(share, resolve)
},
diff --git a/apps/files_sharing/src/views/SharingList.vue b/apps/files_sharing/src/views/SharingList.vue
index 0635ad27635..2167059772e 100644
--- a/apps/files_sharing/src/views/SharingList.vue
+++ b/apps/files_sharing/src/views/SharingList.vue
@@ -1,41 +1,24 @@
<!--
- - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <ul class="sharing-sharee-list">
+ <ul class="sharing-sharee-list" :aria-label="t('files_sharing', 'Shares')">
<SharingEntry v-for="share in shares"
:key="share.id"
:file-info="fileInfo"
:share="share"
:is-unique="isUnique(share)"
- @remove:share="removeShare" />
+ @open-sharing-details="openSharingDetails(share)" />
</ul>
</template>
<script>
-// eslint-disable-next-line no-unused-vars
-import Share from '../models/Share'
-import SharingEntry from '../components/SharingEntry'
-import ShareTypes from '../mixins/ShareTypes'
+import { t } from '@nextcloud/l10n'
+import SharingEntry from '../components/SharingEntry.vue'
+import ShareDetails from '../mixins/ShareDetails.js'
+import { ShareType } from '@nextcloud/sharing'
export default {
name: 'SharingList',
@@ -44,12 +27,12 @@ export default {
SharingEntry,
},
- mixins: [ShareTypes],
+ mixins: [ShareDetails],
props: {
fileInfo: {
type: Object,
- default: () => {},
+ default: () => { },
required: true,
},
shares: {
@@ -59,6 +42,11 @@ export default {
},
},
+ setup() {
+ return {
+ t,
+ }
+ },
computed: {
hasShares() {
return this.shares.length === 0
@@ -66,23 +54,10 @@ export default {
isUnique() {
return (share) => {
return [...this.shares].filter((item) => {
- return share.type === this.SHARE_TYPES.SHARE_TYPE_USER && share.shareWithDisplayName === item.shareWithDisplayName
+ return share.type === ShareType.User && share.shareWithDisplayName === item.shareWithDisplayName
}).length <= 1
}
},
},
-
- methods: {
- /**
- * Remove a share from the shares list
- *
- * @param {Share} share the share to remove
- */
- removeShare(share) {
- const index = this.shares.findIndex(item => item === share)
- // eslint-disable-next-line vue/no-mutating-props
- this.shares.splice(index, 1)
- },
- },
}
</script>
diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue
index f7920346981..2ed44a4b5ad 100644
--- a/apps/files_sharing/src/views/SharingTab.vue
+++ b/apps/files_sharing/src/views/SharingTab.vue
@@ -1,27 +1,10 @@
<!--
- - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <div :class="{ 'icon-loading': loading }">
+ <div class="sharingTab" :class="{ 'icon-loading': loading }">
<!-- error message -->
<div v-if="error" class="emptycontent" :class="{ emptyContentWithSections: sections.length > 0 }">
<div class="icon icon-error" />
@@ -29,100 +12,204 @@
</div>
<!-- shares content -->
- <div v-else class="sharingTab__content">
+ <div v-show="!showSharingDetailsView"
+ class="sharingTab__content">
<!-- shared with me information -->
- <SharingEntrySimple v-if="isSharedWithMe" v-bind="sharedWithMe" class="sharing-entry__reshare">
- <template #avatar>
- <NcAvatar :user="sharedWithMe.user"
- :title="sharedWithMe.displayName"
- class="sharing-entry__avatar" />
- </template>
- </SharingEntrySimple>
-
- <!-- add new share input -->
- <SharingInput v-if="!loading"
- :can-reshare="canReshare"
- :file-info="fileInfo"
- :link-shares="linkShares"
- :reshare="reshare"
- :shares="shares"
- @add:share="addShare" />
-
- <!-- link shares list -->
- <SharingLinkList v-if="!loading"
- ref="linkShareList"
- :can-reshare="canReshare"
- :file-info="fileInfo"
- :shares="linkShares" />
-
- <!-- other shares list -->
- <SharingList v-if="!loading"
- ref="shareList"
- :shares="shares"
- :file-info="fileInfo" />
-
- <!-- inherited shares -->
- <SharingInherited v-if="canReshare && !loading" :file-info="fileInfo" />
-
- <!-- internal link copy -->
- <SharingEntryInternal :file-info="fileInfo" />
-
- <!-- projects -->
- <CollectionList v-if="projectsEnabled && fileInfo"
- :id="`${fileInfo.id}`"
- type="file"
- :name="fileInfo.name" />
+ <ul v-if="isSharedWithMe">
+ <SharingEntrySimple v-bind="sharedWithMe" class="sharing-entry__reshare">
+ <template #avatar>
+ <NcAvatar :user="sharedWithMe.user"
+ :display-name="sharedWithMe.displayName"
+ class="sharing-entry__avatar" />
+ </template>
+ </SharingEntrySimple>
+ </ul>
+
+ <section>
+ <div class="section-header">
+ <h4>{{ t('files_sharing', 'Internal shares') }}</h4>
+ <NcPopover popup-role="dialog">
+ <template #trigger>
+ <NcButton class="hint-icon"
+ type="tertiary-no-background"
+ :aria-label="t('files_sharing', 'Internal shares explanation')">
+ <template #icon>
+ <InfoIcon :size="20" />
+ </template>
+ </NcButton>
+ </template>
+ <p class="hint-body">
+ {{ internalSharesHelpText }}
+ </p>
+ </NcPopover>
+ </div>
+ <!-- add new share input -->
+ <SharingInput v-if="!loading"
+ :can-reshare="canReshare"
+ :file-info="fileInfo"
+ :link-shares="linkShares"
+ :reshare="reshare"
+ :shares="shares"
+ :placeholder="internalShareInputPlaceholder"
+ @open-sharing-details="toggleShareDetailsView" />
+
+ <!-- other shares list -->
+ <SharingList v-if="!loading"
+ ref="shareList"
+ :shares="shares"
+ :file-info="fileInfo"
+ @open-sharing-details="toggleShareDetailsView" />
+
+ <!-- inherited shares -->
+ <SharingInherited v-if="canReshare && !loading" :file-info="fileInfo" />
+
+ <!-- internal link copy -->
+ <SharingEntryInternal :file-info="fileInfo" />
+ </section>
+
+ <section>
+ <div class="section-header">
+ <h4>{{ t('files_sharing', 'External shares') }}</h4>
+ <NcPopover popup-role="dialog">
+ <template #trigger>
+ <NcButton class="hint-icon"
+ type="tertiary-no-background"
+ :aria-label="t('files_sharing', 'External shares explanation')">
+ <template #icon>
+ <InfoIcon :size="20" />
+ </template>
+ </NcButton>
+ </template>
+ <p class="hint-body">
+ {{ externalSharesHelpText }}
+ </p>
+ </NcPopover>
+ </div>
+ <SharingInput v-if="!loading"
+ :can-reshare="canReshare"
+ :file-info="fileInfo"
+ :link-shares="linkShares"
+ :is-external="true"
+ :placeholder="externalShareInputPlaceholder"
+ :reshare="reshare"
+ :shares="shares"
+ @open-sharing-details="toggleShareDetailsView" />
+ <!-- Non link external shares list -->
+ <SharingList v-if="!loading"
+ :shares="externalShares"
+ :file-info="fileInfo"
+ @open-sharing-details="toggleShareDetailsView" />
+ <!-- link shares list -->
+ <SharingLinkList v-if="!loading && isLinkSharingAllowed"
+ ref="linkShareList"
+ :can-reshare="canReshare"
+ :file-info="fileInfo"
+ :shares="linkShares"
+ @open-sharing-details="toggleShareDetailsView" />
+ </section>
+
+ <section v-if="sections.length > 0 && !showSharingDetailsView">
+ <div class="section-header">
+ <h4>{{ t('files_sharing', 'Additional shares') }}</h4>
+ <NcPopover popup-role="dialog">
+ <template #trigger>
+ <NcButton class="hint-icon"
+ type="tertiary-no-background"
+ :aria-label="t('files_sharing', 'Additional shares explanation')">
+ <template #icon>
+ <InfoIcon :size="20" />
+ </template>
+ </NcButton>
+ </template>
+ <p class="hint-body">
+ {{ additionalSharesHelpText }}
+ </p>
+ </NcPopover>
+ </div>
+ <!-- additional entries, use it with cautious -->
+ <div v-for="(component, index) in sectionComponents"
+ :key="index"
+ class="sharingTab__additionalContent">
+ <component :is="component" :file-info="fileInfo" />
+ </div>
+
+ <!-- projects (deprecated as of NC25 (replaced by related_resources) - see instance config "projects.enabled" ; ignore this / remove it / move into own section) -->
+ <div v-if="projectsEnabled"
+ v-show="!showSharingDetailsView && fileInfo"
+ class="sharingTab__additionalContent">
+ <NcCollectionList :id="`${fileInfo.id}`"
+ type="file"
+ :name="fileInfo.name" />
+ </div>
+ </section>
</div>
- <!-- additional entries, use it with cautious -->
- <div v-for="(section, index) in sections"
- :ref="'section-' + index"
- :key="index"
- class="sharingTab__additionalContent">
- <component :is="section($refs['section-'+index], fileInfo)" :file-info="fileInfo" />
- </div>
+ <!-- share details -->
+ <SharingDetailsTab v-if="showSharingDetailsView"
+ :file-info="shareDetailsData.fileInfo"
+ :share="shareDetailsData.share"
+ @close-sharing-details="toggleShareDetailsView"
+ @add:share="addShare"
+ @remove:share="removeShare" />
</div>
</template>
<script>
-import { CollectionList } from 'nextcloud-vue-collections'
+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'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar'
+import { ShareType } from '@nextcloud/sharing'
+
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCollectionList from '@nextcloud/vue/components/NcCollectionList'
+import NcPopover from '@nextcloud/vue/components/NcPopover'
+import InfoIcon from 'vue-material-design-icons/InformationOutline.vue'
+
import axios from '@nextcloud/axios'
-import { loadState } from '@nextcloud/initial-state'
+import moment from '@nextcloud/moment'
-import Config from '../services/ConfigService'
-import { shareWithTitle } from '../utils/SharedWithMe'
-import Share from '../models/Share'
-import ShareTypes from '../mixins/ShareTypes'
-import SharingEntryInternal from '../components/SharingEntryInternal'
-import SharingEntrySimple from '../components/SharingEntrySimple'
-import SharingInput from '../components/SharingInput'
+import { shareWithTitle } from '../utils/SharedWithMe.js'
-import SharingInherited from './SharingInherited'
-import SharingLinkList from './SharingLinkList'
-import SharingList from './SharingList'
+import Config from '../services/ConfigService.ts'
+import Share from '../models/Share.ts'
+import SharingEntryInternal from '../components/SharingEntryInternal.vue'
+import SharingEntrySimple from '../components/SharingEntrySimple.vue'
+import SharingInput from '../components/SharingInput.vue'
+
+import SharingInherited from './SharingInherited.vue'
+import SharingLinkList from './SharingLinkList.vue'
+import SharingList from './SharingList.vue'
+import SharingDetailsTab from './SharingDetailsTab.vue'
+
+import ShareDetails from '../mixins/ShareDetails.js'
+import logger from '../services/logger.ts'
export default {
name: 'SharingTab',
components: {
+ InfoIcon,
NcAvatar,
- CollectionList,
+ NcButton,
+ NcCollectionList,
+ NcPopover,
SharingEntryInternal,
SharingEntrySimple,
SharingInherited,
SharingInput,
SharingLinkList,
SharingList,
+ SharingDetailsTab,
},
-
- mixins: [ShareTypes],
+ mixins: [ShareDetails],
data() {
return {
config: new Config(),
-
+ deleteEvent: null,
error: '',
expirationInterval: null,
loading: true,
@@ -134,9 +221,17 @@ export default {
sharedWithMe: {},
shares: [],
linkShares: [],
+ externalShares: [],
sections: OCA.Sharing.ShareTabSections.getSections(),
projectsEnabled: loadState('core', 'projects_enabled', false),
+ showSharingDetailsView: false,
+ shareDetailsData: {},
+ returnFocusElement: null,
+
+ internalSharesHelpText: t('files_sharing', 'Share files within your organization. Recipients who can already view the file can also use this link for easy access.'),
+ externalSharesHelpText: t('files_sharing', 'Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID.'),
+ additionalSharesHelpText: t('files_sharing', 'Shares from apps or other sources which are not included in internal or external shares.'),
}
},
@@ -147,15 +242,54 @@ export default {
* @return {boolean}
*/
isSharedWithMe() {
- return Object.keys(this.sharedWithMe).length > 0
+ return !!this.sharedWithMe?.user
+ },
+
+ /**
+ * 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 && this.config.isFederationEnabled
+ // TRANSLATORS: Type as in with a keyboard
+ ? t('files_sharing', 'Type names, teams, federated cloud IDs')
+ // TRANSLATORS: Type as in with a keyboard
+ : t('files_sharing', 'Type names or teams')
+ },
+
+ externalShareInputPlaceholder() {
+ if (!this.isLinkSharingAllowed) {
+ // TRANSLATORS: Type as in with a keyboard
+ return this.config.isFederationEnabled ? t('files_sharing', 'Type a federated cloud ID') : ''
+ }
+ return !this.config.showFederatedSharesAsInternal && !this.config.isFederationEnabled
+ // TRANSLATORS: Type as in with a keyboard
+ ? t('files_sharing', 'Type an email')
+ // TRANSLATORS: Type as in with a keyboard
+ : t('files_sharing', 'Type an email or federated cloud ID')
+ },
+
+ sectionComponents() {
+ return this.sections.map((section) => section(undefined, this.fileInfo))
+ },
+ },
methods: {
/**
* Update current fileInfo and fetch new data
@@ -167,7 +301,6 @@ export default {
this.resetState()
this.getShares()
},
-
/**
* Get the existing shares infos
*/
@@ -205,7 +338,7 @@ export default {
this.processSharedWithMe(sharedWithMe)
this.processShares(shares)
} catch (error) {
- if (error.response.data?.ocs?.meta?.message) {
+ if (error?.response?.data?.ocs?.meta?.message) {
this.error = error.response.data.ocs.meta.message
} else {
this.error = t('files_sharing', 'Unable to load the shares list')
@@ -225,6 +358,8 @@ export default {
this.sharedWithMe = {}
this.shares = []
this.linkShares = []
+ this.showSharingDetailsView = false
+ this.shareDetailsData = {}
},
/**
@@ -236,7 +371,7 @@ export default {
updateExpirationSubtitle(share) {
const expiration = moment(share.expireDate).unix()
this.$set(this.sharedWithMe, 'subtitle', t('files_sharing', 'Expires {relativetime}', {
- relativetime: OC.Util.relativeModifiedDate(expiration * 1000),
+ relativetime: moment(expiration * 1000).fromNow(),
}))
// share have expired
@@ -256,16 +391,41 @@ export default {
*/
processShares({ data }) {
if (data.ocs && data.ocs.data && data.ocs.data.length > 0) {
- // create Share objects and sort by newest
- const shares = data.ocs.data
- .map(share => new Share(share))
- .sort((a, b) => b.createdTime - a.createdTime)
-
- this.linkShares = shares.filter(share => share.type === this.SHARE_TYPES.SHARE_TYPE_LINK || share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL)
- this.shares = shares.filter(share => share.type !== this.SHARE_TYPES.SHARE_TYPE_LINK && share.type !== this.SHARE_TYPES.SHARE_TYPE_EMAIL)
+ const shares = orderBy(
+ data.ocs.data.map(share => new Share(share)),
+ [
+ // First order by the "share with" label
+ (share) => share.shareWithDisplayName,
+ // Then by the label
+ (share) => share.label,
+ // And last resort order by createdTime
+ (share) => share.createdTime,
+ ],
+ )
+
+ for (const share of shares) {
+ if ([ShareType.Link, ShareType.Email].includes(share.type)) {
+ this.linkShares.push(share)
+ } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) {
+ if (this.config.showFederatedSharesToTrustedServersAsInternal) {
+ if (share.isTrustedServer) {
+ this.shares.push(share)
+ } else {
+ this.externalShares.push(share)
+ }
+ } else if (this.config.showFederatedSharesAsInternal) {
+ this.shares.push(share)
+ } else {
+ this.externalShares.push(share)
+ }
+ } else {
+ this.shares.push(share)
+ }
+ }
- console.debug('Processed', this.linkShares.length, 'link share(s)')
- console.debug('Processed', this.shares.length, 'share(s)')
+ logger.debug(`Processed ${this.linkShares.length} link share(s)`)
+ logger.debug(`Processed ${this.shares.length} share(s)`)
+ logger.debug(`Processed ${this.externalShares.length} external share(s)`)
}
},
@@ -298,7 +458,7 @@ export default {
// interval update
this.expirationInterval = setInterval(this.updateExpirationSubtitle, 10000, share)
}
- } else if (this.fileInfo && this.fileInfo.shareOwnerId !== undefined ? this.fileInfo.shareOwnerId !== OC.currentUser : false) {
+ } else if (this.fileInfo && this.fileInfo.shareOwnerId !== undefined ? this.fileInfo.shareOwnerId !== getCurrentUser().uid : false) {
// Fallback to compare owner and current user.
this.sharedWithMe = {
displayName: this.fileInfo.shareOwner,
@@ -307,7 +467,7 @@ export default {
'Shared with you by {owner}',
{ owner: this.fileInfo.shareOwner },
undefined,
- { escape: false }
+ { escape: false },
),
user: this.fileInfo.shareOwnerId,
}
@@ -321,17 +481,43 @@ export default {
* @param {Share} share the share to add to the array
* @param {Function} [resolve] a function to run after the share is added and its component initialized
*/
- addShare(share, resolve = () => {}) {
+ addShare(share, resolve = () => { }) {
// only catching share type MAIL as link shares are added differently
// meaning: not from the ShareInput
- if (share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
+ if (share.type === ShareType.Email) {
this.linkShares.unshift(share)
+ } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) {
+ if (this.config.showFederatedSharesAsInternal) {
+ this.shares.unshift(share)
+ } if (this.config.showFederatedSharesToTrustedServersAsInternal) {
+ if (share.isTrustedServer) {
+ this.shares.unshift(share)
+ }
+ } else {
+ this.externalShares.unshift(share)
+ }
} else {
this.shares.unshift(share)
}
this.awaitForShare(share, resolve)
},
-
+ /**
+ * Remove a share from the shares list
+ *
+ * @param {Share} share the share to remove
+ */
+ removeShare(share) {
+ // Get reference for this.linkShares or this.shares
+ const shareList
+ = share.type === ShareType.Email
+ || share.type === ShareType.Link
+ ? this.linkShares
+ : this.shares
+ const index = shareList.findIndex(item => item.id === share.id)
+ if (index !== -1) {
+ shareList.splice(index, 1)
+ }
+ },
/**
* Await for next tick and render after the list updated
* Then resolve with the matched vue component of the
@@ -341,20 +527,45 @@ export default {
* @param {Function} resolve a function to execute after
*/
awaitForShare(share, resolve) {
- let listComponent = this.$refs.shareList
- // Only mail shares comes from the input, link shares
- // are managed internally in the SharingLinkList component
- if (share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
- listComponent = this.$refs.linkShareList
- }
-
this.$nextTick(() => {
+ let listComponent = this.$refs.shareList
+ // Only mail shares comes from the input, link shares
+ // are managed internally in the SharingLinkList component
+ if (share.type === ShareType.Email) {
+ listComponent = this.$refs.linkShareList
+ }
const newShare = listComponent.$children.find(component => component.share === share)
if (newShare) {
resolve(newShare)
}
})
},
+
+ toggleShareDetailsView(eventData) {
+ if (!this.showSharingDetailsView) {
+ const isAction = Array.from(document.activeElement.classList)
+ .some(className => className.startsWith('action-'))
+ if (isAction) {
+ const menuId = document.activeElement.closest('[role="menu"]')?.id
+ this.returnFocusElement = document.querySelector(`[aria-controls="${menuId}"]`)
+ } else {
+ this.returnFocusElement = document.activeElement
+ }
+ }
+
+ if (eventData) {
+ this.shareDetailsData = eventData
+ }
+
+ this.showSharingDetailsView = !this.showSharingDetailsView
+
+ if (!this.showSharingDetailsView) {
+ this.$nextTick(() => { // Wait for next tick as the element must be visible to be focused
+ this.returnFocusElement?.focus()
+ this.returnFocusElement = null
+ })
+ }
+ },
},
}
</script>
@@ -365,11 +576,52 @@ export default {
}
.sharingTab {
+ position: relative;
+ height: 100%;
+
&__content {
padding: 0 6px;
+
+ section {
+ padding-bottom: 16px;
+
+ .section-header {
+ margin-top: 2px;
+ margin-bottom: 2px;
+ display: flex;
+ align-items: center;
+ padding-bottom: 4px;
+
+ h4 {
+ margin: 0;
+ font-size: 16px;
+ }
+
+ .visually-hidden {
+ display: none;
+ }
+
+ .hint-icon {
+ color: var(--color-primary-element);
+ }
+
+ }
+
+ }
+
+ & > section:not(:last-child) {
+ border-bottom: 2px solid var(--color-border);
+ }
+
}
+
&__additionalContent {
margin: 44px 0;
}
}
+
+.hint-body {
+ max-width: 300px;
+ padding: var(--border-radius-element);
+}
</style>