diff options
author | Louis <louis@chmn.me> | 2024-01-26 19:07:50 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-26 19:07:50 +0100 |
commit | beec3028fbf934b3f510f2f5ca79cf197a74a5e8 (patch) | |
tree | 2c51d0ba2b59a6c339362ce11a7a57d45da548ba /apps | |
parent | 8d114c9e7441f6de4637d38fdec86a7a7f3b5085 (diff) | |
parent | ea732fa69942908bf45eae83bc39845ddf367f8f (diff) | |
download | nextcloud-server-beec3028fbf934b3f510f2f5ca79cf197a74a5e8.tar.gz nextcloud-server-beec3028fbf934b3f510f2f5ca79cf197a74a5e8.zip |
Merge pull request #43084 from nextcloud/artonge/feat/version_list
Move modal outside of the Version component.
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files_versions/src/components/Version.vue | 225 | ||||
-rw-r--r-- | apps/files_versions/src/components/VersionLabelForm.vue | 115 | ||||
-rw-r--r-- | apps/files_versions/src/utils/versions.ts (renamed from apps/files_versions/src/utils/versions.js) | 70 | ||||
-rw-r--r-- | apps/files_versions/src/views/VersionTab.vue | 117 |
4 files changed, 290 insertions, 237 deletions
diff --git a/apps/files_versions/src/components/Version.vue b/apps/files_versions/src/components/Version.vue index 5f4e7b447ea..a03e71bc5df 100644 --- a/apps/files_versions/src/components/Version.vue +++ b/apps/files_versions/src/components/Version.vue @@ -16,110 +16,78 @@ - along with this program. If not, see <http://www.gnu.org/licenses/>. --> <template> - <div> - <NcListItem class="version" - :name="versionLabel" - :force-display-actions="true" - data-files-versions-version - @click="click"> - <template #icon> - <div v-if="!(loadPreview || previewLoaded)" class="version__image" /> - <img v-else-if="(isCurrent || version.hasPreview) && !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> - <template #subname> - <div class="version__info"> - <span :title="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> - </div> - </template> - <template #actions> - <NcActionButton v-if="enableLabeling" - :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 && canView && canCompare" - :close-after-click="true" - @click="compareVersion"> - <template #icon> - <FileCompare :size="22" /> - </template> - {{ t('files_versions', 'Compare to current version') }} - </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 && enableDeletion" - :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('files_versions', 'Version name') }}</div> - <NcTextField ref="labelInput" - :value.sync="formVersionLabelValue" - :placeholder="t('files_versions', 'Version name')" - :label-outside="true" /> - </label> - - <div class="version-label-modal__info"> - {{ t('files_versions', 'Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.') }} - </div> - - <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> + <NcListItem class="version" + :name="versionLabel" + :force-display-actions="true" + data-files-versions-version + @click="click"> + <template #icon> + <div v-if="!(loadPreview || previewLoaded)" class="version__image" /> + <img v-else-if="(isCurrent || version.hasPreview) && !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> + <template #subname> + <div class="version__info"> + <span :title="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> + </div> + </template> + <template #actions> + <NcActionButton v-if="enableLabeling" + :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" + :close-after-click="true" + @click="compareVersion"> + <template #icon> + <FileCompare :size="22" /> + </template> + {{ t('files_versions', 'Compare to current version') }} + </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 && enableDeletion" + :close-after-click="true" + @click="deleteVersion"> + <template #icon> + <Delete :size="22" /> + </template> + {{ t('files_versions', 'Delete version') }} + </NcActionButton> + </template> + </NcListItem> </template> <script> @@ -127,15 +95,11 @@ import BackupRestore from 'vue-material-design-icons/BackupRestore.vue' import Download from 'vue-material-design-icons/Download.vue' import FileCompare from 'vue-material-design-icons/FileCompare.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.vue' import ImageOffOutline from 'vue-material-design-icons/ImageOffOutline.vue' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js' import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js' -import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip.js' import moment from '@nextcloud/moment' import { translate as t } from '@nextcloud/l10n' @@ -149,14 +113,10 @@ export default { NcActionLink, NcActionButton, NcListItem, - NcModal, - NcButton, - NcTextField, BackupRestore, Download, FileCompare, Pencil, - Check, Delete, ImageOffOutline, }, @@ -180,7 +140,7 @@ export default { }, }, props: { - /** @type {Vue.PropOptions<import('../utils/versions.js').Version>} */ + /** @type {Vue.PropOptions<import('../utils/versions.ts').Version>} */ version: { type: Object, required: true, @@ -214,8 +174,6 @@ export default { return { previewLoaded: false, previewErrored: false, - showVersionLabelForm: false, - formVersionLabelValue: this.version.label, capabilities: loadState('core', 'capabilities', { files: { version_labeling: false, version_deletion: false } }), } }, @@ -268,23 +226,14 @@ export default { }, }, 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) - }, - deleteVersion() { this.$emit('delete', this.version) }, @@ -337,28 +286,4 @@ export default { color: var(--color-text-light); } } - -.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 { - display: flex; - justify-content: space-between; - margin-top: 64px; - } -} </style> diff --git a/apps/files_versions/src/components/VersionLabelForm.vue b/apps/files_versions/src/components/VersionLabelForm.vue new file mode 100644 index 00000000000..5b8251b36c0 --- /dev/null +++ b/apps/files_versions/src/components/VersionLabelForm.vue @@ -0,0 +1,115 @@ +<!-- + - @copyright Copyright (c) 2024 Louis Chemineau <louis@chmn.me> + - + - @author Louis Chemineau <louis@chmn.me> + - + - @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/>. + --> +<template> + <form class="version-label-modal" + @submit.prevent="setVersionLabel(innerVersionLabel)"> + <label> + <div class="version-label-modal__title">{{ t('files_versions', 'Version name') }}</div> + <NcTextField ref="labelInput" + :value.sync="innerVersionLabel" + :placeholder="t('files_versions', 'Version name')" + :label-outside="true" /> + </label> + + <div class="version-label-modal__info"> + {{ t('files_versions', 'Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.') }} + </div> + + <div class="version-label-modal__actions"> + <NcButton :disabled="innerVersionLabel.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> +</template> + +<script lang="ts"> +import Check from 'vue-material-design-icons/Check.vue' +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' +import { translate } from '@nextcloud/l10n' + +import { defineComponent } from 'vue' + +export default defineComponent({ + name: 'VersionLabelForm', + components: { + NcButton, + NcTextField, + Check, + }, + props: { + versionLabel: { + type: String, + default: '', + }, + }, + data() { + return { + innerVersionLabel: this.versionLabel, + } + }, + mounted() { + this.$nextTick(() => { + (this.$refs.labelInput as Vue).$el.getElementsByTagName('input')[0].focus() + }) + }, + methods: { + setVersionLabel(label: string) { + this.$emit('label-update', label) + }, + + t: translate, + }, +}) +</script> + +<style scoped lang="scss"> +.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 { + display: flex; + justify-content: space-between; + margin-top: 64px; + } +} +</style> diff --git a/apps/files_versions/src/utils/versions.js b/apps/files_versions/src/utils/versions.ts index 98df139a87f..b33f75aefa7 100644 --- a/apps/files_versions/src/utils/versions.js +++ b/apps/files_versions/src/utils/versions.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable jsdoc/require-param */ +/* eslint-disable jsdoc/require-jsdoc */ /** * @copyright 2022 Louis Chemineau <mlouis@chmn.me> * @@ -29,39 +32,35 @@ import { encodeFilePath } from '../../../files/src/utils/fileUtils.ts' import client from '../utils/davClient.js' import davRequest from '../utils/davRequest.js' import logger from '../utils/logger.js' +import type { FileStat, ResponseDataDetailed } from 'webdav' -/** - * @typedef {object} Version - * @property {string} fileId - The id of the file associated to the version. - * @property {string} label - 'Current version' or '' - * @property {string} filename - File name relative to the version DAV endpoint - * @property {string} basename - A base name generated from the mtime - * @property {string} mime - Empty for the current version, else the actual mime type of the version - * @property {string} etag - Empty for the current version, else the actual mime type of the version - * @property {string} size - Human readable size - * @property {string} type - 'file' - * @property {number} mtime - Version creation date as a timestamp - * @property {string} permissions - Only readable: 'R' - * @property {boolean} hasPreview - Whether the version has a preview - * @property {string} previewUrl - Preview URL of the version - * @property {string} url - Download URL of the version - * @property {string} source - The WebDAV endpoint of the ressource - * @property {string|null} fileVersion - The version id, null for the current version - */ +export interface Version { + fileId: string, // The id of the file associated to the version. + label: string, // 'Current version' or '' + filename: string, // File name relative to the version DAV endpoint + basename: string, // A base name generated from the mtime + mime: string, // Empty for the current version, else the actual mime type of the version + etag: string, // Empty for the current version, else the actual mime type of the version + size: string, // Human readable size + type: string, // 'file' + mtime: number, // Version creation date as a timestamp + permissions: string, // Only readable: 'R' + hasPreview: boolean, // Whether the version has a preview + previewUrl: string, // Preview URL of the version + url: string, // Download URL of the version + source: string, // The WebDAV endpoint of the ressource + fileVersion: string|null, // The version id, null for the current version +} -/** - * @param fileInfo - * @return {Promise<Version[]>} - */ -export async function fetchVersions(fileInfo) { +export async function fetchVersions(fileInfo: any): Promise<Version[]> { const path = `/versions/${getCurrentUser()?.uid}/versions/${fileInfo.id}` try { - /** @type {import('webdav').ResponseDataDetailed<import('webdav').FileStat[]>} */ const response = await client.getDirectoryContents(path, { data: davRequest, details: true, - }) + }) as ResponseDataDetailed<FileStat[]> + return response.data // Filter out root .filter(({ mime }) => mime !== '') @@ -74,10 +73,8 @@ export async function fetchVersions(fileInfo) { /** * Restore the given version - * - * @param {Version} version */ -export async function restoreVersion(version) { +export async function restoreVersion(version: Version) { try { logger.debug('Restoring version', { url: version.url }) await client.moveFile( @@ -92,12 +89,8 @@ export async function restoreVersion(version) { /** * Format version - * - * @param {object} version - raw version received from the versions DAV endpoint - * @param {object} fileInfo - file properties received from the files DAV endpoint - * @return {Version} */ -function formatVersion(version, fileInfo) { +function formatVersion(version: any, fileInfo: any): Version { const mtime = moment(version.lastmod).unix() * 1000 let previewUrl = '' @@ -132,11 +125,7 @@ function formatVersion(version, fileInfo) { } } -/** - * @param {Version} version - * @param {string} newLabel - */ -export async function setVersionLabel(version, newLabel) { +export async function setVersionLabel(version: Version, newLabel: string) { return await client.customRequest( version.filename, { @@ -156,9 +145,6 @@ export async function setVersionLabel(version, newLabel) { ) } -/** - * @param {Version} version - */ -export async function deleteVersion(version) { +export async function deleteVersion(version: Version) { await client.deleteFile(version.filename) } diff --git a/apps/files_versions/src/views/VersionTab.vue b/apps/files_versions/src/views/VersionTab.vue index c039e4a6c3a..745b9d0f58e 100644 --- a/apps/files_versions/src/views/VersionTab.vue +++ b/apps/files_versions/src/views/VersionTab.vue @@ -16,30 +16,37 @@ - along with this program. If not, see <http://www.gnu.org/licenses/>. --> <template> - <VirtualScrolling :sections="sections" - :header-height="0"> - <template slot-scope="{visibleSections}"> - <ul data-files-versions-versions-list> - <template v-if="visibleSections.length === 1"> - <Version v-for="(row) of visibleSections[0].rows" - :key="row.items[0].mtime" - :can-view="canView" - :can-compare="canCompare" - :load-preview="isActive" - :version="row.items[0]" - :file-info="fileInfo" - :is-current="row.items[0].mtime === fileInfo.mtime" - :is-first-version="row.items[0].mtime === initialVersionMtime" - @click="openVersion" - @compare="compareVersion" - @restore="handleRestore" - @label-update="handleLabelUpdate" - @delete="handleDelete" /> - </template> - </ul> - </template> - <NcLoadingIcon v-if="loading" slot="loader" class="files-list-viewer__loader" /> - </VirtualScrolling> + <div class="versions-tab__container"> + <VirtualScrolling :sections="sections" + :header-height="0"> + <template slot-scope="{visibleSections}"> + <ul data-files-versions-versions-list> + <template v-if="visibleSections.length === 1"> + <Version v-for="(row) of visibleSections[0].rows" + :key="row.items[0].mtime" + :can-view="canView" + :can-compare="canCompare" + :load-preview="isActive" + :version="row.items[0]" + :file-info="fileInfo" + :is-current="row.items[0].mtime === fileInfo.mtime" + :is-first-version="row.items[0].mtime === initialVersionMtime" + @click="openVersion" + @compare="compareVersion" + @restore="handleRestore" + @label-update-request="handleLabelUpdateRequest(row.items[0])" + @delete="handleDelete" /> + </template> + </ul> + </template> + <NcLoadingIcon v-if="loading" slot="loader" class="files-list-viewer__loader" /> + </VirtualScrolling> + <NcModal v-if="showVersionLabelForm" + :title="t('files_versions', 'Name this version')" + @close="showVersionLabelForm = false"> + <VersionLabelForm :version-label="editedVersion.label" @label-update="handleLabelUpdate" /> + </NcModal> + </div> </template> <script> @@ -49,18 +56,22 @@ import { showError, showSuccess } from '@nextcloud/dialogs' import isMobile from '@nextcloud/vue/dist/Mixins/isMobile.js' import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' import { getCurrentUser } from '@nextcloud/auth' -import { NcLoadingIcon } from '@nextcloud/vue' +import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' +import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' -import { fetchVersions, deleteVersion, restoreVersion, setVersionLabel } from '../utils/versions.js' +import { fetchVersions, deleteVersion, restoreVersion, setVersionLabel } from '../utils/versions.ts' import Version from '../components/Version.vue' import VirtualScrolling from '../components/VirtualScrolling.vue' +import VersionLabelForm from '../components/VersionLabelForm.vue' export default { name: 'VersionTab', components: { Version, VirtualScrolling, + VersionLabelForm, NcLoadingIcon, + NcModal, }, mixins: [ isMobile, @@ -69,17 +80,12 @@ export default { return { fileInfo: null, isActive: false, - /** @type {import('../utils/versions.js').Version[]} */ + /** @type {import('../utils/versions.ts').Version[]} */ versions: [], loading: false, + showVersionLabelForm: false, } }, - mounted() { - subscribe('files_versions:restore:restored', this.fetchVersions) - }, - beforeUnmount() { - unsubscribe('files_versions:restore:restored', this.fetchVersions) - }, computed: { sections() { const rows = this.orderedVersions.map(version => ({ key: version.mtime, height: 68, sectionKey: 'versions', items: [version] })) @@ -90,7 +96,7 @@ export default { * Order versions by mtime. * Put the current version at the top. * - * @return {import('../utils/versions.js').Version[]} + * @return {import('../utils/versions.ts').Version[]} */ orderedVersions() { return [...this.versions].sort((a, b) => { @@ -146,6 +152,12 @@ export default { return !this.isMobile }, }, + mounted() { + subscribe('files_versions:restore:restored', this.fetchVersions) + }, + beforeUnmount() { + unsubscribe('files_versions:restore:restored', this.fetchVersions) + }, methods: { /** * Update current fileInfo and fetch new data @@ -180,7 +192,7 @@ export default { /** * Handle restored event from Version.vue * - * @param {import('../utils/versions.js').Version} version + * @param {import('../utils/versions.ts').Version} version */ async handleRestore(version) { // Update local copy of fileInfo as rendering depends on it. @@ -220,26 +232,36 @@ export default { /** * Handle label-updated event from Version.vue - * - * @param {import('../utils/versions.js').Version} version - * @param {string} newName + * @param {import('../utils/versions.ts').Version} version + */ + handleLabelUpdateRequest(version) { + this.showVersionLabelForm = true + this.editedVersion = version + }, + + /** + * Handle label-updated event from Version.vue + * @param {string} newLabel */ - async handleLabelUpdate(version, newName) { - const oldLabel = version.label - version.label = newName + async handleLabelUpdate(newLabel) { + const oldLabel = this.editedVersion.label + this.editedVersion.label = newLabel + this.showVersionLabelForm = false try { - await setVersionLabel(version, newName) + await setVersionLabel(this.editedVersion, newLabel) + this.editedVersion = null } catch (exception) { - version.label = oldLabel - showError(t('files_versions', 'Could not set version name')) + this.editedVersion.label = oldLabel + showError(this.t('files_versions', 'Could not set version label')) + logger.error('Could not set version label', { exception }) } }, /** * Handle deleted event from Version.vue * - * @param {import('../utils/versions.js').Version} version + * @param {import('../utils/versions.ts').Version} version * @param {string} newName */ async handleDelete(version) { @@ -292,3 +314,8 @@ export default { }, } </script> +<style lang="scss"> +.versions-tab__container { + height: 100%; +} +</style> |