diff options
Diffstat (limited to 'apps/files_versions/src/components/Version.vue')
-rw-r--r-- | apps/files_versions/src/components/Version.vue | 492 |
1 files changed, 291 insertions, 201 deletions
diff --git a/apps/files_versions/src/components/Version.vue b/apps/files_versions/src/components/Version.vue index 6a9bac977a8..dc36e4134f9 100644 --- a/apps/files_versions/src/components/Version.vue +++ b/apps/files_versions/src/components/Version.vue @@ -1,162 +1,177 @@ <!-- - - @copyright 2022 Carl Schwan <carl@carlschwan.eu> - - @license AGPL-3.0-or-later - - - - 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: 2022 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <div> - <NcListItem class="version" - :title="versionLabel" - :href="downloadURL" - :force-display-actions="true" - data-files-versions-version> - <template #icon> - <img lazy="true" - :src="previewURL" - alt="" - height="256" - width="256" - class="version__image"> - </template> - <template #subtitle> - <div class="version__info"> - <span v-tooltip="formattedDate">{{ version.mtime | humanDateFromNow }}</span> - <!-- Separate dot to improve alignement --> - <span class="version__info__size">•</span> - <span class="version__info__size">{{ version.size | humanReadableSize }}</span> + <NcListItem class="version" + :force-display-actions="true" + :actions-aria-label="t('files_versions', 'Actions for version from {versionHumanExplicitDate}', { versionHumanExplicitDate })" + :data-files-versions-version="version.fileVersion" + @click="click"> + <!-- Icon --> + <template #icon> + <div v-if="!(loadPreview || previewLoaded)" class="version__image" /> + <img v-else-if="version.previewUrl && !previewErrored" + :src="version.previewUrl" + alt="" + decoding="async" + fetchpriority="low" + loading="lazy" + class="version__image" + @load="previewLoaded = true" + @error="previewErrored = true"> + <div v-else + class="version__image"> + <ImageOffOutline :size="20" /> + </div> + </template> + + <!-- author --> + <template #name> + <div class="version__info"> + <div v-if="versionLabel" + class="version__info__label" + data-cy-files-version-label + :title="versionLabel"> + {{ versionLabel }} </div> - </template> - <template #actions> - <NcActionButton v-if="capabilities.files.version_labeling === true" - :close-after-click="true" - @click="openVersionLabelModal"> - <template #icon> - <Pencil :size="22" /> - </template> - {{ version.label === '' ? t('files_versions', 'Name this version') : t('files_versions', 'Edit version name') }} - </NcActionButton> - <NcActionButton v-if="!isCurrent" - :close-after-click="true" - @click="restoreVersion"> - <template #icon> - <BackupRestore :size="22" /> - </template> - {{ t('files_versions', 'Restore version') }} - </NcActionButton> - <NcActionLink :href="downloadURL" - :close-after-click="true" - :download="downloadURL"> - <template #icon> - <Download :size="22" /> - </template> - {{ t('files_versions', 'Download version') }} - </NcActionLink> - <NcActionButton v-if="!isCurrent && capabilities.files.version_deletion === true" - :close-after-click="true" - @click="deleteVersion"> - <template #icon> - <Delete :size="22" /> - </template> - {{ t('files_versions', 'Delete version') }} - </NcActionButton> - </template> - </NcListItem> - <NcModal v-if="showVersionLabelForm" - :title="t('files_versions', 'Name this version')" - @close="showVersionLabelForm = false"> - <form class="version-label-modal" - @submit.prevent="setVersionLabel(formVersionLabelValue)"> - <label> - <div class="version-label-modal__title">{{ t('photos', 'Version name') }}</div> - <NcTextField ref="labelInput" - :value.sync="formVersionLabelValue" - :placeholder="t('photos', 'Version name')" - :label-outside="true" /> - </label> - - <div class="version-label-modal__info"> - {{ t('photos', 'Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.') }} + <div v-if="versionAuthor" + class="version__info" + data-cy-files-version-author-name> + <span v-if="versionLabel">•</span> + <NcAvatar class="avatar" + :user="version.author" + :size="20" + disable-menu + disable-tooltip + :show-user-status="false" /> + <div class="version__info__author_name" + :title="versionAuthor"> + {{ versionAuthor }} + </div> </div> + </div> + </template> - <div class="version-label-modal__actions"> - <NcButton :disabled="formVersionLabelValue.trim().length === 0" @click="setVersionLabel('')"> - {{ t('files_versions', 'Remove version name') }} - </NcButton> - <NcButton type="primary" native-type="submit"> - <template #icon> - <Check /> - </template> - {{ t('files_versions', 'Save version name') }} - </NcButton> - </div> - </form> - </NcModal> - </div> + <!-- Version file size as subline --> + <template #subname> + <div class="version__info version__info__subline"> + <NcDateTime class="version__info__date" + relative-time="short" + :timestamp="version.mtime" /> + <!-- Separate dot to improve alignment --> + <span>•</span> + <span>{{ humanReadableSize }}</span> + </div> + </template> + + <!-- Actions --> + <template #actions> + <NcActionButton v-if="enableLabeling && hasUpdatePermissions" + data-cy-files-versions-version-action="label" + :close-after-click="true" + @click="labelUpdate"> + <template #icon> + <Pencil :size="22" /> + </template> + {{ version.label === '' ? t('files_versions', 'Name this version') : t('files_versions', 'Edit version name') }} + </NcActionButton> + <NcActionButton v-if="!isCurrent && canView && canCompare" + data-cy-files-versions-version-action="compare" + :close-after-click="true" + @click="compareVersion"> + <template #icon> + <FileCompare :size="22" /> + </template> + {{ t('files_versions', 'Compare to current version') }} + </NcActionButton> + <NcActionButton v-if="!isCurrent && hasUpdatePermissions" + data-cy-files-versions-version-action="restore" + :close-after-click="true" + @click="restoreVersion"> + <template #icon> + <BackupRestore :size="22" /> + </template> + {{ t('files_versions', 'Restore version') }} + </NcActionButton> + <NcActionLink v-if="isDownloadable" + data-cy-files-versions-version-action="download" + :href="downloadURL" + :close-after-click="true" + :download="downloadURL"> + <template #icon> + <Download :size="22" /> + </template> + {{ t('files_versions', 'Download version') }} + </NcActionLink> + <NcActionButton v-if="!isCurrent && enableDeletion && hasDeletePermissions" + data-cy-files-versions-version-action="delete" + :close-after-click="true" + @click="deleteVersion"> + <template #icon> + <Delete :size="22" /> + </template> + {{ t('files_versions', 'Delete version') }} + </NcActionButton> + </template> + </NcListItem> </template> +<script lang="ts"> +import type { PropType } from 'vue' +import type { Version } from '../utils/versions' + +import { getCurrentUser } from '@nextcloud/auth' +import { Permission, formatFileSize } from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' +import { t } from '@nextcloud/l10n' +import { joinPaths } from '@nextcloud/paths' +import { getRootUrl, generateUrl } from '@nextcloud/router' +import { defineComponent } from 'vue' + +import axios from '@nextcloud/axios' +import moment from '@nextcloud/moment' +import logger from '../utils/logger' -<script> import BackupRestore from 'vue-material-design-icons/BackupRestore.vue' +import Delete from 'vue-material-design-icons/Delete.vue' import Download from 'vue-material-design-icons/Download.vue' -import Pencil from 'vue-material-design-icons/Pencil.vue' -import Check from 'vue-material-design-icons/Check.vue' -import Delete from 'vue-material-design-icons/Delete' -import { NcActionButton, NcActionLink, NcListItem, NcModal, NcButton, NcTextField, Tooltip } from '@nextcloud/vue' -import moment from '@nextcloud/moment' -import { translate } from '@nextcloud/l10n' -import { joinPaths } from '@nextcloud/paths' -import { generateUrl } from '@nextcloud/router' -import { loadState } from '@nextcloud/initial-state' +import FileCompare from 'vue-material-design-icons/FileCompare.vue' +import ImageOffOutline from 'vue-material-design-icons/ImageOffOutline.vue' +import Pencil from 'vue-material-design-icons/PencilOutline.vue' -export default { +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcActionLink from '@nextcloud/vue/components/NcActionLink' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcDateTime from '@nextcloud/vue/components/NcDateTime' +import NcListItem from '@nextcloud/vue/components/NcListItem' +import Tooltip from '@nextcloud/vue/directives/Tooltip' + +const hasPermission = (permissions: number, permission: number): boolean => (permissions & permission) !== 0 + +export default defineComponent({ name: 'Version', + components: { NcActionLink, NcActionButton, + NcAvatar, + NcDateTime, NcListItem, - NcModal, - NcButton, - NcTextField, BackupRestore, Download, + FileCompare, Pencil, - Check, Delete, + ImageOffOutline, }, + directives: { tooltip: Tooltip, }, - filters: { - /** - * @param {number} bytes - * @return {string} - */ - humanReadableSize(bytes) { - return OC.Util.humanFileSize(bytes) - }, - /** - * @param {number} timestamp - * @return {string} - */ - humanDateFromNow(timestamp) { - return moment(timestamp).fromNow() - }, - }, + props: { - /** @type {Vue.PropOptions<import('../utils/versions.js').Version>} */ version: { - type: Object, + type: Object as PropType<Version>, required: true, }, fileInfo: { @@ -171,86 +186,158 @@ export default { type: Boolean, default: false, }, + loadPreview: { + type: Boolean, + default: false, + }, + canView: { + type: Boolean, + default: false, + }, + canCompare: { + type: Boolean, + default: false, + }, }, + + emits: ['click', 'compare', 'restore', 'delete', 'label-update-request'], + data() { return { - showVersionLabelForm: false, - formVersionLabelValue: this.version.label, + previewLoaded: false, + previewErrored: false, capabilities: loadState('core', 'capabilities', { files: { version_labeling: false, version_deletion: false } }), + versionAuthor: '' as string | null, } }, + computed: { - /** - * @return {string} - */ - versionLabel() { + humanReadableSize() { + return formatFileSize(this.version.size) + }, + + versionLabel(): string { + const label = this.version.label ?? '' + if (this.isCurrent) { - if (this.version.label === undefined || this.version.label === '') { - return translate('files_versions', 'Current version') + if (label === '') { + return t('files_versions', 'Current version') } else { - return `${this.version.label} (${translate('files_versions', 'Current version')})` + return `${label} (${t('files_versions', 'Current version')})` } } - if (this.isFirstVersion && this.version.label === '') { - return translate('files_versions', 'Initial version') + if (this.isFirstVersion && label === '') { + return t('files_versions', 'Initial version') } - return this.version.label + return label + }, + + versionHumanExplicitDate(): string { + return moment(this.version.mtime).format('LLLL') }, - /** - * @return {string} - */ - downloadURL() { + downloadURL(): string { if (this.isCurrent) { - return joinPaths('/remote.php/webdav', this.fileInfo.path, this.fileInfo.name) + return getRootUrl() + joinPaths('/remote.php/webdav', this.fileInfo.path, this.fileInfo.name) } else { - return this.version.url + return getRootUrl() + this.version.url } }, - /** - * @return {string} - */ - previewURL() { - if (this.isCurrent) { - return generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0', { - fileId: this.fileInfo.id, - fileEtag: this.fileInfo.etag, - }) - } else { - return this.version.preview + enableLabeling(): boolean { + return this.capabilities.files.version_labeling === true + }, + + enableDeletion(): boolean { + return this.capabilities.files.version_deletion === true + }, + + hasDeletePermissions(): boolean { + return hasPermission(this.fileInfo.permissions, Permission.DELETE) + }, + + hasUpdatePermissions(): boolean { + return hasPermission(this.fileInfo.permissions, Permission.UPDATE) + }, + + isDownloadable(): boolean { + if ((this.fileInfo.permissions & Permission.READ) === 0) { + return false } + + // If the mount type is a share, ensure it got download permissions. + if (this.fileInfo.mountType === 'shared') { + const downloadAttribute = this.fileInfo.shareAttributes + .find((attribute) => attribute.scope === 'permissions' && attribute.key === 'download') || {} + // If the download attribute is set to false, the file is not downloadable + if (downloadAttribute?.value === false) { + return false + } + } + + return true }, }, + + created() { + this.fetchDisplayName() + }, + methods: { - openVersionLabelModal() { - this.showVersionLabelForm = true - this.$nextTick(() => { - this.$refs.labelInput.$el.getElementsByTagName('input')[0].focus() - }) + labelUpdate() { + this.$emit('label-update-request') }, restoreVersion() { this.$emit('restore', this.version) }, - setVersionLabel(label) { - this.formVersionLabelValue = label - this.showVersionLabelForm = false - this.$emit('label-update', this.version, label) + async deleteVersion() { + // Let @nc-vue properly remove the popover before we delete the version. + // This prevents @nc-vue from throwing a error. + await this.$nextTick() + await this.$nextTick() + this.$emit('delete', this.version) }, - deleteVersion() { - this.$emit('delete', this.version) + async fetchDisplayName() { + this.versionAuthor = null + if (!this.version.author) { + return + } + + if (this.version.author === getCurrentUser()?.uid) { + this.versionAuthor = t('files_versions', 'You') + } else { + try { + const { data } = await axios.post(generateUrl('/displaynames'), { users: [this.version.author] }) + this.versionAuthor = data.users[this.version.author] + } catch (error) { + logger.warn('Could not load user display name', { error }) + } + } }, - formattedDate() { - return moment(this.version.mtime) + click() { + if (!this.canView) { + window.location.href = this.downloadURL + return + } + this.$emit('click', { version: this.version }) }, + + compareVersion() { + if (!this.canView) { + throw new Error('Cannot compare version of this file') + } + this.$emit('compare', { version: this.version }) + }, + + t, }, -} +}) </script> <style scoped lang="scss"> @@ -263,9 +350,31 @@ export default { flex-direction: row; align-items: center; gap: 0.5rem; + color: var(--color-main-text); + font-weight: 500; + overflow: hidden; + + &__label { + font-weight: 700; + // Fix overflow on narrow screens + overflow: hidden; + text-overflow: ellipsis; + min-width: 110px; + } + + &__author_name { + overflow: hidden; + text-overflow: ellipsis; + } - &__size { - color: var(--color-text-lighter); + &__date { + // Fix overflow on narrow screens + overflow: hidden; + text-overflow: ellipsis; + } + + &__subline { + color: var(--color-text-maxcontrast) } } @@ -274,30 +383,11 @@ export default { height: 3rem; border: 1px solid var(--color-border); border-radius: var(--border-radius-large); - } -} - -.version-label-modal { - display: flex; - justify-content: space-between; - flex-direction: column; - height: 250px; - padding: 16px; - - &__title { - margin-bottom: 12px; - font-weight: 600; - } - - &__info { - margin-top: 12px; - color: var(--color-text-maxcontrast); - } - &__actions { + // Useful to display no preview icon. display: flex; - justify-content: space-between; - margin-top: 64px; + justify-content: center; + color: var(--color-text-light); } } </style> |