Add OCA.Files.Sidebar and migrate sharing (#15719)
Add OCA.Files.Sidebar and migrate sharing
This commit is contained in:
commit
f420ac94b0
98 changed files with 4943 additions and 603 deletions
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/
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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 {
|
||||
|
|
BIN
apps/files/js/dist/sidebar.js
vendored
Normal file
BIN
apps/files/js/dist/sidebar.js
vendored
Normal file
Binary file not shown.
BIN
apps/files/js/dist/sidebar.js.map
vendored
Normal file
BIN
apps/files/js/dist/sidebar.js.map
vendored
Normal file
Binary file not shown.
|
@ -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'];
|
||||
|
|
31
apps/files/lib/Event/LoadSidebar.php
Normal file
31
apps/files/lib/Event/LoadSidebar.php
Normal file
|
@ -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 {
|
||||
|
||||
}
|
94
apps/files/src/components/LegacyTab.vue
Normal file
94
apps/files/src/components/LegacyTab.vue
Normal file
|
@ -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>
|
59
apps/files/src/components/LegacyView.vue
Normal file
59
apps/files/src/components/LegacyView.vue
Normal file
|
@ -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>
|
59
apps/files/src/models/Tab.js
Normal file
59
apps/files/src/models/Tab.js
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
67
apps/files/src/services/FileInfo.js
Normal file
67
apps/files/src/services/FileInfo.js
Normal file
|
@ -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
|
||||
}
|
109
apps/files/src/services/Sidebar.js
Normal file
109
apps/files/src/services/Sidebar.js
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
59
apps/files/src/sidebar.js
Normal file
59
apps/files/src/sidebar.js
Normal file
|
@ -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')
|
||||
})
|
358
apps/files/src/views/Sidebar.vue
Normal file
358
apps/files/src/views/Sidebar.vue
Normal file
|
@ -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();
|
||||
|
|
13
apps/files/webpack.js
Normal file
13
apps/files/webpack.js
Normal file
|
@ -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 () {
|
||||
|
|
32
apps/files_sharing/css/icons.scss
Normal file
32
apps/files_sharing/css/icons.scss
Normal file
|
@ -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);
|
||||
}
|
BIN
apps/files_sharing/js/dist/additionalScripts.js
vendored
BIN
apps/files_sharing/js/dist/additionalScripts.js
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/additionalScripts.js.map
vendored
BIN
apps/files_sharing/js/dist/additionalScripts.js.map
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/collaboration.js
vendored
BIN
apps/files_sharing/js/dist/collaboration.js
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/collaboration.js.map
vendored
BIN
apps/files_sharing/js/dist/collaboration.js.map
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.3.js
vendored
BIN
apps/files_sharing/js/dist/files_sharing.3.js
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.3.js.map
vendored
BIN
apps/files_sharing/js/dist/files_sharing.3.js.map
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.4.js
vendored
BIN
apps/files_sharing/js/dist/files_sharing.4.js
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.4.js.map
vendored
BIN
apps/files_sharing/js/dist/files_sharing.4.js.map
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.5.js
vendored
Normal file
BIN
apps/files_sharing/js/dist/files_sharing.5.js
vendored
Normal file
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.5.js.map
vendored
Normal file
BIN
apps/files_sharing/js/dist/files_sharing.5.js.map
vendored
Normal file
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.js
vendored
BIN
apps/files_sharing/js/dist/files_sharing.js
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.js.map
vendored
BIN
apps/files_sharing/js/dist/files_sharing.js.map
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing_tab.js
vendored
Normal file
BIN
apps/files_sharing/js/dist/files_sharing_tab.js
vendored
Normal file
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing_tab.js.map
vendored
Normal file
BIN
apps/files_sharing/js/dist/files_sharing_tab.js.map
vendored
Normal file
Binary file not shown.
|
@ -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();
|
||||
|
|
254
apps/files_sharing/src/components/SharingEntry.vue
Normal file
254
apps/files_sharing/src/components/SharingEntry.vue
Normal file
|
@ -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>
|
117
apps/files_sharing/src/components/SharingEntryInternal.vue
Normal file
117
apps/files_sharing/src/components/SharingEntryInternal.vue
Normal file
|
@ -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>
|
779
apps/files_sharing/src/components/SharingEntryLink.vue
Normal file
779
apps/files_sharing/src/components/SharingEntryLink.vue
Normal file
|
@ -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>
|
97
apps/files_sharing/src/components/SharingEntrySimple.vue
Normal file
97
apps/files_sharing/src/components/SharingEntrySimple.vue
Normal file
|
@ -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>
|
444
apps/files_sharing/src/components/SharingInput.vue
Normal file
444
apps/files_sharing/src/components/SharingInput.vue
Normal file
|
@ -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>
|
43
apps/files_sharing/src/files_sharing_tab.js
Normal file
43
apps/files_sharing/src/files_sharing_tab.js
Normal file
|
@ -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))
|
||||
}
|
||||
})
|
114
apps/files_sharing/src/mixins/ShareRequests.js
Normal file
114
apps/files_sharing/src/mixins/ShareRequests.js
Normal file
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
apps/files_sharing/src/mixins/ShareTypes.js
Normal file
39
apps/files_sharing/src/mixins/ShareTypes.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
303
apps/files_sharing/src/mixins/SharesMixin.js
Normal file
303
apps/files_sharing/src/mixins/SharesMixin.js
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
469
apps/files_sharing/src/models/Share.js
Normal file
469
apps/files_sharing/src/models/Share.js
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
223
apps/files_sharing/src/services/ConfigService.js
Normal file
223
apps/files_sharing/src/services/ConfigService.js
Normal file
|
@ -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 : {}
|
||||
}
|
||||
|
||||
}
|
63
apps/files_sharing/src/services/ExternalLinkActions.js
Normal file
63
apps/files_sharing/src/services/ExternalLinkActions.js
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
71
apps/files_sharing/src/services/ShareSearch.js
Normal file
71
apps/files_sharing/src/services/ShareSearch.js
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
49
apps/files_sharing/src/services/TabSections.js
Normal file
49
apps/files_sharing/src/services/TabSections.js
Normal file
|
@ -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')
|
||||
}
|
||||
})
|
||||
|
||||
|
|
86
apps/files_sharing/src/utils/SharedWithMe.js
Normal file
86
apps/files_sharing/src/utils/SharedWithMe.js
Normal file
|
@ -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 }
|
141
apps/files_sharing/src/views/SharingLinkList.vue
Normal file
141
apps/files_sharing/src/views/SharingLinkList.vue
Normal file
|
@ -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>
|
76
apps/files_sharing/src/views/SharingList.vue
Normal file
76
apps/files_sharing/src/views/SharingList.vue
Normal file
|
@ -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>
|
334
apps/files_sharing/src/views/SharingTab.vue
Normal file
334
apps/files_sharing/src/views/SharingTab.vue
Normal file
|
@ -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: {
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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();
|
||||
|
|
14
babel.config.js
Normal file
14
babel.config.js
Normal file
|
@ -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;
|
||||
}
|
||||
|
|
BIN
core/js/dist/login.js
vendored
BIN
core/js/dist/login.js
vendored
Binary file not shown.
BIN
core/js/dist/login.js.map
vendored
BIN
core/js/dist/login.js.map
vendored
Binary file not shown.
BIN
core/js/dist/main.js
vendored
BIN
core/js/dist/main.js
vendored
Binary file not shown.
BIN
core/js/dist/main.js.map
vendored
BIN
core/js/dist/main.js.map
vendored
Binary file not shown.
BIN
core/js/dist/maintenance.js
vendored
BIN
core/js/dist/maintenance.js
vendored
Binary file not shown.
BIN
core/js/dist/maintenance.js.map
vendored
BIN
core/js/dist/maintenance.js.map
vendored
Binary file not shown.
BIN
core/js/dist/systemtags.js
vendored
BIN
core/js/dist/systemtags.js
vendored
Binary file not shown.
BIN
core/js/dist/systemtags.js.map
vendored
BIN
core/js/dist/systemtags.js.map
vendored
Binary file not shown.
|
@ -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;
|
||||
|
|
19
core/src/Polyfill/closest.js
Normal file
19
core/src/Polyfill/closest.js
Normal file
|
@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
40
package-lock.json
generated
40
package-lock.json
generated
|
@ -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 a new issue