From e7f5516b4d04c16ed2c12dcc9c9c5f34d9f1f73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Sun, 4 Oct 2020 15:33:17 +0200 Subject: [PATCH] Init vue comments tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- .../composer/composer/autoload_classmap.php | 2 + .../composer/composer/autoload_static.php | 2 + apps/comments/lib/AppInfo/Application.php | 6 + apps/comments/lib/Event/LoadCommentsApp.php | 35 +++ .../lib/Listener/LoadCommentsAppListener.php | 55 ++++ .../lib/Listener/LoadSidebarScripts.php | 13 + apps/comments/src/comments-app.js | 32 ++ apps/comments/src/comments-tab.js | 58 ++++ apps/comments/src/components/Comment.vue | 295 ++++++++++++++++++ apps/comments/src/components/Moment.vue | 31 ++ apps/comments/src/mixins/CommentMixin.js | 117 +++++++ .../comments/src/services/CommentsInstance.js | 69 ++++ apps/comments/src/services/DavClient.js | 37 +++ apps/comments/src/services/DeleteComment.js | 37 +++ apps/comments/src/services/EditComment.js | 49 +++ apps/comments/src/services/GetComments.js | 80 +++++ apps/comments/src/services/NewComment.js | 60 ++++ apps/comments/src/utils/cancelableRequest.js | 62 ++++ apps/comments/src/utils/davUtils.js | 29 ++ apps/comments/src/utils/fileUtils.js | 122 ++++++++ apps/comments/src/utils/numberUtil.js | 30 ++ apps/comments/src/views/Comments.vue | 264 ++++++++++++++++ apps/comments/webpack.js | 14 +- apps/files/src/components/SidebarTab.vue | 8 +- apps/files/src/models/Tab.js | 15 +- apps/files/src/views/Sidebar.vue | 1 + core/Controller/AutoCompleteController.php | 17 +- .../Collaborators/UserPlugin.php | 6 + package-lock.json | 111 ++++++- package.json | 4 +- 30 files changed, 1642 insertions(+), 19 deletions(-) create mode 100644 apps/comments/lib/Event/LoadCommentsApp.php create mode 100644 apps/comments/lib/Listener/LoadCommentsAppListener.php create mode 100644 apps/comments/src/comments-app.js create mode 100644 apps/comments/src/comments-tab.js create mode 100644 apps/comments/src/components/Comment.vue create mode 100644 apps/comments/src/components/Moment.vue create mode 100644 apps/comments/src/mixins/CommentMixin.js create mode 100644 apps/comments/src/services/CommentsInstance.js create mode 100644 apps/comments/src/services/DavClient.js create mode 100644 apps/comments/src/services/DeleteComment.js create mode 100644 apps/comments/src/services/EditComment.js create mode 100644 apps/comments/src/services/GetComments.js create mode 100644 apps/comments/src/services/NewComment.js create mode 100644 apps/comments/src/utils/cancelableRequest.js create mode 100644 apps/comments/src/utils/davUtils.js create mode 100644 apps/comments/src/utils/fileUtils.js create mode 100644 apps/comments/src/utils/numberUtil.js create mode 100644 apps/comments/src/views/Comments.vue diff --git a/apps/comments/composer/composer/autoload_classmap.php b/apps/comments/composer/composer/autoload_classmap.php index c4d8a9e331..b0485a5c4e 100644 --- a/apps/comments/composer/composer/autoload_classmap.php +++ b/apps/comments/composer/composer/autoload_classmap.php @@ -15,9 +15,11 @@ return array( 'OCA\\Comments\\Collaboration\\CommentersSorter' => $baseDir . '/../lib/Collaboration/CommentersSorter.php', 'OCA\\Comments\\Controller\\Notifications' => $baseDir . '/../lib/Controller/Notifications.php', 'OCA\\Comments\\EventHandler' => $baseDir . '/../lib/EventHandler.php', + 'OCA\\Comments\\Event\\LoadCommentsApp' => $baseDir . '/../lib/Event/LoadCommentsApp.php', 'OCA\\Comments\\JSSettingsHelper' => $baseDir . '/../lib/JSSettingsHelper.php', 'OCA\\Comments\\Listener\\CommentsEntityEventListener' => $baseDir . '/../lib/Listener/CommentsEntityEventListener.php', 'OCA\\Comments\\Listener\\LoadAdditionalScripts' => $baseDir . '/../lib/Listener/LoadAdditionalScripts.php', + 'OCA\\Comments\\Listener\\LoadCommentsAppListener' => $baseDir . '/../lib/Listener/LoadCommentsAppListener.php', 'OCA\\Comments\\Listener\\LoadSidebarScripts' => $baseDir . '/../lib/Listener/LoadSidebarScripts.php', 'OCA\\Comments\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php', 'OCA\\Comments\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', diff --git a/apps/comments/composer/composer/autoload_static.php b/apps/comments/composer/composer/autoload_static.php index 72b37969ec..bc69b25743 100644 --- a/apps/comments/composer/composer/autoload_static.php +++ b/apps/comments/composer/composer/autoload_static.php @@ -30,9 +30,11 @@ class ComposerStaticInitComments 'OCA\\Comments\\Collaboration\\CommentersSorter' => __DIR__ . '/..' . '/../lib/Collaboration/CommentersSorter.php', 'OCA\\Comments\\Controller\\Notifications' => __DIR__ . '/..' . '/../lib/Controller/Notifications.php', 'OCA\\Comments\\EventHandler' => __DIR__ . '/..' . '/../lib/EventHandler.php', + 'OCA\\Comments\\Event\\LoadCommentsApp' => __DIR__ . '/..' . '/../lib/Event/LoadCommentsApp.php', 'OCA\\Comments\\JSSettingsHelper' => __DIR__ . '/..' . '/../lib/JSSettingsHelper.php', 'OCA\\Comments\\Listener\\CommentsEntityEventListener' => __DIR__ . '/..' . '/../lib/Listener/CommentsEntityEventListener.php', 'OCA\\Comments\\Listener\\LoadAdditionalScripts' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalScripts.php', + 'OCA\\Comments\\Listener\\LoadCommentsAppListener' => __DIR__ . '/..' . '/../lib/Listener/LoadCommentsAppListener.php', 'OCA\\Comments\\Listener\\LoadSidebarScripts' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarScripts.php', 'OCA\\Comments\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php', 'OCA\\Comments\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', diff --git a/apps/comments/lib/AppInfo/Application.php b/apps/comments/lib/AppInfo/Application.php index 4eb097ff00..0f22cd309e 100644 --- a/apps/comments/lib/AppInfo/Application.php +++ b/apps/comments/lib/AppInfo/Application.php @@ -30,6 +30,8 @@ namespace OCA\Comments\AppInfo; use Closure; use OCA\Comments\Capabilities; use OCA\Comments\Controller\Notifications; +use OCA\Comments\Event\LoadCommentsApp; +use OCA\Comments\Listener\LoadCommentsAppListener; use OCA\Comments\EventHandler; use OCA\Comments\JSSettingsHelper; use OCA\Comments\Listener\CommentsEntityEventListener; @@ -70,6 +72,10 @@ class Application extends App implements IBootstrap { LoadSidebar::class, LoadSidebarScripts::class ); + $context->registerEventListener( + LoadCommentsApp::class, + LoadCommentsAppListener::class + ); $context->registerEventListener( CommentsEntityEvent::EVENT_ENTITY, CommentsEntityEventListener::class diff --git a/apps/comments/lib/Event/LoadCommentsApp.php b/apps/comments/lib/Event/LoadCommentsApp.php new file mode 100644 index 0000000000..74ed93ad44 --- /dev/null +++ b/apps/comments/lib/Event/LoadCommentsApp.php @@ -0,0 +1,35 @@ + + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ +namespace OCA\Comments\Event; + +use OCP\EventDispatcher\Event; + +/** + * This event is used to load and init the comments app + * + * @since 21.0.0 + */ +class LoadCommentsApp extends Event { +} diff --git a/apps/comments/lib/Listener/LoadCommentsAppListener.php b/apps/comments/lib/Listener/LoadCommentsAppListener.php new file mode 100644 index 0000000000..755bdaee1b --- /dev/null +++ b/apps/comments/lib/Listener/LoadCommentsAppListener.php @@ -0,0 +1,55 @@ + + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +namespace OCA\Comments\Listener; + +use OCA\Comments\AppInfo\Application; +use OCA\Comments\Event\LoadCommentsApp; +use OCP\AppFramework\Services\IInitialState; +use OCP\Comments\IComment; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Util; + +class LoadCommentsAppListener implements IEventListener { + + /** @var IInitialState */ + private $initialStateService; + + public function __construct(IInitialState $initialStateService) { + $this->initialStateService = $initialStateService; + } + + public function handle(Event $event): void { + if (!($event instanceof LoadCommentsApp)) { + return; + } + + $this->initialStateService->provideInitialState('max-message-length', IComment::MAX_MESSAGE_LENGTH); + + Util::addScript(Application::APP_ID, 'comments-app'); + } +} diff --git a/apps/comments/lib/Listener/LoadSidebarScripts.php b/apps/comments/lib/Listener/LoadSidebarScripts.php index dfa7e511b1..0b76d88363 100644 --- a/apps/comments/lib/Listener/LoadSidebarScripts.php +++ b/apps/comments/lib/Listener/LoadSidebarScripts.php @@ -28,19 +28,32 @@ declare(strict_types=1); namespace OCA\Comments\Listener; use OCA\Comments\AppInfo\Application; +use OCA\Comments\Event\LoadCommentsApp; use OCA\Files\Event\LoadSidebar; use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventListener; use OCP\Util; class LoadSidebarScripts implements IEventListener { + + /** @var IEventDispatcher */ + private $eventDispatcher; + + public function __construct(IEventDispatcher $eventDispatcher) { + $this->eventDispatcher = $eventDispatcher; + } + public function handle(Event $event): void { if (!($event instanceof LoadSidebar)) { return; } + $this->eventDispatcher->dispatchTyped(new LoadCommentsApp()); + // TODO: make sure to only include the sidebar script when // we properly split it between files list and sidebar Util::addScript(Application::APP_ID, 'comments'); + Util::addScript(Application::APP_ID, 'comments-tab'); } } diff --git a/apps/comments/src/comments-app.js b/apps/comments/src/comments-app.js new file mode 100644 index 0000000000..ced5577d5f --- /dev/null +++ b/apps/comments/src/comments-app.js @@ -0,0 +1,32 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import CommentsInstance from './services/CommentsInstance' + +// Init Comments +if (window.OCA && !window.OCA.Comments) { + Object.assign(window.OCA, { Comments: {} }) +} + +// Init Comments App view +Object.assign(window.OCA.Comments, { View: CommentsInstance }) +console.debug('OCA.Comments.View initialized') diff --git a/apps/comments/src/comments-tab.js b/apps/comments/src/comments-tab.js new file mode 100644 index 0000000000..50126dc8d2 --- /dev/null +++ b/apps/comments/src/comments-tab.js @@ -0,0 +1,58 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +// Init Comments tab component +let TabInstance = null +const commentTab = new OCA.Files.Sidebar.Tab({ + id: 'comments', + name: t('comments', 'Comments'), + icon: 'icon-comment', + + async mount(el, fileInfo, context) { + if (TabInstance) { + TabInstance.$destroy() + } + TabInstance = new OCA.Comments.View('files', { + // Better integration with vue parent component + parent: context, + }) + // Only mount after we have all the info we need + await TabInstance.update(fileInfo.id) + TabInstance.$mount(el) + }, + update(fileInfo) { + TabInstance.update(fileInfo.id) + }, + destroy() { + TabInstance.$destroy() + TabInstance = null + }, + scrollBottomReached() { + TabInstance.onScrollBottomReached() + }, +}) + +window.addEventListener('DOMContentLoaded', function() { + if (OCA.Files && OCA.Files.Sidebar) { + OCA.Files.Sidebar.registerTab(commentTab) + } +}) diff --git a/apps/comments/src/components/Comment.vue b/apps/comments/src/components/Comment.vue new file mode 100644 index 0000000000..acacb156f7 --- /dev/null +++ b/apps/comments/src/components/Comment.vue @@ -0,0 +1,295 @@ + + + + + + diff --git a/apps/comments/src/components/Moment.vue b/apps/comments/src/components/Moment.vue new file mode 100644 index 0000000000..a91ed8b9ce --- /dev/null +++ b/apps/comments/src/components/Moment.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/apps/comments/src/mixins/CommentMixin.js b/apps/comments/src/mixins/CommentMixin.js new file mode 100644 index 0000000000..03f5db0846 --- /dev/null +++ b/apps/comments/src/mixins/CommentMixin.js @@ -0,0 +1,117 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import NewComment from '../services/NewComment' +import DeleteComment from '../services/DeleteComment' +import EditComment from '../services/EditComment' +import { showError, showUndo, TOAST_UNDO_TIMEOUT } from '@nextcloud/dialogs' + +export default { + props: { + id: { + type: Number, + default: null, + }, + message: { + // GenFileInfo can convert message as numbers if they doesn't contains text + type: [String, Number], + default: '', + }, + ressourceId: { + type: [String, Number], + required: true, + }, + }, + + data() { + return { + deleted: false, + editing: false, + loading: false, + } + }, + + methods: { + // EDITION + onEdit() { + this.editing = true + }, + onEditCancel() { + this.editing = false + // Restore original value + this.updateLocalMessage(this.message) + }, + async onEditComment(message) { + this.loading = true + try { + await EditComment(this.commentsType, this.ressourceId, this.id, message) + this.logger.debug('Comment edited', { commentsType: this.commentsType, ressourceId: this.ressourceId, id: this.id, message }) + this.$emit('update:message', message) + this.editing = false + } catch (error) { + showError(t('comments', 'An error occurred while trying to edit the comment')) + console.error(error) + } finally { + this.loading = false + } + }, + + // DELETION + onDeleteWithUndo() { + this.deleted = true + const timeOutDelete = setTimeout(this.onDelete, TOAST_UNDO_TIMEOUT) + showUndo(t('comments', 'Comment deleted'), () => { + clearTimeout(timeOutDelete) + this.deleted = false + }) + }, + async onDelete() { + try { + await DeleteComment(this.commentsType, this.ressourceId, this.id) + this.logger.debug('Comment deleted', { commentsType: this.commentsType, ressourceId: this.ressourceId, id: this.id }) + this.$emit('delete', this.id) + } catch (error) { + showError(t('comments', 'An error occurred while trying to delete the comment')) + console.error(error) + this.deleted = false + } + }, + + // CREATION + async onNewComment(message) { + this.loading = true + try { + const newComment = await NewComment(this.commentsType, this.ressourceId, message) + this.logger.debug('New comment posted', { commentsType: this.commentsType, ressourceId: this.ressourceId, newComment }) + this.$emit('new', newComment) + // Clear old content + this.$emit('update:message', '') + this.localMessage = '' + } catch (error) { + showError(t('comments', 'An error occurred while trying to create the comment')) + console.error(error) + } finally { + this.loading = false + } + }, + }, +} diff --git a/apps/comments/src/services/CommentsInstance.js b/apps/comments/src/services/CommentsInstance.js new file mode 100644 index 0000000000..9eeea19876 --- /dev/null +++ b/apps/comments/src/services/CommentsInstance.js @@ -0,0 +1,69 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import { getLoggerBuilder } from '@nextcloud/logger' +import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import CommentsApp from '../views/Comments' +import Vue from 'vue' + +const logger = getLoggerBuilder() + .setApp('comments') + .detectUser() + .build() + +// Add translates functions +Vue.mixin({ + data() { + return { + logger, + } + }, + methods: { + t, + n, + }, +}) + +export default class CommentInstance { + + /** + * Initialize a new Comments instance for the desired type + * + * @param {string} commentsType the comments endpoint type + * @param {Object} options the vue options (propsData, parent, el...) + */ + constructor(commentsType = 'files', options) { + // Add comments type as a global mixin + Vue.mixin({ + data() { + return { + commentsType, + } + }, + }) + + // Init Comments component + const View = Vue.extend(CommentsApp) + return new View(options) + } + +} diff --git a/apps/comments/src/services/DavClient.js b/apps/comments/src/services/DavClient.js new file mode 100644 index 0000000000..9fc67b52c9 --- /dev/null +++ b/apps/comments/src/services/DavClient.js @@ -0,0 +1,37 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import webdav from 'webdav' +import axios from '@nextcloud/axios' +import { getRootPath } from '../utils/davUtils' + +// Add this so the server knows it is an request from the browser +axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest' + +// force our axios +const patcher = webdav.getPatcher() +patcher.patch('request', axios) + +// init webdav client +const client = webdav.createClient(getRootPath()) + +export default client diff --git a/apps/comments/src/services/DeleteComment.js b/apps/comments/src/services/DeleteComment.js new file mode 100644 index 0000000000..d9954a5603 --- /dev/null +++ b/apps/comments/src/services/DeleteComment.js @@ -0,0 +1,37 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import client from './DavClient' + +/** + * Delete a comment + * + * @param {string} commentsType the ressource type + * @param {number} ressourceId the ressource ID + * @param {number} commentId the comment iD + */ +export default async function(commentsType, ressourceId, commentId) { + const commentPath = ['', commentsType, ressourceId, commentId].join('/') + + // Fetch newly created comment data + await client.deleteFile(commentPath) +} diff --git a/apps/comments/src/services/EditComment.js b/apps/comments/src/services/EditComment.js new file mode 100644 index 0000000000..fd6624c7da --- /dev/null +++ b/apps/comments/src/services/EditComment.js @@ -0,0 +1,49 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import client from './DavClient' + +/** + * Edit an existing comment + * + * @param {string} commentsType the ressource type + * @param {number} ressourceId the ressource ID + * @param {number} commentId the comment iD + * @param {string} message the message content + */ +export default async function(commentsType, ressourceId, commentId, message) { + const commentPath = ['', commentsType, ressourceId, commentId].join('/') + + return await client.customRequest(commentPath, Object.assign({ + method: 'PROPPATCH', + data: ` + + + + ${message} + + + `, + })) +} diff --git a/apps/comments/src/services/GetComments.js b/apps/comments/src/services/GetComments.js new file mode 100644 index 0000000000..a1ac89069e --- /dev/null +++ b/apps/comments/src/services/GetComments.js @@ -0,0 +1,80 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import { parseXML, prepareFileFromProps } from 'webdav/dist/node/interface/dav' +import { processResponsePayload } from 'webdav/dist/node/response' +import client from './DavClient' +import { genFileInfo } from '../utils/fileUtils' + +export const DEFAULT_LIMIT = 5 +/** + * Retrieve the comments list + * + * @param {Object} data destructuring object + * @param {string} data.commentsType the ressource type + * @param {number} data.ressourceId the ressource ID + * @param {Object} [options] optional options for axios + * @returns {Object[]} the comments list + */ +export default async function({ commentsType, ressourceId }, options = {}) { + let response = null + const ressourcePath = ['', commentsType, ressourceId].join('/') + + return await client.customRequest(ressourcePath, Object.assign({ + method: 'REPORT', + data: ` + + ${DEFAULT_LIMIT} + ${options.offset || 0} + `, + }, options)) + // See example on how it's done normaly + // https://github.com/perry-mitchell/webdav-client/blob/9de2da4a2599e06bd86c2778145b7ade39fe0b3c/source/interface/stat.js#L19 + // Waiting for proper REPORT integration https://github.com/perry-mitchell/webdav-client/issues/207 + .then(res => { + response = res + return res.data + }) + .then(parseXML) + .then(xml => processMultistatus(xml, true)) + .then(comments => processResponsePayload(response, comments, true)) + .then(response => response.data.map(genFileInfo)) +} + +// https://github.com/perry-mitchell/webdav-client/blob/9de2da4a2599e06bd86c2778145b7ade39fe0b3c/source/interface/directoryContents.js#L32 +function processMultistatus(result, isDetailed = false) { + // Extract the response items (directory contents) + const { + multistatus: { response: responseItems }, + } = result + return responseItems.map(item => { + // Each item should contain a stat object + const { + propstat: { prop: props }, + } = item + return prepareFileFromProps(props, props.id.toString(), isDetailed) + }) +} diff --git a/apps/comments/src/services/NewComment.js b/apps/comments/src/services/NewComment.js new file mode 100644 index 0000000000..96aee85e01 --- /dev/null +++ b/apps/comments/src/services/NewComment.js @@ -0,0 +1,60 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import { genFileInfo } from '../utils/fileUtils' +import { getCurrentUser } from '@nextcloud/auth' +import { getRootPath } from '../utils/davUtils' +import axios from '@nextcloud/axios' +import client from './DavClient' + +/** + * Retrieve the comments list + * + * @param {string} commentsType the ressource type + * @param {number} ressourceId the ressource ID + * @param {string} message the message + * @returns {Object} the new comment + */ +export default async function(commentsType, ressourceId, message) { + const ressourcePath = ['', commentsType, ressourceId].join('/') + + const response = await axios.post(getRootPath() + ressourcePath, { + actorDisplayName: getCurrentUser().displayName, + actorId: getCurrentUser().uid, + actorType: 'users', + creationDateTime: (new Date()).toUTCString(), + message, + objectType: 'files', + verb: 'comment', + }) + + // Retrieve comment id from ressource location + const commentId = parseInt(response.headers['content-location'].split('/').pop()) + const commentPath = ressourcePath + '/' + commentId + + // Fetch newly created comment data + const comment = await client.stat(commentPath, { + details: true, + }) + + return genFileInfo(comment) +} diff --git a/apps/comments/src/utils/cancelableRequest.js b/apps/comments/src/utils/cancelableRequest.js new file mode 100644 index 0000000000..425e94a787 --- /dev/null +++ b/apps/comments/src/utils/cancelableRequest.js @@ -0,0 +1,62 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import axios from '@nextcloud/axios' + +/** + * Create a cancel token + * @returns {CancelTokenSource} + */ +const createCancelToken = () => axios.CancelToken.source() + +/** + * Creates a cancelable axios 'request object'. + * + * @param {function} request the axios promise request + * @returns {Object} + */ +const cancelableRequest = function(request) { + /** + * Generate an axios cancel token + */ + const cancelToken = createCancelToken() + + /** + * Execute the request + * + * @param {string} url the url to send the request to + * @param {Object} [options] optional config for the request + */ + const fetch = async function(url, options) { + return request( + url, + Object.assign({ cancelToken: cancelToken.token }, options) + ) + } + + return { + request: fetch, + cancel: cancelToken.cancel, + } +} + +export default cancelableRequest diff --git a/apps/comments/src/utils/davUtils.js b/apps/comments/src/utils/davUtils.js new file mode 100644 index 0000000000..b10b62e4f3 --- /dev/null +++ b/apps/comments/src/utils/davUtils.js @@ -0,0 +1,29 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +import { generateRemoteUrl } from '@nextcloud/router' + +const getRootPath = function() { + return generateRemoteUrl('dav/comments') +} + +export { getRootPath } diff --git a/apps/comments/src/utils/fileUtils.js b/apps/comments/src/utils/fileUtils.js new file mode 100644 index 0000000000..298732c8af --- /dev/null +++ b/apps/comments/src/utils/fileUtils.js @@ -0,0 +1,122 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ +import camelcase from 'camelcase' +import { isNumber } from './numberUtil' + +/** + * Get an url encoded path + * + * @param {String} path the full path + * @returns {string} url encoded file path + */ +const encodeFilePath = function(path) { + const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/') + let relativePath = '' + pathSections.forEach((section) => { + if (section !== '') { + relativePath += '/' + encodeURIComponent(section) + } + }) + return relativePath +} + +/** + * Extract dir and name from file path + * + * @param {String} path the full path + * @returns {String[]} [dirPath, fileName] + */ +const extractFilePaths = function(path) { + const pathSections = path.split('/') + const fileName = pathSections[pathSections.length - 1] + const dirPath = pathSections.slice(0, pathSections.length - 1).join('/') + return [dirPath, fileName] +} + +/** + * Sorting comparison function + * + * @param {Object} fileInfo1 file 1 fileinfo + * @param {Object} fileInfo2 file 2 fileinfo + * @param {string} key key to sort with + * @param {boolean} [asc=true] sort ascending? + * @returns {number} + */ +const sortCompare = function(fileInfo1, fileInfo2, key, asc = true) { + + if (fileInfo1.isFavorite && !fileInfo2.isFavorite) { + return -1 + } else if (!fileInfo1.isFavorite && fileInfo2.isFavorite) { + return 1 + } + + // if this is a number, let's sort by integer + if (isNumber(fileInfo1[key]) && isNumber(fileInfo2[key])) { + return Number(fileInfo1[key]) - Number(fileInfo2[key]) + } + + // else we sort by string, so let's sort directories first + if (fileInfo1.type === 'directory' && fileInfo2.type !== 'directory') { + return -1 + } else if (fileInfo1.type !== 'directory' && fileInfo2.type === 'directory') { + return 1 + } + + // finally sort by name + return asc + ? fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage()) + : -fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage()) +} + +/** + * Generate a fileinfo object based on the full dav properties + * It will flatten everything and put all keys to camelCase + * + * @param {Object} obj the object + * @returns {Object} + */ +const genFileInfo = function(obj) { + const fileInfo = {} + + Object.keys(obj).forEach(key => { + const data = obj[key] + + // flatten object if any + if (!!data && typeof data === 'object' && !Array.isArray(data)) { + Object.assign(fileInfo, genFileInfo(data)) + } else { + // format key and add it to the fileInfo + if (data === 'false') { + fileInfo[camelcase(key)] = false + } else if (data === 'true') { + fileInfo[camelcase(key)] = true + } else { + fileInfo[camelcase(key)] = isNumber(data) + ? Number(data) + : data + } + } + }) + return fileInfo +} + +export { encodeFilePath, extractFilePaths, sortCompare, genFileInfo } diff --git a/apps/comments/src/utils/numberUtil.js b/apps/comments/src/utils/numberUtil.js new file mode 100644 index 0000000000..018c34c49e --- /dev/null +++ b/apps/comments/src/utils/numberUtil.js @@ -0,0 +1,30 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + * + */ + +const isNumber = function(num) { + if (!num) { + return false + } + return Number(num).toString() === num.toString() +} + +export { isNumber } diff --git a/apps/comments/src/views/Comments.vue b/apps/comments/src/views/Comments.vue new file mode 100644 index 0000000000..8c3ec66c32 --- /dev/null +++ b/apps/comments/src/views/Comments.vue @@ -0,0 +1,264 @@ + + + + + + + diff --git a/apps/comments/webpack.js b/apps/comments/webpack.js index 8244389aea..b9cba1ca21 100644 --- a/apps/comments/webpack.js +++ b/apps/comments/webpack.js @@ -1,14 +1,18 @@ const path = require('path') module.exports = { - entry: path.join(__dirname, 'src', 'comments.js'), + entry: { + comments: path.join(__dirname, 'src', 'comments.js'), + 'comments-app': path.join(__dirname, 'src', 'comments-app.js'), + 'comments-tab': path.join(__dirname, 'src', 'comments-tab.js'), + }, output: { path: path.resolve(__dirname, './js'), publicPath: '/js/', - filename: 'comments.js', - jsonpFunction: 'webpackJsonpComments' + filename: '[name].js', + jsonpFunction: 'webpackJsonpComments', }, externals: { - jquery: 'jQuery' - } + jquery: 'jQuery', + }, } diff --git a/apps/files/src/components/SidebarTab.vue b/apps/files/src/components/SidebarTab.vue index 1fc93486bc..bead5cad1e 100644 --- a/apps/files/src/components/SidebarTab.vue +++ b/apps/files/src/components/SidebarTab.vue @@ -25,7 +25,8 @@ :id="id" ref="tab" :name="name" - :icon="icon"> + :icon="icon" + @bottomReached="onScrollBottomReached"> @@ -83,6 +84,10 @@ export default { type: Function, required: true, }, + onScrollBottomReached: { + type: Function, + default: () => {}, + }, }, data() { @@ -120,6 +125,5 @@ export default { // unmount the tab await this.onDestroy() }, - } diff --git a/apps/files/src/models/Tab.js b/apps/files/src/models/Tab.js index 2c587e5f70..670c72e3a3 100644 --- a/apps/files/src/models/Tab.js +++ b/apps/files/src/models/Tab.js @@ -29,6 +29,7 @@ export default class Tab { #update #destroy #enabled + #scrollBottomReached /** * Create a new tab instance @@ -41,11 +42,15 @@ export default class 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 } = {}) { + constructor({ id, name, icon, mount, update, destroy, enabled, scrollBottomReached } = {}) { if (enabled === undefined) { enabled = () => true } + if (scrollBottomReached === undefined) { + scrollBottomReached = () => {} + } // Sanity checks if (typeof id !== 'string' || id.trim() === '') { @@ -69,6 +74,9 @@ export default class Tab { if (typeof enabled !== 'function') { throw new Error('The enabled argument should be a function') } + if (typeof scrollBottomReached !== 'function') { + throw new Error('The scrollBottomReached argument should be a function') + } this.#id = id this.#name = name @@ -77,6 +85,7 @@ export default class Tab { this.#update = update this.#destroy = destroy this.#enabled = enabled + this.#scrollBottomReached = scrollBottomReached } @@ -108,4 +117,8 @@ export default class Tab { return this.#enabled } + get scrollBottomReached() { + return this.#scrollBottomReached + } + } diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue index 664bc6f407..0b3f2bb174 100644 --- a/apps/files/src/views/Sidebar.vue +++ b/apps/files/src/views/Sidebar.vue @@ -69,6 +69,7 @@ :on-mount="tab.mount" :on-update="tab.update" :on-destroy="tab.destroy" + :on-scroll-bottom-reached="tab.scrollBottomReached" :file-info="fileInfo" /> diff --git a/core/Controller/AutoCompleteController.php b/core/Controller/AutoCompleteController.php index 56ad21f421..0a29d2fa15 100644 --- a/core/Controller/AutoCompleteController.php +++ b/core/Controller/AutoCompleteController.php @@ -41,18 +41,18 @@ use OCP\Share\IShare; class AutoCompleteController extends Controller { /** @var ISearch */ private $collaboratorSearch; + /** @var IManager */ private $autoCompleteManager; + /** @var IEventDispatcher */ private $dispatcher; - public function __construct( - string $appName, - IRequest $request, - ISearch $collaboratorSearch, - IManager $autoCompleteManager, - IEventDispatcher $dispatcher - ) { + public function __construct(string $appName, + IRequest $request, + ISearch $collaboratorSearch, + IManager $autoCompleteManager, + IEventDispatcher $dispatcher) { parent::__construct($appName, $request); $this->collaboratorSearch = $collaboratorSearch; @@ -114,7 +114,10 @@ class AutoCompleteController extends Controller { $output[] = [ 'id' => (string) $result['value']['shareWith'], 'label' => $result['label'], + 'icon' => $result['icon'], 'source' => $type, + 'status' => $result['status'], + 'subline' => $result['subline'] ]; } } diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php index 2d21c6a16f..0b3a279182 100644 --- a/lib/private/Collaboration/Collaborators/UserPlugin.php +++ b/lib/private/Collaboration/Collaborators/UserPlugin.php @@ -156,6 +156,8 @@ class UserPlugin implements ISearchPlugin { } $result['exact'][] = [ 'label' => $userDisplayName, + 'subline' => $status['message'], + 'icon' => 'icon-user', 'value' => [ 'shareType' => IShare::TYPE_USER, 'shareWith' => $uid, @@ -178,6 +180,8 @@ class UserPlugin implements ISearchPlugin { if ($addToWideResults) { $result['wide'][] = [ 'label' => $userDisplayName, + 'subline' => $status['message'], + 'icon' => 'icon-user', 'value' => [ 'shareType' => IShare::TYPE_USER, 'shareWith' => $uid, @@ -217,6 +221,8 @@ class UserPlugin implements ISearchPlugin { $result['exact'][] = [ 'label' => $user->getDisplayName(), + 'icon' => 'icon-user', + 'subline' => $status['message'], 'value' => [ 'shareType' => IShare::TYPE_USER, 'shareWith' => $user->getUID(), diff --git a/package-lock.json b/package-lock.json index c60f22c98e..15857a58db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2354,6 +2354,11 @@ } } }, + "base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" + }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", @@ -2692,9 +2697,9 @@ "dev": true }, "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", + "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==" }, "camelcase-keys": { "version": "2.1.0", @@ -3159,6 +3164,12 @@ "semver": "^6.3.0" }, "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -4431,6 +4442,11 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-xml-parser": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.4.tgz", + "integrity": "sha512-qudnQuyYBgnvzf5Lj/yxMcf4L9NcVWihXJg7CiU1L+oUCq8MUnFEfH2/nXR/W5uq+yvUN1h7z6s7vs2v1WkL1A==" + }, "fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -5187,6 +5203,11 @@ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, + "hot-patcher": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-0.5.0.tgz", + "integrity": "sha512-2Uu2W0s8+dnqXzdlg0MRsRzPoDCs1wVjOGSyMRRaMzLDX4bgHw6xDYKccsWafXPPxQpkQfEjgW6+17pwcg60bw==" + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -6658,6 +6679,11 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "nested-property": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-1.0.4.tgz", + "integrity": "sha512-6fNIumJJUyP3rkB4FyVYCYpdW+PKUCaxRWRGLLf0kv/RKoG4mbTvInedA9x3zOyuOmOkGudKuAtPSI+dnhwj2g==" + }, "nextcloud-vue-collections": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/nextcloud-vue-collections/-/nextcloud-vue-collections-0.8.1.tgz", @@ -7286,6 +7312,11 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, + "path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=" + }, "path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -7656,6 +7687,11 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -9340,6 +9376,15 @@ } } }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "url-search-params-polyfill": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/url-search-params-polyfill/-/url-search-params-polyfill-8.1.0.tgz", @@ -9566,6 +9611,11 @@ "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "dev": true }, + "vue-virtual-scroll-list": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.1.tgz", + "integrity": "sha512-2p0bvcmUIMet5tln+cOKt/XjNvgP+ebq9bBD+gquK2rivsSSAFHeqQidzMO3wPFfxWeTB1JpoSzkyL9nzZ9yfA==" + }, "vue-virtual-scroller": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-1.0.10.tgz", @@ -9759,6 +9809,54 @@ "chokidar": "^2.1.8" } }, + "webdav": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-3.3.0.tgz", + "integrity": "sha512-wTfLNbeK1++T1ooL/ZJaUTJGb5NUuO4zAwuTShNPbzN0mRMRIaoZYG7sI5TtyH1uqOPIOW5ZGTtZiBypLG86KQ==", + "requires": { + "axios": "^0.19.2", + "base-64": "^0.1.0", + "fast-xml-parser": "^3.16.0", + "he": "^1.2.0", + "hot-patcher": "^0.5.0", + "minimatch": "^3.0.4", + "nested-property": "^1.0.4", + "path-posix": "^1.0.0", + "url-join": "^4.0.1", + "url-parse": "^1.4.7" + }, + "dependencies": { + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + } + } + }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -10066,6 +10164,13 @@ "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + } } }, "yargs-unparser": { diff --git a/package.json b/package.json index 291fafe8d2..ebecf31559 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "backbone": "^1.4.0", "blueimp-md5": "^2.18.0", "bootstrap": "^4.5.2", + "camelcase": "^6.0.0", "clipboard": "^2.0.6", "core-js": "^3.6.5", "css-vars-ponyfill": "^2.3.2", @@ -85,7 +86,8 @@ "vue-router": "^3.4.7", "vuedraggable": "^2.24.2", "vuex": "^3.5.1", - "vuex-router-sync": "^5.0.0" + "vuex-router-sync": "^5.0.0", + "webdav": "^3.3.0" }, "devDependencies": { "@babel/core": "^7.11.6",