Add OCA.Files.Sidebar and migrate sharing (#15719)
Add OCA.Files.Sidebar and migrate sharing
This commit is contained in:
commit
f420ac94b0
11
.babelrc.js
11
.babelrc.js
|
@ -1,11 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: ['@babel/plugin-syntax-dynamic-import'],
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
2
Makefile
2
Makefile
|
@ -30,6 +30,7 @@ lint-fix-watch:
|
|||
clean:
|
||||
rm -rf apps/accessibility/js/
|
||||
rm -rf apps/comments/js/
|
||||
rm -rf apps/files/js/dist/
|
||||
rm -rf apps/files_sharing/js/dist/
|
||||
rm -rf apps/files_trashbin/js/
|
||||
rm -rf apps/files_versions/js/
|
||||
|
@ -47,6 +48,7 @@ clean-dev:
|
|||
clean-git: clean
|
||||
git checkout -- apps/accessibility/js/
|
||||
git checkout -- apps/comments/js/
|
||||
git checkout -- apps/files/js/dist/
|
||||
git checkout -- apps/files_sharing/js/dist/
|
||||
git checkout -- apps/files_trashbin/js/
|
||||
git checkout -- apps/files_versions/js/
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -104,7 +104,7 @@
|
|||
actionHandler: function(fileName, context) {
|
||||
context.$file.find('.action-comment').tooltip('hide')
|
||||
// open sidebar in comments section
|
||||
context.fileList.showDetailsView(fileName, 'commentsTabView')
|
||||
context.fileList.showDetailsView(fileName, 'comments')
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ describe('OCA.Comments.FilesPlugin tests', function() {
|
|||
|
||||
expect(sidebarStub.calledOnce).toEqual(true);
|
||||
expect(sidebarStub.lastCall.args[0]).toEqual('One.txt');
|
||||
expect(sidebarStub.lastCall.args[1]).toEqual('commentsTabView');
|
||||
expect(sidebarStub.lastCall.args[1]).toEqual('comments');
|
||||
});
|
||||
});
|
||||
describe('elementToFile', function() {
|
||||
|
|
|
@ -33,6 +33,7 @@ return array(
|
|||
'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
|
||||
'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php',
|
||||
'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => $baseDir . '/../lib/Event/LoadAdditionalScriptsEvent.php',
|
||||
'OCA\\Files\\Event\\LoadSidebar' => $baseDir . '/../lib/Event/LoadSidebar.php',
|
||||
'OCA\\Files\\Helper' => $baseDir . '/../lib/Helper.php',
|
||||
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => $baseDir . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
|
||||
'OCA\\Files\\Service\\TagService' => $baseDir . '/../lib/Service/TagService.php',
|
||||
|
|
|
@ -48,6 +48,7 @@ class ComposerStaticInitFiles
|
|||
'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
|
||||
'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php',
|
||||
'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => __DIR__ . '/..' . '/../lib/Event/LoadAdditionalScriptsEvent.php',
|
||||
'OCA\\Files\\Event\\LoadSidebar' => __DIR__ . '/..' . '/../lib/Event/LoadSidebar.php',
|
||||
'OCA\\Files\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
|
||||
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => __DIR__ . '/..' . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
|
||||
'OCA\\Files\\Service\\TagService' => __DIR__ . '/..' . '/../lib/Service/TagService.php',
|
||||
|
|
|
@ -85,8 +85,9 @@
|
|||
}
|
||||
|
||||
/* fit app list view heights */
|
||||
.app-files #app-content>.viewcontainer {
|
||||
.app-files #app-content > .viewcontainer {
|
||||
min-height: 0%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-files #app-content {
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -704,6 +704,12 @@
|
|||
}
|
||||
context.fileList.do_delete(fileName, context.dir);
|
||||
$('.tipsy').remove();
|
||||
|
||||
// close sidebar on delete
|
||||
const path = context.dir + '/' + fileName
|
||||
if (OCA.Files.Sidebar && OCA.Files.Sidebar.file === path) {
|
||||
OCA.Files.Sidebar.file = undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -610,11 +610,11 @@
|
|||
* @param {string} [tabId] optional tab id to select
|
||||
*/
|
||||
showDetailsView: function(fileName, tabId) {
|
||||
console.warn('showDetailsView is deprecated! Use OCA.Files.Sidebar.activeTab. It will be removed in nextcloud 20.');
|
||||
this._updateDetailsView(fileName);
|
||||
if (tabId) {
|
||||
this._detailsView.selectTab(tabId);
|
||||
OCA.Files.Sidebar.activeTab = tabId;
|
||||
}
|
||||
OC.Apps.showAppSidebar(this._detailsView.$el);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -623,48 +623,28 @@
|
|||
* @param {string|OCA.Files.FileInfoModel} fileName file name from the current list or a FileInfoModel object
|
||||
* @param {boolean} [show=true] whether to open the sidebar if it was closed
|
||||
*/
|
||||
_updateDetailsView: function(fileName, show) {
|
||||
if (!this._detailsView) {
|
||||
_updateDetailsView: function(fileName) {
|
||||
if (!(OCA.Files && OCA.Files.Sidebar)) {
|
||||
console.error('No sidebar available');
|
||||
return;
|
||||
}
|
||||
|
||||
// show defaults to true
|
||||
show = _.isUndefined(show) || !!show;
|
||||
var oldFileInfo = this._detailsView.getFileInfo();
|
||||
if (oldFileInfo) {
|
||||
// TODO: use more efficient way, maybe track the highlight
|
||||
this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.get('id')).removeClass('highlighted');
|
||||
oldFileInfo.off('change', this._onSelectedModelChanged, this);
|
||||
}
|
||||
|
||||
if (!fileName) {
|
||||
this._detailsView.setFileInfo(null);
|
||||
if (this._currentFileModel) {
|
||||
this._currentFileModel.off();
|
||||
}
|
||||
this._currentFileModel = null;
|
||||
OC.Apps.hideAppSidebar(this._detailsView.$el);
|
||||
return;
|
||||
OCA.Files.Sidebar.file = null
|
||||
return
|
||||
} else if (typeof fileName !== 'string') {
|
||||
fileName = ''
|
||||
}
|
||||
|
||||
if (show && this._detailsView.$el.hasClass('disappear')) {
|
||||
OC.Apps.showAppSidebar(this._detailsView.$el);
|
||||
}
|
||||
// this is the old (terrible) way of getting the context.
|
||||
// don't use it anywhere else. Just provide the full path
|
||||
// of the file to the sidebar service
|
||||
var tr = this.findFileEl(fileName)
|
||||
var model = this.getModelForFile(tr)
|
||||
var path = model.attributes.path + '/' + model.attributes.name
|
||||
|
||||
if (fileName instanceof OCA.Files.FileInfoModel) {
|
||||
var model = fileName;
|
||||
} else {
|
||||
var $tr = this.findFileEl(fileName);
|
||||
var model = this.getModelForFile($tr);
|
||||
$tr.addClass('highlighted');
|
||||
}
|
||||
|
||||
this._currentFileModel = model;
|
||||
|
||||
this._replaceDetailsViewElementIfNeeded();
|
||||
|
||||
this._detailsView.setFileInfo(model);
|
||||
this._detailsView.$el.scrollTop(0);
|
||||
// open sidebar and set file
|
||||
OCA.Files.Sidebar.file = path.replace('//', '/')
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1404,6 +1384,13 @@
|
|||
return OC.MimeType.getIconUrl('dir-external');
|
||||
} else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
|
||||
return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType);
|
||||
} else if (fileInfo.shareTypes && (
|
||||
fileInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_LINK) > -1
|
||||
|| fileInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_EMAIL) > -1)
|
||||
) {
|
||||
return OC.MimeType.getIconUrl('dir-public')
|
||||
} else if (fileInfo.shareTypes && fileInfo.shareTypes.length > 0) {
|
||||
return OC.MimeType.getIconUrl('dir-shared')
|
||||
}
|
||||
return OC.MimeType.getIconUrl('dir');
|
||||
}
|
||||
|
@ -3654,8 +3641,10 @@
|
|||
* Register a tab view to be added to all views
|
||||
*/
|
||||
registerTabView: function(tabView) {
|
||||
if (this._detailsView) {
|
||||
this._detailsView.addTabView(tabView);
|
||||
console.warn('registerTabView is deprecated! It will be removed in nextcloud 20.');
|
||||
const name = tabView.getLabel()
|
||||
if (name) {
|
||||
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab(name, tabView, true))
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -3663,8 +3652,9 @@
|
|||
* Register a detail view to be added to all views
|
||||
*/
|
||||
registerDetailView: function(detailView) {
|
||||
if (this._detailsView) {
|
||||
this._detailsView.addDetailView(detailView);
|
||||
console.warn('registerDetailView is deprecated! It will be removed in nextcloud 20.');
|
||||
if (detailView.el) {
|
||||
OCA.Files.Sidebar.registerSecondaryView(detailView)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,33 +1,34 @@
|
|||
[
|
||||
"dist/sidebar.js",
|
||||
"app.js",
|
||||
"templates.js",
|
||||
"file-upload.js",
|
||||
"newfilemenu.js",
|
||||
"jquery.fileupload.js",
|
||||
"jquery-visibility.js",
|
||||
"fileinfomodel.js",
|
||||
"filesummary.js",
|
||||
"filemultiselectmenu.js",
|
||||
"breadcrumb.js",
|
||||
"filelist.js",
|
||||
"search.js",
|
||||
"favoritesfilelist.js",
|
||||
"recentfilelist.js",
|
||||
"tagsplugin.js",
|
||||
"gotoplugin.js",
|
||||
"favoritesplugin.js",
|
||||
"recentplugin.js",
|
||||
"detailfileinfoview.js",
|
||||
"sidebarpreviewmanager.js",
|
||||
"sidebarpreviewtext.js",
|
||||
"detailtabview.js",
|
||||
"semaphore.js",
|
||||
"mainfileinfodetailview.js",
|
||||
"operationprogressbar.js",
|
||||
"detailsview.js",
|
||||
"detailtabview.js",
|
||||
"favoritesfilelist.js",
|
||||
"favoritesplugin.js",
|
||||
"file-upload.js",
|
||||
"fileactions.js",
|
||||
"fileactionsmenu.js",
|
||||
"fileinfomodel.js",
|
||||
"filelist.js",
|
||||
"filemultiselectmenu.js",
|
||||
"files.js",
|
||||
"filesummary.js",
|
||||
"gotoplugin.js",
|
||||
"jquery-visibility.js",
|
||||
"jquery.fileupload.js",
|
||||
"keyboardshortcuts.js",
|
||||
"navigation.js"
|
||||
"mainfileinfodetailview.js",
|
||||
"navigation.js",
|
||||
"newfilemenu.js",
|
||||
"operationprogressbar.js",
|
||||
"recentfilelist.js",
|
||||
"recentplugin.js",
|
||||
"search.js",
|
||||
"semaphore.js",
|
||||
"sidebarpreviewmanager.js",
|
||||
"sidebarpreviewtext.js",
|
||||
"tagsplugin.js",
|
||||
"templates.js"
|
||||
]
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
||||
* @author Vincent Petry <pvince81@owncloud.com>
|
||||
* @author Felix Nüsse <felix.nuesse@t-online.de>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
|
@ -30,6 +31,7 @@ namespace OCA\Files\Controller;
|
|||
|
||||
use OCA\Files\Activity\Helper;
|
||||
use OCA\Files\Event\LoadAdditionalScriptsEvent;
|
||||
use OCA\Files\Event\LoadSidebar;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\RedirectResponse;
|
||||
|
@ -269,6 +271,8 @@ class ViewController extends Controller {
|
|||
$event = new LoadAdditionalScriptsEvent();
|
||||
$this->eventDispatcher->dispatch(LoadAdditionalScriptsEvent::class, $event);
|
||||
|
||||
$this->eventDispatcher->dispatch(LoadSidebar::class, new LoadSidebar());
|
||||
|
||||
$params = [];
|
||||
$params['usedSpacePercent'] = (int) $storageInfo['relative'];
|
||||
$params['owner'] = $storageInfo['owner'];
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @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\Files\Event;
|
||||
|
||||
use OCP\EventDispatcher\Event;
|
||||
|
||||
class LoadSidebar extends Event {
|
||||
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<AppSidebarTab :icon="icon"
|
||||
:name="name"
|
||||
:active-tab="activeTab" />
|
||||
</template>
|
||||
<script>
|
||||
import AppSidebarTab from 'nextcloud-vue/dist/Components/AppSidebarTab'
|
||||
|
||||
export default {
|
||||
name: 'LegacyTab',
|
||||
components: {
|
||||
AppSidebarTab: AppSidebarTab
|
||||
},
|
||||
props: {
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
return this.component.getIcon()
|
||||
},
|
||||
id() {
|
||||
// copied from AppSidebarTab
|
||||
return this.name.toLowerCase().replace(/ /g, '-')
|
||||
},
|
||||
order() {
|
||||
return this.component.order
|
||||
? this.component.order
|
||||
: 0
|
||||
},
|
||||
// needed because AppSidebarTab also uses $parent.activeTab
|
||||
activeTab() {
|
||||
return this.$parent.activeTab
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeTab(activeTab) {
|
||||
if (activeTab === this.id && this.fileInfo) {
|
||||
this.setFileInfo(this.fileInfo)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.setFileInfo(this.fileInfo)
|
||||
},
|
||||
mounted() {
|
||||
// append the backbone element and set the FileInfo
|
||||
this.component.$el.appendTo(this.$el)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.component.remove()
|
||||
},
|
||||
methods: {
|
||||
setFileInfo(fileInfo) {
|
||||
this.component.setFileInfo(new OCA.Files.FileInfoModel(fileInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,59 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'LegacyView',
|
||||
props: {
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fileInfo(fileInfo) {
|
||||
// update the backbone model FileInfo
|
||||
this.setFileInfo(fileInfo)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// append the backbone element and set the FileInfo
|
||||
this.component.$el.replaceAll(this.$el)
|
||||
this.setFileInfo(this.fileInfo)
|
||||
},
|
||||
methods: {
|
||||
setFileInfo(fileInfo) {
|
||||
this.component.setFileInfo(new OCA.Files.FileInfoModel(fileInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class Tab {
|
||||
|
||||
#component;
|
||||
#legacy;
|
||||
#name;
|
||||
|
||||
/**
|
||||
* Create a new tab instance
|
||||
*
|
||||
* @param {string} name the name of this tab
|
||||
* @param {Object} component the vue component
|
||||
* @param {boolean} [legacy] is this a legacy tab
|
||||
*/
|
||||
constructor(name, component, legacy) {
|
||||
this.#name = name
|
||||
this.#component = component
|
||||
this.#legacy = legacy === true
|
||||
|
||||
if (this.#legacy) {
|
||||
console.warn('Legacy tabs are deprecated! They will be removed in nextcloud 20.')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.#name
|
||||
}
|
||||
|
||||
get component() {
|
||||
return this.#component
|
||||
}
|
||||
|
||||
get isLegacyTab() {
|
||||
return this.#legacy === true
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* @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 axios from '@nextcloud/axios'
|
||||
|
||||
export default async function(url) {
|
||||
const response = await axios({
|
||||
method: 'PROPFIND',
|
||||
url,
|
||||
data: `<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:"
|
||||
xmlns:oc="http://owncloud.org/ns"
|
||||
xmlns:nc="http://nextcloud.org/ns"
|
||||
xmlns:ocs="http://open-collaboration-services.org/ns">
|
||||
<d:prop>
|
||||
<d:getlastmodified />
|
||||
<d:getetag />
|
||||
<d:getcontenttype />
|
||||
<d:resourcetype />
|
||||
<oc:fileid />
|
||||
<oc:permissions />
|
||||
<oc:size />
|
||||
<d:getcontentlength />
|
||||
<nc:has-preview />
|
||||
<nc:mount-type />
|
||||
<nc:is-encrypted />
|
||||
<ocs:share-permissions />
|
||||
<oc:tags />
|
||||
<oc:favorite />
|
||||
<oc:comments-unread />
|
||||
<oc:owner-id />
|
||||
<oc:owner-display-name />
|
||||
<oc:share-types />
|
||||
</d:prop>
|
||||
</d:propfind>`
|
||||
})
|
||||
|
||||
// TODO: create new parser or use cdav-lib when available
|
||||
const file = OCA.Files.App.fileList.filesClient._client.parseMultiStatus(response.data)
|
||||
// TODO: create new parser or use cdav-lib when available
|
||||
const fileInfo = OCA.Files.App.fileList.filesClient._parseFileInfo(file[0])
|
||||
|
||||
// TODO remove when no more legacy backbone is used
|
||||
fileInfo.get = (key) => fileInfo[key]
|
||||
fileInfo.isDirectory = () => fileInfo.mimetype === 'httpd/unix-directory'
|
||||
|
||||
return fileInfo
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class Sidebar {
|
||||
|
||||
#state;
|
||||
#view;
|
||||
|
||||
constructor() {
|
||||
// init empty state
|
||||
this.#state = {}
|
||||
|
||||
// init default values
|
||||
this.#state.tabs = []
|
||||
this.#state.views = []
|
||||
this.#state.file = ''
|
||||
this.#state.activeTab = ''
|
||||
console.debug('OCA.Files.Sidebar initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sidebar state
|
||||
*
|
||||
* @readonly
|
||||
* @memberof Sidebar
|
||||
* @returns {Object} the data state
|
||||
*/
|
||||
get state() {
|
||||
return this.#state
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof Sidebar
|
||||
* Register a new tab view
|
||||
*
|
||||
* @param {Object} tab a new unregistered tab
|
||||
* @memberof Sidebar
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
registerTab(tab) {
|
||||
const hasDuplicate = this.#state.tabs.findIndex(check => check.name === tab.name) > -1
|
||||
if (!hasDuplicate) {
|
||||
this.#state.tabs.push(tab)
|
||||
return true
|
||||
}
|
||||
console.error(`An tab with the same name ${tab.name} already exists`, tab)
|
||||
return false
|
||||
}
|
||||
|
||||
registerSecondaryView(view) {
|
||||
const hasDuplicate = this.#state.views.findIndex(check => check.name === view.name) > -1
|
||||
if (!hasDuplicate) {
|
||||
this.#state.views.push(view)
|
||||
return true
|
||||
}
|
||||
console.error(`A similar view already exists`, view)
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current sidebar file data
|
||||
*
|
||||
* @param {string} path the file path to load
|
||||
* @memberof Sidebar
|
||||
*/
|
||||
set file(path) {
|
||||
this.#state.file = path
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current sidebar file data
|
||||
*
|
||||
* @returns {String} the current opened file
|
||||
* @memberof Sidebar
|
||||
*/
|
||||
get file() {
|
||||
return this.#state.file
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current sidebar tab
|
||||
*
|
||||
* @param {string} id the tab unique id
|
||||
* @memberof Sidebar
|
||||
*/
|
||||
set activeTab(id) {
|
||||
this.#state.activeTab = id
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* @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 Vue from 'vue'
|
||||
import SidebarView from './views/Sidebar.vue'
|
||||
import Sidebar from './services/Sidebar'
|
||||
import Tab from './models/Tab'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
|
||||
Vue.use(VueClipboard)
|
||||
|
||||
Vue.prototype.t = t
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// Init Sidebar Service
|
||||
if (window.OCA && window.OCA.Files) {
|
||||
Object.assign(window.OCA.Files, { Sidebar: new Sidebar() })
|
||||
Object.assign(window.OCA.Files.Sidebar, { Tab })
|
||||
}
|
||||
|
||||
// Make sure we have a proper layout
|
||||
if (document.getElementById('content')) {
|
||||
|
||||
// Make sure we have a mountpoint
|
||||
if (!document.getElementById('app-sidebar')) {
|
||||
var contentElement = document.getElementById('content')
|
||||
var sidebarElement = document.createElement('div')
|
||||
sidebarElement.id = 'app-sidebar'
|
||||
contentElement.appendChild(sidebarElement)
|
||||
}
|
||||
}
|
||||
|
||||
// Init vue app
|
||||
const AppSidebar = new Vue({
|
||||
// eslint-disable-next-line vue/match-component-file-name
|
||||
name: 'SidebarRoot',
|
||||
render: h => h(SidebarView)
|
||||
})
|
||||
AppSidebar.$mount('#app-sidebar')
|
||||
})
|
|
@ -0,0 +1,358 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<AppSidebar
|
||||
v-if="file"
|
||||
ref="sidebar"
|
||||
v-bind="appSidebar"
|
||||
@close="onClose"
|
||||
@update:active="setActiveTab"
|
||||
@update:starred="toggleStarred"
|
||||
@[defaultActionListener].stop.prevent="onDefaultAction">
|
||||
<!-- TODO: create a standard to allow multiple elements here? -->
|
||||
<template v-if="fileInfo" #primary-actions>
|
||||
<LegacyView v-for="view in views"
|
||||
:key="view.cid"
|
||||
:component="view"
|
||||
:file-info="fileInfo" />
|
||||
</template>
|
||||
|
||||
<!-- Error display -->
|
||||
<div v-if="error" class="emptycontent">
|
||||
<div class="icon-error" />
|
||||
<h2>{{ error }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- If fileInfo fetch is complete, display tabs -->
|
||||
<template v-for="tab in tabs" v-else-if="fileInfo">
|
||||
<component
|
||||
:is="tabComponent(tab).is"
|
||||
v-if="canDisplay(tab)"
|
||||
:key="tab.id"
|
||||
:component="tabComponent(tab).component"
|
||||
:name="tab.name"
|
||||
:dav-path="davPath"
|
||||
:file-info="fileInfo" />
|
||||
</template>
|
||||
</AppSidebar>
|
||||
</template>
|
||||
<script>
|
||||
import $ from 'jquery'
|
||||
import axios from '@nextcloud/axios'
|
||||
import AppSidebar from 'nextcloud-vue/dist/Components/AppSidebar'
|
||||
import FileInfo from '../services/FileInfo'
|
||||
import LegacyTab from '../components/LegacyTab'
|
||||
import LegacyView from '../components/LegacyView'
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
|
||||
components: {
|
||||
AppSidebar,
|
||||
LegacyView
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// reactive state
|
||||
Sidebar: OCA.Files.Sidebar.state,
|
||||
error: null,
|
||||
fileInfo: null,
|
||||
starLoading: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Current filename
|
||||
* This is bound to the Sidebar service and
|
||||
* is used to load a new file
|
||||
* @returns {string}
|
||||
*/
|
||||
file() {
|
||||
return this.Sidebar.file
|
||||
},
|
||||
|
||||
/**
|
||||
* List of all the registered tabs
|
||||
* @returns {Array}
|
||||
*/
|
||||
tabs() {
|
||||
return this.Sidebar.tabs
|
||||
},
|
||||
|
||||
/**
|
||||
* List of all the registered views
|
||||
* @returns {Array}
|
||||
*/
|
||||
views() {
|
||||
return this.Sidebar.views
|
||||
},
|
||||
|
||||
/**
|
||||
* Current user dav root path
|
||||
* @returns {string}
|
||||
*/
|
||||
davPath() {
|
||||
const user = OC.getCurrentUser().uid
|
||||
return OC.linkToRemote(`dav/files/${user}${this.file}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* Current active tab handler
|
||||
* @param {string} id the tab id to set as active
|
||||
* @returns {string} the current active tab
|
||||
*/
|
||||
activeTab() {
|
||||
return this.Sidebar.activeTab
|
||||
},
|
||||
|
||||
/**
|
||||
* Sidebar subtitle
|
||||
* @returns {string}
|
||||
*/
|
||||
subtitle() {
|
||||
return `${this.size}, ${this.time}`
|
||||
},
|
||||
|
||||
/**
|
||||
* File last modified formatted string
|
||||
* @returns {string}
|
||||
*/
|
||||
time() {
|
||||
return OC.Util.relativeModifiedDate(this.fileInfo.mtime)
|
||||
},
|
||||
|
||||
/**
|
||||
* File size formatted string
|
||||
* @returns {string}
|
||||
*/
|
||||
size() {
|
||||
return OC.Util.humanFileSize(this.fileInfo.size)
|
||||
},
|
||||
|
||||
/**
|
||||
* File background/figure to illustrate the sidebar header
|
||||
* @returns {string}
|
||||
*/
|
||||
background() {
|
||||
return this.getPreviewIfAny(this.fileInfo)
|
||||
},
|
||||
|
||||
/**
|
||||
* App sidebar v-binding object
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
appSidebar() {
|
||||
if (this.fileInfo) {
|
||||
return {
|
||||
background: this.background,
|
||||
active: this.activeTab,
|
||||
class: { 'has-preview': this.fileInfo.hasPreview },
|
||||
compact: !this.fileInfo.hasPreview,
|
||||
'star-loading': this.starLoading,
|
||||
starred: this.fileInfo.isFavourited,
|
||||
subtitle: this.subtitle,
|
||||
title: this.fileInfo.name
|
||||
}
|
||||
} else if (this.error) {
|
||||
return {
|
||||
key: 'error', // force key to re-render
|
||||
subtitle: '',
|
||||
title: ''
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
class: 'icon-loading',
|
||||
subtitle: '',
|
||||
title: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Default action object for the current file
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
defaultAction() {
|
||||
return this.fileInfo
|
||||
&& OCA.Files && OCA.Files.App && OCA.Files.App.fileList
|
||||
&& OCA.Files.App.fileList
|
||||
.fileActions.getDefaultFileAction(this.fileInfo.mimetype, this.fileInfo.type, OC.PERMISSION_READ)
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Dynamic header click listener to ensure
|
||||
* nothing is listening for a click if there
|
||||
* is no default action
|
||||
*
|
||||
* @returns {string|null}
|
||||
*/
|
||||
defaultActionListener() {
|
||||
return this.defaultAction ? 'figure-click' : null
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
// update the sidebar data
|
||||
async file(curr, prev) {
|
||||
this.resetData()
|
||||
if (curr && curr.trim() !== '') {
|
||||
try {
|
||||
this.fileInfo = await FileInfo(this.davPath)
|
||||
// adding this as fallback because other apps expect it
|
||||
this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/')
|
||||
|
||||
// DEPRECATED legacy views
|
||||
// TODO: remove
|
||||
this.views.forEach(view => {
|
||||
view.setFileInfo(this.fileInfo)
|
||||
})
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.sidebar) {
|
||||
this.$refs.sidebar.updateTabs()
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
this.error = t('files', 'Error while loading the file data')
|
||||
console.error('Error while loading the file data')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Can this tab be displayed ?
|
||||
*
|
||||
* @param {Object} tab a registered tab
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canDisplay(tab) {
|
||||
if (tab.isLegacyTab) {
|
||||
return this.fileInfo && tab.component.canDisplay && tab.component.canDisplay(this.fileInfo)
|
||||
}
|
||||
// if the tab does not have an enabled method, we assume it's always available
|
||||
return tab.enabled ? tab.enabled(this.fileInfo) : true
|
||||
},
|
||||
onClose() {
|
||||
this.resetData()
|
||||
OCA.Files.Sidebar.file = ''
|
||||
},
|
||||
resetData() {
|
||||
this.error = null
|
||||
this.fileInfo = null
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.sidebar) {
|
||||
this.$refs.sidebar.updateTabs()
|
||||
}
|
||||
})
|
||||
},
|
||||
getPreviewIfAny(fileInfo) {
|
||||
if (fileInfo.hasPreview) {
|
||||
return OC.generateUrl(`/core/preview?fileId=${fileInfo.id}&x=${screen.width}&y=${screen.height}&a=true`)
|
||||
}
|
||||
return OCA.Files.App.fileList._getIconUrl(fileInfo)
|
||||
},
|
||||
|
||||
tabComponent(tab) {
|
||||
if (tab.isLegacyTab) {
|
||||
return {
|
||||
is: LegacyTab,
|
||||
component: tab.component
|
||||
}
|
||||
}
|
||||
return {
|
||||
is: tab.component
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set current active tab
|
||||
*
|
||||
* @param {string} id tab unique id
|
||||
*/
|
||||
setActiveTab(id) {
|
||||
OCA.Files.Sidebar.activeTab = id
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle favourite state
|
||||
* TODO: better implementation
|
||||
*
|
||||
* @param {Boolean} state favourited or not
|
||||
*/
|
||||
async toggleStarred(state) {
|
||||
try {
|
||||
this.starLoading = true
|
||||
await axios({
|
||||
method: 'PROPPATCH',
|
||||
url: this.davPath,
|
||||
data: `<?xml version="1.0"?>
|
||||
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||
${state ? '<d:set>' : '<d:remove>'}
|
||||
<d:prop>
|
||||
<oc:favorite>1</oc:favorite>
|
||||
</d:prop>
|
||||
${state ? '</d:set>' : '</d:remove>'}
|
||||
</d:propertyupdate>`
|
||||
})
|
||||
|
||||
// TODO: Obliterate as soon as possible and use events with new files app
|
||||
// Terrible fallback for legacy files: toggle filelist as well
|
||||
if (OCA.Files && OCA.Files.App && OCA.Files.App.fileList && OCA.Files.App.fileList.fileActions) {
|
||||
OCA.Files.App.fileList.fileActions.triggerAction('Favorite', OCA.Files.App.fileList.getModelForFile(this.fileInfo.name), OCA.Files.App.fileList)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
OC.Notification.showTemporary(t('files', 'Unable to change the favourite state of the file'))
|
||||
console.error('Unable to change favourite state', error)
|
||||
}
|
||||
this.starLoading = false
|
||||
},
|
||||
|
||||
onDefaultAction() {
|
||||
if (this.defaultAction) {
|
||||
// generate fake context
|
||||
this.defaultAction.action(this.fileInfo.name, {
|
||||
fileInfo: this.fileInfo,
|
||||
dir: this.fileInfo.dir,
|
||||
fileList: OCA.Files.App.fileList,
|
||||
$file: $('body')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
#app-sidebar {
|
||||
&.has-preview::v-deep .app-sidebar-header__figure {
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -748,36 +748,6 @@ describe('OCA.Files.FileList tests', function() {
|
|||
|
||||
expect(notificationStub.calledOnce).toEqual(true);
|
||||
});
|
||||
it('Shows renamed file details if rename ajax call suceeded', function() {
|
||||
fileList.showDetailsView('One.txt');
|
||||
|
||||
expect($('#app-sidebar').hasClass('disappear')).toEqual(false);
|
||||
expect(fileList._detailsView.getFileInfo().get('id')).toEqual(1);
|
||||
expect(fileList._detailsView.getFileInfo().get('name')).toEqual('One.txt');
|
||||
|
||||
doRename();
|
||||
|
||||
deferredRename.resolve(201);
|
||||
|
||||
expect($('#app-sidebar').hasClass('disappear')).toEqual(false);
|
||||
expect(fileList._detailsView.getFileInfo().get('id')).toEqual(1);
|
||||
expect(fileList._detailsView.getFileInfo().get('name')).toEqual('Tu_after_three.txt');
|
||||
});
|
||||
it('Shows again file details if rename ajax call failed', function() {
|
||||
fileList.showDetailsView('One.txt');
|
||||
|
||||
expect($('#app-sidebar').hasClass('disappear')).toEqual(false);
|
||||
expect(fileList._detailsView.getFileInfo().get('id')).toEqual(1);
|
||||
expect(fileList._detailsView.getFileInfo().get('name')).toEqual('One.txt');
|
||||
|
||||
doRename();
|
||||
|
||||
deferredRename.reject(403);
|
||||
|
||||
expect($('#app-sidebar').hasClass('disappear')).toEqual(false);
|
||||
expect(fileList._detailsView.getFileInfo().get('id')).toEqual(1);
|
||||
expect(fileList._detailsView.getFileInfo().get('name')).toEqual('One.txt');
|
||||
});
|
||||
it('Correctly updates file link after rename', function() {
|
||||
var $tr;
|
||||
doRename();
|
||||
|
@ -2460,184 +2430,6 @@ describe('OCA.Files.FileList tests', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe('Details sidebar', function() {
|
||||
beforeEach(function() {
|
||||
fileList.setFiles(testFiles);
|
||||
fileList.showDetailsView('Two.jpg');
|
||||
});
|
||||
describe('registering', function() {
|
||||
var addTabStub;
|
||||
var addDetailStub;
|
||||
|
||||
beforeEach(function() {
|
||||
addTabStub = sinon.stub(OCA.Files.DetailsView.prototype, 'addTabView');
|
||||
addDetailStub = sinon.stub(OCA.Files.DetailsView.prototype, 'addDetailView');
|
||||
getDetailsStub = sinon.stub(OCA.Files.DetailsView.prototype, 'getDetailViews');
|
||||
});
|
||||
afterEach(function() {
|
||||
addTabStub.restore();
|
||||
addDetailStub.restore();
|
||||
getDetailsStub.restore();
|
||||
});
|
||||
it('forward the registered views to the underlying DetailsView', function() {
|
||||
fileList.destroy();
|
||||
fileList = new OCA.Files.FileList($('#app-content-files'), {
|
||||
detailsViewEnabled: true
|
||||
});
|
||||
fileList.registerTabView(new OCA.Files.DetailTabView());
|
||||
fileList.registerDetailView(new OCA.Files.DetailFileInfoView());
|
||||
|
||||
expect(addTabStub.calledOnce).toEqual(true);
|
||||
// twice because the filelist already registers one by default
|
||||
expect(addDetailStub.calledTwice).toEqual(true);
|
||||
});
|
||||
it('forward getting the registered views to the underlying DetailsView', function() {
|
||||
fileList.destroy();
|
||||
fileList = new OCA.Files.FileList($('#app-content-files'), {
|
||||
detailsViewEnabled: true
|
||||
});
|
||||
var expectedRegisteredDetailsView = [];
|
||||
getDetailsStub.returns(expectedRegisteredDetailsView);
|
||||
|
||||
var registeredDetailViews = fileList.getRegisteredDetailViews();
|
||||
|
||||
expect(getDetailsStub.calledOnce).toEqual(true);
|
||||
expect(registeredDetailViews).toEqual(expectedRegisteredDetailsView);
|
||||
});
|
||||
it('does not error when registering panels when not details view configured', function() {
|
||||
fileList.destroy();
|
||||
fileList = new OCA.Files.FileList($('#app-content-files'), {
|
||||
detailsViewEnabled: false
|
||||
});
|
||||
fileList.registerTabView(new OCA.Files.DetailTabView());
|
||||
fileList.registerDetailView(new OCA.Files.DetailFileInfoView());
|
||||
|
||||
expect(addTabStub.notCalled).toEqual(true);
|
||||
expect(addDetailStub.notCalled).toEqual(true);
|
||||
});
|
||||
it('returns null when getting the registered views when not details view configured', function() {
|
||||
fileList.destroy();
|
||||
fileList = new OCA.Files.FileList($('#app-content-files'), {
|
||||
detailsViewEnabled: false
|
||||
});
|
||||
|
||||
var registeredDetailViews = fileList.getRegisteredDetailViews();
|
||||
|
||||
expect(getDetailsStub.notCalled).toEqual(true);
|
||||
expect(registeredDetailViews).toBeNull();
|
||||
});
|
||||
});
|
||||
it('triggers file action when clicking on row if no details view configured', function() {
|
||||
fileList.destroy();
|
||||
fileList = new OCA.Files.FileList($('#app-content-files'), {
|
||||
detailsViewEnabled: false
|
||||
});
|
||||
var updateDetailsViewStub = sinon.stub(fileList, '_updateDetailsView');
|
||||
var actionStub = sinon.stub();
|
||||
fileList.setFiles(testFiles);
|
||||
fileList.fileActions.register(
|
||||
'text/plain',
|
||||
'Test',
|
||||
OC.PERMISSION_ALL,
|
||||
function() {
|
||||
// Specify icon for hitory button
|
||||
return OC.imagePath('core','actions/history');
|
||||
},
|
||||
actionStub
|
||||
);
|
||||
fileList.fileActions.setDefault('text/plain', 'Test');
|
||||
var $tr = fileList.findFileEl('One.txt');
|
||||
$tr.find('td.filesize').click();
|
||||
expect(actionStub.calledOnce).toEqual(true);
|
||||
expect(updateDetailsViewStub.notCalled).toEqual(true);
|
||||
updateDetailsViewStub.restore();
|
||||
});
|
||||
it('highlights current file when clicked and updates sidebar', function() {
|
||||
fileList.fileActions.setDefault('text/plain', 'Test');
|
||||
var $tr = fileList.findFileEl('One.txt');
|
||||
$tr.find('td.filesize').click();
|
||||
expect($tr.hasClass('highlighted')).toEqual(true);
|
||||
|
||||
expect(fileList._detailsView.getFileInfo().id).toEqual(1);
|
||||
});
|
||||
it('keeps the last highlighted file when clicking outside', function() {
|
||||
var $tr = fileList.findFileEl('One.txt');
|
||||
$tr.find('td.filesize').click();
|
||||
|
||||
fileList.$el.find('tfoot').click();
|
||||
|
||||
expect($tr.hasClass('highlighted')).toEqual(true);
|
||||
expect(fileList._detailsView.getFileInfo().id).toEqual(1);
|
||||
});
|
||||
it('removes last highlighted file when selecting via checkbox', function() {
|
||||
var $tr = fileList.findFileEl('One.txt');
|
||||
|
||||
// select
|
||||
$tr.find('td.filesize').click();
|
||||
$tr.find('input:checkbox').click();
|
||||
expect($tr.hasClass('highlighted')).toEqual(false);
|
||||
|
||||
// deselect
|
||||
$tr.find('td.filesize').click();
|
||||
$tr.find('input:checkbox').click();
|
||||
expect($tr.hasClass('highlighted')).toEqual(false);
|
||||
|
||||
expect(fileList._detailsView.getFileInfo()).toEqual(null);
|
||||
});
|
||||
it('removes last highlighted file when selecting all files via checkbox', function() {
|
||||
var $tr = fileList.findFileEl('One.txt');
|
||||
|
||||
// select
|
||||
$tr.find('td.filesize').click();
|
||||
fileList.$el.find('.select-all.checkbox').click();
|
||||
expect($tr.hasClass('highlighted')).toEqual(false);
|
||||
|
||||
// deselect
|
||||
$tr.find('td.filesize').click();
|
||||
fileList.$el.find('.select-all.checkbox').click();
|
||||
expect($tr.hasClass('highlighted')).toEqual(false);
|
||||
|
||||
expect(fileList._detailsView.getFileInfo()).toEqual(null);
|
||||
});
|
||||
it('closes sidebar whenever the currently highlighted file was removed from the list', function() {
|
||||
jQuery.fx.off = true;
|
||||
var $tr = fileList.findFileEl('One.txt');
|
||||
$tr.find('td.filesize').click();
|
||||
expect($tr.hasClass('highlighted')).toEqual(true);
|
||||
|
||||
expect(fileList._detailsView.getFileInfo().id).toEqual(1);
|
||||
|
||||
expect($('#app-sidebar').hasClass('disappear')).toEqual(false);
|
||||
fileList.remove('One.txt');
|
||||
// sidebar is removed on close before being
|
||||
expect($('#app-sidebar').length).toEqual(0);
|
||||
jQuery.fx.off = false;
|
||||
});
|
||||
it('returns the currently selected model instance when calling getModelForFile', function() {
|
||||
var $tr = fileList.findFileEl('One.txt');
|
||||
$tr.find('td.filesize').click();
|
||||
|
||||
var model1 = fileList.getModelForFile('One.txt');
|
||||
var model2 = fileList.getModelForFile('One.txt');
|
||||
model1.set('test', true);
|
||||
|
||||
// it's the same model
|
||||
expect(model2).toEqual(model1);
|
||||
|
||||
var model3 = fileList.getModelForFile($tr);
|
||||
expect(model3).toEqual(model1);
|
||||
});
|
||||
it('closes the sidebar when switching folders', function() {
|
||||
jQuery.fx.off = true;
|
||||
var $tr = fileList.findFileEl('One.txt');
|
||||
$tr.find('td.filesize').click();
|
||||
|
||||
expect($('#app-sidebar').hasClass('disappear')).toEqual(false);
|
||||
fileList.changeDirectory('/another');
|
||||
expect($('#app-sidebar').length).toEqual(0);
|
||||
jQuery.fx.off = false;
|
||||
});
|
||||
});
|
||||
describe('File actions', function() {
|
||||
it('Clicking on a file name will trigger default action', function() {
|
||||
var actionStub = sinon.stub();
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
'sidebar': path.join(__dirname, 'src', 'sidebar.js'),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, './js/dist/'),
|
||||
publicPath: '/js/',
|
||||
filename: '[name].js',
|
||||
chunkFilename: 'files.[id].js'
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@ $eventDispatcher->addListener(
|
|||
'OCA\Files::loadAdditionalScripts',
|
||||
function() {
|
||||
\OCP\Util::addScript('files_sharing', 'dist/additionalScripts');
|
||||
\OCP\Util::addStyle('files_sharing', 'icons');
|
||||
}
|
||||
);
|
||||
\OC::$server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', function () {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// This is the icons used in the sharing ui (multiselect)
|
||||
.icon-room {
|
||||
@include icon-color('app', 'spreed', $color-black);
|
||||
}
|
||||
.icon-circle {
|
||||
@include icon-color('circles', 'circles', $color-black, 3, false);
|
||||
}
|
||||
.icon-guests {
|
||||
@include icon-color('app', 'guests', $color-black);
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,2 +1,2 @@
|
|||
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=0)}([function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}]);
|
||||
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=67)}({67:function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}});
|
||||
//# sourceMappingURL=collaboration.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,24 @@
|
|||
(window.webpackJsonpFilesSharing=window.webpackJsonpFilesSharing||[]).push([[5],{348:function(e,o,i){"use strict";i.r(o);var n=i(3),l=i(349),r=i(350),u=i(351),s=i.n(u),a={name:"CollaborationView",components:{CollectionList:i(103).a},computed:{fileId:function(){return this.$root.model&&this.$root.model.id?""+this.$root.model.id:null},filename:function(){return this.$root.model&&this.$root.model.name?""+this.$root.model.name:""}}},d=i(0),c=Object(d.a)(a,(function(){var t=this.$createElement,e=this._self._c||t;return this.fileId?e("CollectionList",{attrs:{id:this.fileId,type:"file",name:this.filename}}):this._e()}),[],!1,null,null,null).exports;i.d(o,"Vue",(function(){return n.default})),i.d(o,"View",(function(){return c})),
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
n.default.prototype.t=t,r.Tooltip.options.defaultHtml=!1,n.default.component("PopoverMenu",r.PopoverMenu),n.default.directive("ClickOutside",s.a),n.default.directive("Tooltip",r.Tooltip),n.default.use(l.a)}}]);
|
||||
//# sourceMappingURL=files_sharing.5.js.map?v=8e6d26aadf8403a07b5b
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -154,7 +154,11 @@ class ShareAPIController extends OCSController {
|
|||
'share_type' => $share->getShareType(),
|
||||
'uid_owner' => $share->getSharedBy(),
|
||||
'displayname_owner' => $sharedBy !== null ? $sharedBy->getDisplayName() : $share->getSharedBy(),
|
||||
// recipient permissions
|
||||
'permissions' => $share->getPermissions(),
|
||||
// current user permissions on this share
|
||||
'can_edit' => $this->canEditShare($share),
|
||||
'can_delete' => $this->canDeleteShare($share),
|
||||
'stime' => $share->getShareTime()->getTimestamp(),
|
||||
'parent' => null,
|
||||
'expiration' => null,
|
||||
|
|
|
@ -33,6 +33,7 @@ $tmpl = new OCP\Template('files_sharing', 'list', '');
|
|||
$tmpl->assign('showgridview', $showgridview && !$isIE);
|
||||
|
||||
OCP\Util::addScript('files_sharing', 'dist/files_sharing');
|
||||
OCP\Util::addScript('files_sharing', 'dist/files_sharing_tab');
|
||||
\OC::$server->getEventDispatcher()->dispatch('\OCP\Collaboration\Resources::loadAdditionalScripts');
|
||||
|
||||
$tmpl->printPage();
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<li class="sharing-entry">
|
||||
<Avatar class="sharing-entry__avatar"
|
||||
:user="share.shareWith"
|
||||
:display-name="share.shareWithDisplayName"
|
||||
:url="share.shareWithAvatar" />
|
||||
<div v-tooltip.auto="tooltip" class="sharing-entry__desc">
|
||||
<h5>{{ title }}</h5>
|
||||
</div>
|
||||
<Actions menu-align="right" class="sharing-entry__actions">
|
||||
<template v-if="share.canEdit">
|
||||
<!-- edit permission -->
|
||||
<ActionCheckbox
|
||||
ref="canEdit"
|
||||
:checked.sync="canEdit"
|
||||
:value="permissionsEdit"
|
||||
:disabled="saving">
|
||||
{{ t('files_sharing', 'Allow editing') }}
|
||||
</ActionCheckbox>
|
||||
|
||||
<!-- reshare permission -->
|
||||
<ActionCheckbox
|
||||
ref="canReshare"
|
||||
:checked.sync="canReshare"
|
||||
:value="permissionsShare"
|
||||
:disabled="saving">
|
||||
{{ t('files_sharing', 'Can reshare') }}
|
||||
</ActionCheckbox>
|
||||
|
||||
<!-- expiration date -->
|
||||
<ActionCheckbox :checked.sync="hasExpirationDate"
|
||||
:disabled="config.isDefaultExpireDateEnforced || saving"
|
||||
@uncheck="onExpirationDisable">
|
||||
{{ config.isDefaultExpireDateEnforced
|
||||
? t('files_sharing', 'Expiration date enforced')
|
||||
: t('files_sharing', 'Set expiration date') }}
|
||||
</ActionCheckbox>
|
||||
<ActionInput v-if="hasExpirationDate"
|
||||
ref="expireDate"
|
||||
v-tooltip.auto="{
|
||||
content: errors.expireDate,
|
||||
show: errors.expireDate,
|
||||
trigger: 'manual'
|
||||
}"
|
||||
:class="{ error: errors.expireDate}"
|
||||
:disabled="saving"
|
||||
:first-day-of-week="firstDay"
|
||||
:lang="lang"
|
||||
:value="share.expireDate"
|
||||
icon="icon-calendar-dark"
|
||||
type="date"
|
||||
:not-before="dateTomorrow"
|
||||
:not-after="dateMaxEnforced"
|
||||
@update:value="onExpirationChange">
|
||||
{{ t('files_sharing', 'Enter a date') }}
|
||||
</ActionInput>
|
||||
|
||||
<!-- note -->
|
||||
<template v-if="canHaveNote">
|
||||
<ActionCheckbox
|
||||
:checked.sync="hasNote"
|
||||
:disabled="saving"
|
||||
@uncheck="queueUpdate('note')">
|
||||
{{ t('files_sharing', 'Note to recipient') }}
|
||||
</ActionCheckbox>
|
||||
<ActionTextEditable v-if="hasNote"
|
||||
ref="note"
|
||||
v-tooltip.auto="{
|
||||
content: errors.note,
|
||||
show: errors.note,
|
||||
trigger: 'manual'
|
||||
}"
|
||||
:class="{ error: errors.note}"
|
||||
:disabled="saving"
|
||||
:value.sync="share.note"
|
||||
icon="icon-edit"
|
||||
@update:value="debounceQueueUpdate('note')" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<ActionButton v-if="share.canDelete"
|
||||
icon="icon-delete"
|
||||
:disabled="saving"
|
||||
@click.prevent="onDelete">
|
||||
{{ t('files_sharing', 'Unshare') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
|
||||
import Actions from 'nextcloud-vue/dist/Components/Actions'
|
||||
import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
|
||||
import ActionCheckbox from 'nextcloud-vue/dist/Components/ActionCheckbox'
|
||||
import ActionInput from 'nextcloud-vue/dist/Components/ActionInput'
|
||||
import ActionTextEditable from 'nextcloud-vue/dist/Components/ActionTextEditable'
|
||||
import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Share from '../models/Share'
|
||||
import SharesMixin from '../mixins/SharesMixin'
|
||||
|
||||
export default {
|
||||
name: 'SharingEntry',
|
||||
|
||||
components: {
|
||||
Actions,
|
||||
ActionButton,
|
||||
ActionCheckbox,
|
||||
ActionInput,
|
||||
ActionTextEditable,
|
||||
Avatar
|
||||
},
|
||||
|
||||
directives: {
|
||||
Tooltip
|
||||
},
|
||||
|
||||
mixins: [SharesMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
permissionsEdit: OC.PERMISSION_UPDATE,
|
||||
permissionsRead: OC.PERMISSION_READ,
|
||||
permissionsShare: OC.PERMISSION_SHARE
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
title() {
|
||||
let title = this.share.shareWithDisplayName
|
||||
if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GROUP) {
|
||||
title += ` (${t('files_sharing', 'group')})`
|
||||
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_ROOM) {
|
||||
title += ` (${t('files_sharing', 'conversation')})`
|
||||
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE) {
|
||||
title += ` (${t('files_sharing', 'remote')})`
|
||||
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP) {
|
||||
title += ` (${t('files_sharing', 'remote group')})`
|
||||
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GUEST) {
|
||||
title += ` (${t('files_sharing', 'guest')})`
|
||||
}
|
||||
return title
|
||||
},
|
||||
|
||||
tooltip() {
|
||||
if (this.share.owner !== this.share.uidFileOwner) {
|
||||
const data = {
|
||||
// todo: strong or italic?
|
||||
// but the t function escape any html from the data :/
|
||||
user: this.share.shareWithDisplayName,
|
||||
owner: this.share.owner
|
||||
}
|
||||
|
||||
if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GROUP) {
|
||||
return t('files_sharing', 'Shared with the group {user} by {owner}', data)
|
||||
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_ROOM) {
|
||||
return t('files_sharing', 'Shared with the conversation {user} by {owner}', data)
|
||||
}
|
||||
|
||||
return t('files_sharing', 'Shared with {user} by {owner}', data)
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
canHaveNote() {
|
||||
return this.share.type !== this.SHARE_TYPES.SHARE_TYPE_REMOTE
|
||||
&& this.share.type !== this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP
|
||||
},
|
||||
|
||||
/**
|
||||
* Can the sharee edit the shared file ?
|
||||
*/
|
||||
canEdit: {
|
||||
get: function() {
|
||||
return this.share.hasUpdatePermission
|
||||
},
|
||||
set: function(checked) {
|
||||
this.updatePermissions(checked, this.canReshare)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Can the sharee reshare the file ?
|
||||
*/
|
||||
canReshare: {
|
||||
get: function() {
|
||||
return this.share.hasSharePermission
|
||||
},
|
||||
set: function(checked) {
|
||||
this.updatePermissions(this.canEdit, checked)
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
updatePermissions(isEditChecked, isReshareChecked) {
|
||||
// calc permissions if checked
|
||||
const permissions = this.permissionsRead
|
||||
| (isEditChecked ? this.permissionsEdit : 0)
|
||||
| (isReshareChecked ? this.permissionsShare : 0)
|
||||
|
||||
this.share.permissions = permissions
|
||||
this.queueUpdate('permissions')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sharing-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
&__desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
line-height: 1.2em;
|
||||
p {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,117 @@
|
|||
|
||||
<template>
|
||||
<SharingEntrySimple
|
||||
class="sharing-entry__internal"
|
||||
:title="t('files_sharing', 'Internal link')"
|
||||
:subtitle="internalLinkSubtitle">
|
||||
<template #avatar>
|
||||
<div class="avatar-external icon-external-white" />
|
||||
</template>
|
||||
|
||||
<ActionLink ref="copyButton"
|
||||
:href="internalLink"
|
||||
target="_blank"
|
||||
:icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
|
||||
@click.prevent="copyLink">
|
||||
{{ clipboardTooltip }}
|
||||
</ActionLink>
|
||||
</SharingEntrySimple>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import ActionLink from 'nextcloud-vue/dist/Components/ActionLink'
|
||||
import SharingEntrySimple from './SharingEntrySimple'
|
||||
|
||||
export default {
|
||||
name: 'SharingEntryInternal',
|
||||
|
||||
components: {
|
||||
ActionLink,
|
||||
SharingEntrySimple
|
||||
},
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
copied: false,
|
||||
copySuccess: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the internal link to this file id
|
||||
* @returns {string}
|
||||
*/
|
||||
internalLink() {
|
||||
return window.location.protocol + '//' + window.location.host + generateUrl('/f/') + this.fileInfo.id
|
||||
},
|
||||
|
||||
/**
|
||||
* Clipboard v-tooltip message
|
||||
* @returns {string}
|
||||
*/
|
||||
clipboardTooltip() {
|
||||
if (this.copied) {
|
||||
return this.copySuccess
|
||||
? t('files_sharing', 'Link copied')
|
||||
: t('files_sharing', 'Cannot copy, please copy the link manually')
|
||||
}
|
||||
return t('files_sharing', 'Copy to clipboard')
|
||||
},
|
||||
|
||||
internalLinkSubtitle() {
|
||||
if (this.fileInfo.type === 'dir') {
|
||||
return t('files_sharing', 'Only works for users with access to this folder')
|
||||
}
|
||||
return t('files_sharing', 'Only works for users with access to this file')
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async copyLink() {
|
||||
try {
|
||||
await this.$copyText(this.internalLink)
|
||||
// focus and show the tooltip
|
||||
this.$refs.copyButton.$el.focus()
|
||||
this.copySuccess = true
|
||||
this.copied = true
|
||||
} catch (error) {
|
||||
this.copySuccess = false
|
||||
this.copied = true
|
||||
console.error(error)
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
this.copySuccess = false
|
||||
this.copied = false
|
||||
}, 4000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sharing-entry__internal {
|
||||
.avatar-external {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
font-size: 18px;
|
||||
background-color: var(--color-text-maxcontrast);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.icon-checkmark-color {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,779 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<li :class="{'sharing-entry--share': share}" class="sharing-entry sharing-entry__link">
|
||||
<Avatar :is-no-user="true"
|
||||
:class="isEmailShareType ? 'icon-mail-white' : 'icon-public-white'"
|
||||
class="sharing-entry__avatar" />
|
||||
<div class="sharing-entry__desc">
|
||||
<h5>{{ title }}</h5>
|
||||
</div>
|
||||
|
||||
<!-- clipboard -->
|
||||
<Actions v-if="share && !isEmailShareType && share.token"
|
||||
ref="copyButton"
|
||||
class="sharing-entry__copy">
|
||||
<ActionLink :href="shareLink"
|
||||
target="_blank"
|
||||
:icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
|
||||
@click.stop.prevent="copyLink">
|
||||
{{ clipboardTooltip }}
|
||||
</ActionLink>
|
||||
</Actions>
|
||||
|
||||
<!-- pending actions -->
|
||||
<Actions v-if="!loading && (pendingPassword || pendingExpirationDate)"
|
||||
class="sharing-entry__actions"
|
||||
menu-align="right"
|
||||
:open.sync="open"
|
||||
@close="onNewLinkShare">
|
||||
<!-- pending data menu -->
|
||||
<ActionText v-if="errors.pending"
|
||||
icon="icon-error"
|
||||
:class="{ error: errors.pending}">
|
||||
{{ errors.pending }}
|
||||
</ActionText>
|
||||
<ActionText v-else icon="icon-info">
|
||||
{{ t('files_sharing', 'Please enter the following required information before creating the share') }}
|
||||
</ActionText>
|
||||
|
||||
<!-- password -->
|
||||
<ActionText v-if="pendingPassword" icon="icon-password">
|
||||
{{ t('files_sharing', 'Password protection (enforced)') }}
|
||||
</ActionText>
|
||||
<ActionCheckbox v-else-if="config.enableLinkPasswordByDefault"
|
||||
:checked.sync="isPasswordProtected"
|
||||
:disabled="config.enforcePasswordForPublicLink || saving"
|
||||
class="share-link-password-checkbox"
|
||||
@uncheck="onPasswordDisable">
|
||||
{{ t('files_sharing', 'Password protection') }}
|
||||
</ActionCheckbox>
|
||||
<ActionInput v-if="pendingPassword || share.password"
|
||||
v-tooltip.auto="{
|
||||
content: errors.password,
|
||||
show: errors.password,
|
||||
trigger: 'manual',
|
||||
defaultContainer: '#app-sidebar'
|
||||
}"
|
||||
class="share-link-password"
|
||||
:value.sync="share.password"
|
||||
:disabled="saving"
|
||||
:required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink"
|
||||
:minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength"
|
||||
icon=""
|
||||
autocomplete="new-password"
|
||||
@submit="onNewLinkShare">
|
||||
{{ t('files_sharing', 'Enter a password') }}
|
||||
</ActionInput>
|
||||
|
||||
<!-- expiration date -->
|
||||
<ActionText v-if="pendingExpirationDate" icon="icon-calendar-dark">
|
||||
{{ t('files_sharing', 'Expiration date (enforced)') }}
|
||||
</ActionText>
|
||||
<ActionInput v-if="pendingExpirationDate"
|
||||
v-model="share.expireDate"
|
||||
v-tooltip.auto="{
|
||||
content: errors.expireDate,
|
||||
show: errors.expireDate,
|
||||
trigger: 'manual',
|
||||
defaultContainer: '#app-sidebar'
|
||||
}"
|
||||
class="share-link-expire-date"
|
||||
:disabled="saving"
|
||||
:first-day-of-week="firstDay"
|
||||
:lang="lang"
|
||||
icon=""
|
||||
type="date"
|
||||
:not-before="dateTomorrow"
|
||||
:not-after="dateMaxEnforced">
|
||||
<!-- let's not submit when picked, the user
|
||||
might want to still edit or copy the password -->
|
||||
{{ t('files_sharing', 'Enter a date') }}
|
||||
</ActionInput>
|
||||
|
||||
<ActionButton icon="icon-close" @click.prevent.stop="onCancel">
|
||||
{{ t('files_sharing', 'Cancel') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
|
||||
<!-- actions -->
|
||||
<Actions v-else-if="!loading"
|
||||
class="sharing-entry__actions"
|
||||
menu-align="right"
|
||||
:open.sync="open"
|
||||
@close="onPasswordSubmit">
|
||||
<template v-if="share">
|
||||
<template v-if="share.canEdit">
|
||||
<!-- folder -->
|
||||
<template v-if="isFolder && fileHasCreatePermission && config.isPublicUploadEnabled">
|
||||
<ActionRadio :checked="share.permissions === publicUploadRValue"
|
||||
:value="publicUploadRValue"
|
||||
:name="randomId"
|
||||
:disabled="saving"
|
||||
@change="togglePermissions">
|
||||
{{ t('files_sharing', 'Read only') }}
|
||||
</ActionRadio>
|
||||
<ActionRadio :checked="share.permissions === publicUploadRWValue"
|
||||
:value="publicUploadRWValue"
|
||||
:disabled="saving"
|
||||
:name="randomId"
|
||||
@change="togglePermissions">
|
||||
{{ t('files_sharing', 'Allow upload and editing') }}
|
||||
</ActionRadio>
|
||||
<ActionRadio :checked="share.permissions === publicUploadWValue"
|
||||
:value="publicUploadWValue"
|
||||
:disabled="saving"
|
||||
:name="randomId"
|
||||
class="sharing-entry__action--public-upload"
|
||||
@change="togglePermissions">
|
||||
{{ t('files_sharing', 'File drop (upload only)') }}
|
||||
</ActionRadio>
|
||||
</template>
|
||||
|
||||
<!-- file -->
|
||||
<ActionCheckbox v-else
|
||||
:checked.sync="canUpdate"
|
||||
:disabled="saving"
|
||||
@change="queueUpdate('permissions')">
|
||||
{{ t('files_sharing', 'Allow editing') }}
|
||||
</ActionCheckbox>
|
||||
|
||||
<ActionCheckbox
|
||||
:checked.sync="share.hideDownload"
|
||||
:disabled="saving"
|
||||
@change="queueUpdate('hideDownload')">
|
||||
{{ t('files_sharing', 'Hide download') }}
|
||||
</ActionCheckbox>
|
||||
|
||||
<!-- password -->
|
||||
<ActionCheckbox :checked.sync="isPasswordProtected"
|
||||
:disabled="config.enforcePasswordForPublicLink || saving"
|
||||
class="share-link-password-checkbox"
|
||||
@uncheck="onPasswordDisable">
|
||||
{{ config.enforcePasswordForPublicLink
|
||||
? t('files_sharing', 'Password protection (enforced)')
|
||||
: t('files_sharing', 'Password protect') }}
|
||||
</ActionCheckbox>
|
||||
<ActionInput v-if="isPasswordProtected"
|
||||
ref="password"
|
||||
v-tooltip.auto="{
|
||||
content: errors.password,
|
||||
show: errors.password,
|
||||
trigger: 'manual',
|
||||
defaultContainer: '#app-sidebar'
|
||||
}"
|
||||
class="share-link-password"
|
||||
:class="{ error: errors.password}"
|
||||
:disabled="saving"
|
||||
:required="config.enforcePasswordForPublicLink"
|
||||
:value="hasUnsavedPassword ? share.newPassword : '***************'"
|
||||
icon="icon-password"
|
||||
autocomplete="new-password"
|
||||
:type="hasUnsavedPassword ? 'text': 'password'"
|
||||
@update:value="onPasswordChange"
|
||||
@submit="onPasswordSubmit">
|
||||
{{ t('files_sharing', 'Enter a password') }}
|
||||
</ActionInput>
|
||||
|
||||
<!-- expiration date -->
|
||||
<ActionCheckbox :checked.sync="hasExpirationDate"
|
||||
:disabled="config.isDefaultExpireDateEnforced || saving"
|
||||
class="share-link-expire-date-checkbox"
|
||||
@uncheck="onExpirationDisable">
|
||||
{{ config.isDefaultExpireDateEnforced
|
||||
? t('files_sharing', 'Expiration date (enforced)')
|
||||
: t('files_sharing', 'Set expiration date') }}
|
||||
</ActionCheckbox>
|
||||
<ActionInput v-if="hasExpirationDate"
|
||||
ref="expireDate"
|
||||
v-tooltip.auto="{
|
||||
content: errors.expireDate,
|
||||
show: errors.expireDate,
|
||||
trigger: 'manual',
|
||||
defaultContainer: '#app-sidebar'
|
||||
}"
|
||||
class="share-link-expire-date"
|
||||
:class="{ error: errors.expireDate}"
|
||||
:disabled="saving"
|
||||
:first-day-of-week="firstDay"
|
||||
:lang="lang"
|
||||
:value="share.expireDate"
|
||||
icon="icon-calendar-dark"
|
||||
type="date"
|
||||
:not-before="dateTomorrow"
|
||||
:not-after="dateMaxEnforced"
|
||||
@update:value="onExpirationChange">
|
||||
{{ t('files_sharing', 'Enter a date') }}
|
||||
</ActionInput>
|
||||
|
||||
<!-- note -->
|
||||
<ActionCheckbox :checked.sync="hasNote"
|
||||
:disabled="saving"
|
||||
@uncheck="queueUpdate('note')">
|
||||
{{ t('files_sharing', 'Note to recipient') }}
|
||||
</ActionCheckbox>
|
||||
<ActionTextEditable v-if="hasNote"
|
||||
ref="note"
|
||||
v-tooltip.auto="{
|
||||
content: errors.note,
|
||||
show: errors.note,
|
||||
trigger: 'manual',
|
||||
defaultContainer: '#app-sidebar'
|
||||
}"
|
||||
:class="{ error: errors.note}"
|
||||
:disabled="saving"
|
||||
:value.sync="share.note"
|
||||
icon="icon-edit"
|
||||
@update:value="debounceQueueUpdate('note')" />
|
||||
</template>
|
||||
|
||||
<!-- external sharing via url (social...) -->
|
||||
<ActionLink v-for="({icon, url, name}, index) in externalActions"
|
||||
:key="index"
|
||||
:href="url(shareLink)"
|
||||
:icon="icon"
|
||||
target="_blank">
|
||||
{{ name }}
|
||||
</ActionLink>
|
||||
|
||||
<ActionButton v-if="share.canDelete"
|
||||
icon="icon-delete"
|
||||
:disabled="saving"
|
||||
@click.prevent="onDelete">
|
||||
{{ t('files_sharing', 'Delete share') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="!isEmailShareType && canReshare"
|
||||
class="new-share-link"
|
||||
icon="icon-add"
|
||||
@click.prevent.stop="onNewLinkShare">
|
||||
{{ t('files_sharing', 'Add another link') }}
|
||||
</ActionButton>
|
||||
</template>
|
||||
|
||||
<!-- Create new share -->
|
||||
<ActionButton v-else-if="canReshare"
|
||||
class="new-share-link"
|
||||
icon="icon-add"
|
||||
@click.prevent.stop="onNewLinkShare">
|
||||
{{ t('files_sharing', 'Create a new share link') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
|
||||
<!-- loading indicator to replace the menu -->
|
||||
<div v-else class="icon-loading-small sharing-entry__loading" />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
|
||||
import ActionCheckbox from 'nextcloud-vue/dist/Components/ActionCheckbox'
|
||||
import ActionRadio from 'nextcloud-vue/dist/Components/ActionRadio'
|
||||
import ActionInput from 'nextcloud-vue/dist/Components/ActionInput'
|
||||
import ActionText from 'nextcloud-vue/dist/Components/ActionText'
|
||||
import ActionTextEditable from 'nextcloud-vue/dist/Components/ActionTextEditable'
|
||||
import ActionLink from 'nextcloud-vue/dist/Components/ActionLink'
|
||||
import Actions from 'nextcloud-vue/dist/Components/Actions'
|
||||
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
|
||||
import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
|
||||
|
||||
import Share from '../models/Share'
|
||||
import SharesMixin from '../mixins/SharesMixin'
|
||||
|
||||
const passwordSet = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789'
|
||||
|
||||
export default {
|
||||
name: 'SharingEntryLink',
|
||||
|
||||
components: {
|
||||
Actions,
|
||||
ActionButton,
|
||||
ActionCheckbox,
|
||||
ActionRadio,
|
||||
ActionInput,
|
||||
ActionLink,
|
||||
ActionText,
|
||||
ActionTextEditable,
|
||||
Avatar
|
||||
},
|
||||
|
||||
directives: {
|
||||
Tooltip
|
||||
},
|
||||
|
||||
mixins: [SharesMixin],
|
||||
|
||||
props: {
|
||||
canReshare: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
copySuccess: true,
|
||||
copied: false,
|
||||
|
||||
publicUploadRWValue: OC.PERMISSION_UPDATE | OC.PERMISSION_CREATE | OC.PERMISSION_READ | OC.PERMISSION_DELETE,
|
||||
publicUploadRValue: OC.PERMISSION_READ,
|
||||
publicUploadWValue: OC.PERMISSION_CREATE,
|
||||
|
||||
ExternalLinkActions: OCA.Sharing.ExternalLinkActions.state
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Generate a unique random id for this SharingEntryLink only
|
||||
* This allows ActionRadios to have the same name prop
|
||||
* but not to impact others SharingEntryLink
|
||||
* @returns {string}
|
||||
*/
|
||||
randomId() {
|
||||
return Math.random().toString(27).substr(2)
|
||||
},
|
||||
|
||||
/**
|
||||
* Link share label
|
||||
* TODO: allow editing
|
||||
* @returns {string}
|
||||
*/
|
||||
title() {
|
||||
// if we have a valid existing share (not pending)
|
||||
if (this.share && this.share.id) {
|
||||
if (!this.isShareOwner && this.share.ownerDisplayName) {
|
||||
return t('files_sharing', 'Shared via link by {initiator}', {
|
||||
initiator: this.share.ownerDisplayName
|
||||
})
|
||||
}
|
||||
if (this.share.label && this.share.label.trim() !== '') {
|
||||
return this.share.label
|
||||
}
|
||||
if (this.isEmailShareType) {
|
||||
return this.share.shareWith
|
||||
}
|
||||
}
|
||||
return t('files_sharing', 'Share link')
|
||||
},
|
||||
|
||||
/**
|
||||
* Is the current share password protected ?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isPasswordProtected: {
|
||||
get: function() {
|
||||
return this.config.enforcePasswordForPublicLink
|
||||
|| !!this.share.password
|
||||
},
|
||||
set: async function(enabled) {
|
||||
// TODO: directly save after generation to make sure the share is always protected
|
||||
this.share.password = enabled ? await this.generatePassword() : ''
|
||||
this.share.newPassword = this.share.password
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Is the current share an email share ?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEmailShareType() {
|
||||
return this.share
|
||||
? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL
|
||||
: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Pending data.
|
||||
* If the share still doesn't have an id, it is not synced
|
||||
* Therefore this is still not valid and requires user input
|
||||
* @returns {boolean}
|
||||
*/
|
||||
pendingPassword() {
|
||||
return this.config.enforcePasswordForPublicLink && this.share && !this.share.id
|
||||
},
|
||||
pendingExpirationDate() {
|
||||
return this.config.isDefaultExpireDateEnforced && this.share && !this.share.id
|
||||
},
|
||||
|
||||
/**
|
||||
* Can the recipient edit the file ?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canUpdate: {
|
||||
get: function() {
|
||||
return this.share.hasUpdatePermission
|
||||
},
|
||||
set: function(enabled) {
|
||||
this.share.permissions = enabled
|
||||
? OC.PERMISSION_READ | OC.PERMISSION_UPDATE
|
||||
: OC.PERMISSION_READ
|
||||
}
|
||||
},
|
||||
|
||||
// if newPassword exists, but is empty, it means
|
||||
// the user deleted the original password
|
||||
hasUnsavedPassword() {
|
||||
return this.share.newPassword !== undefined
|
||||
},
|
||||
|
||||
/**
|
||||
* Is the current share a folder ?
|
||||
* TODO: move to a proper FileInfo model?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isFolder() {
|
||||
return this.fileInfo.type === 'dir'
|
||||
},
|
||||
|
||||
/**
|
||||
* Does the current file/folder have create permissions
|
||||
* TODO: move to a proper FileInfo model?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
fileHasCreatePermission() {
|
||||
return !!(this.fileInfo.permissions & OC.PERMISSION_CREATE)
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the public share link
|
||||
* @returns {string}
|
||||
*/
|
||||
shareLink() {
|
||||
return window.location.protocol + '//' + window.location.host + generateUrl('/s/') + this.share.token
|
||||
},
|
||||
|
||||
/**
|
||||
* Clipboard v-tooltip message
|
||||
* @returns {string}
|
||||
*/
|
||||
clipboardTooltip() {
|
||||
if (this.copied) {
|
||||
return this.copySuccess
|
||||
? t('files_sharing', 'Link copied')
|
||||
: t('files_sharing', 'Cannot copy, please copy the link manually')
|
||||
}
|
||||
return t('files_sharing', 'Copy to clipboard')
|
||||
},
|
||||
|
||||
/**
|
||||
* External aditionnal actions for the menu
|
||||
* @returns {Array}
|
||||
*/
|
||||
externalActions() {
|
||||
return this.ExternalLinkActions.actions
|
||||
},
|
||||
|
||||
isPasswordPolicyEnabled() {
|
||||
return typeof this.config.passwordPolicy === 'object'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Create a new share link and append it to the list
|
||||
*/
|
||||
async onNewLinkShare() {
|
||||
const shareDefaults = {
|
||||
share_type: OC.Share.SHARE_TYPE_LINK
|
||||
}
|
||||
if (this.config.isDefaultExpireDateEnforced) {
|
||||
// default is empty string if not set
|
||||
// expiration is the share object key, not expireDate
|
||||
shareDefaults.expiration = this.config.defaultExpirationDateString
|
||||
}
|
||||
if (this.config.enableLinkPasswordByDefault) {
|
||||
shareDefaults.password = await this.generatePassword()
|
||||
}
|
||||
|
||||
// do not push yet if we need a password or an expiration date
|
||||
if (this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced) {
|
||||
this.loading = true
|
||||
// if a share already exists, pushing it
|
||||
if (this.share && !this.share.id) {
|
||||
if (this.checkShare(this.share)) {
|
||||
await this.pushNewLinkShare(this.share, true)
|
||||
return true
|
||||
} else {
|
||||
this.open = true
|
||||
OC.Notification.showTemporary(t('files_sharing', 'Error, please enter proper password and/or expiration date'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ELSE, show the pending popovermenu
|
||||
// if password enforced, pre-fill with random one
|
||||
if (this.config.enforcePasswordForPublicLink) {
|
||||
shareDefaults.password = await this.generatePassword()
|
||||
}
|
||||
|
||||
// create share & close menu
|
||||
const share = new Share(shareDefaults)
|
||||
const component = await new Promise(resolve => {
|
||||
this.$emit('add:share', share, resolve)
|
||||
})
|
||||
|
||||
// open the menu on the
|
||||
// freshly created share component
|
||||
this.open = false
|
||||
this.loading = false
|
||||
component.open = true
|
||||
|
||||
// Nothing enforced, creating share directly
|
||||
} else {
|
||||
const share = new Share(shareDefaults)
|
||||
await this.pushNewLinkShare(share)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Push a new link share to the server
|
||||
* And update or append to the list
|
||||
* accordingly
|
||||
*
|
||||
* @param {Share} share the new share
|
||||
* @param {boolean} [update=false] do we update the current share ?
|
||||
*/
|
||||
async pushNewLinkShare(share, update) {
|
||||
try {
|
||||
this.loading = true
|
||||
this.errors = {}
|
||||
|
||||
const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
|
||||
const newShare = await this.createShare({
|
||||
path,
|
||||
shareType: OC.Share.SHARE_TYPE_LINK,
|
||||
password: share.password,
|
||||
expireDate: share.expireDate
|
||||
// we do not allow setting the publicUpload
|
||||
// before the share creation.
|
||||
// Todo: We also need to fix the createShare method in
|
||||
// lib/Controller/ShareAPIController.php to allow file drop
|
||||
// (currently not supported on create, only update)
|
||||
})
|
||||
|
||||
this.open = false
|
||||
|
||||
console.debug('Link share created', newShare)
|
||||
|
||||
// if share already exists, copy link directly on next tick
|
||||
let component
|
||||
if (update) {
|
||||
component = await new Promise(resolve => {
|
||||
this.$emit('update:share', newShare, resolve)
|
||||
})
|
||||
} else {
|
||||
// adding new share to the array and copying link to clipboard
|
||||
// using promise so that we can copy link in the same click function
|
||||
// and avoid firefox copy permissions issue
|
||||
component = await new Promise(resolve => {
|
||||
this.$emit('add:share', newShare, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
// Execute the copy link method
|
||||
// freshly created share component
|
||||
// ! somehow does not works on firefox !
|
||||
component.copyLink()
|
||||
|
||||
} catch ({ response }) {
|
||||
const message = response.data.ocs.meta.message
|
||||
if (message.match(/password/i)) {
|
||||
this.onSyncError('password', message)
|
||||
} else if (message.match(/date/i)) {
|
||||
this.onSyncError('expireDate', message)
|
||||
} else {
|
||||
this.onSyncError('pending', message)
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On permissions change
|
||||
* @param {Event} event js event
|
||||
*/
|
||||
togglePermissions(event) {
|
||||
const permissions = parseInt(event.target.value, 10)
|
||||
this.share.permissions = permissions
|
||||
this.queueUpdate('permissions')
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a valid policy password or
|
||||
* request a valid password if password_policy
|
||||
* is enabled
|
||||
*
|
||||
* @returns {string} a valid password
|
||||
*/
|
||||
async generatePassword() {
|
||||
// password policy is enabled, let's request a pass
|
||||
if (this.config.passwordPolicy.api && this.config.passwordPolicy.api.generate) {
|
||||
try {
|
||||
const request = await axios.get(this.config.passwordPolicy.api.generate)
|
||||
if (request.data.ocs.data.password) {
|
||||
return request.data.ocs.data.password
|
||||
}
|
||||
} catch (error) {
|
||||
console.info('Error generating password from password_policy', error)
|
||||
}
|
||||
}
|
||||
|
||||
// generate password of 10 length based on passwordSet
|
||||
return Array(10).fill(0)
|
||||
.reduce((prev, curr) => {
|
||||
prev += passwordSet.charAt(Math.floor(Math.random() * passwordSet.length))
|
||||
return prev
|
||||
}, '')
|
||||
},
|
||||
|
||||
async copyLink() {
|
||||
try {
|
||||
await this.$copyText(this.shareLink)
|
||||
// focus and show the tooltip
|
||||
this.$refs.copyButton.$el.focus()
|
||||
this.copySuccess = true
|
||||
this.copied = true
|
||||
} catch (error) {
|
||||
this.copySuccess = false
|
||||
this.copied = true
|
||||
console.error(error)
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
this.copySuccess = false
|
||||
this.copied = false
|
||||
}, 4000)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update newPassword values
|
||||
* of share. If password is set but not newPassword
|
||||
* then the user did not changed the password
|
||||
* If both co-exists, the password have changed and
|
||||
* we show it in plain text.
|
||||
* Then on submit (or menu close), we sync it.
|
||||
* @param {string} password the changed password
|
||||
*/
|
||||
onPasswordChange(password) {
|
||||
this.$set(this.share, 'newPassword', password)
|
||||
},
|
||||
|
||||
/**
|
||||
* Uncheck password protection
|
||||
* We need this method because @update:checked
|
||||
* is ran simultaneously as @uncheck, so
|
||||
* so we cannot ensure data is up-to-date
|
||||
*/
|
||||
onPasswordDisable() {
|
||||
this.share.password = ''
|
||||
|
||||
// reset password state after sync
|
||||
this.$delete(this.share, 'newPassword')
|
||||
|
||||
// only update if valid share.
|
||||
if (this.share.id) {
|
||||
this.queueUpdate('password')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Menu have been closed or password has been submited.
|
||||
* The only property that does not get
|
||||
* synced automatically is the password
|
||||
* So let's check if we have an unsaved
|
||||
* password.
|
||||
* expireDate is saved on datepicker pick
|
||||
* or close.
|
||||
*/
|
||||
onPasswordSubmit() {
|
||||
if (this.hasUnsavedPassword) {
|
||||
this.share.password = this.share.newPassword
|
||||
this.queueUpdate('password')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the share creation
|
||||
* Used in the pending popover
|
||||
*/
|
||||
onCancel() {
|
||||
// this.share already exists at this point,
|
||||
// but is incomplete as not pushed to server
|
||||
// YET. We can safely delete the share :)
|
||||
this.$emit('remove:share', this.share)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sharing-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
&__desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
&:not(.sharing-entry--share) &__actions {
|
||||
.new-share-link {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.sharing-entry__action--public-upload {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
&__loading {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
// put menus to the left
|
||||
// but only the first one
|
||||
.action-item {
|
||||
margin-left: auto;
|
||||
~ .action-item,
|
||||
~ .sharing-entry__loading {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-checkmark-color {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,97 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<li class="sharing-entry">
|
||||
<slot name="avatar" />
|
||||
<div v-tooltip="tooltip" class="sharing-entry__desc">
|
||||
<h5>{{ title }}</h5>
|
||||
<p v-if="subtitle">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
<Actions v-if="$slots['default']" menu-align="right" class="sharing-entry__actions">
|
||||
<slot />
|
||||
</Actions>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Actions from 'nextcloud-vue/dist/Components/Actions'
|
||||
import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
|
||||
|
||||
export default {
|
||||
name: 'SharingEntrySimple',
|
||||
|
||||
components: {
|
||||
Actions
|
||||
},
|
||||
|
||||
directives: {
|
||||
Tooltip
|
||||
},
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
tooltip: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sharing-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
&__desc {
|
||||
padding: 8px;
|
||||
line-height: 1.2em;
|
||||
position: relative;
|
||||
flex: 1 1;
|
||||
min-width: 0;
|
||||
h5 {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: inherit;
|
||||
}
|
||||
p {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,444 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<Multiselect ref="multiselect"
|
||||
class="sharing-input"
|
||||
:disabled="!canReshare"
|
||||
:hide-selected="true"
|
||||
:internal-search="false"
|
||||
:loading="loading"
|
||||
:options="options"
|
||||
:placeholder="inputPlaceholder"
|
||||
:preselect-first="true"
|
||||
:preserve-search="true"
|
||||
:searchable="true"
|
||||
:user-select="true"
|
||||
@search-change="asyncFind"
|
||||
@select="addShare">
|
||||
<template #noOptions>
|
||||
{{ t('files_sharing', 'No recommendations. Start typing.') }}
|
||||
</template>
|
||||
<template #noResult>
|
||||
{{ noResultText }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import Multiselect from 'nextcloud-vue/dist/Components/Multiselect'
|
||||
|
||||
import Config from '../services/ConfigService'
|
||||
import Share from '../models/Share'
|
||||
import ShareRequests from '../mixins/ShareRequests'
|
||||
import ShareTypes from '../mixins/ShareTypes'
|
||||
|
||||
export default {
|
||||
name: 'SharingInput',
|
||||
|
||||
components: {
|
||||
Multiselect
|
||||
},
|
||||
|
||||
mixins: [ShareTypes, ShareRequests],
|
||||
|
||||
props: {
|
||||
shares: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true
|
||||
},
|
||||
linkShares: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true
|
||||
},
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
},
|
||||
reshare: {
|
||||
type: Share,
|
||||
default: null
|
||||
},
|
||||
canReshare: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: new Config(),
|
||||
loading: false,
|
||||
query: '',
|
||||
recommendations: [],
|
||||
ShareSearch: OCA.Sharing.ShareSearch.state,
|
||||
suggestions: []
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Implement ShareSearch
|
||||
* allows external appas to inject new
|
||||
* results into the autocomplete dropdown
|
||||
* Used for the guests app
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
externalResults() {
|
||||
return this.ShareSearch.results
|
||||
},
|
||||
inputPlaceholder() {
|
||||
const allowRemoteSharing = this.config.isRemoteShareAllowed
|
||||
const allowMailSharing = this.config.isMailShareAllowed
|
||||
|
||||
if (!this.canReshare) {
|
||||
return t('files_sharing', 'Resharing is not allowed')
|
||||
}
|
||||
if (!allowRemoteSharing && allowMailSharing) {
|
||||
return t('files_sharing', 'Name or email address...')
|
||||
}
|
||||
if (allowRemoteSharing && !allowMailSharing) {
|
||||
return t('files_sharing', 'Name or federated cloud ID...')
|
||||
}
|
||||
if (allowRemoteSharing && allowMailSharing) {
|
||||
return t('files_sharing', 'Name, federated cloud ID or email address...')
|
||||
}
|
||||
|
||||
return t('files_sharing', 'Name...')
|
||||
},
|
||||
|
||||
isValidQuery() {
|
||||
return this.query && this.query.trim() !== '' && this.query.length > this.config.minSearchStringLength
|
||||
},
|
||||
|
||||
options() {
|
||||
if (this.isValidQuery) {
|
||||
return this.suggestions
|
||||
}
|
||||
return this.recommendations
|
||||
},
|
||||
|
||||
noResultText() {
|
||||
if (this.loading) {
|
||||
return t('files_sharing', 'Searching...')
|
||||
}
|
||||
return t('files_sharing', 'No elements found.')
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.getRecommendations()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async asyncFind(query, id) {
|
||||
// save current query to check if we display
|
||||
// recommendations or search results
|
||||
this.query = query.trim()
|
||||
if (this.isValidQuery) {
|
||||
// start loading now to have proper ux feedback
|
||||
// during the debounce
|
||||
this.loading = true
|
||||
await this.debounceGetSuggestions(query)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get suggestions
|
||||
*
|
||||
* @param {string} search the search query
|
||||
* @param {boolean} [lookup=false] search on lookup server
|
||||
*/
|
||||
async getSuggestions(search, lookup) {
|
||||
this.loading = true
|
||||
lookup = lookup || false
|
||||
console.info(search, lookup)
|
||||
|
||||
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', {
|
||||
params: {
|
||||
format: 'json',
|
||||
itemType: this.fileInfo.type === 'dir' ? 'folder' : 'file',
|
||||
search,
|
||||
lookup,
|
||||
perPage: this.config.maxAutocompleteResults
|
||||
}
|
||||
})
|
||||
|
||||
if (request.data.ocs.meta.statuscode !== 100) {
|
||||
console.error('Error fetching suggestions', request)
|
||||
return
|
||||
}
|
||||
|
||||
const data = request.data.ocs.data
|
||||
const exact = request.data.ocs.data.exact
|
||||
data.exact = [] // removing exact from general results
|
||||
|
||||
// flatten array of arrays
|
||||
const rawExactSuggestions = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
|
||||
const rawSuggestions = Object.values(data).reduce((arr, elem) => arr.concat(elem), [])
|
||||
|
||||
// remove invalid data and format to user-select layout
|
||||
const exactSuggestions = this.filterOutExistingShares(rawExactSuggestions)
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
const suggestions = this.filterOutExistingShares(rawSuggestions)
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
|
||||
// lookup clickable entry
|
||||
const lookupEntry = []
|
||||
if (data.lookupEnabled) {
|
||||
lookupEntry.push({
|
||||
isNoUser: true,
|
||||
displayName: t('files_sharing', 'Search globally'),
|
||||
lookup: true
|
||||
})
|
||||
}
|
||||
|
||||
// if there is a condition specified, filter it
|
||||
const externalResults = this.externalResults.filter(result => !result.condition || result.condition(this))
|
||||
|
||||
this.suggestions = exactSuggestions.concat(suggestions).concat(externalResults).concat(lookupEntry)
|
||||
|
||||
this.loading = false
|
||||
console.info('suggestions', this.suggestions)
|
||||
},
|
||||
|
||||
/**
|
||||
* Debounce getSuggestions
|
||||
*
|
||||
* @param {...*} args the arguments
|
||||
*/
|
||||
debounceGetSuggestions: debounce(function(...args) {
|
||||
this.getSuggestions(...args)
|
||||
}, 300),
|
||||
|
||||
/**
|
||||
* Get the sharing recommendations
|
||||
*/
|
||||
async getRecommendations() {
|
||||
this.loading = true
|
||||
|
||||
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees_recommended', {
|
||||
params: {
|
||||
format: 'json',
|
||||
itemType: this.fileInfo.type
|
||||
}
|
||||
})
|
||||
|
||||
if (request.data.ocs.meta.statuscode !== 100) {
|
||||
console.error('Error fetching recommendations', request)
|
||||
return
|
||||
}
|
||||
|
||||
const exact = request.data.ocs.data.exact
|
||||
|
||||
// flatten array of arrays
|
||||
const rawRecommendations = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
|
||||
|
||||
// remove invalid data and format to user-select layout
|
||||
this.recommendations = this.filterOutExistingShares(rawRecommendations)
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
|
||||
this.loading = false
|
||||
console.info('recommendations', this.recommendations)
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter out existing shares from
|
||||
* the provided shares search results
|
||||
*
|
||||
* @param {Object[]} shares the array of shares object
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
filterOutExistingShares(shares) {
|
||||
return shares.reduce((arr, share) => {
|
||||
// only check proper objects
|
||||
if (typeof share !== 'object') {
|
||||
return arr
|
||||
}
|
||||
try {
|
||||
// filter out current user
|
||||
if (share.value.shareWith === getCurrentUser().uid) {
|
||||
return arr
|
||||
}
|
||||
|
||||
// filter out the owner of the share
|
||||
if (this.reshare && share.value.shareWith === this.reshare.owner) {
|
||||
return arr
|
||||
}
|
||||
|
||||
// filter out existing mail shares
|
||||
if (share.value.shareType === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
|
||||
const emails = this.linkShares.map(elem => elem.shareWith)
|
||||
if (emails.indexOf(share.value.shareWith.trim()) !== -1) {
|
||||
return arr
|
||||
}
|
||||
} else { // filter out existing shares
|
||||
// creating an object of uid => type
|
||||
const sharesObj = this.shares.reduce((obj, elem) => {
|
||||
obj[elem.shareWith] = elem.type
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
// if shareWith is the same and the share type too, ignore it
|
||||
const key = share.value.shareWith.trim()
|
||||
if (key in sharesObj
|
||||
&& sharesObj[key] === share.value.shareType) {
|
||||
return arr
|
||||
}
|
||||
}
|
||||
|
||||
// ALL GOOD
|
||||
// let's add the suggestion
|
||||
arr.push(share)
|
||||
} catch {
|
||||
return arr
|
||||
}
|
||||
return arr
|
||||
}, [])
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the icon based on the share type
|
||||
* @param {number} type the share type
|
||||
* @returns {string} the icon class
|
||||
*/
|
||||
shareTypeToIcon(type) {
|
||||
switch (type) {
|
||||
case this.SHARE_TYPES.SHARE_TYPE_GUEST:
|
||||
// default is a user, other icons are here to differenciate
|
||||
// themselves from it, so let's not display the user icon
|
||||
// case this.SHARE_TYPES.SHARE_TYPE_REMOTE:
|
||||
// case this.SHARE_TYPES.SHARE_TYPE_USER:
|
||||
return 'icon-user'
|
||||
case this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP:
|
||||
case this.SHARE_TYPES.SHARE_TYPE_GROUP:
|
||||
return 'icon-group'
|
||||
case this.SHARE_TYPES.SHARE_TYPE_EMAIL:
|
||||
return 'icon-mail'
|
||||
case this.SHARE_TYPES.SHARE_TYPE_CIRCLE:
|
||||
return 'icon-circle'
|
||||
case this.SHARE_TYPES.SHARE_TYPE_ROOM:
|
||||
return 'icon-room'
|
||||
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format shares for the multiselect options
|
||||
* @param {Object} result select entry item
|
||||
* @returns {Object}
|
||||
*/
|
||||
formatForMultiselect(result) {
|
||||
let desc
|
||||
if ((result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_REMOTE
|
||||
|| result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP
|
||||
) && result.value.server) {
|
||||
desc = t('files_sharing', 'on {server}', { server: result.value.server })
|
||||
} else if (result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
|
||||
desc = result.value.shareWith
|
||||
}
|
||||
|
||||
return {
|
||||
shareWith: result.value.shareWith,
|
||||
shareType: result.value.shareType,
|
||||
user: result.uuid || result.value.shareWith,
|
||||
isNoUser: !result.uuid,
|
||||
displayName: result.name || result.label,
|
||||
desc,
|
||||
icon: this.shareTypeToIcon(result.value.shareType)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Process the new share request
|
||||
* @param {Object} value the multiselect option
|
||||
*/
|
||||
async addShare(value) {
|
||||
if (value.lookup) {
|
||||
return this.getSuggestions(this.query, true)
|
||||
}
|
||||
|
||||
// handle externalResults from OCA.Sharing.ShareSearch
|
||||
if (value.handler) {
|
||||
const share = await value.handler(this)
|
||||
this.$emit('add:share', new Share(share))
|
||||
return true
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
|
||||
const share = await this.createShare({
|
||||
path,
|
||||
shareType: value.shareType,
|
||||
shareWith: value.shareWith
|
||||
})
|
||||
this.$emit('add:share', share)
|
||||
|
||||
this.getRecommendations()
|
||||
|
||||
} catch (response) {
|
||||
// focus back if any error
|
||||
const input = this.$refs.multiselect.$el.querySelector('input')
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
this.query = value.shareWith
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sharing-input {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
|
||||
// properly style the lookup entry
|
||||
.multiselect__option {
|
||||
span[lookup] {
|
||||
.avatardiv {
|
||||
background-image: var(--icon-search-fff);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: var(--color-text-maxcontrast) !important;
|
||||
div {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* @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 SharingTab from './views/SharingTab'
|
||||
import ShareSearch from './services/ShareSearch'
|
||||
import ExternalLinkActions from './services/ExternalLinkActions'
|
||||
|
||||
import TabSections from './services/TabSections'
|
||||
|
||||
if (window.OCA && window.OCA.Sharing) {
|
||||
Object.assign(window.OCA.Sharing, { ShareSearch: new ShareSearch() })
|
||||
}
|
||||
|
||||
if (window.OCA && window.OCA.Sharing) {
|
||||
Object.assign(window.OCA.Sharing, { ExternalLinkActions: new ExternalLinkActions() })
|
||||
}
|
||||
|
||||
Object.assign(window.OCA.Sharing, { ShareTabSections: new TabSections() })
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
if (OCA.Files && OCA.Files.Sidebar) {
|
||||
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab('sharing', SharingTab))
|
||||
}
|
||||
})
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// TODO: remove when ie not supported
|
||||
import 'url-search-params-polyfill'
|
||||
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import Share from '../models/Share'
|
||||
|
||||
const shareUrl = generateOcsUrl('apps/files_sharing/api/v1', 2) + 'shares'
|
||||
const headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
}
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
/**
|
||||
* Create a new share
|
||||
*
|
||||
* @param {Object} data destructuring object
|
||||
* @param {string} data.path path to the file/folder which should be shared
|
||||
* @param {number} data.shareType 0 = user; 1 = group; 3 = public link; 6 = federated cloud share
|
||||
* @param {string} data.shareWith user/group id with which the file should be shared (optional for shareType > 1)
|
||||
* @param {boolean} [data.publicUpload=false] allow public upload to a public shared folder
|
||||
* @param {string} [data.password] password to protect public link Share with
|
||||
* @param {number} [data.permissions=31] 1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1)
|
||||
* @param {boolean} [data.sendPasswordByTalk=false] send the password via a talk conversation
|
||||
* @param {string} [data.expireDate=''] expire the shareautomatically after
|
||||
* @param {string} [data.label=''] custom label
|
||||
* @returns {Share} the new share
|
||||
* @throws {Error}
|
||||
*/
|
||||
async createShare({ path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label }) {
|
||||
try {
|
||||
const request = await axios.post(shareUrl, { path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label })
|
||||
if (!('ocs' in request.data)) {
|
||||
throw request
|
||||
}
|
||||
return new Share(request.data.ocs.data)
|
||||
} catch (error) {
|
||||
console.error('Error while creating share', error)
|
||||
OC.Notification.showTemporary(t('files_sharing', 'Error creating the share'), { type: 'error' })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a share
|
||||
*
|
||||
* @param {number} id share id
|
||||
* @throws {Error}
|
||||
*/
|
||||
async deleteShare(id) {
|
||||
try {
|
||||
const request = await axios.delete(shareUrl + `/${id}`)
|
||||
if (!('ocs' in request.data)) {
|
||||
throw request
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error while deleting share', error)
|
||||
OC.Notification.showTemporary(t('files_sharing', 'Error deleting the share'), { type: 'error' })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a share
|
||||
*
|
||||
* @param {number} id share id
|
||||
* @param {Object} data destructuring object
|
||||
* @param {string} data.property property to update
|
||||
* @param {any} data.value value to set
|
||||
*/
|
||||
async updateShare(id, { property, value }) {
|
||||
try {
|
||||
// ocs api requires x-www-form-urlencoded
|
||||
const data = new URLSearchParams()
|
||||
data.append(property, value)
|
||||
|
||||
const request = await axios.put(shareUrl + `/${id}`, { [property]: value }, headers)
|
||||
if (!('ocs' in request.data)) {
|
||||
throw request
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error while updating share', error)
|
||||
OC.Notification.showTemporary(t('files_sharing', 'Error updating the share'), { type: 'error' })
|
||||
const message = error.response.data.ocs.meta.message
|
||||
throw new Error(`${property}, ${message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
SHARE_TYPES: {
|
||||
SHARE_TYPE_USER: OC.Share.SHARE_TYPE_USER,
|
||||
SHARE_TYPE_GROUP: OC.Share.SHARE_TYPE_GROUP,
|
||||
SHARE_TYPE_LINK: OC.Share.SHARE_TYPE_LINK,
|
||||
SHARE_TYPE_EMAIL: OC.Share.SHARE_TYPE_EMAIL,
|
||||
SHARE_TYPE_REMOTE: OC.Share.SHARE_TYPE_REMOTE,
|
||||
SHARE_TYPE_CIRCLE: OC.Share.SHARE_TYPE_CIRCLE,
|
||||
SHARE_TYPE_GUEST: OC.Share.SHARE_TYPE_GUEST,
|
||||
SHARE_TYPE_REMOTE_GROUP: OC.Share.SHARE_TYPE_REMOTE_GROUP,
|
||||
SHARE_TYPE_ROOM: OC.Share.SHARE_TYPE_ROOM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
/**
|
||||
* @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 PQueue from 'p-queue'
|
||||
import debounce from 'debounce'
|
||||
|
||||
import Share from '../models/Share'
|
||||
import SharesRequests from './ShareRequests'
|
||||
import ShareTypes from './ShareTypes'
|
||||
import Config from '../services/ConfigService'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
export default {
|
||||
mixins: [SharesRequests, ShareTypes],
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
},
|
||||
share: {
|
||||
type: Share,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: new Config(),
|
||||
|
||||
// errors helpers
|
||||
errors: {},
|
||||
|
||||
// component status toggles
|
||||
loading: false,
|
||||
saving: false,
|
||||
open: false,
|
||||
|
||||
// concurrency management queue
|
||||
// we want one queue per share
|
||||
updateQueue: new PQueue({ concurrency: 1 }),
|
||||
|
||||
/**
|
||||
* ! This allow vue to make the Share class state reactive
|
||||
* ! do not remove it ot you'll lose all reactivity here
|
||||
*/
|
||||
reactiveState: this.share && this.share.state,
|
||||
|
||||
SHARE_TYPES: {
|
||||
SHARE_TYPE_USER: OC.Share.SHARE_TYPE_USER,
|
||||
SHARE_TYPE_GROUP: OC.Share.SHARE_TYPE_GROUP,
|
||||
SHARE_TYPE_LINK: OC.Share.SHARE_TYPE_LINK,
|
||||
SHARE_TYPE_EMAIL: OC.Share.SHARE_TYPE_EMAIL,
|
||||
SHARE_TYPE_REMOTE: OC.Share.SHARE_TYPE_REMOTE,
|
||||
SHARE_TYPE_CIRCLE: OC.Share.SHARE_TYPE_CIRCLE,
|
||||
SHARE_TYPE_GUEST: OC.Share.SHARE_TYPE_GUEST,
|
||||
SHARE_TYPE_REMOTE_GROUP: OC.Share.SHARE_TYPE_REMOTE_GROUP,
|
||||
SHARE_TYPE_ROOM: OC.Share.SHARE_TYPE_ROOM
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
/**
|
||||
* Does the current share have an expiration date
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasExpirationDate: {
|
||||
get: function() {
|
||||
return this.config.isDefaultExpireDateEnforced || !!this.share.expireDate
|
||||
},
|
||||
set: function(enabled) {
|
||||
this.share.expireDate = enabled
|
||||
? this.config.defaultExpirationDateString !== ''
|
||||
? this.config.defaultExpirationDateString
|
||||
: moment().format('YYYY-MM-DD')
|
||||
: ''
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Does the current share have a note
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasNote: {
|
||||
get: function() {
|
||||
return !!this.share.note
|
||||
},
|
||||
set: function(enabled) {
|
||||
this.share.note = enabled
|
||||
? t('files_sharing', 'Enter a note for the share recipient')
|
||||
: ''
|
||||
}
|
||||
},
|
||||
|
||||
dateTomorrow() {
|
||||
return moment().add(1, 'days')
|
||||
},
|
||||
|
||||
dateMaxEnforced() {
|
||||
return this.config.isDefaultExpireDateEnforced
|
||||
&& moment().add(1 + this.config.defaultExpireDate, 'days')
|
||||
},
|
||||
|
||||
/**
|
||||
* Datepicker lang values
|
||||
* https://github.com/nextcloud/nextcloud-vue/pull/146
|
||||
* TODO: have this in vue-components
|
||||
*
|
||||
* @returns {int}
|
||||
*/
|
||||
firstDay() {
|
||||
return window.firstDay
|
||||
? window.firstDay
|
||||
: 0 // sunday as default
|
||||
},
|
||||
lang() {
|
||||
// fallback to default in case of unavailable data
|
||||
return {
|
||||
days: window.dayNamesShort
|
||||
? window.dayNamesShort // provided by nextcloud
|
||||
: ['Sun.', 'Mon.', 'Tue.', 'Wed.', 'Thu.', 'Fri.', 'Sat.'],
|
||||
months: window.monthNamesShort
|
||||
? window.monthNamesShort // provided by nextcloud
|
||||
: ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'],
|
||||
placeholder: {
|
||||
date: 'Select Date' // TODO: Translate
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isShareOwner() {
|
||||
return this.share && this.share.owner === getCurrentUser().uid
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Check if a share is valid before
|
||||
* firing the request
|
||||
*
|
||||
* @param {Share} share the share to check
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
checkShare(share) {
|
||||
if (share.password) {
|
||||
if (typeof share.password !== 'string' || share.password.trim() === '') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (share.expirationDate) {
|
||||
const date = moment(share.expirationDate)
|
||||
if (!date.isValid()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
* ActionInput can be a little tricky to work with.
|
||||
* Since we expect a string and not a Date,
|
||||
* we need to process the value here
|
||||
*
|
||||
* @param {Date} date js date to be parsed by moment.js
|
||||
*/
|
||||
onExpirationChange(date) {
|
||||
// format to YYYY-MM-DD
|
||||
const value = moment(date).format('YYYY-MM-DD')
|
||||
this.share.expireDate = value
|
||||
this.queueUpdate('expireDate')
|
||||
},
|
||||
|
||||
/**
|
||||
* Uncheck expire date
|
||||
* We need this method because @update:checked
|
||||
* is ran simultaneously as @uncheck, so
|
||||
* so we cannot ensure data is up-to-date
|
||||
*/
|
||||
onExpirationDisable() {
|
||||
this.share.expireDate = ''
|
||||
this.queueUpdate('expireDate')
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete share button handler
|
||||
*/
|
||||
async onDelete() {
|
||||
try {
|
||||
this.loading = true
|
||||
this.open = false
|
||||
await this.deleteShare(this.share.id)
|
||||
console.debug('Share deleted', this.share.id)
|
||||
this.$emit('remove:share', this.share)
|
||||
} catch (error) {
|
||||
// re-open menu if error
|
||||
this.open = true
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send an update of the share to the queue
|
||||
*
|
||||
* @param {string} property the property to sync
|
||||
*/
|
||||
queueUpdate(property) {
|
||||
if (this.share.id) {
|
||||
// force value to string because that is what our
|
||||
// share api controller accepts
|
||||
const value = this.share[property].toString()
|
||||
|
||||
this.updateQueue.add(async() => {
|
||||
this.saving = true
|
||||
this.errors = {}
|
||||
try {
|
||||
await this.updateShare(this.share.id, {
|
||||
property,
|
||||
value
|
||||
})
|
||||
|
||||
// clear any previous errors
|
||||
this.$delete(this.errors, property)
|
||||
|
||||
// reset password state after sync
|
||||
this.$delete(this.share, 'newPassword')
|
||||
} catch ({ property, message }) {
|
||||
this.onSyncError(property, message)
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error('Cannot update share.', this.share, 'No valid id')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Manage sync errors
|
||||
* @param {string} property the errored property, e.g. 'password'
|
||||
* @param {string} message the error message
|
||||
*/
|
||||
onSyncError(property, message) {
|
||||
// re-open menu if closed
|
||||
this.open = true
|
||||
switch (property) {
|
||||
case 'password':
|
||||
case 'pending':
|
||||
case 'expireDate':
|
||||
case 'note': {
|
||||
// show error
|
||||
this.$set(this.errors, property, message)
|
||||
|
||||
let propertyEl = this.$refs[property]
|
||||
if (propertyEl) {
|
||||
if (propertyEl.$el) {
|
||||
propertyEl = propertyEl.$el
|
||||
}
|
||||
// focus if there is a focusable action element
|
||||
const focusable = propertyEl.querySelector('.focusable')
|
||||
if (focusable) {
|
||||
focusable.focus()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Debounce queueUpdate to avoid requests spamming
|
||||
* more importantly for text data
|
||||
*
|
||||
* @param {string} property the property to sync
|
||||
*/
|
||||
debounceQueueUpdate: debounce(function(property) {
|
||||
this.queueUpdate(property)
|
||||
}, 500)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,469 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class Share {
|
||||
|
||||
#share;
|
||||
|
||||
/**
|
||||
* Create the share object
|
||||
*
|
||||
* @param {Object} ocsData ocs request response
|
||||
*/
|
||||
constructor(ocsData) {
|
||||
if (ocsData.ocs && ocsData.ocs.data && ocsData.ocs.data[0]) {
|
||||
ocsData = ocsData.ocs.data[0]
|
||||
}
|
||||
|
||||
// convert int into boolean
|
||||
ocsData.hide_download = !!ocsData.hide_download
|
||||
ocsData.mail_send = !!ocsData.mail_send
|
||||
|
||||
// store state
|
||||
this.#share = ocsData
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the share state
|
||||
* ! used for reactivity purpose
|
||||
* Do not remove. It allow vuejs to
|
||||
* inject its watchers into the #share
|
||||
* state and make the whole class reactive
|
||||
*
|
||||
* @returns {Object} the share raw state
|
||||
* @readonly
|
||||
* @memberof Sidebar
|
||||
*/
|
||||
get state() {
|
||||
return this.#share
|
||||
}
|
||||
|
||||
/**
|
||||
* get the share id
|
||||
*
|
||||
* @returns {int}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get id() {
|
||||
return this.#share.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the share type
|
||||
*
|
||||
* @returns {int}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get type() {
|
||||
return this.#share.share_type
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the share permissions
|
||||
* See OC.PERMISSION_* variables
|
||||
*
|
||||
* @returns {int}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get permissions() {
|
||||
return this.#share.permissions
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the share permissions
|
||||
* See OC.PERMISSION_* variables
|
||||
*
|
||||
* @param {int} permissions valid permission, See OC.PERMISSION_* variables
|
||||
* @memberof Share
|
||||
*/
|
||||
set permissions(permissions) {
|
||||
this.#share.permissions = permissions
|
||||
}
|
||||
|
||||
// SHARE OWNER --------------------------------------------------
|
||||
/**
|
||||
* Get the share owner uid
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get owner() {
|
||||
return this.#share.uid_owner
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the share owner's display name
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get ownerDisplayName() {
|
||||
return this.#share.displayname_owner
|
||||
}
|
||||
|
||||
// SHARED WITH --------------------------------------------------
|
||||
/**
|
||||
* Get the share with entity uid
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get shareWith() {
|
||||
return this.#share.share_with
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the share with entity display name
|
||||
* fallback to its uid if none
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get shareWithDisplayName() {
|
||||
return this.#share.share_with_displayname
|
||||
|| this.#share.share_with
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the share with avatar if any
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get shareWithAvatar() {
|
||||
return this.#share.share_with_avatar
|
||||
}
|
||||
|
||||
// SHARED FILE OR FOLDER OWNER ----------------------------------
|
||||
/**
|
||||
* Get the shared item owner uid
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get uidFileOwner() {
|
||||
return this.#share.uid_file_owner
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared item display name
|
||||
* fallback to its uid if none
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get displaynameFileOwner() {
|
||||
return this.#share.displayname_file_owner
|
||||
|| this.#share.uid_file_owner
|
||||
}
|
||||
|
||||
// TIME DATA ----------------------------------------------------
|
||||
/**
|
||||
* Get the share creation timestamp
|
||||
*
|
||||
* @returns {int}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get createdTime() {
|
||||
return this.#share.stime
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expiration date as a string format
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get expireDate() {
|
||||
return this.#share.expiration
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the expiration date as a string format
|
||||
* e.g. YYYY-MM-DD
|
||||
*
|
||||
* @param {string} date the share expiration date
|
||||
* @memberof Share
|
||||
*/
|
||||
set expireDate(date) {
|
||||
this.#share.expiration = date
|
||||
}
|
||||
|
||||
// EXTRA DATA ---------------------------------------------------
|
||||
/**
|
||||
* Get the public share token
|
||||
*
|
||||
* @returns {string} the token
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get token() {
|
||||
return this.#share.token
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the share note if any
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get note() {
|
||||
return this.#share.note
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the share note if any
|
||||
*
|
||||
* @param {string} note the note
|
||||
* @memberof Share
|
||||
*/
|
||||
set note(note) {
|
||||
this.#share.note = note.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Have a mail been sent
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get mailSend() {
|
||||
return this.#share.mail_send === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the download button on public page
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get hideDownload() {
|
||||
return this.#share.hide_download === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the download button on public page
|
||||
*
|
||||
* @param {boolean} state hide the button ?
|
||||
* @memberof Share
|
||||
*/
|
||||
set hideDownload(state) {
|
||||
this.#share.hide_download = state === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Password protection of the share
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get password() {
|
||||
return this.#share.password
|
||||
}
|
||||
|
||||
/**
|
||||
* Password protection of the share
|
||||
*
|
||||
* @param {string} password the share password
|
||||
* @memberof Share
|
||||
*/
|
||||
set password(password) {
|
||||
this.#share.password = password.trim()
|
||||
}
|
||||
|
||||
// SHARED ITEM DATA ---------------------------------------------
|
||||
/**
|
||||
* Get the shared item absolute full path
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get path() {
|
||||
return this.#share.path
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the item type: file or folder
|
||||
*
|
||||
* @returns {string} 'folder' or 'file'
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get itemType() {
|
||||
return this.#share.item_type
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared item mimetype
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get mimetype() {
|
||||
return this.#share.mimetype
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared item id
|
||||
*
|
||||
* @returns {int}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get fileSource() {
|
||||
return this.#share.file_source
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target path on the receiving end
|
||||
* e.g the file /xxx/aaa will be shared in
|
||||
* the receiving root as /aaa, the fileTarget is /aaa
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get fileTarget() {
|
||||
return this.#share.file_target
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent folder id if any
|
||||
*
|
||||
* @returns {int}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get fileParent() {
|
||||
return this.#share.file_parent
|
||||
}
|
||||
|
||||
// PERMISSIONS Shortcuts
|
||||
/**
|
||||
* Does this share have CREATE permissions
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get hasCreatePermission() {
|
||||
return !!((this.permissions & OC.PERMISSION_CREATE))
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this share have DELETE permissions
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get hasDeletePermission() {
|
||||
return !!((this.permissions & OC.PERMISSION_DELETE))
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this share have UPDATE permissions
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get hasUpdatePermission() {
|
||||
return !!((this.permissions & OC.PERMISSION_UPDATE))
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this share have SHARE permissions
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get hasSharePermission() {
|
||||
return !!((this.permissions & OC.PERMISSION_SHARE))
|
||||
}
|
||||
|
||||
// PERMISSIONS Shortcuts for the CURRENT USER
|
||||
// ! the permissions above are the share settings,
|
||||
// ! meaning the permissions for the recipient
|
||||
/**
|
||||
* Can the current user EDIT this share ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get canEdit() {
|
||||
return this.#share.can_edit === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the current user DELETE this share ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
get canDelete() {
|
||||
return this.#share.can_delete === true
|
||||
}
|
||||
|
||||
// TODO: SORT THOSE PROPERTIES
|
||||
get label() {
|
||||
return this.#share.label
|
||||
}
|
||||
|
||||
get parent() {
|
||||
return this.#share.parent
|
||||
}
|
||||
|
||||
get storageId() {
|
||||
return this.#share.storage_id
|
||||
}
|
||||
|
||||
get storage() {
|
||||
return this.#share.storage
|
||||
}
|
||||
|
||||
get itemSource() {
|
||||
return this.#share.item_source
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class Config {
|
||||
|
||||
/**
|
||||
* Is public upload allowed on link shares ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isPublicUploadEnabled() {
|
||||
return document.getElementById('filestable')
|
||||
&& document.getElementById('filestable').dataset.allowPublicUpload === 'yes'
|
||||
}
|
||||
|
||||
/**
|
||||
* Are link share allowed ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isShareWithLinkAllowed() {
|
||||
return document.getElementById('allowShareWithLink')
|
||||
&& document.getElementById('allowShareWithLink').value === 'yes'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the federated sharing documentation link
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get federatedShareDocLink() {
|
||||
return OC.appConfig.core.federatedCloudShareDoc
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default expiration date as string
|
||||
*
|
||||
* @returns {string}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get defaultExpirationDateString() {
|
||||
let expireDateString = ''
|
||||
if (this.isDefaultExpireDateEnabled) {
|
||||
const date = window.moment.utc()
|
||||
const expireAfterDays = this.defaultExpireDate
|
||||
date.add(expireAfterDays, 'days')
|
||||
expireDateString = date.format('YYYY-MM-DD')
|
||||
}
|
||||
return expireDateString
|
||||
}
|
||||
|
||||
/**
|
||||
* Are link shares password-enforced ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get enforcePasswordForPublicLink() {
|
||||
return OC.appConfig.core.enforcePasswordForPublicLink === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Is password asked by default on link shares ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get enableLinkPasswordByDefault() {
|
||||
return OC.appConfig.core.enableLinkPasswordByDefault === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Is link shares expiration enforced ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isDefaultExpireDateEnforced() {
|
||||
return OC.appConfig.core.defaultExpireDateEnforced === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there a default expiration date for new link shares ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isDefaultExpireDateEnabled() {
|
||||
return OC.appConfig.core.defaultExpireDateEnabled === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Are users on this server allowed to send shares to other servers ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isRemoteShareAllowed() {
|
||||
return OC.appConfig.core.remoteShareAllowed === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Is sharing my mail (link share) enabled ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isMailShareAllowed() {
|
||||
return OC.appConfig.shareByMailEnabled !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default days to expiration
|
||||
*
|
||||
* @returns {int}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get defaultExpireDate() {
|
||||
return OC.appConfig.core.defaultExpireDate
|
||||
}
|
||||
|
||||
/**
|
||||
* Is resharing allowed ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isResharingAllowed() {
|
||||
return OC.appConfig.core.resharingAllowed === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Is password enforced for mail shares ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isPasswordForMailSharesRequired() {
|
||||
return (OC.appConfig.shareByMail === undefined) ? false : OC.appConfig.shareByMail.enforcePasswordProtection === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Is sharing with groups allowed ?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get allowGroupSharing() {
|
||||
return OC.appConfig.core.allowGroupSharing === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum results of a share search
|
||||
*
|
||||
* @returns {int}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get maxAutocompleteResults() {
|
||||
return parseInt(OC.config['sharing.maxAutocompleteResults'], 10) || 200
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimal string length
|
||||
* to initiate a share search
|
||||
*
|
||||
* @returns {int}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get minSearchStringLength() {
|
||||
return parseInt(OC.config['sharing.minSearchStringLength'], 10) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password policy config
|
||||
*
|
||||
* @returns {Object}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get passwordPolicy() {
|
||||
const capabilities = OC.getCapabilities()
|
||||
return capabilities.password_policy ? capabilities.password_policy : {}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class ExternalLinkActions {
|
||||
|
||||
#state;
|
||||
|
||||
constructor() {
|
||||
// init empty state
|
||||
this.#state = {}
|
||||
|
||||
// init default values
|
||||
this.#state.actions = []
|
||||
console.debug('OCA.Sharing.ExternalLinkActions initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state
|
||||
*
|
||||
* @readonly
|
||||
* @memberof ExternalLinkActions
|
||||
* @returns {Object} the data state
|
||||
*/
|
||||
get state() {
|
||||
return this.#state
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new action for the link share
|
||||
* Mostly used by the social sharing app.
|
||||
*
|
||||
* @param {Object} action new action component to register
|
||||
* @returns {boolean}
|
||||
*/
|
||||
registerAction(action) {
|
||||
if (typeof action === 'object' && action.icon && action.name && action.url) {
|
||||
this.#state.actions.push(action)
|
||||
return true
|
||||
}
|
||||
console.error(`Invalid action provided`, action)
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class ShareSearch {
|
||||
|
||||
#state;
|
||||
|
||||
constructor() {
|
||||
// init empty state
|
||||
this.#state = {}
|
||||
|
||||
// init default values
|
||||
this.#state.results = []
|
||||
console.debug('OCA.Sharing.ShareSearch initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state
|
||||
*
|
||||
* @readonly
|
||||
* @memberof ShareSearch
|
||||
* @returns {Object} the data state
|
||||
*/
|
||||
get state() {
|
||||
return this.#state
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new result
|
||||
* Mostly used by the guests app.
|
||||
* We should consider deprecation and add results via php ?
|
||||
*
|
||||
* @param {Object} result entry to append
|
||||
* @param {string} [result.user] entry user
|
||||
* @param {string} result.displayName entry first line
|
||||
* @param {string} [result.desc] entry second line
|
||||
* @param {string} [result.icon] entry icon
|
||||
* @param {function} result.handler function to run on entry selection
|
||||
* @param {function} [result.condition] condition to add entry or not
|
||||
* @returns {boolean}
|
||||
*/
|
||||
addNewResult(result) {
|
||||
if (result.displayName.trim() !== ''
|
||||
&& typeof result.handler === 'function') {
|
||||
this.#state.results.push(result)
|
||||
return true
|
||||
}
|
||||
console.error(`Invalid search result provided`, result)
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Callback for adding two numbers.
|
||||
*
|
||||
* @callback registerSectionCallback
|
||||
* @param {Element} el The DOM element where the section is rendered
|
||||
* @param {FileInfo} fileInfo current file FileInfo
|
||||
*/
|
||||
export default class TabSections {
|
||||
|
||||
#sections;
|
||||
|
||||
constructor() {
|
||||
this.#sections = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {registerSectionCallback} section To be called to mount the section to the sharing sidebar
|
||||
*/
|
||||
registerSection(section) {
|
||||
this.#sections.push(section)
|
||||
}
|
||||
|
||||
getSections() {
|
||||
return this.#sections
|
||||
}
|
||||
|
||||
}
|
|
@ -195,7 +195,7 @@
|
|||
// do not open sidebar if permission is set and equal to 0
|
||||
var permissions = parseInt(context.$file.data('share-permissions'), 10)
|
||||
if (isNaN(permissions) || permissions > 0) {
|
||||
fileList.showDetailsView(fileName, 'shareTabView')
|
||||
fileList.showDetailsView(fileName, 'sharing')
|
||||
}
|
||||
},
|
||||
render: function(actionSpec, isDefault, context) {
|
||||
|
@ -209,37 +209,8 @@
|
|||
}
|
||||
})
|
||||
|
||||
var shareTab = new OCA.Sharing.ShareTabView('shareTabView', { order: -20 })
|
||||
// detect changes and change the matching list entry
|
||||
shareTab.on('sharesChanged', function(shareModel) {
|
||||
var fileInfoModel = shareModel.fileInfoModel
|
||||
var $tr = fileList.findFileEl(fileInfoModel.get('name'))
|
||||
|
||||
// We count email shares as link share
|
||||
var hasLinkShares = shareModel.hasLinkShares()
|
||||
shareModel.get('shares').forEach(function(share) {
|
||||
if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
|
||||
hasLinkShares = true
|
||||
}
|
||||
})
|
||||
|
||||
OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel)
|
||||
if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) {
|
||||
// remove icon, if applicable
|
||||
OC.Share.markFileAsShared($tr, false, false)
|
||||
}
|
||||
|
||||
// FIXME: this is too convoluted. We need to get rid of the above updates
|
||||
// and only ever update the model and let the events take care of rerendering
|
||||
fileInfoModel.set({
|
||||
shareTypes: shareModel.getShareTypes(),
|
||||
// in case markFileAsShared decided to change the icon,
|
||||
// we need to modify the model
|
||||
// (FIXME: yes, this is hacky)
|
||||
icon: $tr.attr('data-icon')
|
||||
})
|
||||
})
|
||||
fileList.registerTabView(shareTab)
|
||||
// register share breadcrumbs component
|
||||
var shareTab = new OCA.Sharing.ShareTabView('sharing', {order: -20})
|
||||
|
||||
var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({ shareTab: shareTab })
|
||||
fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView)
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
dirInfo: self._dirInfo
|
||||
})
|
||||
})
|
||||
OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'shareTabView')
|
||||
OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'sharing')
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the shared with me title
|
||||
*
|
||||
* @param {Share} share current share
|
||||
* @returns {string} the title
|
||||
*/
|
||||
const shareWithTitle = function(share) {
|
||||
if (share.type === OC.Share.type_GROUP) {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you and the group {group} by {owner}',
|
||||
{
|
||||
group: share.shareWithDisplayName,
|
||||
owner: share.ownerDisplayName
|
||||
},
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
} else if (share.type === OC.Share.type_CIRCLE) {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you and {circle} by {owner}',
|
||||
{
|
||||
circle: share.shareWithDisplayName,
|
||||
owner: share.ownerDisplayName
|
||||
},
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
} else if (share.type === OC.Share.type_ROOM) {
|
||||
if (this.model.get('reshare').share_with_displayname) {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you and the conversation {conversation} by {owner}',
|
||||
{
|
||||
conversation: share.shareWithDisplayName,
|
||||
owner: share.ownerDisplayName
|
||||
},
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
} else {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you in a conversation by {owner}',
|
||||
{
|
||||
owner: share.ownerDisplayName
|
||||
},
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you by {owner}',
|
||||
{ owner: share.ownerDisplayName },
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export { shareWithTitle }
|
|
@ -0,0 +1,141 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ul class="sharing-link-list">
|
||||
<!-- If no link shares, show the add link default entry -->
|
||||
<SharingEntryLink v-if="!hasLinkShares && canReshare"
|
||||
:can-reshare="canReshare"
|
||||
:file-info="fileInfo"
|
||||
@add:share="addShare" />
|
||||
|
||||
<!-- Else we display the list -->
|
||||
<template v-if="hasShares">
|
||||
<!-- using shares[index] to work with .sync -->
|
||||
<SharingEntryLink v-for="(share, index) in shares"
|
||||
:key="share.id"
|
||||
:can-reshare="canReshare"
|
||||
:share.sync="shares[index]"
|
||||
:file-info="fileInfo"
|
||||
@add:share="addShare(...arguments)"
|
||||
@update:share="awaitForShare(...arguments)"
|
||||
@remove:share="removeShare" />
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Share from '../models/Share'
|
||||
import ShareTypes from '../mixins/ShareTypes'
|
||||
import SharingEntryLink from '../components/SharingEntryLink'
|
||||
|
||||
export default {
|
||||
name: 'SharingLinkList',
|
||||
|
||||
components: {
|
||||
SharingEntryLink
|
||||
},
|
||||
|
||||
mixins: [ShareTypes],
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
},
|
||||
shares: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true
|
||||
},
|
||||
canReshare: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Do we have link shares?
|
||||
* Using this to still show the `new link share`
|
||||
* button regardless of mail shares
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
hasLinkShares() {
|
||||
return this.shares.filter(share => share.type === this.SHARE_TYPES.SHARE_TYPE_LINK).length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Do we have any link or email shares?
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasShares() {
|
||||
return this.shares.length > 0
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Add a new share into the link shares list
|
||||
* and return the newly created share component
|
||||
*
|
||||
* @param {Share} share the share to add to the array
|
||||
* @param {Function} resolve a function to run after the share is added and its component initialized
|
||||
*/
|
||||
addShare(share, resolve) {
|
||||
this.shares.unshift(share)
|
||||
this.awaitForShare(share, resolve)
|
||||
},
|
||||
|
||||
/**
|
||||
* Await for next tick and render after the list updated
|
||||
* Then resolve with the matched vue component of the
|
||||
* provided share object
|
||||
*
|
||||
* @param {Share} share newly created share
|
||||
* @param {Function} resolve a function to execute after
|
||||
*/
|
||||
awaitForShare(share, resolve) {
|
||||
this.$nextTick(() => {
|
||||
const newShare = this.$children.find(component => component.share === share)
|
||||
if (newShare) {
|
||||
resolve(newShare)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a share from the shares list
|
||||
*
|
||||
* @param {Share} share the share to remove
|
||||
*/
|
||||
removeShare(share) {
|
||||
const index = this.shares.findIndex(item => item === share)
|
||||
this.shares.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,76 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ul class="sharing-sharee-list">
|
||||
<SharingEntry v-for="share in shares"
|
||||
:key="share.id"
|
||||
:file-info="fileInfo"
|
||||
:share="share"
|
||||
@remove:share="removeShare" />
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Share from '../models/Share'
|
||||
import SharingEntry from '../components/SharingEntry'
|
||||
|
||||
export default {
|
||||
name: 'SharingList',
|
||||
|
||||
components: {
|
||||
SharingEntry
|
||||
},
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
},
|
||||
shares: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasShares() {
|
||||
return this.shares.length === 0
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Remove a share from the shares list
|
||||
*
|
||||
* @param {Share} share the share to remove
|
||||
*/
|
||||
removeShare(share) {
|
||||
const index = this.shares.findIndex(item => item === share)
|
||||
this.shares.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,334 @@
|
|||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<Tab :icon="icon" :name="name" :class="{ 'icon-loading': loading }">
|
||||
<!-- error message -->
|
||||
<div v-if="error" class="emptycontent">
|
||||
<div class="icon icon-error" />
|
||||
<h2>{{ error }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- shares content -->
|
||||
<template v-else>
|
||||
<!-- shared with me information -->
|
||||
<SharingEntrySimple v-if="isSharedWithMe" v-bind="sharedWithMe" class="sharing-entry__reshare">
|
||||
<template #avatar>
|
||||
<Avatar #avatar
|
||||
:user="sharedWithMe.user"
|
||||
:display-name="sharedWithMe.displayName"
|
||||
class="sharing-entry__avatar"
|
||||
tooltip-message="" />
|
||||
</template>
|
||||
</SharingEntrySimple>
|
||||
|
||||
<!-- add new share input -->
|
||||
<SharingInput v-if="!loading"
|
||||
:can-reshare="canReshare"
|
||||
:file-info="fileInfo"
|
||||
:link-shares="linkShares"
|
||||
:reshare="reshare"
|
||||
:shares="shares"
|
||||
@add:share="addShare" />
|
||||
|
||||
<!-- link shares list -->
|
||||
<SharingLinkList v-if="!loading"
|
||||
:can-reshare="canReshare"
|
||||
:file-info="fileInfo"
|
||||
:shares="linkShares" />
|
||||
|
||||
<!-- other shares list -->
|
||||
<SharingList v-if="!loading"
|
||||
:shares="shares"
|
||||
:file-info="fileInfo" />
|
||||
|
||||
<!-- internal link copy -->
|
||||
<SharingEntryInternal :file-info="fileInfo" />
|
||||
|
||||
<!-- projects -->
|
||||
<CollectionList v-if="fileInfo"
|
||||
:id="`${fileInfo.id}`"
|
||||
type="file"
|
||||
:name="fileInfo.name" />
|
||||
|
||||
<!-- additionnal entries, use it with cautious -->
|
||||
<div v-for="(section, index) in sections"
|
||||
:ref="'section-' + index"
|
||||
:key="index"
|
||||
class="sharingTab__additionalContent">
|
||||
<component :is="section($refs['section-'+index], fileInfo)" :file-info="fileInfo" />
|
||||
</div>
|
||||
</template>
|
||||
</Tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import Tab from 'nextcloud-vue/dist/Components/AppSidebarTab'
|
||||
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { CollectionList } from 'nextcloud-vue-collections'
|
||||
|
||||
import { shareWithTitle } from '../utils/SharedWithMe'
|
||||
import Share from '../models/Share'
|
||||
import ShareTypes from '../mixins/ShareTypes'
|
||||
import SharingEntryInternal from '../components/SharingEntryInternal'
|
||||
import SharingEntrySimple from '../components/SharingEntrySimple'
|
||||
import SharingInput from '../components/SharingInput'
|
||||
|
||||
import SharingLinkList from './SharingLinkList'
|
||||
import SharingList from './SharingList'
|
||||
|
||||
export default {
|
||||
name: 'SharingTab',
|
||||
|
||||
components: {
|
||||
Avatar,
|
||||
CollectionList,
|
||||
SharingEntryInternal,
|
||||
SharingEntrySimple,
|
||||
SharingInput,
|
||||
SharingLinkList,
|
||||
SharingList,
|
||||
Tab
|
||||
},
|
||||
|
||||
mixins: [ShareTypes],
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
error: '',
|
||||
expirationInterval: null,
|
||||
icon: 'icon-share',
|
||||
loading: true,
|
||||
name: t('files_sharing', 'Sharing'),
|
||||
// reshare Share object
|
||||
reshare: null,
|
||||
sharedWithMe: {},
|
||||
shares: [],
|
||||
linkShares: [],
|
||||
sections: OCA.Sharing.ShareTabSections.getSections()
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Needed to differenciate the tabs
|
||||
* pulled from the AppSidebarTab component
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
id() {
|
||||
return this.name.toLowerCase().replace(/ /g, '-')
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current active tab
|
||||
* needed because AppSidebarTab also uses $parent.activeTab
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
activeTab() {
|
||||
return this.$parent.activeTab
|
||||
},
|
||||
|
||||
/**
|
||||
* Is this share shared with me?
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSharedWithMe() {
|
||||
return Object.keys(this.sharedWithMe).length > 0
|
||||
},
|
||||
|
||||
canReshare() {
|
||||
return !!(this.fileInfo.permissions & OC.PERMISSION_SHARE)
|
||||
|| !!(this.reshare && this.reshare.hasSharePermission)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
fileInfo() {
|
||||
this.resetState()
|
||||
this.getShares()
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount() {
|
||||
this.getShares()
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the existing shares infos
|
||||
*/
|
||||
async getShares() {
|
||||
try {
|
||||
this.loading = true
|
||||
|
||||
// init params
|
||||
const shareUrl = generateOcsUrl('apps/files_sharing/api/v1', 2) + 'shares'
|
||||
const format = 'json'
|
||||
// TODO: replace with proper getFUllpath implementation of our own FileInfo model
|
||||
const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
|
||||
|
||||
// fetch shares
|
||||
const fetchShares = axios.get(shareUrl, {
|
||||
params: {
|
||||
format,
|
||||
path,
|
||||
reshares: true
|
||||
}
|
||||
})
|
||||
const fetchSharedWithMe = axios.get(shareUrl, {
|
||||
params: {
|
||||
format,
|
||||
path,
|
||||
shared_with_me: true
|
||||
}
|
||||
})
|
||||
|
||||
// wait for data
|
||||
const [shares, sharedWithMe] = await Promise.all([fetchShares, fetchSharedWithMe])
|
||||
this.loading = false
|
||||
|
||||
// process results
|
||||
this.processSharedWithMe(sharedWithMe)
|
||||
this.processShares(shares)
|
||||
} catch (error) {
|
||||
this.error = t('files_sharing', 'Unable to load the shares list')
|
||||
this.loading = false
|
||||
console.error('Error loading the shares list', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the current view to its default state
|
||||
*/
|
||||
resetState() {
|
||||
clearInterval(this.expirationInterval)
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
this.sharedWithMe = {}
|
||||
this.shares = []
|
||||
},
|
||||
|
||||
/**
|
||||
* Update sharedWithMe.subtitle with the appropriate
|
||||
* expiration time left
|
||||
*
|
||||
* @param {Share} share the sharedWith Share object
|
||||
*/
|
||||
updateExpirationSubtitle(share) {
|
||||
const expiration = moment(share.expireDate).unix()
|
||||
this.$set(this.sharedWithMe, 'subtitle', t('files_sharing', 'Expires {relativetime}', {
|
||||
relativetime: OC.Util.relativeModifiedDate(expiration * 1000)
|
||||
}))
|
||||
|
||||
// share have expired
|
||||
if (moment().unix() > expiration) {
|
||||
clearInterval(this.expirationInterval)
|
||||
// TODO: clear ui if share is expired
|
||||
this.$set(this.sharedWithMe, 'subtitle', t('files_sharing', 'this share just expired.'))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Process the current shares data
|
||||
* and init shares[]
|
||||
*
|
||||
* @param {Object} share the share ocs api request data
|
||||
* @param {Object} share.data the request data
|
||||
*/
|
||||
processShares({ data }) {
|
||||
if (data.ocs && data.ocs.data && data.ocs.data.length > 0) {
|
||||
// create Share objects and sort by newest
|
||||
const shares = data.ocs.data
|
||||
.map(share => new Share(share))
|
||||
.sort((a, b) => b.createdTime - a.createdTime)
|
||||
|
||||
this.linkShares = shares.filter(share => share.type === this.SHARE_TYPES.SHARE_TYPE_LINK || share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL)
|
||||
this.shares = shares.filter(share => share.type !== this.SHARE_TYPES.SHARE_TYPE_LINK && share.type !== this.SHARE_TYPES.SHARE_TYPE_EMAIL)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Process the sharedWithMe share data
|
||||
* and init sharedWithMe
|
||||
*
|
||||
* @param {Object} share the share ocs api request data
|
||||
* @param {Object} share.data the request data
|
||||
*/
|
||||
processSharedWithMe({ data }) {
|
||||
if (data.ocs && data.ocs.data && data.ocs.data[0]) {
|
||||
const share = new Share(data)
|
||||
const title = shareWithTitle(share)
|
||||
const displayName = share.ownerDisplayName
|
||||
const user = share.owner
|
||||
|
||||
this.sharedWithMe = {
|
||||
displayName,
|
||||
title,
|
||||
user
|
||||
}
|
||||
this.reshare = share
|
||||
|
||||
// If we have an expiration date, use it as subtitle
|
||||
// Refresh the status every 10s and clear if expired
|
||||
if (share.expireDate && moment(share.expireDate).unix() > moment().unix()) {
|
||||
// first update
|
||||
this.updateExpirationSubtitle(share)
|
||||
// interval update
|
||||
this.expirationInterval = setInterval(this.updateExpirationSubtitle, 10000, share)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert share at top of arrays
|
||||
*
|
||||
* @param {Share} share the share to insert
|
||||
*/
|
||||
addShare(share) {
|
||||
// only catching share type MAIL as link shares are added differently
|
||||
// meaning: not from the ShareInput
|
||||
if (share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
|
||||
this.linkShares.unshift(share)
|
||||
} else {
|
||||
this.shares.unshift(share)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -577,6 +577,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'displayname_file_owner' => 'ownerDisplay',
|
||||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
];
|
||||
$data[] = [$share, $expected];
|
||||
|
||||
|
@ -623,6 +625,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'displayname_file_owner' => 'ownerDisplay',
|
||||
'mimetype' => 'myFolderMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
];
|
||||
$data[] = [$share, $expected];
|
||||
|
||||
|
@ -676,6 +680,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'displayname_file_owner' => 'ownerDisplay',
|
||||
'mimetype' => 'myFolderMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
];
|
||||
$data[] = [$share, $expected];
|
||||
|
||||
|
@ -3431,6 +3437,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mail_send' => 0,
|
||||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
// User backend up
|
||||
|
@ -3462,6 +3470,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mail_send' => 0,
|
||||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [
|
||||
['owner', $owner],
|
||||
['initiator', $initiator],
|
||||
|
@ -3509,6 +3519,53 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mail_send' => 0,
|
||||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
$share = \OC::$server->getShareManager()->newShare();
|
||||
$share->setShareType(\OCP\Share::SHARE_TYPE_USER)
|
||||
->setSharedWith('recipient')
|
||||
->setSharedBy('initiator')
|
||||
->setShareOwner('currentUser')
|
||||
->setPermissions(\OCP\Constants::PERMISSION_READ)
|
||||
->setNode($file)
|
||||
->setShareTime(new \DateTime('2000-01-01T00:01:02'))
|
||||
->setTarget('myTarget')
|
||||
->setNote('personal note')
|
||||
->setId(42);
|
||||
// User backend down
|
||||
$result[] = [
|
||||
[
|
||||
'id' => 42,
|
||||
'share_type' => \OCP\Share::SHARE_TYPE_USER,
|
||||
'uid_owner' => 'initiator',
|
||||
'displayname_owner' => 'initiator',
|
||||
'permissions' => 1,
|
||||
'stime' => 946684862,
|
||||
'parent' => null,
|
||||
'expiration' => null,
|
||||
'token' => null,
|
||||
'uid_file_owner' => 'currentUser',
|
||||
'displayname_file_owner' => 'currentUser',
|
||||
'note' => 'personal note',
|
||||
'label' => null,
|
||||
'path' => 'file',
|
||||
'item_type' => 'file',
|
||||
'storage_id' => 'storageId',
|
||||
'storage' => 100,
|
||||
'item_source' => 3,
|
||||
'file_source' => 3,
|
||||
'file_parent' => 1,
|
||||
'file_target' => 'myTarget',
|
||||
'share_with' => 'recipient',
|
||||
'share_with_displayname' => 'recipient',
|
||||
'mail_send' => 0,
|
||||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => true,
|
||||
'can_delete' => true,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3554,6 +3611,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mail_send' => 0,
|
||||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3597,6 +3656,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mail_send' => 0,
|
||||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3646,6 +3707,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'url' => 'myLink',
|
||||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3696,6 +3759,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'url' => 'myLink',
|
||||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3739,6 +3804,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mail_send' => 0,
|
||||
'mimetype' => 'myFolderMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3785,6 +3852,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mail_send' => 0,
|
||||
'mimetype' => 'myFolderMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3829,6 +3898,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mail_send' => 0,
|
||||
'mimetype' => 'myFolderMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3873,6 +3944,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mail_send' => 0,
|
||||
'mimetype' => 'myFolderMimeType',
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3933,6 +4006,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'password' => 'password',
|
||||
'send_password_by_talk' => false,
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -3979,6 +4054,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'password' => 'password',
|
||||
'send_password_by_talk' => true,
|
||||
'hide_download' => 0,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, [], false
|
||||
];
|
||||
|
||||
|
@ -4120,6 +4197,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'label' => '',
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, false, []
|
||||
];
|
||||
|
||||
|
@ -4163,6 +4242,8 @@ class ShareAPIControllerTest extends TestCase {
|
|||
'mimetype' => 'myMimeType',
|
||||
'hide_download' => 0,
|
||||
'label' => '',
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
], $share, true, [
|
||||
'share_with_displayname' => 'recipientRoomName'
|
||||
]
|
||||
|
|
|
@ -234,198 +234,6 @@ describe('OCA.Sharing.Util tests', function() {
|
|||
expect($tr.find('.action-share').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('Share action', function() {
|
||||
var shareTab;
|
||||
|
||||
function makeDummyShareItem(displayName) {
|
||||
return {
|
||||
share_with_displayname: displayName
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
// make it look like not the "All files" list
|
||||
fileList.id = 'test';
|
||||
shareTab = fileList._detailsView._tabViews[0];
|
||||
});
|
||||
afterEach(function() {
|
||||
shareTab = null;
|
||||
});
|
||||
it('clicking share action opens sidebar and share tab', function() {
|
||||
var showDetailsViewStub = sinon.stub(fileList, 'showDetailsView');
|
||||
|
||||
fileList.setFiles([{
|
||||
id: 1,
|
||||
type: 'file',
|
||||
name: 'One.txt',
|
||||
path: '/subdir',
|
||||
mimetype: 'text/plain',
|
||||
size: 12,
|
||||
permissions: OC.PERMISSION_ALL,
|
||||
etag: 'abc'
|
||||
}]);
|
||||
|
||||
var $tr = fileList.$el.find('tr:first');
|
||||
$tr.find('.action-share').click();
|
||||
|
||||
expect(showDetailsViewStub.calledOnce).toEqual(true);
|
||||
expect(showDetailsViewStub.getCall(0).args[0]).toEqual('One.txt');
|
||||
expect(showDetailsViewStub.getCall(0).args[1]).toEqual('shareTabView');
|
||||
|
||||
showDetailsViewStub.restore();
|
||||
});
|
||||
it('adds share icon after sharing a non-shared file', function() {
|
||||
var $action, $tr;
|
||||
OC.Share.statuses = {};
|
||||
fileList.setFiles([{
|
||||
id: 1,
|
||||
type: 'file',
|
||||
name: 'One.txt',
|
||||
path: '/subdir',
|
||||
mimetype: 'text/plain',
|
||||
size: 12,
|
||||
permissions: OC.PERMISSION_ALL,
|
||||
etag: 'abc'
|
||||
}]);
|
||||
$action = fileList.$el.find('tbody tr:first .action-share');
|
||||
$tr = fileList.$el.find('tr:first');
|
||||
|
||||
$tr.find('.action-share').click();
|
||||
|
||||
// simulate updating shares
|
||||
shareTab._dialog.model.set({
|
||||
shares: [
|
||||
{share_with_displayname: 'User One', share_with: 'User One'},
|
||||
{share_with_displayname: 'User Two', share_with: 'User Two'},
|
||||
{share_with_displayname: 'Group One', share_with: 'Group One'},
|
||||
{share_with_displayname: 'Group Two', share_with: 'Group Two'}
|
||||
]
|
||||
});
|
||||
|
||||
expect($action.text().trim()).toEqual('Shared with Group One Shared with Group Two Shared with User One Shared with User Two');
|
||||
expect($action.find('.icon').hasClass('icon-shared')).toEqual(true);
|
||||
expect($action.find('.icon').hasClass('icon-public')).toEqual(false);
|
||||
});
|
||||
it('updates share icon after updating shares of a file', function() {
|
||||
var $action, $tr;
|
||||
OC.Share.statuses = {1: {link: false, path: '/subdir'}};
|
||||
fileList.setFiles([{
|
||||
id: 1,
|
||||
type: 'file',
|
||||
name: 'One.txt',
|
||||
path: '/subdir',
|
||||
mimetype: 'text/plain',
|
||||
size: 12,
|
||||
permissions: OC.PERMISSION_ALL,
|
||||
etag: 'abc'
|
||||
}]);
|
||||
$action = fileList.$el.find('tbody tr:first .action-share');
|
||||
$tr = fileList.$el.find('tr:first');
|
||||
|
||||
$tr.find('.action-share').click();
|
||||
|
||||
// simulate updating shares
|
||||
shareTab._dialog.model.set({
|
||||
shares: [
|
||||
{share_with_displayname: 'User One', share_with: 'User One'},
|
||||
{share_with_displayname: 'User Two', share_with: 'User Two'},
|
||||
{share_with_displayname: 'User Three', share_with: 'User Three'}
|
||||
]
|
||||
});
|
||||
|
||||
expect($action.text().trim()).toEqual('Shared with User One Shared with User Three Shared with User Two');
|
||||
expect($action.find('.icon').hasClass('icon-shared')).toEqual(true);
|
||||
expect($action.find('.icon').hasClass('icon-public')).toEqual(false);
|
||||
});
|
||||
it('removes share icon after removing all shares from a file', function() {
|
||||
var $action, $tr;
|
||||
OC.Share.statuses = {1: {link: false, path: '/subdir'}};
|
||||
fileList.setFiles([{
|
||||
id: 1,
|
||||
type: 'file',
|
||||
name: 'One.txt',
|
||||
path: '/subdir',
|
||||
mimetype: 'text/plain',
|
||||
size: 12,
|
||||
permissions: OC.PERMISSION_ALL,
|
||||
etag: 'abc',
|
||||
recipients: 'User One, User Two'
|
||||
}]);
|
||||
$action = fileList.$el.find('tbody tr:first .action-share');
|
||||
$tr = fileList.$el.find('tr:first');
|
||||
|
||||
$tr.find('.action-share').click();
|
||||
|
||||
// simulate updating shares
|
||||
shareTab._dialog.model.set({
|
||||
shares: []
|
||||
});
|
||||
|
||||
expect($tr.attr('data-share-recipient-data')).not.toBeDefined();
|
||||
});
|
||||
it('keep share text after updating reshare', function() {
|
||||
var $action, $tr;
|
||||
OC.Share.statuses = {1: {link: false, path: '/subdir'}};
|
||||
fileList.setFiles([{
|
||||
id: 1,
|
||||
type: 'file',
|
||||
name: 'One.txt',
|
||||
path: '/subdir',
|
||||
mimetype: 'text/plain',
|
||||
size: 12,
|
||||
permissions: OC.PERMISSION_ALL,
|
||||
etag: 'abc',
|
||||
shareOwner: 'User One',
|
||||
shareOwnerId: 'User One'
|
||||
}]);
|
||||
$action = fileList.$el.find('tbody tr:first .action-share');
|
||||
$tr = fileList.$el.find('tr:first');
|
||||
|
||||
$tr.find('.action-share').click();
|
||||
|
||||
// simulate updating shares
|
||||
shareTab._dialog.model.set({
|
||||
shares: [{share_with_displayname: 'User Two'}]
|
||||
});
|
||||
|
||||
expect($action.find('>span').text().trim()).toEqual('Shared by User One');
|
||||
expect($action.find('.icon').hasClass('icon-shared')).toEqual(false);
|
||||
expect($action.find('.icon').hasClass('icon-public')).toEqual(false);
|
||||
});
|
||||
it('keep share text after unsharing reshare', function() {
|
||||
var $action, $tr;
|
||||
OC.Share.statuses = {1: {link: false, path: '/subdir'}};
|
||||
fileList.setFiles([{
|
||||
id: 1,
|
||||
type: 'file',
|
||||
name: 'One.txt',
|
||||
path: '/subdir',
|
||||
mimetype: 'text/plain',
|
||||
size: 12,
|
||||
permissions: OC.PERMISSION_ALL,
|
||||
etag: 'abc',
|
||||
shareOwner: 'User One',
|
||||
shareOwnerId: 'User One',
|
||||
recipients: 'User Two',
|
||||
recipientData: {'User Two': 'User Two'}
|
||||
}]);
|
||||
$action = fileList.$el.find('tbody tr:first .action-share');
|
||||
$tr = fileList.$el.find('tr:first');
|
||||
|
||||
$tr.find('.action-share').click();
|
||||
|
||||
// simulate updating shares
|
||||
shareTab._dialog.model.set({
|
||||
shares: []
|
||||
});
|
||||
|
||||
expect($tr.attr('data-share-recipient-data')).not.toBeDefined();
|
||||
|
||||
expect($action.find('>span').text().trim()).toEqual('Shared by User One');
|
||||
expect($action.find('.icon').hasClass('icon-shared')).toEqual(false);
|
||||
expect($action.find('.icon').hasClass('icon-public')).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('Excluded lists', function() {
|
||||
function createListThenAttach(listId) {
|
||||
var fileActions = new OCA.Files.FileActions();
|
||||
|
@ -513,20 +321,5 @@ describe('OCA.Sharing.Util tests', function() {
|
|||
afterEach(function() {
|
||||
shareTabSpy.restore();
|
||||
});
|
||||
|
||||
it('updates fileInfoModel when shares changed', function() {
|
||||
var changeHandler = sinon.stub();
|
||||
fileInfoModel.on('change', changeHandler);
|
||||
|
||||
shareTabSpy.getCall(0).returnValue.trigger('sharesChanged', shareModel);
|
||||
|
||||
expect(changeHandler.calledOnce).toEqual(true);
|
||||
expect(changeHandler.getCall(0).args[0].changed).toEqual({
|
||||
shareTypes: [
|
||||
OC.Share.SHARE_TYPE_USER,
|
||||
OC.Share.SHARE_TYPE_REMOTE
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ module.exports = {
|
|||
entry: {
|
||||
'additionalScripts': path.join(__dirname, 'src', 'additionalScripts.js'),
|
||||
'files_sharing': path.join(__dirname, 'src', 'files_sharing.js'),
|
||||
'files_sharing_tab': path.join(__dirname, 'src', 'files_sharing_tab.js'),
|
||||
'collaboration': path.join(__dirname, 'src', 'collaborationresourceshandler.js'),
|
||||
},
|
||||
output: {
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -30,7 +30,8 @@
|
|||
|
||||
_rendered: false,
|
||||
|
||||
className: 'systemTagsInfoView hidden',
|
||||
className: 'systemTagsInfoView',
|
||||
name: 'systemTags',
|
||||
|
||||
/**
|
||||
* @type OC.SystemTags.SystemTagsInputField
|
||||
|
@ -123,11 +124,7 @@
|
|||
var appliedTags = collection.map(modelToSelection)
|
||||
self._inputView.setData(appliedTags)
|
||||
|
||||
if (appliedTags.length !== 0) {
|
||||
self.show()
|
||||
} else {
|
||||
self.hide()
|
||||
}
|
||||
self.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ describe('OCA.SystemTags.SystemTagsInfoView tests', function() {
|
|||
var fetchStub = sinon.stub(OC.SystemTags.SystemTagsMappingCollection.prototype, 'fetch');
|
||||
var setDataStub = sinon.stub(OC.SystemTags.SystemTagsInputField.prototype, 'setData');
|
||||
|
||||
expect(view.$el.hasClass('hidden')).toEqual(true);
|
||||
expect(view.$el.hasClass('hidden')).toEqual(false);
|
||||
|
||||
view.setFileInfo({id: '123'});
|
||||
expect(view.$el.find('input[name=tags]').length).toEqual(1);
|
||||
|
@ -211,10 +211,10 @@ describe('OCA.SystemTags.SystemTagsInfoView tests', function() {
|
|||
|
||||
expect(view.isVisible()).toBeTruthy();
|
||||
});
|
||||
it('is not visible after rendering', function() {
|
||||
it('is visible after rendering', function() {
|
||||
view.render();
|
||||
|
||||
expect(view.isVisible()).toBeFalsy();
|
||||
expect(view.isVisible()).toBeTruthy();
|
||||
});
|
||||
it('shows and hides the element', function() {
|
||||
view.show();
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }]
|
||||
],
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
|
@ -22,7 +22,6 @@
|
|||
$expectedFiles = [
|
||||
'.',
|
||||
'..',
|
||||
'.babelrc.js',
|
||||
'.codecov.yml',
|
||||
'.drone.yml',
|
||||
'.eslintrc.js',
|
||||
|
@ -46,6 +45,7 @@ $expectedFiles = [
|
|||
'autotest-external.sh',
|
||||
'autotest-js.sh',
|
||||
'autotest.sh',
|
||||
'babel.config.js',
|
||||
'build',
|
||||
'CHANGELOG.md',
|
||||
'CODE_OF_CONDUCT.md',
|
||||
|
|
|
@ -206,7 +206,7 @@ Feature: sharees
|
|||
Then "exact groups" sharees returned is empty
|
||||
Then "groups" sharees returned is empty
|
||||
Then "exact remotes" sharees returned are
|
||||
| test@localhost | 6 | test@localhost |
|
||||
| test (localhost) | 6 | test@localhost |
|
||||
Then "remotes" sharees returned is empty
|
||||
|
||||
Scenario: Remote sharee for calendars not allowed
|
||||
|
|
|
@ -206,7 +206,7 @@ Feature: sharees_provisioningapiv2
|
|||
Then "exact groups" sharees returned is empty
|
||||
Then "groups" sharees returned is empty
|
||||
Then "exact remotes" sharees returned are
|
||||
| test@localhost | 6 | test@localhost |
|
||||
| test (localhost) | 6 | test@localhost |
|
||||
Then "remotes" sharees returned is empty
|
||||
|
||||
Scenario: Remote sharee for calendars not allowed
|
||||
|
|
|
@ -69,8 +69,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.systemTagsInfoView,
|
||||
.systemtags-select2-container {
|
||||
width: 100%;
|
||||
|
||||
.select2-choices .select2-search-choice.select2-locked .label {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -323,6 +323,13 @@
|
|||
data.isEncrypted = false;
|
||||
}
|
||||
|
||||
var isFavouritedProp = props['{' + Client.NS_OWNCLOUD + '}favorite'];
|
||||
if (!_.isUndefined(isFavouritedProp)) {
|
||||
data.isFavourited = isFavouritedProp === '1';
|
||||
} else {
|
||||
data.isFavourited = false;
|
||||
}
|
||||
|
||||
var contentType = props[Client.PROPERTY_GETCONTENTTYPE];
|
||||
if (!_.isUndefined(contentType)) {
|
||||
data.mimetype = contentType;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
|
||||
|
||||
if (!Element.prototype.matches) {
|
||||
Element.prototype.matches
|
||||
= Element.prototype.msMatchesSelector
|
||||
|| Element.prototype.webkitMatchesSelector
|
||||
}
|
||||
|
||||
if (!Element.prototype.closest) {
|
||||
Element.prototype.closest = function(s) {
|
||||
var el = this
|
||||
|
||||
do {
|
||||
if (el.matches(s)) return el
|
||||
el = el.parentElement || el.parentNode
|
||||
} while (el !== null && el.nodeType === 1)
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
|
@ -20,4 +20,5 @@
|
|||
*/
|
||||
|
||||
import './console'
|
||||
import './closest'
|
||||
import './windows-phone'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
|
@ -20,8 +20,8 @@
|
|||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import '@babel/polyfill'
|
||||
import './Polyfill/index'
|
||||
import '@babel/polyfill'
|
||||
|
||||
// If you remove the line below, tests won't pass
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<LoginForm
|
||||
:username.sync="user"
|
||||
:redirect-url="redirectUrl"
|
||||
:directLogin="directLogin"
|
||||
:direct-login="directLogin"
|
||||
:messages="messages"
|
||||
:errors="errors"
|
||||
:throttle-delay="throttleDelay"
|
||||
|
|
|
@ -57,11 +57,15 @@ class RemoteGroupPlugin implements ISearchPlugin {
|
|||
$resultType = new SearchResultType('remote_groups');
|
||||
|
||||
if ($this->enabled && $this->cloudIdManager->isValidCloudId($search) && $offset === 0) {
|
||||
list($remoteGroup, $serverUrl) = $this->splitGroupRemote($search);
|
||||
$result['exact'][] = [
|
||||
'label' => $search,
|
||||
'label' => $remoteGroup . " ($serverUrl)",
|
||||
'guid' => $remoteGroup,
|
||||
'name' => $remoteGroup,
|
||||
'value' => [
|
||||
'shareType' => Share::SHARE_TYPE_REMOTE_GROUP,
|
||||
'shareWith' => $search,
|
||||
'server' => $serverUrl,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -71,4 +75,20 @@ class RemoteGroupPlugin implements ISearchPlugin {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* split group and remote from federated cloud id
|
||||
*
|
||||
* @param string $address federated share address
|
||||
* @return array [user, remoteURL]
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function splitGroupRemote($address) {
|
||||
try {
|
||||
$cloudId = $this->cloudIdManager->resolveCloudId($address);
|
||||
return [$cloudId->getUser(), $cloudId->getRemote()];
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new \InvalidArgumentException('Invalid Federated Cloud ID', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -152,10 +152,13 @@ class RemotePlugin implements ISearchPlugin {
|
|||
$localUser = $this->userManager->get($remoteUser);
|
||||
if ($localUser === null || $search !== $localUser->getCloudId()) {
|
||||
$result['exact'][] = [
|
||||
'label' => $search,
|
||||
'label' => $remoteUser . " ($serverUrl)",
|
||||
'uuid' => $remoteUser,
|
||||
'name' => $remoteUser,
|
||||
'value' => [
|
||||
'shareType' => Share::SHARE_TYPE_REMOTE,
|
||||
'shareWith' => $search,
|
||||
'server' => $serverUrl,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -359,6 +359,16 @@
|
|||
"@babel/plugin-syntax-async-generators": "^7.2.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-class-properties": {
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz",
|
||||
"integrity": "sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.5.5",
|
||||
"@babel/helper-plugin-utils": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-dynamic-import": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz",
|
||||
|
@ -2384,6 +2394,11 @@
|
|||
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
||||
"dev": true
|
||||
},
|
||||
"debounce": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
|
||||
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -6085,8 +6100,7 @@
|
|||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
|
||||
"dev": true
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"p-is-promise": {
|
||||
"version": "2.1.0",
|
||||
|
@ -6110,6 +6124,23 @@
|
|||
"p-limit": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-queue": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.2.0.tgz",
|
||||
"integrity": "sha512-B2LXNONcyn/G6uz2UBFsGjmSa0e/br3jznlzhEyCXg56c7VhEpiT2pZxGOfv32Q3FSyugAdys9KGpsv3kV+Sbg==",
|
||||
"requires": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"p-timeout": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"requires": {
|
||||
"p-finally": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
|
@ -8184,6 +8215,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"url-search-params-polyfill": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-search-params-polyfill/-/url-search-params-polyfill-7.0.0.tgz",
|
||||
"integrity": "sha512-0SEH3s+wCNbxEE/rWUalN004ICNi23Q74Ksc0gS2kG8EXnbayxGOrV97JdwnIVPKZ75Xk0hvKXvtIC4xReLMgg=="
|
||||
},
|
||||
"use": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
|
|
10
package.json
10
package.json
|
@ -25,6 +25,7 @@
|
|||
"dependencies": {
|
||||
"@babel/polyfill": "^7.6.0",
|
||||
"@chenfengyuan/vue-qrcode": "^1.0.1",
|
||||
"@nextcloud/auth": "^0.3.1",
|
||||
"@nextcloud/axios": "^0.5.0",
|
||||
"@nextcloud/event-bus": "^0.2.1",
|
||||
"@nextcloud/initial-state": "^0.2.0",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"clipboard": "^2.0.4",
|
||||
"css-vars-ponyfill": "^2.1.2",
|
||||
"davclient.js": "git+https://github.com/owncloud/davclient.js.git#0.2.1",
|
||||
"debounce": "^1.2.0",
|
||||
"dompurify": "^2.0.7",
|
||||
"escape-html": "^1.0.3",
|
||||
"handlebars": "^4.4.5",
|
||||
|
@ -53,12 +55,14 @@
|
|||
"nextcloud-router": "0.0.9",
|
||||
"nextcloud-vue": "^0.12.7",
|
||||
"nextcloud-vue-collections": "^0.6.0",
|
||||
"p-queue": "^6.1.0",
|
||||
"query-string": "^5.1.1",
|
||||
"select2": "3.5.1",
|
||||
"snap.js": "^2.0.9",
|
||||
"strengthify": "git+https://github.com/MorrisJobke/strengthify.git#0.5.8",
|
||||
"toastify-js": "^1.6.1",
|
||||
"underscore": "^1.9.1",
|
||||
"url-search-params-polyfill": "^7.0.0",
|
||||
"v-tooltip": "^2.0.2",
|
||||
"vue": "^2.6.10",
|
||||
"vue-click-outside": "^1.0.7",
|
||||
|
@ -72,6 +76,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"@nextcloud/browserslist-config": "^1.0.0",
|
||||
|
@ -104,5 +109,8 @@
|
|||
},
|
||||
"browserslist": [
|
||||
"extends @nextcloud/browserslist-config"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,8 +126,7 @@ Feature: app-files-sharing-link
|
|||
Given I am logged in
|
||||
And I share the link for "welcome.txt"
|
||||
When I protect the shared link with the password "abcdef"
|
||||
Then I see that the working icon for password protect is shown
|
||||
And I see that the working icon for password protect is eventually not shown
|
||||
Then I see that the password protect is disabled while loading
|
||||
And I see that the link share is password protected
|
||||
# As Talk is not enabled in the acceptance tests of the server the checkbox
|
||||
# is never shown.
|
||||
|
|
|
@ -19,15 +19,6 @@ Feature: app-files-tags
|
|||
# When I open the input field for tags in the details view
|
||||
# Then I see that the input field for tags in the details view is shown
|
||||
|
||||
Scenario: show the input field for tags in the details view after the sharing tab has loaded
|
||||
Given I am logged in
|
||||
And I open the details view for "welcome.txt"
|
||||
And I see that the details view is open
|
||||
And I open the "Sharing" tab in the details view
|
||||
And I see that the "Sharing" tab in the details view is eventually loaded
|
||||
When I open the input field for tags in the details view
|
||||
Then I see that the input field for tags in the details view is shown
|
||||
|
||||
Scenario: create tags using the Administration settings
|
||||
Given I am logged in as the admin
|
||||
And I visit the settings page
|
||||
|
|
|
@ -98,7 +98,7 @@ class FilesAppContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function fileNameInDetailsView() {
|
||||
return Locator::forThe()->css(".fileName")->
|
||||
return Locator::forThe()->css(".app-sidebar-header__title")->
|
||||
descendantOf(self::detailsView())->
|
||||
describedAs("File name in details view in Files app");
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ class FilesAppContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function favoriteActionInFileDetailsInDetailsView() {
|
||||
return Locator::forThe()->css(".action-favorite")->
|
||||
return Locator::forThe()->css(".app-sidebar-header__star")->
|
||||
descendantOf(self::fileDetailsInDetailsView())->
|
||||
describedAs("Favorite action in file details in details view in Files app");
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ class FilesAppContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
private static function fileDetailsInDetailsView() {
|
||||
return Locator::forThe()->css(".file-details")->
|
||||
return Locator::forThe()->css(".app-sidebar-header__desc")->
|
||||
descendantOf(self::detailsView())->
|
||||
describedAs("File details in details view in Files app");
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ class FilesAppContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
private static function tabHeadersInDetailsView() {
|
||||
return Locator::forThe()->css(".tabHeaders")->
|
||||
return Locator::forThe()->css(".app-sidebar-tabs__nav")->
|
||||
descendantOf(self::detailsView())->
|
||||
describedAs("Tab headers in details view in Files app");
|
||||
}
|
||||
|
@ -214,7 +214,7 @@ class FilesAppContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function tabInDetailsViewNamed($tabName) {
|
||||
return Locator::forThe()->xpath("//div[@id=//*[contains(concat(' ', normalize-space(@class), ' '), ' tabHeader ') and normalize-space() = '$tabName']/@data-tabid]")->
|
||||
return Locator::forThe()->xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' app-sidebar-tabs__content ')]/section[@aria-labelledby = '$tabName' and @role = 'tabpanel']")->
|
||||
descendantOf(self::detailsView())->
|
||||
describedAs("Tab named $tabName in details view in Files app");
|
||||
}
|
||||
|
@ -223,7 +223,7 @@ class FilesAppContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function loadingIconForTabInDetailsViewNamed($tabName) {
|
||||
return Locator::forThe()->css(".loading")->
|
||||
return Locator::forThe()->css(".icon-loading")->
|
||||
descendantOf(self::tabInDetailsViewNamed($tabName))->
|
||||
describedAs("Loading icon for tab named $tabName in details view in Files app");
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function sharedByLabel() {
|
||||
return Locator::forThe()->css(".reshare")->
|
||||
return Locator::forThe()->css(".sharing-entry__reshare")->
|
||||
descendantOf(FilesAppContext::detailsView())->
|
||||
describedAs("Shared by label in the details view in Files app");
|
||||
}
|
||||
|
@ -40,16 +40,34 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function shareWithInput() {
|
||||
return Locator::forThe()->css(".shareWithField")->
|
||||
return Locator::forThe()->css(".sharing-input .multiselect__input")->
|
||||
descendantOf(FilesAppContext::detailsView())->
|
||||
describedAs("Share with input in the details view in Files app");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Locator
|
||||
*/
|
||||
public static function shareWithInputResults() {
|
||||
return Locator::forThe()->css(".sharing-input .multiselect__content-wrapper")->
|
||||
descendantOf(FilesAppContext::detailsView())->
|
||||
describedAs("Share with input results list in the details view in Files app");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Locator
|
||||
*/
|
||||
public static function shareWithInputResult($result) {
|
||||
return Locator::forThe()->xpath("//li[contains(concat(' ', normalize-space(@class), ' '), ' multiselect__element ')]//span[normalize-space() = '$result']/ancestor::li")->
|
||||
descendantOf(self::shareWithInputResults())->
|
||||
describedAs("Share with input result from the results list in the details view in Files app");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Locator
|
||||
*/
|
||||
public static function shareeList() {
|
||||
return Locator::forThe()->css(".shareeListView")->
|
||||
return Locator::forThe()->css(".sharing-sharee-list")->
|
||||
descendantOf(FilesAppContext::detailsView())->
|
||||
describedAs("Sharee list in the details view in Files app");
|
||||
}
|
||||
|
@ -60,7 +78,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
public static function sharedWithRow($sharedWithName) {
|
||||
// "username" class is used for any type of share, not only for shares
|
||||
// with users.
|
||||
return Locator::forThe()->xpath("//span[contains(concat(' ', normalize-space(@class), ' '), ' username ') and normalize-space() = '$sharedWithName']/ancestor::li")->
|
||||
return Locator::forThe()->xpath("//li[contains(concat(' ', normalize-space(@class), ' '), ' sharing-entry ')]//h5[normalize-space() = '$sharedWithName']/ancestor::li")->
|
||||
descendantOf(self::shareeList())->
|
||||
describedAs("Shared with $sharedWithName row in the details view in Files app");
|
||||
}
|
||||
|
@ -69,7 +87,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function shareWithMenuButton($sharedWithName) {
|
||||
return Locator::forThe()->css(".share-menu > .icon")->
|
||||
return Locator::forThe()->css(".sharing-entry__actions > .action-item__menutoggle")->
|
||||
descendantOf(self::sharedWithRow($sharedWithName))->
|
||||
describedAs("Share with $sharedWithName menu button in the details view in Files app");
|
||||
}
|
||||
|
@ -78,7 +96,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function shareWithMenu($sharedWithName) {
|
||||
return Locator::forThe()->css(".share-menu > .menu")->
|
||||
return Locator::forThe()->css(".sharing-entry__actions > .action-item__menu")->
|
||||
descendantOf(self::sharedWithRow($sharedWithName))->
|
||||
describedAs("Share with $sharedWithName menu in the details view in Files app");
|
||||
}
|
||||
|
@ -108,7 +126,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function shareLinkRow() {
|
||||
return Locator::forThe()->css(".linkShareView .shareWithList:first-child")->
|
||||
return Locator::forThe()->css(".sharing-link-list .sharing-entry__link:first-child")->
|
||||
descendantOf(FilesAppContext::detailsView())->
|
||||
describedAs("Share link row in the details view in Files app");
|
||||
}
|
||||
|
@ -119,7 +137,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
public static function shareLinkAddNewButton() {
|
||||
// When there is no link share the "Add new share" item is shown instead
|
||||
// of the menu button as a direct child of ".share-menu".
|
||||
return Locator::forThe()->css(".share-menu > .new-share")->
|
||||
return Locator::forThe()->css(".action-item.icon-add")->
|
||||
descendantOf(self::shareLinkRow())->
|
||||
describedAs("Add new share link button in the details view in Files app");
|
||||
}
|
||||
|
@ -128,7 +146,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function copyLinkButton() {
|
||||
return Locator::forThe()->css("a.clipboard-button")->
|
||||
return Locator::forThe()->css("a.sharing-entry__copy")->
|
||||
descendantOf(self::shareLinkRow())->
|
||||
describedAs("Copy link button in the details view in Files app");
|
||||
}
|
||||
|
@ -137,7 +155,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function shareLinkMenuButton() {
|
||||
return Locator::forThe()->css(".share-menu > .icon")->
|
||||
return Locator::forThe()->css(".sharing-entry__actions .action-item__menutoggle")->
|
||||
descendantOf(self::shareLinkRow())->
|
||||
describedAs("Share link menu button in the details view in Files app");
|
||||
}
|
||||
|
@ -146,7 +164,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function shareLinkMenu() {
|
||||
return Locator::forThe()->css(".share-menu > .menu")->
|
||||
return Locator::forThe()->css(".sharing-entry__actions .action-item__menu")->
|
||||
descendantOf(self::shareLinkRow())->
|
||||
describedAs("Share link menu in the details view in Files app");
|
||||
}
|
||||
|
@ -209,16 +227,16 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function passwordProtectField() {
|
||||
return Locator::forThe()->css(".linkPassText")->descendantOf(self::shareLinkMenu())->
|
||||
return Locator::forThe()->css(".share-link-password input.action-input__input")->descendantOf(self::shareLinkMenu())->
|
||||
describedAs("Password protect field in the details view in Files app");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Locator
|
||||
*/
|
||||
public static function passwordProtectWorkingIcon() {
|
||||
return Locator::forThe()->css(".linkPassMenu .icon-loading-small")->descendantOf(self::shareLinkMenu())->
|
||||
describedAs("Password protect working icon in the details view in Files app");
|
||||
public static function disabledPasswordProtectField() {
|
||||
return Locator::forThe()->css(".share-link-password input.action-input__input[disabled]")->descendantOf(self::shareLinkMenu())->
|
||||
describedAs("Disabled password protect field in the details view in Files app");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -257,7 +275,12 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
public function iShareWith($fileName, $shareWithName) {
|
||||
$this->actor->find(FileListContext::shareActionForFile(FilesAppContext::currentSectionMainView(), $fileName), 10)->click();
|
||||
|
||||
$this->actor->find(self::shareWithInput(), 5)->setValue($shareWithName . "\r");
|
||||
$this->actor->find(self::shareWithInput(), 5)->setValue($shareWithName);
|
||||
// "setValue()" ends sending a tab, which unfocuses the input and causes
|
||||
// the results to be hidden, so the input needs to be clicked to show
|
||||
// the results again.
|
||||
$this->actor->find(self::shareWithInput())->click();
|
||||
$this->actor->find(self::shareWithInputResult($shareWithName), 5)->click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -269,7 +292,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
// Clicking on the menu item copies the link to the clipboard, but it is
|
||||
// not possible to access that value from the acceptance tests. Due to
|
||||
// this the value of the attribute that holds the URL is used instead.
|
||||
$this->actor->getSharedNotebook()["shared link"] = $this->actor->find(self::copyLinkButton(), 2)->getWrappedElement()->getAttribute("data-clipboard-text");
|
||||
$this->actor->getSharedNotebook()["shared link"] = $this->actor->find(self::copyLinkButton(), 2)->getWrappedElement()->getAttribute("href");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -412,21 +435,16 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* @Then I see that the working icon for password protect is shown
|
||||
* @Then I see that the password protect is disabled while loading
|
||||
*/
|
||||
public function iSeeThatTheWorkingIconForPasswordProtectIsShown() {
|
||||
PHPUnit_Framework_Assert::assertNotNull($this->actor->find(self::passwordProtectWorkingIcon(), 10));
|
||||
}
|
||||
public function iSeeThatThePasswordProtectIsDisabledWhileLoading() {
|
||||
PHPUnit_Framework_Assert::assertNotNull($this->actor->find(self::disabledPasswordProtectField(), 10));
|
||||
|
||||
/**
|
||||
* @Then I see that the working icon for password protect is eventually not shown
|
||||
*/
|
||||
public function iSeeThatTheWorkingIconForPasswordProtectIsEventuallyNotShown() {
|
||||
if (!WaitFor::elementToBeEventuallyNotShown(
|
||||
$this->actor,
|
||||
self::passwordProtectWorkingIcon(),
|
||||
self::disabledPasswordProtectField(),
|
||||
$timeout = 10 * $this->actor->getFindTimeoutMultiplier())) {
|
||||
PHPUnit_Framework_Assert::fail("The working icon for password protect is still shown after $timeout seconds");
|
||||
PHPUnit_Framework_Assert::fail("The password protect field is still disabled after $timeout seconds");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -477,8 +495,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface {
|
|||
public function iShareTheLinkForProtectedByThePassword($fileName, $password) {
|
||||
$this->iShareTheLinkFor($fileName);
|
||||
$this->iProtectTheSharedLinkWithThePassword($password);
|
||||
$this->iSeeThatTheWorkingIconForPasswordProtectIsShown();
|
||||
$this->iSeeThatTheWorkingIconForPasswordProtectIsEventuallyNotShown();
|
||||
$this->iSeeThatThePasswordProtectIsDisabledWhileLoading();
|
||||
}
|
||||
|
||||
private function showShareLinkMenuIfNeeded() {
|
||||
|
|
|
@ -152,7 +152,7 @@ class RemotePluginTest extends TestCase {
|
|||
'test@remote',
|
||||
[],
|
||||
true,
|
||||
['remotes' => [], 'exact' => ['remotes' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]]]],
|
||||
['remotes' => [], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote'], 'uuid' => 'test', 'name' => 'test']]]],
|
||||
false,
|
||||
true,
|
||||
],
|
||||
|
@ -160,7 +160,7 @@ class RemotePluginTest extends TestCase {
|
|||
'test@remote',
|
||||
[],
|
||||
false,
|
||||
['remotes' => [], 'exact' => ['remotes' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]]]],
|
||||
['remotes' => [], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote'], 'uuid' => 'test', 'name' => 'test']]]],
|
||||
false,
|
||||
true,
|
||||
],
|
||||
|
@ -238,7 +238,7 @@ class RemotePluginTest extends TestCase {
|
|||
],
|
||||
],
|
||||
true,
|
||||
['remotes' => [['name' => 'User @ Localhost', 'label' => 'User @ Localhost (username@localhost)', 'uuid' => 'uid', 'type' => '', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']]], 'exact' => ['remotes' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]]]],
|
||||
['remotes' => [['name' => 'User @ Localhost', 'label' => 'User @ Localhost (username@localhost)', 'uuid' => 'uid', 'type' => '', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']]], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote'], 'uuid' => 'test', 'name' => 'test']]]],
|
||||
false,
|
||||
true,
|
||||
],
|
||||
|
@ -264,7 +264,7 @@ class RemotePluginTest extends TestCase {
|
|||
],
|
||||
],
|
||||
false,
|
||||
['remotes' => [], 'exact' => ['remotes' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]]]],
|
||||
['remotes' => [], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote'], 'uuid' => 'test', 'name' => 'test']]]],
|
||||
false,
|
||||
true,
|
||||
],
|
||||
|
@ -370,7 +370,7 @@ class RemotePluginTest extends TestCase {
|
|||
],
|
||||
],
|
||||
false,
|
||||
['remotes' => [], 'exact' => ['remotes' => [['label' => 'user space@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user space@remote']]]]],
|
||||
['remotes' => [], 'exact' => ['remotes' => [['label' => 'user space (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user space@remote', 'server' => 'remote'], 'uuid' => 'user space', 'name' => 'user space']]]],
|
||||
false,
|
||||
true,
|
||||
],
|
||||
|
|
|
@ -3,10 +3,10 @@ const path = require('path')
|
|||
const merge = require('webpack-merge')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
|
||||
const core = require('./core/webpack')
|
||||
|
||||
const accessibility = require('./apps/accessibility/webpack')
|
||||
const comments = require('./apps/comments/webpack')
|
||||
const core = require('./core/webpack')
|
||||
const files = require('./apps/files/webpack')
|
||||
const files_sharing = require('./apps/files_sharing/webpack')
|
||||
const files_trashbin = require('./apps/files_trashbin/webpack')
|
||||
const files_versions = require('./apps/files_versions/webpack')
|
||||
|
@ -18,14 +18,15 @@ const updatenotifications = require('./apps/updatenotification/webpack')
|
|||
const workflowengine = require('./apps/workflowengine/webpack')
|
||||
|
||||
const modules = {
|
||||
core,
|
||||
settings,
|
||||
accessibility,
|
||||
comments,
|
||||
core,
|
||||
files,
|
||||
files_sharing,
|
||||
files_trashbin,
|
||||
files_versions,
|
||||
oauth2,
|
||||
settings,
|
||||
systemtags,
|
||||
twofactor_backupscodes,
|
||||
updatenotifications,
|
||||
|
@ -80,7 +81,10 @@ module.exports = []
|
|||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
// automatically detect necessary packages to
|
||||
// transpile in the node_modules folder
|
||||
exclude: /node_modules(?!(\/|\\)(p-finally|p-limit|p-locate|p-queue|p-timeout|p-try)(\/|\\))/
|
||||
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif)$/,
|
||||
|
|
Loading…
Reference in New Issue