Merge pull request #34769 from nextcloud/port/vue/files_version

Port files_versions to vue
This commit is contained in:
Louis 2022-11-29 10:20:20 +01:00 committed by GitHub
commit a884f311b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 539 additions and 934 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
dist/core-common.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
dist/files-sidebar.js vendored

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.

54
package-lock.json generated
View File

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

View File

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

View File

@ -116,7 +116,10 @@ module.exports = {
test: /\.handlebars/,
loader: 'handlebars-loader',
},
{
resourceQuery: /raw/,
type: 'asset/source',
},
],
},

View File

@ -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'),