mirror of https://github.com/nextcloud/server
Merge pull request #34769 from nextcloud/port/vue/files_version
Port files_versions to vue
This commit is contained in:
commit
a884f311b7
|
@ -20,12 +20,15 @@
|
|||
*
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line node/no-missing-import, import/no-unresolved
|
||||
import MessageReplyText from '@mdi/svg/svg/message-reply-text.svg?raw'
|
||||
|
||||
// Init Comments tab component
|
||||
let TabInstance = null
|
||||
const commentTab = new OCA.Files.Sidebar.Tab({
|
||||
id: 'comments',
|
||||
name: t('comments', 'Comments'),
|
||||
icon: 'icon-comment',
|
||||
iconSvg: MessageReplyText,
|
||||
|
||||
async mount(el, fileInfo, context) {
|
||||
if (TabInstance) {
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
:name="name"
|
||||
:icon="icon"
|
||||
@bottomReached="onScrollBottomReached">
|
||||
<template #icon>
|
||||
<slot name="icon" />
|
||||
</template>
|
||||
<!-- Fallback loading -->
|
||||
<NcEmptyContent v-if="loading" icon="icon-loading" />
|
||||
|
||||
|
@ -63,7 +66,7 @@ export default {
|
|||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,12 +19,14 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import { sanitizeSVG } from '@skjnldsv/sanitize-svg'
|
||||
|
||||
export default class Tab {
|
||||
|
||||
_id
|
||||
_name
|
||||
_icon
|
||||
_iconSvgSanitized
|
||||
_mount
|
||||
_update
|
||||
_destroy
|
||||
|
@ -37,19 +39,20 @@ export default class Tab {
|
|||
* @param {object} options destructuring object
|
||||
* @param {string} options.id the unique id of this tab
|
||||
* @param {string} options.name the translated tab name
|
||||
* @param {string} options.icon the vue component
|
||||
* @param {?string} options.icon the icon css class
|
||||
* @param {?string} options.iconSvg the icon in svg format
|
||||
* @param {Function} options.mount function to mount the tab
|
||||
* @param {Function} options.update function to update the tab
|
||||
* @param {Function} options.destroy function to destroy the tab
|
||||
* @param {Function} [options.enabled] define conditions whether this tab is active. Must returns a boolean
|
||||
* @param {Function} [options.scrollBottomReached] executed when the tab is scrolled to the bottom
|
||||
*/
|
||||
constructor({ id, name, icon, mount, update, destroy, enabled, scrollBottomReached } = {}) {
|
||||
constructor({ id, name, icon, iconSvg, mount, update, destroy, enabled, scrollBottomReached } = {}) {
|
||||
if (enabled === undefined) {
|
||||
enabled = () => true
|
||||
}
|
||||
if (scrollBottomReached === undefined) {
|
||||
scrollBottomReached = () => {}
|
||||
scrollBottomReached = () => { }
|
||||
}
|
||||
|
||||
// Sanity checks
|
||||
|
@ -59,8 +62,8 @@ export default class Tab {
|
|||
if (typeof name !== 'string' || name.trim() === '') {
|
||||
throw new Error('The name argument is not a valid string')
|
||||
}
|
||||
if (typeof icon !== 'string' || icon.trim() === '') {
|
||||
throw new Error('The icon argument is not a valid string')
|
||||
if ((typeof icon !== 'string' || icon.trim() === '') && typeof iconSvg !== 'string') {
|
||||
throw new Error('Missing valid string for icon or iconSvg argument')
|
||||
}
|
||||
if (typeof mount !== 'function') {
|
||||
throw new Error('The mount argument should be a function')
|
||||
|
@ -87,6 +90,13 @@ export default class Tab {
|
|||
this._enabled = enabled
|
||||
this._scrollBottomReached = scrollBottomReached
|
||||
|
||||
if (typeof iconSvg === 'string') {
|
||||
sanitizeSVG(iconSvg)
|
||||
.then(sanitizedSvg => {
|
||||
this._iconSvgSanitized = sanitizedSvg
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
get id() {
|
||||
|
@ -101,6 +111,10 @@ export default class Tab {
|
|||
return this._icon
|
||||
}
|
||||
|
||||
get iconSvg() {
|
||||
return this._iconSvgSanitized
|
||||
}
|
||||
|
||||
get mount() {
|
||||
return this._mount
|
||||
}
|
||||
|
|
|
@ -72,7 +72,12 @@
|
|||
:on-update="tab.update"
|
||||
:on-destroy="tab.destroy"
|
||||
:on-scroll-bottom-reached="tab.scrollBottomReached"
|
||||
:file-info="fileInfo" />
|
||||
:file-info="fileInfo">
|
||||
<template v-if="tab.iconSvg !== undefined" #icon>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="svg-icon" v-html="tab.iconSvg" />
|
||||
</template>
|
||||
</SidebarTab>
|
||||
</template>
|
||||
</NcAppSidebar>
|
||||
</template>
|
||||
|
@ -508,5 +513,13 @@ export default {
|
|||
top: 0 !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
::v-deep svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -25,11 +25,14 @@ import Vue from 'vue'
|
|||
import VueClipboard from 'vue-clipboard2'
|
||||
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
|
||||
|
||||
import SharingTab from './views/SharingTab'
|
||||
import ShareSearch from './services/ShareSearch'
|
||||
import ExternalLinkActions from './services/ExternalLinkActions'
|
||||
import ExternalShareActions from './services/ExternalShareActions'
|
||||
import TabSections from './services/TabSections'
|
||||
import SharingTab from './views/SharingTab.vue'
|
||||
import ShareSearch from './services/ShareSearch.js'
|
||||
import ExternalLinkActions from './services/ExternalLinkActions.js'
|
||||
import ExternalShareActions from './services/ExternalShareActions.js'
|
||||
import TabSections from './services/TabSections.js'
|
||||
|
||||
// eslint-disable-next-line node/no-missing-import, import/no-unresolved
|
||||
import ShareVariant from '@mdi/svg/svg/share-variant.svg?raw'
|
||||
|
||||
// Init Sharing Tab Service
|
||||
if (!window.OCA.Sharing) {
|
||||
|
@ -53,7 +56,7 @@ window.addEventListener('DOMContentLoaded', function() {
|
|||
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab({
|
||||
id: 'sharing',
|
||||
name: t('files_sharing', 'Sharing'),
|
||||
icon: 'icon-share',
|
||||
iconSvg: ShareVariant,
|
||||
|
||||
async mount(el, fileInfo, context) {
|
||||
if (TabInstance) {
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
.versionsTabView .clear-float {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.versionsTabView li {
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
height: 56px;
|
||||
float: left;
|
||||
border-bottom: 1px solid rgba(100,100,100,.1);
|
||||
}
|
||||
.versionsTabView li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.versionsTabView a,
|
||||
.versionsTabView div > span {
|
||||
vertical-align: middle;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.versionsTabView li a{
|
||||
padding: 15px 10px 11px;
|
||||
}
|
||||
|
||||
.versionsTabView a:hover,
|
||||
.versionsTabView a:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.versionsTabView .preview-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.versionsTabView .version-container img, .revertVersion img {
|
||||
filter: var(--background-invert-if-dark);
|
||||
}
|
||||
|
||||
.versionsTabView img {
|
||||
cursor: pointer;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.versionsTabView img.preview {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.versionsTabView .version-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.versionsTabView .versiondate {
|
||||
min-width: 100px;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
.versionsTabView .version-details {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.versionsTabView .version-details > span {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.versionsTabView .revertVersion {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
.versionsTabView .emptycontent {
|
||||
margin-top: 50px !important;
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import Vue from 'vue'
|
||||
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
|
||||
|
||||
import VersionTab from './views/VersionTab.vue'
|
||||
import VTooltip from 'v-tooltip'
|
||||
// eslint-disable-next-line node/no-missing-import, import/no-unresolved
|
||||
import BackupRestore from '@mdi/svg/svg/backup-restore.svg?raw'
|
||||
|
||||
Vue.prototype.t = t
|
||||
Vue.prototype.n = n
|
||||
|
||||
Vue.use(VTooltip)
|
||||
|
||||
// Init Sharing tab component
|
||||
const View = Vue.extend(VersionTab)
|
||||
let TabInstance = null
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
if (OCA.Files?.Sidebar === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab({
|
||||
id: 'version_vue',
|
||||
name: t('files_versions', 'Version'),
|
||||
iconSvg: BackupRestore,
|
||||
|
||||
async mount(el, fileInfo, context) {
|
||||
if (TabInstance) {
|
||||
TabInstance.$destroy()
|
||||
}
|
||||
TabInstance = new View({
|
||||
// Better integration with vue parent component
|
||||
parent: context,
|
||||
})
|
||||
// Only mount after we have all the info we need
|
||||
await TabInstance.update(fileInfo)
|
||||
TabInstance.$mount(el)
|
||||
},
|
||||
update(fileInfo) {
|
||||
TabInstance.update(fileInfo)
|
||||
},
|
||||
destroy() {
|
||||
TabInstance.$destroy()
|
||||
TabInstance = null
|
||||
},
|
||||
enabled(fileInfo) {
|
||||
return !(fileInfo?.isDirectory() ?? true)
|
||||
},
|
||||
}))
|
||||
})
|
|
@ -1,22 +0,0 @@
|
|||
<li data-revision="{{id}}">
|
||||
<div>
|
||||
<div class="preview-container">
|
||||
<img class="preview" src="{{previewUrl}}" width="44" height="44"/>
|
||||
</div>
|
||||
<div class="version-container">
|
||||
<div>
|
||||
<a href="{{downloadUrl}}" class="downloadVersion" download="{{downloadName}}"><img src="{{downloadIconUrl}}" />
|
||||
<span class="versiondate has-tooltip live-relative-timestamp" data-timestamp="{{millisecondsTimestamp}}" title="{{formattedTimestamp}}">{{relativeTimestamp}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{#hasDetails}}
|
||||
<div class="version-details">
|
||||
<span class="size has-tooltip" title="{{altSize}}">{{humanReadableSize}}</span>
|
||||
</div>
|
||||
{{/hasDetails}}
|
||||
</div>
|
||||
{{#canRevert}}
|
||||
<a href="#" class="revertVersion" title="{{revertLabel}}"><img src="{{revertIconUrl}}" /></a>
|
||||
{{/canRevert}}
|
||||
</div>
|
||||
</li>
|
|
@ -1,10 +0,0 @@
|
|||
<ul class="versions"></ul>
|
||||
<div class="clear-float"></div>
|
||||
<div class="empty hidden">
|
||||
<div class="emptycontent">
|
||||
<div class="icon-history"></div>
|
||||
<p>{{emptyResultLabel}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="button" class="showMoreVersions hidden" value="{{moreVersionsLabel}}" name="show-more-versions" id="show-more-versions" />
|
||||
<div class="loading hidden" style="height: 50px"></div>
|
|
@ -1,8 +1,7 @@
|
|||
/**
|
||||
* Copyright (c) 2015
|
||||
* @copyright 2022 Louis Chemineau <mlouis@chmn.me>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Vincent Petry <vincent@nextcloud.com>
|
||||
* @author Louis Chemineau <mlouis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
|
@ -18,29 +17,18 @@
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
(function() {
|
||||
OCA.Versions = OCA.Versions || {}
|
||||
import { createClient, getPatcher } from 'webdav'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
OCA.Versions.Util = {
|
||||
/**
|
||||
* Initialize the versions plugin.
|
||||
*
|
||||
* @param {OCA.Files.FileList} fileList file list to be extended
|
||||
*/
|
||||
attach(fileList) {
|
||||
if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
|
||||
return
|
||||
}
|
||||
const rootPath = 'dav'
|
||||
|
||||
fileList.registerTabView(new OCA.Versions.VersionsTabView('versionsTabView', { order: -10 }))
|
||||
},
|
||||
}
|
||||
})()
|
||||
// force our axios
|
||||
const patcher = getPatcher()
|
||||
patcher.patch('request', axios)
|
||||
|
||||
OC.Plugins.register('OCA.Files.FileList', OCA.Versions.Util)
|
||||
// init webdav client on default dav endpoint
|
||||
const remote = generateRemoteUrl(rootPath)
|
||||
export default createClient(remote)
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2019 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export default `<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:"
|
||||
xmlns:oc="http://owncloud.org/ns"
|
||||
xmlns:nc="http://nextcloud.org/ns"
|
||||
xmlns:ocs="http://open-collaboration-services.org/ns">
|
||||
<d:prop>
|
||||
<d:getcontentlength />
|
||||
<d:getcontenttype />
|
||||
<d:getlastmodified />
|
||||
</d:prop>
|
||||
</d:propfind>`
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
|
||||
* @copyright 2022 Louis Chemineau <mlouis@chmn.me>
|
||||
*
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
* @author Louis Chemineau <mlouis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
|
@ -17,13 +17,11 @@
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import './versionmodel'
|
||||
import './versioncollection'
|
||||
import './versionstabview'
|
||||
import './filesplugin'
|
||||
import './css/versions.css'
|
||||
import { getLoggerBuilder } from '@nextcloud/logger'
|
||||
|
||||
window.OCA.Versions = OCA.Versions
|
||||
export default getLoggerBuilder()
|
||||
.setApp('files_version')
|
||||
.detectUser()
|
||||
.build()
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* @copyright 2022 Louis Chemineau <mlouis@chmn.me>
|
||||
*
|
||||
* @author Louis Chemineau <mlouis@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/>.
|
||||
*/
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import client from '../utils/davClient.js'
|
||||
import davRequest from '../utils/davRequest.js'
|
||||
import logger from '../utils/logger.js'
|
||||
import { basename, joinPaths } from '@nextcloud/paths'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import moment from '@nextcloud/moment'
|
||||
|
||||
/**
|
||||
* @typedef {object} Version
|
||||
* @property {string} title - '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} size - Human readable size
|
||||
* @property {string} type - 'file'
|
||||
* @property {number} mtime - Version creation date as a timestamp
|
||||
* @property {string} preview - Preview URL of the version
|
||||
* @property {string} url - Download URL of the version
|
||||
* @property {string|null} fileVersion - The version id, null for the current version
|
||||
* @property {boolean} isCurrent - Whether this is the current version of the file
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param fileInfo
|
||||
* @return {Promise<Version[]>}
|
||||
*/
|
||||
export async function fetchVersions(fileInfo) {
|
||||
const path = `/versions/${getCurrentUser()?.uid}/versions/${fileInfo.id}`
|
||||
|
||||
try {
|
||||
/** @type {import('webdav').FileStat[]} */
|
||||
const response = await client.getDirectoryContents(path, {
|
||||
data: davRequest,
|
||||
})
|
||||
return response.map(version => formatVersion(version, fileInfo))
|
||||
} catch (exception) {
|
||||
logger.error('Could not fetch version', { exception })
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the given version
|
||||
*
|
||||
* @param {Version} version
|
||||
* @param {object} fileInfo
|
||||
*/
|
||||
export async function restoreVersion(version, fileInfo) {
|
||||
try {
|
||||
logger.debug('Restoring version', { url: version.url })
|
||||
await client.moveFile(
|
||||
`/versions/${getCurrentUser()?.uid}/versions/${fileInfo.id}/${version.fileVersion}`,
|
||||
`/versions/${getCurrentUser()?.uid}/restore/target`
|
||||
)
|
||||
} catch (exception) {
|
||||
logger.error('Could not restore version', { exception })
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const isCurrent = version.mime === ''
|
||||
const fileVersion = isCurrent ? null : basename(version.filename)
|
||||
|
||||
let url = null
|
||||
let preview = null
|
||||
|
||||
if (isCurrent) {
|
||||
// https://nextcloud_server2.test/remote.php/webdav/welcome.txt?downloadStartSecret=hl5awd7tbzg
|
||||
url = joinPaths('/remote.php/webdav', fileInfo.path, fileInfo.name)
|
||||
preview = generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0', {
|
||||
fileId: fileInfo.id,
|
||||
fileEtag: fileInfo.etag,
|
||||
})
|
||||
} else {
|
||||
url = joinPaths('/remote.php/dav', version.filename)
|
||||
preview = generateUrl('/apps/files_versions/preview?file={file}&version={fileVersion}', {
|
||||
file: joinPaths(fileInfo.path, fileInfo.name),
|
||||
fileVersion,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
title: isCurrent ? translate('files_versions', 'Current version') : '',
|
||||
fileName: version.filename,
|
||||
mimeType: version.mime,
|
||||
size: isCurrent ? fileInfo.size : version.size,
|
||||
type: version.type,
|
||||
mtime: moment(isCurrent ? fileInfo.mtime : version.lastmod).unix(),
|
||||
preview,
|
||||
url,
|
||||
fileVersion,
|
||||
isCurrent,
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Vincent Petry <vincent@nextcloud.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
(function() {
|
||||
/**
|
||||
* @memberof OCA.Versions
|
||||
*/
|
||||
const VersionCollection = OC.Backbone.Collection.extend({
|
||||
model: OCA.Versions.VersionModel,
|
||||
sync: OC.Backbone.davSync,
|
||||
|
||||
/**
|
||||
* @member OCA.Files.FileInfoModel
|
||||
*/
|
||||
_fileInfo: null,
|
||||
|
||||
_currentUser: null,
|
||||
|
||||
_client: null,
|
||||
|
||||
setFileInfo(fileInfo) {
|
||||
this._fileInfo = fileInfo
|
||||
},
|
||||
|
||||
getFileInfo() {
|
||||
return this._fileInfo
|
||||
},
|
||||
|
||||
setCurrentUser(user) {
|
||||
this._currentUser = user
|
||||
},
|
||||
|
||||
getCurrentUser() {
|
||||
return this._currentUser || OC.getCurrentUser().uid
|
||||
},
|
||||
|
||||
setClient(client) {
|
||||
this._client = client
|
||||
},
|
||||
|
||||
getClient() {
|
||||
return this._client || new OC.Files.Client({
|
||||
host: OC.getHost(),
|
||||
root: OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser(),
|
||||
useHTTPS: OC.getProtocol() === 'https',
|
||||
})
|
||||
},
|
||||
|
||||
url() {
|
||||
return OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser() + '/versions/' + this._fileInfo.get('id')
|
||||
},
|
||||
|
||||
parse(result) {
|
||||
const fullPath = this._fileInfo.getFullPath()
|
||||
const fileId = this._fileInfo.get('id')
|
||||
const name = this._fileInfo.get('name')
|
||||
const user = this.getCurrentUser()
|
||||
const client = this.getClient()
|
||||
return _.map(result, function(version) {
|
||||
version.fullPath = fullPath
|
||||
version.fileId = fileId
|
||||
version.name = name
|
||||
version.timestamp = parseInt(moment(new Date(version.timestamp)).format('X'), 10)
|
||||
version.id = OC.basename(version.href)
|
||||
version.size = parseInt(version.size, 10)
|
||||
version.user = user
|
||||
version.client = client
|
||||
return version
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
OCA.Versions = OCA.Versions || {}
|
||||
|
||||
OCA.Versions.VersionCollection = VersionCollection
|
||||
})()
|
|
@ -1,86 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Vincent Petry <vincent@nextcloud.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
(function() {
|
||||
/**
|
||||
* @memberof OCA.Versions
|
||||
*/
|
||||
const VersionModel = OC.Backbone.Model.extend({
|
||||
sync: OC.Backbone.davSync,
|
||||
|
||||
davProperties: {
|
||||
size: '{DAV:}getcontentlength',
|
||||
mimetype: '{DAV:}getcontenttype',
|
||||
timestamp: '{DAV:}getlastmodified',
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores the original file to this revision
|
||||
*
|
||||
* @param {object} [options] options
|
||||
* @return {Promise}
|
||||
*/
|
||||
revert(options) {
|
||||
options = options ? _.clone(options) : {}
|
||||
const model = this
|
||||
|
||||
const client = this.get('client')
|
||||
|
||||
return client.move('/versions/' + this.get('fileId') + '/' + this.get('id'), '/restore/target', true)
|
||||
.done(function() {
|
||||
if (options.success) {
|
||||
options.success.call(options.context, model, {}, options)
|
||||
}
|
||||
model.trigger('revert', model, options)
|
||||
})
|
||||
.fail(function() {
|
||||
if (options.error) {
|
||||
options.error.call(options.context, model, {}, options)
|
||||
}
|
||||
model.trigger('error', model, {}, options)
|
||||
})
|
||||
},
|
||||
|
||||
getFullPath() {
|
||||
return this.get('fullPath')
|
||||
},
|
||||
|
||||
getPreviewUrl() {
|
||||
const url = OC.generateUrl('/apps/files_versions/preview')
|
||||
const params = {
|
||||
file: this.get('fullPath'),
|
||||
version: this.get('id'),
|
||||
}
|
||||
return url + '?' + OC.buildQueryString(params)
|
||||
},
|
||||
|
||||
getDownloadUrl() {
|
||||
return OC.linkToRemoteBase('dav') + '/versions/' + this.get('user') + '/versions/' + this.get('fileId') + '/' + this.get('id')
|
||||
},
|
||||
})
|
||||
|
||||
OCA.Versions = OCA.Versions || {}
|
||||
|
||||
OCA.Versions.VersionModel = VersionModel
|
||||
})()
|
|
@ -1,231 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* @author Jan-Christoph Borchardt <hey@jancborchardt.net>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Michael Jobst <mjobst+github@tecratech.de>
|
||||
* @author noveens <noveen.sachdeva@research.iiit.ac.in>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Vincent Petry <vincent@nextcloud.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import ItemTemplate from './templates/item.handlebars'
|
||||
import Template from './templates/template.handlebars';
|
||||
|
||||
(function() {
|
||||
if (!OCA.Files.DetailTabView) {
|
||||
// Only register the versions tab within the files app
|
||||
return
|
||||
}
|
||||
/**
|
||||
* @memberof OCA.Versions
|
||||
*/
|
||||
const VersionsTabView = OCA.Files.DetailTabView.extend(/** @lends OCA.Versions.VersionsTabView.prototype */{
|
||||
id: 'versionsTabView',
|
||||
className: 'tab versionsTabView',
|
||||
|
||||
_template: null,
|
||||
|
||||
$versionsContainer: null,
|
||||
|
||||
events: {
|
||||
'click .revertVersion': '_onClickRevertVersion',
|
||||
},
|
||||
|
||||
initialize() {
|
||||
OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments)
|
||||
this.collection = new OCA.Versions.VersionCollection()
|
||||
this.collection.on('request', this._onRequest, this)
|
||||
this.collection.on('sync', this._onEndRequest, this)
|
||||
this.collection.on('update', this._onUpdate, this)
|
||||
this.collection.on('error', this._onError, this)
|
||||
this.collection.on('add', this._onAddModel, this)
|
||||
},
|
||||
|
||||
getLabel() {
|
||||
return t('files_versions', 'Versions')
|
||||
},
|
||||
|
||||
getIcon() {
|
||||
return 'icon-history'
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this._loading) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.collection.getFileInfo() && this.collection.getFileInfo().isDirectory()) {
|
||||
return
|
||||
}
|
||||
this.collection.fetch()
|
||||
},
|
||||
|
||||
_onClickRevertVersion(ev) {
|
||||
const self = this
|
||||
let $target = $(ev.target)
|
||||
const fileInfoModel = this.collection.getFileInfo()
|
||||
if (!$target.is('li')) {
|
||||
$target = $target.closest('li')
|
||||
}
|
||||
|
||||
ev.preventDefault()
|
||||
const revision = $target.attr('data-revision')
|
||||
|
||||
const versionModel = this.collection.get(revision)
|
||||
versionModel.revert({
|
||||
success() {
|
||||
// reset and re-fetch the updated collection
|
||||
self.$versionsContainer.empty()
|
||||
self.collection.setFileInfo(fileInfoModel)
|
||||
self.collection.reset([], { silent: true })
|
||||
self.collection.fetch()
|
||||
|
||||
self.$el.find('.versions').removeClass('hidden')
|
||||
|
||||
// update original model
|
||||
fileInfoModel.trigger('busy', fileInfoModel, false)
|
||||
fileInfoModel.set({
|
||||
size: versionModel.get('size'),
|
||||
mtime: versionModel.get('timestamp') * 1000,
|
||||
// temp dummy, until we can do a PROPFIND
|
||||
etag: versionModel.get('id') + versionModel.get('timestamp'),
|
||||
})
|
||||
},
|
||||
|
||||
error() {
|
||||
fileInfoModel.trigger('busy', fileInfoModel, false)
|
||||
self.$el.find('.versions').removeClass('hidden')
|
||||
self._toggleLoading(false)
|
||||
OC.Notification.show(t('files_version', 'Failed to revert {file} to revision {timestamp}.',
|
||||
{
|
||||
file: versionModel.getFullPath(),
|
||||
timestamp: OC.Util.formatDate(versionModel.get('timestamp') * 1000),
|
||||
}),
|
||||
{
|
||||
type: 'error',
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// spinner
|
||||
this._toggleLoading(true)
|
||||
fileInfoModel.trigger('busy', fileInfoModel, true)
|
||||
},
|
||||
|
||||
_toggleLoading(state) {
|
||||
this._loading = state
|
||||
this.$el.find('.loading').toggleClass('hidden', !state)
|
||||
},
|
||||
|
||||
_onRequest() {
|
||||
this._toggleLoading(true)
|
||||
},
|
||||
|
||||
_onEndRequest() {
|
||||
this._toggleLoading(false)
|
||||
this.$el.find('.empty').toggleClass('hidden', !!this.collection.length)
|
||||
},
|
||||
|
||||
_onAddModel(model) {
|
||||
const $el = $(this.itemTemplate(this._formatItem(model)))
|
||||
this.$versionsContainer.append($el)
|
||||
$el.find('.has-tooltip').tooltip()
|
||||
},
|
||||
|
||||
template(data) {
|
||||
return Template(data)
|
||||
},
|
||||
|
||||
itemTemplate(data) {
|
||||
return ItemTemplate(data)
|
||||
},
|
||||
|
||||
setFileInfo(fileInfo) {
|
||||
if (fileInfo) {
|
||||
this.render()
|
||||
this.collection.setFileInfo(fileInfo)
|
||||
this.collection.reset([], { silent: true })
|
||||
this.nextPage()
|
||||
} else {
|
||||
this.render()
|
||||
this.collection.reset()
|
||||
}
|
||||
},
|
||||
|
||||
_formatItem(version) {
|
||||
const timestamp = version.get('timestamp') * 1000
|
||||
const size = version.has('size') ? version.get('size') : 0
|
||||
const preview = OC.MimeType.getIconUrl(version.get('mimetype'))
|
||||
const img = new Image()
|
||||
img.onload = function() {
|
||||
$('li[data-revision=' + version.get('id') + '] .preview').attr('src', version.getPreviewUrl())
|
||||
}
|
||||
img.src = version.getPreviewUrl()
|
||||
|
||||
return _.extend({
|
||||
versionId: version.get('id'),
|
||||
formattedTimestamp: OC.Util.formatDate(timestamp),
|
||||
relativeTimestamp: OC.Util.relativeModifiedDate(timestamp),
|
||||
millisecondsTimestamp: timestamp,
|
||||
humanReadableSize: OC.Util.humanFileSize(size, true),
|
||||
altSize: n('files', '%n byte', '%n bytes', size),
|
||||
hasDetails: version.has('size'),
|
||||
downloadUrl: version.getDownloadUrl(),
|
||||
downloadIconUrl: OC.imagePath('core', 'actions/download'),
|
||||
downloadName: version.get('name'),
|
||||
revertIconUrl: OC.imagePath('core', 'actions/history'),
|
||||
previewUrl: preview,
|
||||
revertLabel: t('files_versions', 'Restore'),
|
||||
canRevert: (this.collection.getFileInfo().get('permissions') & OC.PERMISSION_UPDATE) !== 0,
|
||||
}, version.attributes)
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders this details view
|
||||
*/
|
||||
render() {
|
||||
this.$el.html(this.template({
|
||||
emptyResultLabel: t('files_versions', 'No other versions available'),
|
||||
}))
|
||||
this.$el.find('.has-tooltip').tooltip()
|
||||
this.$versionsContainer = this.$el.find('ul.versions')
|
||||
this.delegateEvents()
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true for files, false for folders.
|
||||
*
|
||||
* @param {FileInfo} fileInfo fileInfo
|
||||
* @return {boolean} true for files, false for folders
|
||||
*/
|
||||
canDisplay(fileInfo) {
|
||||
if (!fileInfo) {
|
||||
return false
|
||||
}
|
||||
return !fileInfo.isDirectory()
|
||||
},
|
||||
})
|
||||
|
||||
OCA.Versions = OCA.Versions || {}
|
||||
|
||||
OCA.Versions.VersionsTabView = VersionsTabView
|
||||
})()
|
|
@ -0,0 +1,182 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<ul>
|
||||
<NcListItem v-for="version in versions"
|
||||
:key="version.mtime"
|
||||
class="version"
|
||||
:title="version.title"
|
||||
:href="version.url">
|
||||
<template #icon>
|
||||
<img lazy="true"
|
||||
:src="version.preview"
|
||||
alt=""
|
||||
height="256"
|
||||
width="256"
|
||||
class="version__image">
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<div class="version__info">
|
||||
<span>{{ 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 v-if="!version.isCurrent" #actions>
|
||||
<NcActionLink :href="version.url"
|
||||
:download="version.url">
|
||||
<template #icon>
|
||||
<Download :size="22" />
|
||||
</template>
|
||||
{{ t('files_versions', 'Download version') }}
|
||||
</NcActionLink>
|
||||
<NcActionButton @click="restoreVersion(version)">
|
||||
<template #icon>
|
||||
<BackupRestore :size="22" />
|
||||
</template>
|
||||
{{ t('files_versions', 'Restore version') }}
|
||||
</NcActionButton>
|
||||
</template>
|
||||
</NcListItem>
|
||||
<NcEmptyContent v-if="!loading && versions.length === 1"
|
||||
:title="t('files_version', 'No versions yet')">
|
||||
<!-- length === 1, since we don't want to show versions if there is only the current file -->
|
||||
<template #icon>
|
||||
<BackupRestore />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BackupRestore from 'vue-material-design-icons/BackupRestore.vue'
|
||||
import Download from 'vue-material-design-icons/Download.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 NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { fetchVersions, restoreVersion } from '../utils/versions.js'
|
||||
import moment from '@nextcloud/moment'
|
||||
|
||||
export default {
|
||||
name: 'VersionTab',
|
||||
components: {
|
||||
NcEmptyContent,
|
||||
NcActionLink,
|
||||
NcActionButton,
|
||||
NcListItem,
|
||||
BackupRestore,
|
||||
Download,
|
||||
},
|
||||
filters: {
|
||||
humanReadableSize(bytes) {
|
||||
return OC.Util.humanFileSize(bytes)
|
||||
},
|
||||
humanDateFromNow(timestamp) {
|
||||
return moment(timestamp * 1000).fromNow()
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fileInfo: null,
|
||||
/** @type {import('../utils/versions.js').Version[]} */
|
||||
versions: [],
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Update current fileInfo and fetch new data
|
||||
*
|
||||
* @param {object} fileInfo the current file FileInfo
|
||||
*/
|
||||
async update(fileInfo) {
|
||||
this.fileInfo = fileInfo
|
||||
this.resetState()
|
||||
this.fetchVersions()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the existing versions infos
|
||||
*/
|
||||
async fetchVersions() {
|
||||
try {
|
||||
this.loading = true
|
||||
this.versions = await fetchVersions(this.fileInfo)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore the given version
|
||||
*
|
||||
* @param version
|
||||
*/
|
||||
async restoreVersion(version) {
|
||||
try {
|
||||
await restoreVersion(version, this.fileInfo)
|
||||
// File info is not updated so we manually update its size and mtime if the restoration went fine.
|
||||
this.fileInfo.size = version.size
|
||||
this.fileInfo.mtime = version.lastmod
|
||||
showSuccess(t('files_versions', 'Version restored'))
|
||||
await this.fetchVersions()
|
||||
} catch (exception) {
|
||||
showError(t('files_versions', 'Could not restore version'))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the current view to its default state
|
||||
*/
|
||||
resetState() {
|
||||
this.versions = []
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scopped lang="scss">
|
||||
.version {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&__size {
|
||||
color: var(--color-text-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 1px solid var(--color-border);
|
||||
margin-right: 1rem;
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,48 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Vincent Petry <vincent@nextcloud.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
describe('OCA.Versions.VersionCollection', function() {
|
||||
var VersionCollection = OCA.Versions.VersionCollection;
|
||||
var collection, fileInfoModel;
|
||||
|
||||
beforeEach(function() {
|
||||
fileInfoModel = new OCA.Files.FileInfoModel({
|
||||
path: '/subdir',
|
||||
name: 'some file.txt',
|
||||
id: 10,
|
||||
});
|
||||
collection = new VersionCollection();
|
||||
collection.setFileInfo(fileInfoModel);
|
||||
collection.setCurrentUser('user');
|
||||
});
|
||||
it('fetches the versions', function() {
|
||||
collection.fetch();
|
||||
|
||||
expect(fakeServer.requests.length).toEqual(1);
|
||||
expect(fakeServer.requests[0].url).toEqual(
|
||||
OC.linkToRemoteBase('dav') + '/versions/user/versions/10'
|
||||
);
|
||||
fakeServer.requests[0].respond(200);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* @author Daniel Calviño Sánchez <danxuliu@gmail.com>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Vincent Petry <vincent@nextcloud.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
describe('OCA.Versions.VersionModel', function() {
|
||||
var VersionModel = OCA.Versions.VersionModel;
|
||||
var model;
|
||||
var uid = OC.currentUser = 'user';
|
||||
|
||||
beforeEach(function() {
|
||||
model = new VersionModel({
|
||||
id: 10000000,
|
||||
fileId: 10,
|
||||
timestamp: 10000000,
|
||||
fullPath: '/subdir/some file.txt',
|
||||
name: 'some file.txt',
|
||||
size: 150,
|
||||
user: 'user',
|
||||
client: new OC.Files.Client({
|
||||
host: 'localhost',
|
||||
port: 80,
|
||||
root: '/remote.php/dav/versions/user',
|
||||
useHTTPS: OC.getProtocol() === 'https'
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the full path', function() {
|
||||
expect(model.getFullPath()).toEqual('/subdir/some file.txt');
|
||||
});
|
||||
it('returns the preview url', function() {
|
||||
expect(model.getPreviewUrl())
|
||||
.toEqual(OC.generateUrl('/apps/files_versions/preview') +
|
||||
'?file=%2Fsubdir%2Fsome%20file.txt&version=10000000'
|
||||
);
|
||||
});
|
||||
it('returns the download url', function() {
|
||||
expect(model.getDownloadUrl())
|
||||
.toEqual(OC.linkToRemoteBase('dav') + '/versions/' + uid +
|
||||
'/versions/10/10000000'
|
||||
);
|
||||
});
|
||||
describe('reverting', function() {
|
||||
var revertEventStub;
|
||||
var successStub;
|
||||
var errorStub;
|
||||
|
||||
beforeEach(function() {
|
||||
revertEventStub = sinon.stub();
|
||||
errorStub = sinon.stub();
|
||||
successStub = sinon.stub();
|
||||
|
||||
model.on('revert', revertEventStub);
|
||||
model.on('error', errorStub);
|
||||
});
|
||||
it('tells the server to revert when calling the revert method', function(done) {
|
||||
var promise = model.revert({
|
||||
success: successStub
|
||||
});
|
||||
|
||||
expect(fakeServer.requests.length).toEqual(1);
|
||||
var request = fakeServer.requests[0];
|
||||
expect(request.url)
|
||||
.toEqual(
|
||||
OC.linkToRemoteBase('dav') + '/versions/user/versions/10/10000000'
|
||||
);
|
||||
expect(request.requestHeaders.Destination).toEqual(OC.getRootPath() + '/remote.php/dav/versions/user/restore/target');
|
||||
request.respond(201);
|
||||
|
||||
promise.then(function() {
|
||||
expect(revertEventStub.calledOnce).toEqual(true);
|
||||
expect(successStub.calledOnce).toEqual(true);
|
||||
expect(errorStub.notCalled).toEqual(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('triggers error event when server returns a failure', function(done) {
|
||||
var promise = model.revert({
|
||||
success: successStub
|
||||
});
|
||||
|
||||
expect(fakeServer.requests.length).toEqual(1);
|
||||
var responseErrorHeaders = {
|
||||
"Content-Type": "application/xml"
|
||||
};
|
||||
var responseErrorBody =
|
||||
'<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">' +
|
||||
' <s:exception>Sabre\\DAV\\Exception\\SomeException</s:exception>' +
|
||||
' <s:message>Some error message</s:message>' +
|
||||
'</d:error>';
|
||||
fakeServer.requests[0].respond(404, responseErrorHeaders, responseErrorBody);
|
||||
|
||||
promise.fail(function() {
|
||||
expect(revertEventStub.notCalled).toEqual(true);
|
||||
expect(successStub.notCalled).toEqual(true);
|
||||
expect(errorStub.calledOnce).toEqual(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* @author Michael Jobst <mjobst+github@tecratech.de>
|
||||
* @author Morris Jobke <hey@morrisjobke.de>
|
||||
* @author noveens <noveen.sachdeva@research.iiit.ac.in>
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
* @author Vincent Petry <vincent@nextcloud.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
describe('OCA.Versions.VersionsTabView', function() {
|
||||
var VersionCollection = OCA.Versions.VersionCollection;
|
||||
var VersionModel = OCA.Versions.VersionModel;
|
||||
var VersionsTabView = OCA.Versions.VersionsTabView;
|
||||
|
||||
var fetchStub, fileInfoModel, tabView, testVersions, clock;
|
||||
|
||||
beforeEach(function() {
|
||||
clock = sinon.useFakeTimers(Date.UTC(2015, 6, 17, 1, 2, 0, 3));
|
||||
var time1 = Date.UTC(2015, 6, 17, 1, 2, 0, 3) / 1000;
|
||||
var time2 = Date.UTC(2015, 6, 15, 1, 2, 0, 3) / 1000;
|
||||
|
||||
var version1 = new VersionModel({
|
||||
id: time1,
|
||||
timestamp: time1,
|
||||
name: 'some file.txt',
|
||||
size: 140,
|
||||
fullPath: '/subdir/some file.txt',
|
||||
mimetype: 'text/plain'
|
||||
});
|
||||
var version2 = new VersionModel({
|
||||
id: time2,
|
||||
timestamp: time2,
|
||||
name: 'some file.txt',
|
||||
size: 150,
|
||||
fullPath: '/subdir/some file.txt',
|
||||
mimetype: 'text/plain'
|
||||
});
|
||||
|
||||
testVersions = [version1, version2];
|
||||
|
||||
fetchStub = sinon.stub(VersionCollection.prototype, 'fetch');
|
||||
fileInfoModel = new OCA.Files.FileInfoModel({
|
||||
id: 123,
|
||||
name: 'test.txt',
|
||||
permissions: OC.PERMISSION_READ | OC.PERMISSION_UPDATE
|
||||
});
|
||||
tabView = new VersionsTabView();
|
||||
tabView.render();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
fetchStub.restore();
|
||||
tabView.remove();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
describe('rendering', function() {
|
||||
it('reloads matching versions when setting file info model', function() {
|
||||
tabView.setFileInfo(fileInfoModel);
|
||||
expect(fetchStub.calledOnce).toEqual(true);
|
||||
});
|
||||
|
||||
it('renders loading icon while fetching versions', function() {
|
||||
tabView.setFileInfo(fileInfoModel);
|
||||
tabView.collection.trigger('request');
|
||||
|
||||
expect(tabView.$el.find('.loading').length).toEqual(1);
|
||||
expect(tabView.$el.find('.versions li').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('renders versions', function() {
|
||||
|
||||
tabView.setFileInfo(fileInfoModel);
|
||||
tabView.collection.set(testVersions);
|
||||
|
||||
var version1 = testVersions[0];
|
||||
var version2 = testVersions[1];
|
||||
var $versions = tabView.$el.find('.versions>li');
|
||||
expect($versions.length).toEqual(2);
|
||||
var $item = $versions.eq(0);
|
||||
expect($item.find('.downloadVersion').attr('href')).toEqual(version1.getDownloadUrl());
|
||||
expect($item.find('.versiondate').text()).toEqual('seconds ago');
|
||||
expect($item.find('.size').text()).toEqual('< 1 KB');
|
||||
expect($item.find('.revertVersion').length).toEqual(1);
|
||||
|
||||
$item = $versions.eq(1);
|
||||
expect($item.find('.downloadVersion').attr('href')).toEqual(version2.getDownloadUrl());
|
||||
expect($item.find('.versiondate').text()).toEqual('2 days ago');
|
||||
expect($item.find('.size').text()).toEqual('< 1 KB');
|
||||
expect($item.find('.revertVersion').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('does not render revert button when no update permissions', function() {
|
||||
|
||||
fileInfoModel.set('permissions', OC.PERMISSION_READ);
|
||||
tabView.setFileInfo(fileInfoModel);
|
||||
tabView.collection.set(testVersions);
|
||||
|
||||
var version1 = testVersions[0];
|
||||
var version2 = testVersions[1];
|
||||
var $versions = tabView.$el.find('.versions>li');
|
||||
expect($versions.length).toEqual(2);
|
||||
var $item = $versions.eq(0);
|
||||
expect($item.find('.downloadVersion').attr('href')).toEqual(version1.getDownloadUrl());
|
||||
expect($item.find('.versiondate').text()).toEqual('seconds ago');
|
||||
expect($item.find('.revertVersion').length).toEqual(0);
|
||||
|
||||
$item = $versions.eq(1);
|
||||
expect($item.find('.downloadVersion').attr('href')).toEqual(version2.getDownloadUrl());
|
||||
expect($item.find('.versiondate').text()).toEqual('2 days ago');
|
||||
expect($item.find('.revertVersion').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reverting', function() {
|
||||
var revertStub;
|
||||
|
||||
beforeEach(function() {
|
||||
revertStub = sinon.stub(VersionModel.prototype, 'revert');
|
||||
tabView.setFileInfo(fileInfoModel);
|
||||
tabView.collection.set(testVersions);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
revertStub.restore();
|
||||
});
|
||||
|
||||
it('tells the model to revert when clicking "Revert"', function() {
|
||||
tabView.$el.find('.revertVersion').eq(1).click();
|
||||
|
||||
expect(revertStub.calledOnce).toEqual(true);
|
||||
});
|
||||
it('triggers busy state during revert', function() {
|
||||
var busyStub = sinon.stub();
|
||||
fileInfoModel.on('busy', busyStub);
|
||||
|
||||
tabView.$el.find('.revertVersion').eq(1).click();
|
||||
|
||||
expect(busyStub.calledOnce).toEqual(true);
|
||||
expect(busyStub.calledWith(fileInfoModel, true)).toEqual(true);
|
||||
|
||||
busyStub.reset();
|
||||
revertStub.getCall(0).args[0].success();
|
||||
|
||||
expect(busyStub.calledOnce).toEqual(true);
|
||||
expect(busyStub.calledWith(fileInfoModel, false)).toEqual(true);
|
||||
});
|
||||
it('updates the file info model with the information from the reverted revision', function() {
|
||||
var changeStub = sinon.stub();
|
||||
fileInfoModel.on('change', changeStub);
|
||||
|
||||
tabView.$el.find('.revertVersion').eq(1).click();
|
||||
|
||||
expect(changeStub.notCalled).toEqual(true);
|
||||
|
||||
revertStub.getCall(0).args[0].success();
|
||||
|
||||
expect(changeStub.calledOnce).toEqual(true);
|
||||
var changes = changeStub.getCall(0).args[0].changed;
|
||||
expect(changes.size).toEqual(150);
|
||||
expect(changes.mtime).toEqual(testVersions[1].get('timestamp') * 1000);
|
||||
expect(changes.etag).toBeDefined();
|
||||
});
|
||||
it('shows notification on revert error', function() {
|
||||
var notificationStub = sinon.stub(OC.Notification, 'show');
|
||||
|
||||
tabView.$el.find('.revertVersion').eq(1).click();
|
||||
|
||||
revertStub.getCall(0).args[0].error();
|
||||
|
||||
expect(notificationStub.calledOnce).toEqual(true);
|
||||
|
||||
notificationStub.restore();
|
||||
});
|
||||
});
|
||||
});
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -10,6 +10,7 @@
|
|||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@chenfengyuan/vue-qrcode": "^1.0.2",
|
||||
"@mdi/svg": "^7.0.96",
|
||||
"@nextcloud/auth": "^1.3.0",
|
||||
"@nextcloud/axios": "^1.10.0",
|
||||
"@nextcloud/browser-storage": "^0.1.1",
|
||||
|
@ -29,6 +30,7 @@
|
|||
"@nextcloud/sharing": "^0.1.0",
|
||||
"@nextcloud/vue": "^7.1.0-beta.2",
|
||||
"@nextcloud/vue-dashboard": "^2.0.1",
|
||||
"@skjnldsv/sanitize-svg": "^1.0.2",
|
||||
"autosize": "^5.0.1",
|
||||
"backbone": "^1.4.1",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
|
@ -3469,6 +3471,11 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdi/svg": {
|
||||
"version": "7.0.96",
|
||||
"resolved": "https://registry.npmjs.org/@mdi/svg/-/svg-7.0.96.tgz",
|
||||
"integrity": "sha512-5DC+w7Kl2C82j4aTWCUf6wtHzgY60WBf1gT1qrpkLaMNcH6Vj9FpYPAXdSmtdkmSMvVMs8i1Rtv9cXWcHFQYpw=="
|
||||
},
|
||||
"node_modules/@nextcloud/auth": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-1.3.0.tgz",
|
||||
|
@ -4433,6 +4440,18 @@
|
|||
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@skjnldsv/sanitize-svg": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@skjnldsv/sanitize-svg/-/sanitize-svg-1.0.2.tgz",
|
||||
"integrity": "sha512-blfdQZ9jr4K9IOhifF0FVhKf9LCFH0L8wWR/vEgdA53q8DGNEbjUGMNo4VU1QugglaoQdFy65O2abODRFflsSg==",
|
||||
"dependencies": {
|
||||
"is-svg": "^4.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.0.0",
|
||||
"npm": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
|
@ -10711,6 +10730,20 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-svg": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.3.2.tgz",
|
||||
"integrity": "sha512-mM90duy00JGMyjqIVHu9gNTjywdZV+8qNasX8cm/EEYZ53PHDgajvbBwNVvty5dwSAxLUD3p3bdo+7sR/UMrpw==",
|
||||
"dependencies": {
|
||||
"fast-xml-parser": "^3.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-symbol": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
|
||||
|
@ -22697,6 +22730,11 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"@mdi/svg": {
|
||||
"version": "7.0.96",
|
||||
"resolved": "https://registry.npmjs.org/@mdi/svg/-/svg-7.0.96.tgz",
|
||||
"integrity": "sha512-5DC+w7Kl2C82j4aTWCUf6wtHzgY60WBf1gT1qrpkLaMNcH6Vj9FpYPAXdSmtdkmSMvVMs8i1Rtv9cXWcHFQYpw=="
|
||||
},
|
||||
"@nextcloud/auth": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-1.3.0.tgz",
|
||||
|
@ -23453,6 +23491,14 @@
|
|||
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@skjnldsv/sanitize-svg": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@skjnldsv/sanitize-svg/-/sanitize-svg-1.0.2.tgz",
|
||||
"integrity": "sha512-blfdQZ9jr4K9IOhifF0FVhKf9LCFH0L8wWR/vEgdA53q8DGNEbjUGMNo4VU1QugglaoQdFy65O2abODRFflsSg==",
|
||||
"requires": {
|
||||
"is-svg": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
|
@ -28314,6 +28360,14 @@
|
|||
"has-tostringtag": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-svg": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.3.2.tgz",
|
||||
"integrity": "sha512-mM90duy00JGMyjqIVHu9gNTjywdZV+8qNasX8cm/EEYZ53PHDgajvbBwNVvty5dwSAxLUD3p3bdo+7sR/UMrpw==",
|
||||
"requires": {
|
||||
"fast-xml-parser": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"is-symbol": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@chenfengyuan/vue-qrcode": "^1.0.2",
|
||||
"@mdi/svg": "^7.0.96",
|
||||
"@nextcloud/auth": "^1.3.0",
|
||||
"@nextcloud/axios": "^1.10.0",
|
||||
"@nextcloud/browser-storage": "^0.1.1",
|
||||
|
@ -49,6 +50,7 @@
|
|||
"@nextcloud/sharing": "^0.1.0",
|
||||
"@nextcloud/vue": "^7.1.0-beta.2",
|
||||
"@nextcloud/vue-dashboard": "^2.0.1",
|
||||
"@skjnldsv/sanitize-svg": "^1.0.2",
|
||||
"autosize": "^5.0.1",
|
||||
"backbone": "^1.4.1",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
|
|
|
@ -116,7 +116,10 @@ module.exports = {
|
|||
test: /\.handlebars/,
|
||||
loader: 'handlebars-loader',
|
||||
},
|
||||
|
||||
{
|
||||
resourceQuery: /raw/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ module.exports = {
|
|||
files_trashbin: path.join(__dirname, 'apps/files_trashbin/src', 'files_trashbin.js'),
|
||||
},
|
||||
files_versions: {
|
||||
files_versions: path.join(__dirname, 'apps/files_versions/src', 'files_versions.js'),
|
||||
files_versions: path.join(__dirname, 'apps/files_versions/src', 'files_versions_tab.js'),
|
||||
},
|
||||
oauth2: {
|
||||
oauth2: path.join(__dirname, 'apps/oauth2/src', 'main.js'),
|
||||
|
|
Loading…
Reference in New Issue