Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support opening and comparing versions in viewer #39171

Merged
merged 2 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 39 additions & 20 deletions apps/files_versions/src/components/Version.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
<div>
<NcListItem class="version"
:name="versionLabel"
:href="downloadURL"
:force-display-actions="true"
data-files-versions-version>
data-files-versions-version
@click="click">
<template #icon>
<div v-if="!(loadPreview || previewLoaded)" class="version__image" />
<img v-else-if="isCurrent || version.hasPreview"
:src="previewURL"
:src="version.previewUrl"
alt=""
decoding="async"
fetchpriority="low"
Expand All @@ -46,14 +46,22 @@
</div>
</template>
<template #actions>
<NcActionButton v-if="enableLabeling"
<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">
Expand Down Expand Up @@ -116,6 +124,7 @@
<script>
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'
Expand All @@ -130,7 +139,7 @@ import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip.js'
import moment from '@nextcloud/moment'
import { translate } from '@nextcloud/l10n'
import { joinPaths } from '@nextcloud/paths'
import { generateUrl, getRootUrl } from '@nextcloud/router'
import { getRootUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'

export default {
Expand All @@ -144,6 +153,7 @@ export default {
NcTextField,
BackupRestore,
Download,
FileCompare,
Pencil,
Check,
Delete,
Expand Down Expand Up @@ -190,6 +200,14 @@ export default {
type: Boolean,
default: false,
},
canView: {
type: Boolean,
default: false,
},
canCompare: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand Down Expand Up @@ -232,20 +250,6 @@ export default {
}
},

/**
* @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
}
},

/** @return {string} */
formattedDate() {
return moment(this.version.mtime).format('LLL')
Expand All @@ -259,7 +263,7 @@ export default {
/** @return {boolean} */
enableDeletion() {
return this.capabilities.files.version_deletion === true && this.fileInfo.mountType !== 'group'
}
},
},
methods: {
openVersionLabelModal() {
Expand All @@ -282,6 +286,21 @@ export default {
deleteVersion() {
this.$emit('delete', this.version)
},

click() {
if (!this.canView) {
window.location = 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 })
},
},
}
</script>
Expand Down
1 change: 1 addition & 0 deletions apps/files_versions/src/utils/davRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default `<?xml version="1.0"?>
<d:getcontentlength />
<d:getcontenttype />
<d:getlastmodified />
<d:getetag />
<nc:version-label />
<nc:has-preview />
</d:prop>
Expand Down
63 changes: 45 additions & 18 deletions apps/files_versions/src/utils/versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,33 @@
*/

import { getCurrentUser } from '@nextcloud/auth'
import { joinPaths } from '@nextcloud/paths'
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
import moment from '@nextcloud/moment'

import { encodeFilePath } from '../../../files/src/utils/fileUtils.js'

import client from '../utils/davClient.js'
import davRequest from '../utils/davRequest.js'
import logger from '../utils/logger.js'
import { joinPaths } from '@nextcloud/paths'
import { generateUrl } from '@nextcloud/router'
import moment from '@nextcloud/moment'
import path from 'path'

/**
* @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} mimeType - Empty for the current version, else the actual mime type of the version
* @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} preview - Preview URL of the version
* @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
*/

Expand Down Expand Up @@ -75,7 +83,7 @@ export async function restoreVersion(version) {
logger.debug('Restoring version', { url: version.url })
await client.moveFile(
`/versions/${getCurrentUser()?.uid}/versions/${version.fileId}/${version.fileVersion}`,
`/versions/${getCurrentUser()?.uid}/restore/target`
`/versions/${getCurrentUser()?.uid}/restore/target`,
)
} catch (exception) {
logger.error('Could not restore version', { exception })
Expand All @@ -91,20 +99,39 @@ export async function restoreVersion(version) {
* @return {Version}
*/
function formatVersion(version, fileInfo) {
const mtime = moment(version.lastmod).unix() * 1000
let previewUrl = ''
let filename = ''

if (mtime === fileInfo.mtime) { // Version is the current one
filename = path.join('files', getCurrentUser()?.uid ?? '', fileInfo.path, fileInfo.name)
previewUrl = generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0', {
fileId: fileInfo.id,
fileEtag: fileInfo.etag,
})
} else {
filename = version.filename
previewUrl = generateUrl('/apps/files_versions/preview?file={file}&version={fileVersion}', {
file: joinPaths(fileInfo.path, fileInfo.name),
fileVersion: version.basename,
})
}

return {
fileId: fileInfo.id,
label: version.props['version-label'],
fileName: version.filename,
mimeType: version.mime,
filename,
basename: moment(mtime).format('LLL'),
mime: version.mime,
etag: `${version.props.getetag}`,
size: version.size,
type: version.type,
mtime: moment(version.lastmod).unix() * 1000,
mtime,
permissions: 'R',
hasPreview: version.props['has-preview'] === 1,
preview: generateUrl('/apps/files_versions/preview?file={file}&version={fileVersion}', {
file: joinPaths(fileInfo.path, fileInfo.name),
fileVersion: version.basename,
}),
url: joinPaths('/remote.php/dav', version.filename),
previewUrl,
url: joinPaths('/remote.php/dav', filename),
source: generateRemoteUrl('dav') + encodeFilePath(filename),
fileVersion: version.basename,
}
}
Expand All @@ -115,7 +142,7 @@ function formatVersion(version, fileInfo) {
*/
export async function setVersionLabel(version, newLabel) {
return await client.customRequest(
version.fileName,
version.filename,
{
method: 'PROPPATCH',
data: `<?xml version="1.0"?>
Expand All @@ -129,13 +156,13 @@ export async function setVersionLabel(version, newLabel) {
</d:prop>
</d:set>
</d:propertyupdate>`,
}
},
)
}

/**
* @param {Version} version
*/
export async function deleteVersion(version) {
await client.deleteFile(version.fileName)
await client.deleteFile(version.filename)
}
62 changes: 62 additions & 0 deletions apps/files_versions/src/views/VersionTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@
<ul data-files-versions-versions-list>
<Version v-for="version in orderedVersions"
:key="version.mtime"
:can-view="canView"
:can-compare="canCompare"
:load-preview="isActive"
:version="version"
:file-info="fileInfo"
:is-current="version.mtime === fileInfo.mtime"
:is-first-version="version.mtime === initialVersionMtime"
@click="openVersion"
@compare="compareVersion"
@restore="handleRestore"
@label-update="handleLabelUpdate"
@delete="handleDelete" />
Expand All @@ -32,6 +36,7 @@

<script>
import { showError, showSuccess } from '@nextcloud/dialogs'
import isMobile from '@nextcloud/vue/dist/Mixins/isMobile.js'
import { fetchVersions, deleteVersion, restoreVersion, setVersionLabel } from '../utils/versions.js'
import Version from '../components/Version.vue'

Expand All @@ -40,6 +45,9 @@ export default {
components: {
Version,
},
mixins: [
isMobile,
],
data() {
return {
fileInfo: null,
Expand Down Expand Up @@ -78,6 +86,37 @@ export default {
.map(version => version.mtime)
.reduce((a, b) => Math.min(a, b))
},

viewerFileInfo() {
// We need to remap bitmask to dav permissions as the file info we have is converted through client.js
let davPermissions = ''
if (this.fileInfo.permissions & 1) {
davPermissions += 'R'
}
if (this.fileInfo.permissions & 2) {
davPermissions += 'W'
}
if (this.fileInfo.permissions & 8) {
davPermissions += 'D'
}
return {
...this.fileInfo,
mime: this.fileInfo.mimetype,
basename: this.fileInfo.name,
filename: this.fileInfo.path + this.fileInfo.name,
permissions: davPermissions,
fileid: this.fileInfo.id,
}
},

/** @return {boolean} */
canView() {
return window.OCA.Viewer?.mimetypesCompare?.includes(this.fileInfo.mimetype)
},

canCompare() {
return !this.isMobile
},
},
methods: {
/**
Expand Down Expand Up @@ -182,6 +221,29 @@ export default {
resetState() {
this.$set(this, 'versions', [])
},

openVersion({ version }) {
// Open current file view instead of read only
if (version.mtime === this.fileInfo.mtime) {
OCA.Viewer.open({ fileInfo: this.viewerFileInfo })
return
}

// Versions previews are too small for our use case, so we override hasPreview and previewUrl
// which makes the viewer render the original file.
const versions = this.versions.map(version => ({ ...version, hasPreview: false, previewUrl: undefined }))

OCA.Viewer.open({
fileInfo: versions.find(v => v.source === version.source),
enableSidebar: false,
})
},

compareVersion({ version }) {
const versions = this.versions.map(version => ({ ...version, hasPreview: false, previewUrl: undefined }))

OCA.Viewer.compare(this.viewerFileInfo, versions.find(v => v.source === version.source))
},
},
}
</script>
4 changes: 2 additions & 2 deletions dist/files_versions-files_versions.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files_versions-files_versions.js.map

Large diffs are not rendered by default.