aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_versions/src/components/Version.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_versions/src/components/Version.vue')
-rw-r--r--apps/files_versions/src/components/Version.vue492
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>