зеркало из https://github.com/nextcloud/server.git
Init vue comments tab
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
Родитель
3d2024faf9
Коммит
e7f5516b4d
|
@ -15,9 +15,11 @@ return array(
|
||||||
'OCA\\Comments\\Collaboration\\CommentersSorter' => $baseDir . '/../lib/Collaboration/CommentersSorter.php',
|
'OCA\\Comments\\Collaboration\\CommentersSorter' => $baseDir . '/../lib/Collaboration/CommentersSorter.php',
|
||||||
'OCA\\Comments\\Controller\\Notifications' => $baseDir . '/../lib/Controller/Notifications.php',
|
'OCA\\Comments\\Controller\\Notifications' => $baseDir . '/../lib/Controller/Notifications.php',
|
||||||
'OCA\\Comments\\EventHandler' => $baseDir . '/../lib/EventHandler.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\\JSSettingsHelper' => $baseDir . '/../lib/JSSettingsHelper.php',
|
||||||
'OCA\\Comments\\Listener\\CommentsEntityEventListener' => $baseDir . '/../lib/Listener/CommentsEntityEventListener.php',
|
'OCA\\Comments\\Listener\\CommentsEntityEventListener' => $baseDir . '/../lib/Listener/CommentsEntityEventListener.php',
|
||||||
'OCA\\Comments\\Listener\\LoadAdditionalScripts' => $baseDir . '/../lib/Listener/LoadAdditionalScripts.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\\Listener\\LoadSidebarScripts' => $baseDir . '/../lib/Listener/LoadSidebarScripts.php',
|
||||||
'OCA\\Comments\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php',
|
'OCA\\Comments\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php',
|
||||||
'OCA\\Comments\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
|
'OCA\\Comments\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
|
||||||
|
|
|
@ -30,9 +30,11 @@ class ComposerStaticInitComments
|
||||||
'OCA\\Comments\\Collaboration\\CommentersSorter' => __DIR__ . '/..' . '/../lib/Collaboration/CommentersSorter.php',
|
'OCA\\Comments\\Collaboration\\CommentersSorter' => __DIR__ . '/..' . '/../lib/Collaboration/CommentersSorter.php',
|
||||||
'OCA\\Comments\\Controller\\Notifications' => __DIR__ . '/..' . '/../lib/Controller/Notifications.php',
|
'OCA\\Comments\\Controller\\Notifications' => __DIR__ . '/..' . '/../lib/Controller/Notifications.php',
|
||||||
'OCA\\Comments\\EventHandler' => __DIR__ . '/..' . '/../lib/EventHandler.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\\JSSettingsHelper' => __DIR__ . '/..' . '/../lib/JSSettingsHelper.php',
|
||||||
'OCA\\Comments\\Listener\\CommentsEntityEventListener' => __DIR__ . '/..' . '/../lib/Listener/CommentsEntityEventListener.php',
|
'OCA\\Comments\\Listener\\CommentsEntityEventListener' => __DIR__ . '/..' . '/../lib/Listener/CommentsEntityEventListener.php',
|
||||||
'OCA\\Comments\\Listener\\LoadAdditionalScripts' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalScripts.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\\Listener\\LoadSidebarScripts' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarScripts.php',
|
||||||
'OCA\\Comments\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php',
|
'OCA\\Comments\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php',
|
||||||
'OCA\\Comments\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
|
'OCA\\Comments\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
|
||||||
|
|
|
@ -30,6 +30,8 @@ namespace OCA\Comments\AppInfo;
|
||||||
use Closure;
|
use Closure;
|
||||||
use OCA\Comments\Capabilities;
|
use OCA\Comments\Capabilities;
|
||||||
use OCA\Comments\Controller\Notifications;
|
use OCA\Comments\Controller\Notifications;
|
||||||
|
use OCA\Comments\Event\LoadCommentsApp;
|
||||||
|
use OCA\Comments\Listener\LoadCommentsAppListener;
|
||||||
use OCA\Comments\EventHandler;
|
use OCA\Comments\EventHandler;
|
||||||
use OCA\Comments\JSSettingsHelper;
|
use OCA\Comments\JSSettingsHelper;
|
||||||
use OCA\Comments\Listener\CommentsEntityEventListener;
|
use OCA\Comments\Listener\CommentsEntityEventListener;
|
||||||
|
@ -70,6 +72,10 @@ class Application extends App implements IBootstrap {
|
||||||
LoadSidebar::class,
|
LoadSidebar::class,
|
||||||
LoadSidebarScripts::class
|
LoadSidebarScripts::class
|
||||||
);
|
);
|
||||||
|
$context->registerEventListener(
|
||||||
|
LoadCommentsApp::class,
|
||||||
|
LoadCommentsAppListener::class
|
||||||
|
);
|
||||||
$context->registerEventListener(
|
$context->registerEventListener(
|
||||||
CommentsEntityEvent::EVENT_ENTITY,
|
CommentsEntityEvent::EVENT_ENTITY,
|
||||||
CommentsEntityEventListener::class
|
CommentsEntityEventListener::class
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020, John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
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 {
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020, John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,19 +28,32 @@ declare(strict_types=1);
|
||||||
namespace OCA\Comments\Listener;
|
namespace OCA\Comments\Listener;
|
||||||
|
|
||||||
use OCA\Comments\AppInfo\Application;
|
use OCA\Comments\AppInfo\Application;
|
||||||
|
use OCA\Comments\Event\LoadCommentsApp;
|
||||||
use OCA\Files\Event\LoadSidebar;
|
use OCA\Files\Event\LoadSidebar;
|
||||||
use OCP\EventDispatcher\Event;
|
use OCP\EventDispatcher\Event;
|
||||||
|
use OCP\EventDispatcher\IEventDispatcher;
|
||||||
use OCP\EventDispatcher\IEventListener;
|
use OCP\EventDispatcher\IEventListener;
|
||||||
use OCP\Util;
|
use OCP\Util;
|
||||||
|
|
||||||
class LoadSidebarScripts implements IEventListener {
|
class LoadSidebarScripts implements IEventListener {
|
||||||
|
|
||||||
|
/** @var IEventDispatcher */
|
||||||
|
private $eventDispatcher;
|
||||||
|
|
||||||
|
public function __construct(IEventDispatcher $eventDispatcher) {
|
||||||
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
public function handle(Event $event): void {
|
public function handle(Event $event): void {
|
||||||
if (!($event instanceof LoadSidebar)) {
|
if (!($event instanceof LoadSidebar)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->eventDispatcher->dispatchTyped(new LoadCommentsApp());
|
||||||
|
|
||||||
// TODO: make sure to only include the sidebar script when
|
// TODO: make sure to only include the sidebar script when
|
||||||
// we properly split it between files list and sidebar
|
// we properly split it between files list and sidebar
|
||||||
Util::addScript(Application::APP_ID, 'comments');
|
Util::addScript(Application::APP_ID, 'comments');
|
||||||
|
Util::addScript(Application::APP_ID, 'comments-tab');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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')
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,295 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @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 <http://www.gnu.org/licenses/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div v-show="!deleted"
|
||||||
|
:class="{'comment--loading': loading}"
|
||||||
|
class="comment">
|
||||||
|
<!-- Comment header toolbar -->
|
||||||
|
<div class="comment__header">
|
||||||
|
<!-- Author -->
|
||||||
|
<Avatar class="comment__avatar"
|
||||||
|
:display-name="actorDisplayName"
|
||||||
|
:user="actorId"
|
||||||
|
:size="32" />
|
||||||
|
<span class="comment__author">{{ actorDisplayName }}</span>
|
||||||
|
|
||||||
|
<!-- Comment actions,
|
||||||
|
show if we have a message id and current user is author -->
|
||||||
|
<Actions v-if="isOwnComment && id && !loading" class="comment__actions">
|
||||||
|
<template v-if="!editing">
|
||||||
|
<ActionButton
|
||||||
|
:close-after-click="true"
|
||||||
|
icon="icon-rename"
|
||||||
|
@click="onEdit">
|
||||||
|
{{ t('comments', 'Edit comment') }}
|
||||||
|
</ActionButton>
|
||||||
|
<ActionSeparator />
|
||||||
|
<ActionButton
|
||||||
|
:close-after-click="true"
|
||||||
|
icon="icon-delete"
|
||||||
|
@click="onDeleteWithUndo">
|
||||||
|
{{ t('comments', 'Delete comment') }}
|
||||||
|
</ActionButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ActionButton v-else
|
||||||
|
icon="icon-close"
|
||||||
|
@click="onEditCancel">
|
||||||
|
{{ t('comments', 'Cancel edit') }}
|
||||||
|
</ActionButton>
|
||||||
|
</Actions>
|
||||||
|
|
||||||
|
<!-- Show loading if we're editing or deleting, not on new ones -->
|
||||||
|
<div v-if="id && loading" class="comment_loading icon-loading-small" />
|
||||||
|
|
||||||
|
<!-- Relative time to the comment creation -->
|
||||||
|
<Moment v-else-if="creationDateTime" class="comment__timestamp" :timestamp="timestamp" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message editor -->
|
||||||
|
<div class="comment__message" v-if="editor || editing">
|
||||||
|
<RichContenteditable v-model="localMessage" :auto-complete="autoComplete" :contenteditable="!loading" />
|
||||||
|
<input v-tooltip="t('comments', 'Post comment')"
|
||||||
|
:class="loading ? 'icon-loading-small' :'icon-confirm'"
|
||||||
|
class="comment__submit"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isEmptyMessage"
|
||||||
|
value=""
|
||||||
|
@click="onSubmit">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message content -->
|
||||||
|
<!-- The html is escaped and sanitized before rendering -->
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html-->
|
||||||
|
<div v-else class="comment__message" v-html="renderedContent" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||||
|
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
||||||
|
import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
|
||||||
|
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||||
|
import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable'
|
||||||
|
import RichEditorMixin from '@nextcloud/vue/dist/Mixins/richEditor'
|
||||||
|
|
||||||
|
import Moment from './Moment'
|
||||||
|
import CommentMixin from '../mixins/CommentMixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Comment',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ActionButton,
|
||||||
|
Actions,
|
||||||
|
ActionSeparator,
|
||||||
|
Avatar,
|
||||||
|
Moment,
|
||||||
|
RichContenteditable,
|
||||||
|
},
|
||||||
|
mixins: [RichEditorMixin, CommentMixin],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
source: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
actorDisplayName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
actorId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
creationDateTime: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force the editor display
|
||||||
|
*/
|
||||||
|
editor: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the autocompletion data
|
||||||
|
*/
|
||||||
|
autoComplete: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// Only change data locally and update the original
|
||||||
|
// parent data when the request is sent and resolved
|
||||||
|
localMessage: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the current user the author of this comment
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isOwnComment() {
|
||||||
|
return getCurrentUser().uid === this.actorId
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendered content as html string
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
renderedContent() {
|
||||||
|
if (this.isEmptyMessage) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return this.renderContent(this.localMessage)
|
||||||
|
},
|
||||||
|
|
||||||
|
isEmptyMessage() {
|
||||||
|
return !this.localMessage || this.localMessage.trim() === ''
|
||||||
|
},
|
||||||
|
|
||||||
|
timestamp() {
|
||||||
|
// seconds, not milliseconds
|
||||||
|
return parseInt(moment(this.creationDateTime).format('x'), 10) / 1000
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
// If the data change, update the local value
|
||||||
|
message(message) {
|
||||||
|
this.updateLocalMessage(message)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeMount() {
|
||||||
|
// Init localMessage
|
||||||
|
this.updateLocalMessage(this.message)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Update local Message on outer change
|
||||||
|
* @param {string} message the message to set
|
||||||
|
*/
|
||||||
|
updateLocalMessage(message) {
|
||||||
|
this.localMessage = message.toString()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch message between edit and create
|
||||||
|
*/
|
||||||
|
onSubmit() {
|
||||||
|
if (this.editor) {
|
||||||
|
this.onNewComment(this.localMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.onEditComment(this.localMessage)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$comment-padding: 10px;
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
position: relative;
|
||||||
|
padding: $comment-padding 0 $comment-padding * 1.5;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: $comment-padding / 2 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__author,
|
||||||
|
&__actions {
|
||||||
|
margin-left: $comment-padding !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__author {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_loading,
|
||||||
|
&__timestamp {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
position: relative;
|
||||||
|
// Avatar size, align with author name
|
||||||
|
padding-left: 32px + $comment-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
// Align with input border
|
||||||
|
margin: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: .7;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent !important;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-contenteditable__input {
|
||||||
|
margin: 0;
|
||||||
|
padding: $comment-padding;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!-- TODO: Move to vue components -->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="live-relative-timestamp" :data-timestamp="timestamp * 1000" :title="title">{{ formatted }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from '@nextcloud/moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Moment',
|
||||||
|
props: {
|
||||||
|
timestamp: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
type: String,
|
||||||
|
default: 'LLL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
return moment.unix(this.timestamp).format(this.format)
|
||||||
|
},
|
||||||
|
formatted() {
|
||||||
|
return moment.unix(this.timestamp).fromNow()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,117 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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: `<?xml version="1.0"?>
|
||||||
|
<d:propertyupdate
|
||||||
|
xmlns:d="DAV:"
|
||||||
|
xmlns:oc="http://owncloud.org/ns">
|
||||||
|
<d:set>
|
||||||
|
<d:prop>
|
||||||
|
<oc:message>${message}</oc:message>
|
||||||
|
</d:prop>
|
||||||
|
</d:set>
|
||||||
|
</d:propertyupdate>`,
|
||||||
|
}))
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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: `<?xml version="1.0"?>
|
||||||
|
<oc:filter-comments
|
||||||
|
xmlns:d="DAV:"
|
||||||
|
xmlns:oc="http://owncloud.org/ns"
|
||||||
|
xmlns:nc="http://nextcloud.org/ns"
|
||||||
|
xmlns:ocs="http://open-collaboration-services.org/ns">
|
||||||
|
<oc:limit>${DEFAULT_LIMIT}</oc:limit>
|
||||||
|
<oc:offset>${options.offset || 0}</oc:offset>
|
||||||
|
</oc:filter-comments>`,
|
||||||
|
}, 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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { generateRemoteUrl } from '@nextcloud/router'
|
||||||
|
|
||||||
|
const getRootPath = function() {
|
||||||
|
return generateRemoteUrl('dav/comments')
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getRootPath }
|
|
@ -0,0 +1,122 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
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 }
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const isNumber = function(num) {
|
||||||
|
if (!num) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Number(num).toString() === num.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isNumber }
|
|
@ -0,0 +1,264 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @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 <http://www.gnu.org/licenses/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="comments" :class="{ 'icon-loading': isFirstLoading }">
|
||||||
|
<!-- Editor -->
|
||||||
|
<Comment v-bind="editorData"
|
||||||
|
:auto-complete="autoComplete"
|
||||||
|
:editor="true"
|
||||||
|
:ressource-id="ressourceId"
|
||||||
|
class="comments__writer"
|
||||||
|
@new="onNewComment" />
|
||||||
|
|
||||||
|
<template v-if="!isFirstLoading">
|
||||||
|
<EmptyContent v-if="!hasComments && done" icon="icon-comment">
|
||||||
|
{{ t('comments', 'No comments yet, start the conversation!') }}
|
||||||
|
</EmptyContent>
|
||||||
|
|
||||||
|
<!-- Comments -->
|
||||||
|
<Comment v-for="comment in comments"
|
||||||
|
v-else
|
||||||
|
:key="comment.id"
|
||||||
|
v-bind="comment"
|
||||||
|
:auto-complete="autoComplete"
|
||||||
|
:ressource-id="ressourceId"
|
||||||
|
:message.sync="comment.message"
|
||||||
|
class="comments__list"
|
||||||
|
@delete="onDelete" />
|
||||||
|
|
||||||
|
<!-- Loading more message -->
|
||||||
|
<div v-if="loading && !isFirstLoading" class="comments__info icon-loading" />
|
||||||
|
|
||||||
|
<div v-else-if="hasComments && done" class="comments__info">
|
||||||
|
{{ t('comments', 'No more messages') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error message -->
|
||||||
|
<EmptyContent v-else-if="error" class="comments__error" icon="icon-error">
|
||||||
|
{{ error }}
|
||||||
|
<template #desc>
|
||||||
|
<button icon="icon-history" @click="getComments">
|
||||||
|
{{ t('comments', 'Retry') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</EmptyContent>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { generateOcsUrl } from '@nextcloud/router'
|
||||||
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import VTooltip from 'v-tooltip'
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
||||||
|
|
||||||
|
import Comment from '../components/Comment'
|
||||||
|
import getComments, { DEFAULT_LIMIT } from '../services/GetComments'
|
||||||
|
import cancelableRequest from '../utils/cancelableRequest'
|
||||||
|
|
||||||
|
Vue.use(VTooltip)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Comments',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
// Avatar,
|
||||||
|
Comment,
|
||||||
|
EmptyContent,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
error: '',
|
||||||
|
loading: false,
|
||||||
|
done: false,
|
||||||
|
|
||||||
|
ressourceId: null,
|
||||||
|
offset: 0,
|
||||||
|
comments: [],
|
||||||
|
|
||||||
|
cancelRequest: () => {},
|
||||||
|
|
||||||
|
editorData: {
|
||||||
|
actorDisplayName: getCurrentUser().displayName,
|
||||||
|
actorId: getCurrentUser().uid,
|
||||||
|
key: 'editor',
|
||||||
|
},
|
||||||
|
|
||||||
|
Comment,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hasComments() {
|
||||||
|
return this.comments.length > 0
|
||||||
|
},
|
||||||
|
isFirstLoading() {
|
||||||
|
return this.loading && this.offset === 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Update current ressourceId and fetch new data
|
||||||
|
* @param {Number} ressourceId the current ressourceId (fileId...)
|
||||||
|
*/
|
||||||
|
async update(ressourceId) {
|
||||||
|
this.ressourceId = ressourceId
|
||||||
|
this.resetState()
|
||||||
|
this.getComments()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ran when the bottom of the tab is reached
|
||||||
|
*/
|
||||||
|
onScrollBottomReached() {
|
||||||
|
/**
|
||||||
|
* Do not fetch more if we:
|
||||||
|
* - are showing an error
|
||||||
|
* - already fetched everything
|
||||||
|
* - are currently loading
|
||||||
|
*/
|
||||||
|
if (this.error || this.done || this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.getComments()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the existing shares infos
|
||||||
|
*/
|
||||||
|
async getComments() {
|
||||||
|
// Cancel any ongoing request
|
||||||
|
this.cancelRequest('cancel')
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
// Init cancellable request
|
||||||
|
const { request, cancel } = cancelableRequest(getComments)
|
||||||
|
this.cancelRequest = cancel
|
||||||
|
|
||||||
|
// Fetch comments
|
||||||
|
const comments = await request({
|
||||||
|
commentsType: this.commentsType,
|
||||||
|
ressourceId: this.ressourceId,
|
||||||
|
}, { offset: this.offset })
|
||||||
|
|
||||||
|
this.logger.debug(`Processed ${comments.length} comments`, { comments })
|
||||||
|
|
||||||
|
// We received less than the requested amount,
|
||||||
|
// we're done fetching comments
|
||||||
|
if (comments.length < DEFAULT_LIMIT) {
|
||||||
|
this.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert results
|
||||||
|
this.comments.push(...comments)
|
||||||
|
|
||||||
|
// Increase offset for next fetch
|
||||||
|
this.offset += DEFAULT_LIMIT
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message === 'cancel') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Reverting offset
|
||||||
|
this.error = t('comments', 'Unable to load the comments list')
|
||||||
|
console.error('Error loading the comments list', error)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Autocomplete @mentions
|
||||||
|
* @param {string} search the query
|
||||||
|
* @param {Function} callback the callback to process the results with
|
||||||
|
*/
|
||||||
|
async autoComplete(search, callback) {
|
||||||
|
const results = await axios.get(generateOcsUrl('core', 2) + 'autocomplete/get', {
|
||||||
|
params: {
|
||||||
|
search,
|
||||||
|
itemType: 'files',
|
||||||
|
itemId: this.ressourceId,
|
||||||
|
sorter: 'commenters|share-recipients',
|
||||||
|
limit: OC.appConfig?.comments?.maxAutoCompleteResults || 25,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return callback(results.data.ocs.data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add newly created comment to the list
|
||||||
|
* @param {Object} comment the new comment
|
||||||
|
*/
|
||||||
|
onNewComment(comment) {
|
||||||
|
this.comments.unshift(comment)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove deleted comment from the list
|
||||||
|
* @param {number} id the deleted comment
|
||||||
|
*/
|
||||||
|
onDelete(id) {
|
||||||
|
const index = this.comments.findIndex(comment => comment.id === id)
|
||||||
|
if (index > -1) {
|
||||||
|
this.comments.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
console.error('Could not find the deleted comment in the list', id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the current view to its default state
|
||||||
|
*/
|
||||||
|
resetState() {
|
||||||
|
this.error = ''
|
||||||
|
this.loading = false
|
||||||
|
this.done = false
|
||||||
|
this.offset = 0
|
||||||
|
this.comments = []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.comments {
|
||||||
|
// Do not add emptycontent top margin
|
||||||
|
&__error{
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
height: 60px;
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,14 +1,18 @@
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
module.exports = {
|
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: {
|
output: {
|
||||||
path: path.resolve(__dirname, './js'),
|
path: path.resolve(__dirname, './js'),
|
||||||
publicPath: '/js/',
|
publicPath: '/js/',
|
||||||
filename: 'comments.js',
|
filename: '[name].js',
|
||||||
jsonpFunction: 'webpackJsonpComments'
|
jsonpFunction: 'webpackJsonpComments',
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
jquery: 'jQuery'
|
jquery: 'jQuery',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,8 @@
|
||||||
:id="id"
|
:id="id"
|
||||||
ref="tab"
|
ref="tab"
|
||||||
:name="name"
|
:name="name"
|
||||||
:icon="icon">
|
:icon="icon"
|
||||||
|
@bottomReached="onScrollBottomReached">
|
||||||
<!-- Fallback loading -->
|
<!-- Fallback loading -->
|
||||||
<EmptyContent v-if="loading" icon="icon-loading" />
|
<EmptyContent v-if="loading" icon="icon-loading" />
|
||||||
|
|
||||||
|
@ -83,6 +84,10 @@ export default {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
onScrollBottomReached: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -120,6 +125,5 @@ export default {
|
||||||
// unmount the tab
|
// unmount the tab
|
||||||
await this.onDestroy()
|
await this.onDestroy()
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -29,6 +29,7 @@ export default class Tab {
|
||||||
#update
|
#update
|
||||||
#destroy
|
#destroy
|
||||||
#enabled
|
#enabled
|
||||||
|
#scrollBottomReached
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new tab instance
|
* 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.update function to update the tab
|
||||||
* @param {Function} options.destroy function to destroy 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.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) {
|
if (enabled === undefined) {
|
||||||
enabled = () => true
|
enabled = () => true
|
||||||
}
|
}
|
||||||
|
if (scrollBottomReached === undefined) {
|
||||||
|
scrollBottomReached = () => {}
|
||||||
|
}
|
||||||
|
|
||||||
// Sanity checks
|
// Sanity checks
|
||||||
if (typeof id !== 'string' || id.trim() === '') {
|
if (typeof id !== 'string' || id.trim() === '') {
|
||||||
|
@ -69,6 +74,9 @@ export default class Tab {
|
||||||
if (typeof enabled !== 'function') {
|
if (typeof enabled !== 'function') {
|
||||||
throw new Error('The enabled argument should be a 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.#id = id
|
||||||
this.#name = name
|
this.#name = name
|
||||||
|
@ -77,6 +85,7 @@ export default class Tab {
|
||||||
this.#update = update
|
this.#update = update
|
||||||
this.#destroy = destroy
|
this.#destroy = destroy
|
||||||
this.#enabled = enabled
|
this.#enabled = enabled
|
||||||
|
this.#scrollBottomReached = scrollBottomReached
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,4 +117,8 @@ export default class Tab {
|
||||||
return this.#enabled
|
return this.#enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get scrollBottomReached() {
|
||||||
|
return this.#scrollBottomReached
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
:on-mount="tab.mount"
|
:on-mount="tab.mount"
|
||||||
:on-update="tab.update"
|
:on-update="tab.update"
|
||||||
:on-destroy="tab.destroy"
|
:on-destroy="tab.destroy"
|
||||||
|
:on-scroll-bottom-reached="tab.scrollBottomReached"
|
||||||
:file-info="fileInfo" />
|
:file-info="fileInfo" />
|
||||||
</template>
|
</template>
|
||||||
</AppSidebar>
|
</AppSidebar>
|
||||||
|
|
|
@ -41,18 +41,18 @@ use OCP\Share\IShare;
|
||||||
class AutoCompleteController extends Controller {
|
class AutoCompleteController extends Controller {
|
||||||
/** @var ISearch */
|
/** @var ISearch */
|
||||||
private $collaboratorSearch;
|
private $collaboratorSearch;
|
||||||
|
|
||||||
/** @var IManager */
|
/** @var IManager */
|
||||||
private $autoCompleteManager;
|
private $autoCompleteManager;
|
||||||
|
|
||||||
/** @var IEventDispatcher */
|
/** @var IEventDispatcher */
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(string $appName,
|
||||||
string $appName,
|
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
ISearch $collaboratorSearch,
|
ISearch $collaboratorSearch,
|
||||||
IManager $autoCompleteManager,
|
IManager $autoCompleteManager,
|
||||||
IEventDispatcher $dispatcher
|
IEventDispatcher $dispatcher) {
|
||||||
) {
|
|
||||||
parent::__construct($appName, $request);
|
parent::__construct($appName, $request);
|
||||||
|
|
||||||
$this->collaboratorSearch = $collaboratorSearch;
|
$this->collaboratorSearch = $collaboratorSearch;
|
||||||
|
@ -114,7 +114,10 @@ class AutoCompleteController extends Controller {
|
||||||
$output[] = [
|
$output[] = [
|
||||||
'id' => (string) $result['value']['shareWith'],
|
'id' => (string) $result['value']['shareWith'],
|
||||||
'label' => $result['label'],
|
'label' => $result['label'],
|
||||||
|
'icon' => $result['icon'],
|
||||||
'source' => $type,
|
'source' => $type,
|
||||||
|
'status' => $result['status'],
|
||||||
|
'subline' => $result['subline']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,6 +156,8 @@ class UserPlugin implements ISearchPlugin {
|
||||||
}
|
}
|
||||||
$result['exact'][] = [
|
$result['exact'][] = [
|
||||||
'label' => $userDisplayName,
|
'label' => $userDisplayName,
|
||||||
|
'subline' => $status['message'],
|
||||||
|
'icon' => 'icon-user',
|
||||||
'value' => [
|
'value' => [
|
||||||
'shareType' => IShare::TYPE_USER,
|
'shareType' => IShare::TYPE_USER,
|
||||||
'shareWith' => $uid,
|
'shareWith' => $uid,
|
||||||
|
@ -178,6 +180,8 @@ class UserPlugin implements ISearchPlugin {
|
||||||
if ($addToWideResults) {
|
if ($addToWideResults) {
|
||||||
$result['wide'][] = [
|
$result['wide'][] = [
|
||||||
'label' => $userDisplayName,
|
'label' => $userDisplayName,
|
||||||
|
'subline' => $status['message'],
|
||||||
|
'icon' => 'icon-user',
|
||||||
'value' => [
|
'value' => [
|
||||||
'shareType' => IShare::TYPE_USER,
|
'shareType' => IShare::TYPE_USER,
|
||||||
'shareWith' => $uid,
|
'shareWith' => $uid,
|
||||||
|
@ -217,6 +221,8 @@ class UserPlugin implements ISearchPlugin {
|
||||||
|
|
||||||
$result['exact'][] = [
|
$result['exact'][] = [
|
||||||
'label' => $user->getDisplayName(),
|
'label' => $user->getDisplayName(),
|
||||||
|
'icon' => 'icon-user',
|
||||||
|
'subline' => $status['message'],
|
||||||
'value' => [
|
'value' => [
|
||||||
'shareType' => IShare::TYPE_USER,
|
'shareType' => IShare::TYPE_USER,
|
||||||
'shareWith' => $user->getUID(),
|
'shareWith' => $user->getUID(),
|
||||||
|
|
|
@ -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": {
|
"base64-js": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||||
|
@ -2692,9 +2697,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"camelcase": {
|
"camelcase": {
|
||||||
"version": "5.3.1",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz",
|
||||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
"integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w=="
|
||||||
},
|
},
|
||||||
"camelcase-keys": {
|
"camelcase-keys": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
@ -3159,6 +3164,12 @@
|
||||||
"semver": "^6.3.0"
|
"semver": "^6.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"semver": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
|
@ -4431,6 +4442,11 @@
|
||||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||||
"dev": true
|
"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": {
|
"fastparse": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
|
||||||
|
@ -5187,6 +5203,11 @@
|
||||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
|
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
|
||||||
"dev": true
|
"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": {
|
"html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
"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": {
|
"nextcloud-vue-collections": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/nextcloud-vue-collections/-/nextcloud-vue-collections-0.8.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
|
"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": {
|
"path-to-regexp": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||||
|
@ -7656,6 +7687,11 @@
|
||||||
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
|
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"querystringify": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||||
|
},
|
||||||
"randombytes": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"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": {
|
"url-search-params-polyfill": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/url-search-params-polyfill/-/url-search-params-polyfill-8.1.0.tgz",
|
"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==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"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": {
|
"vue-virtual-scroller": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-1.0.10.tgz",
|
||||||
|
@ -9759,6 +9809,54 @@
|
||||||
"chokidar": "^2.1.8"
|
"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": {
|
"webidl-conversions": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
|
||||||
|
@ -10066,6 +10164,13 @@
|
||||||
"requires": {
|
"requires": {
|
||||||
"camelcase": "^5.0.0",
|
"camelcase": "^5.0.0",
|
||||||
"decamelize": "^1.2.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": {
|
"yargs-unparser": {
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"backbone": "^1.4.0",
|
"backbone": "^1.4.0",
|
||||||
"blueimp-md5": "^2.18.0",
|
"blueimp-md5": "^2.18.0",
|
||||||
"bootstrap": "^4.5.2",
|
"bootstrap": "^4.5.2",
|
||||||
|
"camelcase": "^6.0.0",
|
||||||
"clipboard": "^2.0.6",
|
"clipboard": "^2.0.6",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"css-vars-ponyfill": "^2.3.2",
|
"css-vars-ponyfill": "^2.3.2",
|
||||||
|
@ -85,7 +86,8 @@
|
||||||
"vue-router": "^3.4.7",
|
"vue-router": "^3.4.7",
|
||||||
"vuedraggable": "^2.24.2",
|
"vuedraggable": "^2.24.2",
|
||||||
"vuex": "^3.5.1",
|
"vuex": "^3.5.1",
|
||||||
"vuex-router-sync": "^5.0.0"
|
"vuex-router-sync": "^5.0.0",
|
||||||
|
"webdav": "^3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.11.6",
|
"@babel/core": "^7.11.6",
|
||||||
|
|
Загрузка…
Ссылка в новой задаче