Merge pull request #43084 from nextcloud/artonge/feat/version_list

Move modal outside of the Version component.
pull/43137/head
Louis 2024-01-26 19:07:50 +07:00 committed by GitHub
commit beec3028fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 398 additions and 345 deletions

@ -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>

@ -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>

@ -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)
}

@ -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>

4
dist/1215-1215.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long