Add OCA.Files.Sidebar
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
parent
ea6f423e2c
commit
fd90af50d9
|
@ -1,5 +1,8 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ['@babel/plugin-syntax-dynamic-import'],
|
plugins: [
|
||||||
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
|
['@babel/plugin-proposal-class-properties', { loose: true }]
|
||||||
|
],
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
'@babel/preset-env',
|
'@babel/preset-env',
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -30,6 +30,7 @@ lint-fix-watch:
|
||||||
clean:
|
clean:
|
||||||
rm -rf apps/accessibility/js/
|
rm -rf apps/accessibility/js/
|
||||||
rm -rf apps/comments/js/
|
rm -rf apps/comments/js/
|
||||||
|
rm -rf apps/files/js/dist/
|
||||||
rm -rf apps/files_sharing/js/dist/
|
rm -rf apps/files_sharing/js/dist/
|
||||||
rm -rf apps/files_trashbin/js/
|
rm -rf apps/files_trashbin/js/
|
||||||
rm -rf apps/files_versions/js/
|
rm -rf apps/files_versions/js/
|
||||||
|
@ -47,6 +48,7 @@ clean-dev:
|
||||||
clean-git: clean
|
clean-git: clean
|
||||||
git checkout -- apps/accessibility/js/
|
git checkout -- apps/accessibility/js/
|
||||||
git checkout -- apps/comments/js/
|
git checkout -- apps/comments/js/
|
||||||
|
git checkout -- apps/files/js/dist/
|
||||||
git checkout -- apps/files_sharing/js/dist/
|
git checkout -- apps/files_sharing/js/dist/
|
||||||
git checkout -- apps/files_trashbin/js/
|
git checkout -- apps/files_trashbin/js/
|
||||||
git checkout -- apps/files_versions/js/
|
git checkout -- apps/files_versions/js/
|
||||||
|
|
|
@ -104,7 +104,7 @@
|
||||||
actionHandler: function(fileName, context) {
|
actionHandler: function(fileName, context) {
|
||||||
context.$file.find('.action-comment').tooltip('hide')
|
context.$file.find('.action-comment').tooltip('hide')
|
||||||
// open sidebar in comments section
|
// open sidebar in comments section
|
||||||
context.fileList.showDetailsView(fileName, 'commentsTabView')
|
context.fileList.showDetailsView(fileName, 'comments')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -85,8 +85,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fit app list view heights */
|
/* fit app list view heights */
|
||||||
.app-files #app-content>.viewcontainer {
|
.app-files #app-content > .viewcontainer {
|
||||||
min-height: 0%;
|
min-height: 0%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-files #app-content {
|
.app-files #app-content {
|
||||||
|
|
|
@ -704,6 +704,12 @@
|
||||||
}
|
}
|
||||||
context.fileList.do_delete(fileName, context.dir);
|
context.fileList.do_delete(fileName, context.dir);
|
||||||
$('.tipsy').remove();
|
$('.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
|
* @param {string} [tabId] optional tab id to select
|
||||||
*/
|
*/
|
||||||
showDetailsView: function(fileName, tabId) {
|
showDetailsView: function(fileName, tabId) {
|
||||||
|
console.warn('showDetailsView is deprecated! Use OCA.Files.Sidebar.activeTab. It will be removed in nextcloud 20.');
|
||||||
this._updateDetailsView(fileName);
|
this._updateDetailsView(fileName);
|
||||||
if (tabId) {
|
if (tabId) {
|
||||||
this._detailsView.selectTab(tabId);
|
OCA.Files.Sidebar.activeTab = tabId;
|
||||||
}
|
}
|
||||||
OC.Apps.showAppSidebar(this._detailsView.$el);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -623,48 +623,23 @@
|
||||||
* @param {string|OCA.Files.FileInfoModel} fileName file name from the current list or a FileInfoModel object
|
* @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
|
* @param {boolean} [show=true] whether to open the sidebar if it was closed
|
||||||
*/
|
*/
|
||||||
_updateDetailsView: function(fileName, show) {
|
_updateDetailsView: function(fileName) {
|
||||||
if (!this._detailsView) {
|
if (!(OCA.Files && OCA.Files.Sidebar)) {
|
||||||
|
console.error('No sidebar available');
|
||||||
return;
|
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) {
|
if (!fileName) {
|
||||||
this._detailsView.setFileInfo(null);
|
OCA.Files.Sidebar.file = null
|
||||||
if (this._currentFileModel) {
|
return
|
||||||
this._currentFileModel.off();
|
} else if (typeof fileName !== 'string') {
|
||||||
}
|
fileName = ''
|
||||||
this._currentFileModel = null;
|
|
||||||
OC.Apps.hideAppSidebar(this._detailsView.$el);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (show && this._detailsView.$el.hasClass('disappear')) {
|
// open sidebar and set file
|
||||||
OC.Apps.showAppSidebar(this._detailsView.$el);
|
const dir = `${this.dirInfo.path}/${this.dirInfo.name}`
|
||||||
}
|
const path = `${dir}/${fileName}`
|
||||||
|
OCA.Files.Sidebar.file = path.replace('//', '/')
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1404,6 +1379,13 @@
|
||||||
return OC.MimeType.getIconUrl('dir-external');
|
return OC.MimeType.getIconUrl('dir-external');
|
||||||
} else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
|
} else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
|
||||||
return OC.MimeType.getIconUrl('dir-' + 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');
|
return OC.MimeType.getIconUrl('dir');
|
||||||
}
|
}
|
||||||
|
@ -3654,8 +3636,10 @@
|
||||||
* Register a tab view to be added to all views
|
* Register a tab view to be added to all views
|
||||||
*/
|
*/
|
||||||
registerTabView: function(tabView) {
|
registerTabView: function(tabView) {
|
||||||
if (this._detailsView) {
|
console.warn('registerTabView is deprecated! It will be removed in nextcloud 20.');
|
||||||
this._detailsView.addTabView(tabView);
|
const name = tabView.getLabel()
|
||||||
|
if (name) {
|
||||||
|
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab(name, tabView, true))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -3663,8 +3647,9 @@
|
||||||
* Register a detail view to be added to all views
|
* Register a detail view to be added to all views
|
||||||
*/
|
*/
|
||||||
registerDetailView: function(detailView) {
|
registerDetailView: function(detailView) {
|
||||||
if (this._detailsView) {
|
console.warn('registerDetailView is deprecated! It will be removed in nextcloud 20.');
|
||||||
this._detailsView.addDetailView(detailView);
|
if (detailView.el) {
|
||||||
|
OCA.Files.Sidebar.registerSecondaryView(detailView)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,34 @@
|
||||||
[
|
[
|
||||||
|
"dist/sidebar.js",
|
||||||
"app.js",
|
"app.js",
|
||||||
"templates.js",
|
|
||||||
"file-upload.js",
|
|
||||||
"newfilemenu.js",
|
|
||||||
"jquery.fileupload.js",
|
|
||||||
"jquery-visibility.js",
|
|
||||||
"fileinfomodel.js",
|
|
||||||
"filesummary.js",
|
|
||||||
"filemultiselectmenu.js",
|
|
||||||
"breadcrumb.js",
|
"breadcrumb.js",
|
||||||
"filelist.js",
|
|
||||||
"search.js",
|
|
||||||
"favoritesfilelist.js",
|
|
||||||
"recentfilelist.js",
|
|
||||||
"tagsplugin.js",
|
|
||||||
"gotoplugin.js",
|
|
||||||
"favoritesplugin.js",
|
|
||||||
"recentplugin.js",
|
|
||||||
"detailfileinfoview.js",
|
"detailfileinfoview.js",
|
||||||
"sidebarpreviewmanager.js",
|
|
||||||
"sidebarpreviewtext.js",
|
|
||||||
"detailtabview.js",
|
|
||||||
"semaphore.js",
|
|
||||||
"mainfileinfodetailview.js",
|
|
||||||
"operationprogressbar.js",
|
|
||||||
"detailsview.js",
|
"detailsview.js",
|
||||||
|
"detailtabview.js",
|
||||||
|
"favoritesfilelist.js",
|
||||||
|
"favoritesplugin.js",
|
||||||
|
"file-upload.js",
|
||||||
"fileactions.js",
|
"fileactions.js",
|
||||||
"fileactionsmenu.js",
|
"fileactionsmenu.js",
|
||||||
|
"fileinfomodel.js",
|
||||||
|
"filelist.js",
|
||||||
|
"filemultiselectmenu.js",
|
||||||
"files.js",
|
"files.js",
|
||||||
|
"filesummary.js",
|
||||||
|
"gotoplugin.js",
|
||||||
|
"jquery-visibility.js",
|
||||||
|
"jquery.fileupload.js",
|
||||||
"keyboardshortcuts.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"
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
<!--
|
||||||
|
- @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,
|
||||||
|
default: '',
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// append the backbone element and set the FileInfo
|
||||||
|
this.component.$el.appendTo(this.$el)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setFileInfo(fileInfo) {
|
||||||
|
this.component.setFileInfo(new OCA.Files.FileInfoModel(fileInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -0,0 +1,59 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @license GNU AGPL version 3 or any later version
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as
|
||||||
|
- published by the Free Software Foundation, either version 3 of the
|
||||||
|
- License, or (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'LegacyView',
|
||||||
|
props: {
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
fileInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
fileInfo(fileInfo) {
|
||||||
|
// update the backbone model FileInfo
|
||||||
|
this.setFileInfo(fileInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// append the backbone element and set the FileInfo
|
||||||
|
this.component.$el.replaceAll(this.$el)
|
||||||
|
this.setFileInfo(this.fileInfo)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setFileInfo(fileInfo) {
|
||||||
|
this.component.setFileInfo(new OCA.Files.FileInfoModel(fileInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class Tab {
|
||||||
|
|
||||||
|
#component;
|
||||||
|
#legacy;
|
||||||
|
#name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new tab instance
|
||||||
|
*
|
||||||
|
* @param {string} name the name of this tab
|
||||||
|
* @param {Object} component the vue component
|
||||||
|
* @param {boolean} [legacy] is this a legacy tab
|
||||||
|
*/
|
||||||
|
constructor(name, component, legacy) {
|
||||||
|
this.#name = name
|
||||||
|
this.#component = component
|
||||||
|
this.#legacy = legacy === true
|
||||||
|
|
||||||
|
if (this.#legacy) {
|
||||||
|
console.warn('Legacy tabs are deprecated! They will be removed in nextcloud 20.')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.#name
|
||||||
|
}
|
||||||
|
|
||||||
|
get component() {
|
||||||
|
return this.#component
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLegacyTab() {
|
||||||
|
return this.#legacy === true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
|
||||||
|
export default async function(url) {
|
||||||
|
const response = await axios({
|
||||||
|
method: 'PROPFIND',
|
||||||
|
url,
|
||||||
|
data: `<?xml version="1.0"?>
|
||||||
|
<d:propfind xmlns:d="DAV:"
|
||||||
|
xmlns:oc="http://owncloud.org/ns"
|
||||||
|
xmlns:nc="http://nextcloud.org/ns"
|
||||||
|
xmlns:ocs="http://open-collaboration-services.org/ns">
|
||||||
|
<d:prop>
|
||||||
|
<d:getlastmodified />
|
||||||
|
<d:getetag />
|
||||||
|
<d:getcontenttype />
|
||||||
|
<d:resourcetype />
|
||||||
|
<oc:fileid />
|
||||||
|
<oc:permissions />
|
||||||
|
<oc:size />
|
||||||
|
<d:getcontentlength />
|
||||||
|
<nc:has-preview />
|
||||||
|
<nc:mount-type />
|
||||||
|
<nc:is-encrypted />
|
||||||
|
<ocs:share-permissions />
|
||||||
|
<oc:tags />
|
||||||
|
<oc:favorite />
|
||||||
|
<oc:comments-unread />
|
||||||
|
<oc:owner-id />
|
||||||
|
<oc:owner-display-name />
|
||||||
|
<oc:share-types />
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>`
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: create new parser or use cdav-lib when available
|
||||||
|
const file = OCA.Files.App.fileList.filesClient._client.parseMultiStatus(response.data)
|
||||||
|
// TODO: create new parser or use cdav-lib when available
|
||||||
|
const fileInfo = OCA.Files.App.fileList.filesClient._parseFileInfo(file[0])
|
||||||
|
|
||||||
|
// TODO remove when no more legacy backbone is used
|
||||||
|
fileInfo.get = (key) => fileInfo[key]
|
||||||
|
fileInfo.isDirectory = () => fileInfo.mimetype === 'httpd/unix-directory'
|
||||||
|
|
||||||
|
return fileInfo
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class Sidebar {
|
||||||
|
|
||||||
|
#state;
|
||||||
|
#view;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// init empty state
|
||||||
|
this.#state = {}
|
||||||
|
|
||||||
|
// init default values
|
||||||
|
this.#state.tabs = []
|
||||||
|
this.#state.views = []
|
||||||
|
this.#state.file = ''
|
||||||
|
this.#state.activeTab = ''
|
||||||
|
console.debug('OCA.Files.Sidebar initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the sidebar state
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @memberof Sidebar
|
||||||
|
* @returns {Object} the data state
|
||||||
|
*/
|
||||||
|
get state() {
|
||||||
|
return this.#state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @memberof Sidebar
|
||||||
|
* Register a new tab view
|
||||||
|
*
|
||||||
|
* @param {Object} tab a new unregistered tab
|
||||||
|
* @memberof Sidebar
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
registerTab(tab) {
|
||||||
|
const hasDuplicate = this.#state.tabs.findIndex(check => check.name === tab.name) > -1
|
||||||
|
if (!hasDuplicate) {
|
||||||
|
this.#state.tabs.push(tab)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
console.error(`An tab with the same name ${tab.name} already exists`, tab)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSecondaryView(view) {
|
||||||
|
const hasDuplicate = this.#state.views.findIndex(check => check.cid === view.cid) > -1
|
||||||
|
if (!hasDuplicate) {
|
||||||
|
this.#state.views.push(view)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
console.error(`A similar view already exists`, view)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current sidebar file data
|
||||||
|
*
|
||||||
|
* @param {string} path the file path to load
|
||||||
|
* @memberof Sidebar
|
||||||
|
*/
|
||||||
|
set file(path) {
|
||||||
|
this.#state.file = path
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current sidebar file data
|
||||||
|
*
|
||||||
|
* @returns {String} the current opened file
|
||||||
|
* @memberof Sidebar
|
||||||
|
*/
|
||||||
|
get file() {
|
||||||
|
return this.#state.file
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current sidebar tab
|
||||||
|
*
|
||||||
|
* @param {string} id the tab unique id
|
||||||
|
* @memberof Sidebar
|
||||||
|
*/
|
||||||
|
set activeTab(id) {
|
||||||
|
this.#state.activeTab = id
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Vue from 'vue'
|
||||||
|
import SidebarView from './views/Sidebar.vue'
|
||||||
|
import Sidebar from './services/Sidebar'
|
||||||
|
import Tab from './models/Tab'
|
||||||
|
import VueClipboard from 'vue-clipboard2'
|
||||||
|
|
||||||
|
Vue.use(VueClipboard)
|
||||||
|
|
||||||
|
Vue.prototype.t = t
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Init Sidebar Service
|
||||||
|
if (window.OCA && window.OCA.Files) {
|
||||||
|
Object.assign(window.OCA.Files, { Sidebar: new Sidebar() })
|
||||||
|
Object.assign(window.OCA.Files.Sidebar, { Tab })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we have a proper layout
|
||||||
|
if (document.getElementById('content')) {
|
||||||
|
|
||||||
|
// Make sure we have a mountpoint
|
||||||
|
if (!document.getElementById('app-sidebar')) {
|
||||||
|
var contentElement = document.getElementById('content')
|
||||||
|
var sidebarElement = document.createElement('div')
|
||||||
|
sidebarElement.id = 'app-sidebar'
|
||||||
|
contentElement.appendChild(sidebarElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init vue app
|
||||||
|
const AppSidebar = new Vue({
|
||||||
|
// eslint-disable-next-line vue/match-component-file-name
|
||||||
|
name: 'SidebarRoot',
|
||||||
|
render: h => h(SidebarView)
|
||||||
|
})
|
||||||
|
AppSidebar.$mount('#app-sidebar')
|
||||||
|
})
|
|
@ -0,0 +1,345 @@
|
||||||
|
<!--
|
||||||
|
- @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: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"
|
||||||
|
: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}${encodeURIComponent(this.file)}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current active tab handler
|
||||||
|
* @param {string} id the tab id to set as active
|
||||||
|
* @returns {string} the current active tab
|
||||||
|
*/
|
||||||
|
activeTab: {
|
||||||
|
get: function() {
|
||||||
|
return this.Sidebar.activeTab
|
||||||
|
},
|
||||||
|
set: function(id) {
|
||||||
|
OCA.Files.Sidebar.activeTab = id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>`
|
||||||
|
})
|
||||||
|
} 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>
|
|
@ -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',
|
'OCA\Files::loadAdditionalScripts',
|
||||||
function() {
|
function() {
|
||||||
\OCP\Util::addScript('files_sharing', 'dist/additionalScripts');
|
\OCP\Util::addScript('files_sharing', 'dist/additionalScripts');
|
||||||
|
\OCP\Util::addStyle('files_sharing', 'icons');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
\OC::$server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', function () {
|
\OC::$server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', function () {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This is the icons used in the sharing ui (multiselect)
|
||||||
|
.icon-room {
|
||||||
|
@include icon-color('app', 'spreed', $color-black);
|
||||||
|
}
|
||||||
|
.icon-circle {
|
||||||
|
@include icon-color('circles', 'circles', $color-black, 3, false);
|
||||||
|
}
|
||||||
|
.icon-guests {
|
||||||
|
@include icon-color('app', 'guests', $color-black);
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ $tmpl = new OCP\Template('files_sharing', 'list', '');
|
||||||
$tmpl->assign('showgridview', $showgridview && !$isIE);
|
$tmpl->assign('showgridview', $showgridview && !$isIE);
|
||||||
|
|
||||||
OCP\Util::addScript('files_sharing', 'dist/files_sharing');
|
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');
|
\OC::$server->getEventDispatcher()->dispatch('\OCP\Collaboration\Resources::loadAdditionalScripts');
|
||||||
|
|
||||||
$tmpl->printPage();
|
$tmpl->printPage();
|
||||||
|
|
|
@ -0,0 +1,249 @@
|
||||||
|
<!--
|
||||||
|
- @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">
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<ActionButton icon="icon-delete" :disabled="saving" @click.prevent="onDelete">
|
||||||
|
{{ t('files_sharing', 'Unshare') }}
|
||||||
|
</ActionButton>
|
||||||
|
</Actions>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
|
||||||
|
import Actions from 'nextcloud-vue/dist/Components/Actions'
|
||||||
|
import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
|
||||||
|
import ActionCheckbox from 'nextcloud-vue/dist/Components/ActionCheckbox'
|
||||||
|
import ActionInput from 'nextcloud-vue/dist/Components/ActionInput'
|
||||||
|
import ActionTextEditable from 'nextcloud-vue/dist/Components/ActionTextEditable'
|
||||||
|
import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import Share from '../models/Share'
|
||||||
|
import SharesMixin from '../mixins/SharesMixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SharingEntry',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Actions,
|
||||||
|
ActionButton,
|
||||||
|
ActionCheckbox,
|
||||||
|
ActionInput,
|
||||||
|
ActionTextEditable,
|
||||||
|
Avatar
|
||||||
|
},
|
||||||
|
|
||||||
|
directives: {
|
||||||
|
Tooltip
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [SharesMixin],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
permissionsEdit: OC.PERMISSION_UPDATE,
|
||||||
|
permissionsRead: OC.PERMISSION_READ,
|
||||||
|
permissionsShare: OC.PERMISSION_SHARE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
let title = this.share.shareWithDisplayName
|
||||||
|
if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GROUP) {
|
||||||
|
title += ` (${t('files_sharing', 'group')})`
|
||||||
|
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_ROOM) {
|
||||||
|
title += ` (${t('files_sharing', 'conversation')})`
|
||||||
|
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE) {
|
||||||
|
title += ` (${t('files_sharing', 'remote')})`
|
||||||
|
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP) {
|
||||||
|
title += ` (${t('files_sharing', 'remote group')})`
|
||||||
|
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GUEST) {
|
||||||
|
title += ` (${t('files_sharing', 'guest')})`
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip() {
|
||||||
|
if (this.share.owner !== this.share.uidFileOwner) {
|
||||||
|
const data = {
|
||||||
|
// todo: strong or italic?
|
||||||
|
// but the t function escape any html from the data :/
|
||||||
|
user: this.share.shareWithDisplayName,
|
||||||
|
owner: this.share.owner
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GROUP) {
|
||||||
|
return t('files_sharing', 'Shared with the group {user} by {owner}', data)
|
||||||
|
} else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_ROOM) {
|
||||||
|
return t('files_sharing', 'Shared with the conversation {user} by {owner}', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('files_sharing', 'Shared with {user} by {owner}', data)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
canHaveNote() {
|
||||||
|
return this.share.type !== this.SHARE_TYPES.SHARE_TYPE_REMOTE
|
||||||
|
&& this.share.type !== this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can the sharee edit the shared file ?
|
||||||
|
*/
|
||||||
|
canEdit: {
|
||||||
|
get: function() {
|
||||||
|
return this.share.hasUpdatePermission
|
||||||
|
},
|
||||||
|
set: function(checked) {
|
||||||
|
this.updatePermissions(checked, this.canReshare)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can the sharee reshare the file ?
|
||||||
|
*/
|
||||||
|
canReshare: {
|
||||||
|
get: function() {
|
||||||
|
return this.share.hasSharePermission
|
||||||
|
},
|
||||||
|
set: function(checked) {
|
||||||
|
this.updatePermissions(this.canEdit, checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updatePermissions(isEditChecked, isReshareChecked) {
|
||||||
|
// calc permissions if checked
|
||||||
|
const permissions = this.permissionsRead
|
||||||
|
| (isEditChecked ? this.permissionsEdit : 0)
|
||||||
|
| (isReshareChecked ? this.permissionsShare : 0)
|
||||||
|
|
||||||
|
this.share.permissions = permissions
|
||||||
|
this.queueUpdate('permissions')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sharing-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 44px;
|
||||||
|
&__desc {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
line-height: 1.2em;
|
||||||
|
p {
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,117 @@
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SharingEntrySimple
|
||||||
|
class="sharing-entry__internal"
|
||||||
|
:title="t('files_sharing', 'Internal link')"
|
||||||
|
:subtitle="internalLinkSubtitle">
|
||||||
|
<template #avatar>
|
||||||
|
<div class="avatar-external icon-external-white" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ActionLink ref="copyButton"
|
||||||
|
:href="internalLink"
|
||||||
|
target="_blank"
|
||||||
|
:icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
|
||||||
|
@click.prevent="copyLink">
|
||||||
|
{{ clipboardTooltip }}
|
||||||
|
</ActionLink>
|
||||||
|
</SharingEntrySimple>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
import ActionLink from 'nextcloud-vue/dist/Components/ActionLink'
|
||||||
|
import SharingEntrySimple from './SharingEntrySimple'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SharingEntryInternal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ActionLink,
|
||||||
|
SharingEntrySimple
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
fileInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
copied: false,
|
||||||
|
copySuccess: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Get the internal link to this file id
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
internalLink() {
|
||||||
|
return window.location.protocol + '//' + window.location.host + generateUrl('/f/') + this.fileInfo.id
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clipboard v-tooltip message
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
clipboardTooltip() {
|
||||||
|
if (this.copied) {
|
||||||
|
return this.copySuccess
|
||||||
|
? t('files_sharing', 'Link copied')
|
||||||
|
: t('files_sharing', 'Cannot copy, please copy the link manually')
|
||||||
|
}
|
||||||
|
return t('files_sharing', 'Copy to clipboard')
|
||||||
|
},
|
||||||
|
|
||||||
|
internalLinkSubtitle() {
|
||||||
|
if (this.fileInfo.type === 'dir') {
|
||||||
|
return t('files_sharing', 'Only works for users with access to this folder')
|
||||||
|
}
|
||||||
|
return t('files_sharing', 'Only works for users with access to this file')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async copyLink() {
|
||||||
|
try {
|
||||||
|
await this.$copyText(this.internalLink)
|
||||||
|
// focus and show the tooltip
|
||||||
|
this.$refs.copyButton.$el.focus()
|
||||||
|
this.copySuccess = true
|
||||||
|
this.copied = true
|
||||||
|
} catch (error) {
|
||||||
|
this.copySuccess = false
|
||||||
|
this.copied = true
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.copySuccess = false
|
||||||
|
this.copied = false
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sharing-entry__internal {
|
||||||
|
.avatar-external {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 18px;
|
||||||
|
background-color: var(--color-text-maxcontrast);
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.icon-checkmark-color {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,769 @@
|
||||||
|
<!--
|
||||||
|
- @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="isShareOwner">
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<components :is="action" v-for="(action, index) in externalActions" :key="index" />
|
||||||
|
|
||||||
|
<ActionButton icon="icon-delete" :disabled="saving" @click.prevent="onDelete">
|
||||||
|
{{ t('files_sharing', 'Delete share') }}
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton v-if="!isEmailShareType && canReshare"
|
||||||
|
class="new-share-link"
|
||||||
|
icon="icon-add"
|
||||||
|
@click.prevent.stop="onNewLinkShare">
|
||||||
|
{{ t('files_sharing', 'Add another link') }}
|
||||||
|
</ActionButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Create new share -->
|
||||||
|
<ActionButton v-else-if="canReshare"
|
||||||
|
class="new-share-link"
|
||||||
|
icon="icon-add"
|
||||||
|
@click.prevent.stop="onNewLinkShare">
|
||||||
|
{{ t('files_sharing', 'Create a new share link') }}
|
||||||
|
</ActionButton>
|
||||||
|
</Actions>
|
||||||
|
|
||||||
|
<!-- loading indicator to replace the menu -->
|
||||||
|
<div v-else class="icon-loading-small sharing-entry__loading" />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
|
||||||
|
import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
|
||||||
|
import ActionCheckbox from 'nextcloud-vue/dist/Components/ActionCheckbox'
|
||||||
|
import ActionRadio from 'nextcloud-vue/dist/Components/ActionRadio'
|
||||||
|
import ActionInput from 'nextcloud-vue/dist/Components/ActionInput'
|
||||||
|
import ActionText from 'nextcloud-vue/dist/Components/ActionText'
|
||||||
|
import ActionTextEditable from 'nextcloud-vue/dist/Components/ActionTextEditable'
|
||||||
|
import ActionLink from 'nextcloud-vue/dist/Components/ActionLink'
|
||||||
|
import Actions from 'nextcloud-vue/dist/Components/Actions'
|
||||||
|
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
|
||||||
|
import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
|
||||||
|
|
||||||
|
import Share from '../models/Share'
|
||||||
|
import SharesMixin from '../mixins/SharesMixin'
|
||||||
|
|
||||||
|
const passwordSet = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SharingEntryLink',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Actions,
|
||||||
|
ActionButton,
|
||||||
|
ActionCheckbox,
|
||||||
|
ActionRadio,
|
||||||
|
ActionInput,
|
||||||
|
ActionLink,
|
||||||
|
ActionText,
|
||||||
|
ActionTextEditable,
|
||||||
|
Avatar
|
||||||
|
},
|
||||||
|
|
||||||
|
directives: {
|
||||||
|
Tooltip
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [SharesMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
canReshare: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
copySuccess: true,
|
||||||
|
copied: false,
|
||||||
|
|
||||||
|
publicUploadRWValue: OC.PERMISSION_UPDATE | OC.PERMISSION_CREATE | OC.PERMISSION_READ | OC.PERMISSION_DELETE,
|
||||||
|
publicUploadRValue: OC.PERMISSION_READ,
|
||||||
|
publicUploadWValue: OC.PERMISSION_CREATE,
|
||||||
|
|
||||||
|
ExternalLinkActions: OCA.Sharing.ExternalLinkActions.state
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Generate a unique random id for this SharingEntryLink only
|
||||||
|
* This allows ActionRadios to have the same name prop
|
||||||
|
* but not to impact others SharingEntryLink
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
randomId() {
|
||||||
|
return Math.random().toString(27).substr(2)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link share label
|
||||||
|
* TODO: allow editing
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
title() {
|
||||||
|
// if we have a valid existing share (not pending)
|
||||||
|
if (this.share && this.share.id) {
|
||||||
|
if (!this.isShareOwner && this.share.ownerDisplayName) {
|
||||||
|
return t('files_sharing', 'Shared via link by {initiator}', {
|
||||||
|
initiator: this.share.ownerDisplayName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.share.label && this.share.label.trim() !== '') {
|
||||||
|
return this.share.label
|
||||||
|
}
|
||||||
|
if (this.isEmailShareType) {
|
||||||
|
return this.share.shareWith
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t('files_sharing', 'Share link')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the current share password protected ?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isPasswordProtected: {
|
||||||
|
get: function() {
|
||||||
|
return this.config.enforcePasswordForPublicLink
|
||||||
|
|| !!this.share.password
|
||||||
|
},
|
||||||
|
set: async function(enabled) {
|
||||||
|
// TODO: directly save after generation to make sure the share is always protected
|
||||||
|
this.share.password = enabled ? await this.generatePassword() : ''
|
||||||
|
this.share.newPassword = this.share.password
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the current share an email share ?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isEmailShareType() {
|
||||||
|
return this.share
|
||||||
|
? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL
|
||||||
|
: false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending data.
|
||||||
|
* If the share still doesn't have an id, it is not synced
|
||||||
|
* Therefore this is still not valid and requires user input
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
pendingPassword() {
|
||||||
|
return this.config.enforcePasswordForPublicLink && this.share && !this.share.id
|
||||||
|
},
|
||||||
|
pendingExpirationDate() {
|
||||||
|
return this.config.isDefaultExpireDateEnforced && this.share && !this.share.id
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can the recipient edit the file ?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
canUpdate: {
|
||||||
|
get: function() {
|
||||||
|
return this.share.hasUpdatePermission
|
||||||
|
},
|
||||||
|
set: function(enabled) {
|
||||||
|
this.share.permissions = enabled
|
||||||
|
? OC.PERMISSION_READ | OC.PERMISSION_UPDATE
|
||||||
|
: OC.PERMISSION_READ
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// if newPassword exists, but is empty, it means
|
||||||
|
// the user deleted the original password
|
||||||
|
hasUnsavedPassword() {
|
||||||
|
return this.share.newPassword !== undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the current share a folder ?
|
||||||
|
* TODO: move to a proper FileInfo model?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isFolder() {
|
||||||
|
return this.fileInfo.type === 'dir'
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the current file/folder have create permissions
|
||||||
|
* TODO: move to a proper FileInfo model?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
fileHasCreatePermission() {
|
||||||
|
return !!(this.fileInfo.permissions & OC.PERMISSION_CREATE)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the public share link
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
shareLink() {
|
||||||
|
return window.location.protocol + '//' + window.location.host + generateUrl('/s/') + this.share.token
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clipboard v-tooltip message
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
clipboardTooltip() {
|
||||||
|
if (this.copied) {
|
||||||
|
return this.copySuccess
|
||||||
|
? t('files_sharing', 'Link copied')
|
||||||
|
: t('files_sharing', 'Cannot copy, please copy the link manually')
|
||||||
|
}
|
||||||
|
return t('files_sharing', 'Copy to clipboard')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External aditionnal actions for the menu
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
externalActions() {
|
||||||
|
return this.ExternalLinkActions.actions
|
||||||
|
},
|
||||||
|
|
||||||
|
isPasswordPolicyEnabled() {
|
||||||
|
return typeof this.config.passwordPolicy === 'object'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Create a new share link and append it to the list
|
||||||
|
*/
|
||||||
|
async onNewLinkShare() {
|
||||||
|
const shareDefaults = {
|
||||||
|
share_type: OC.Share.SHARE_TYPE_LINK
|
||||||
|
}
|
||||||
|
if (this.config.isDefaultExpireDateEnforced) {
|
||||||
|
// default is empty string if not set
|
||||||
|
// expiration is the share object key, not expireDate
|
||||||
|
shareDefaults.expiration = this.config.defaultExpirationDateString
|
||||||
|
}
|
||||||
|
if (this.config.enableLinkPasswordByDefault) {
|
||||||
|
shareDefaults.password = await this.generatePassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not push yet if we need a password or an expiration date
|
||||||
|
if (this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced) {
|
||||||
|
this.loading = true
|
||||||
|
// if a share already exists, pushing it
|
||||||
|
if (this.share && !this.share.id) {
|
||||||
|
if (this.checkShare(this.share)) {
|
||||||
|
await this.pushNewLinkShare(this.share, true)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
this.open = true
|
||||||
|
OC.Notification.showTemporary(t('files_sharing', 'Error, please enter proper password and/or expiration date'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ELSE, show the pending popovermenu
|
||||||
|
// if password enforced, pre-fill with random one
|
||||||
|
if (this.config.enforcePasswordForPublicLink) {
|
||||||
|
shareDefaults.password = await this.generatePassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
// create share & close menu
|
||||||
|
const share = new Share(shareDefaults)
|
||||||
|
const component = await new Promise(resolve => {
|
||||||
|
this.$emit('add:share', share, resolve)
|
||||||
|
})
|
||||||
|
|
||||||
|
// open the menu on the
|
||||||
|
// freshly created share component
|
||||||
|
this.open = false
|
||||||
|
this.loading = false
|
||||||
|
component.open = true
|
||||||
|
|
||||||
|
// Nothing enforced, creating share directly
|
||||||
|
} else {
|
||||||
|
const share = new Share(shareDefaults)
|
||||||
|
await this.pushNewLinkShare(share)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a new link share to the server
|
||||||
|
* And update or append to the list
|
||||||
|
* accordingly
|
||||||
|
*
|
||||||
|
* @param {Share} share the new share
|
||||||
|
* @param {boolean} [update=false] do we update the current share ?
|
||||||
|
*/
|
||||||
|
async pushNewLinkShare(share, update) {
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
this.errors = {}
|
||||||
|
|
||||||
|
const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
|
||||||
|
const newShare = await this.createShare({
|
||||||
|
path,
|
||||||
|
shareType: OC.Share.SHARE_TYPE_LINK,
|
||||||
|
password: share.password,
|
||||||
|
expireDate: share.expireDate
|
||||||
|
// we do not allow setting the publicUpload
|
||||||
|
// before the share creation.
|
||||||
|
// Todo: We also need to fix the createShare method in
|
||||||
|
// lib/Controller/ShareAPIController.php to allow file drop
|
||||||
|
// (currently not supported on create, only update)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.open = false
|
||||||
|
|
||||||
|
console.debug('Link share created', newShare)
|
||||||
|
|
||||||
|
// if share already exists, copy link directly on next tick
|
||||||
|
let component
|
||||||
|
if (update) {
|
||||||
|
component = await new Promise(resolve => {
|
||||||
|
this.$emit('update:share', newShare, resolve)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// adding new share to the array and copying link to clipboard
|
||||||
|
// using promise so that we can copy link in the same click function
|
||||||
|
// and avoid firefox copy permissions issue
|
||||||
|
component = await new Promise(resolve => {
|
||||||
|
this.$emit('add:share', newShare, resolve)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the copy link method
|
||||||
|
// freshly created share component
|
||||||
|
// ! somehow does not works on firefox !
|
||||||
|
component.copyLink()
|
||||||
|
|
||||||
|
} catch ({ response }) {
|
||||||
|
const message = response.data.ocs.meta.message
|
||||||
|
if (message.match(/password/i)) {
|
||||||
|
this.onSyncError('password', message)
|
||||||
|
} else if (message.match(/date/i)) {
|
||||||
|
this.onSyncError('expireDate', message)
|
||||||
|
} else {
|
||||||
|
this.onSyncError('pending', message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On permissions change
|
||||||
|
* @param {Event} event js event
|
||||||
|
*/
|
||||||
|
togglePermissions(event) {
|
||||||
|
const permissions = parseInt(event.target.value, 10)
|
||||||
|
this.share.permissions = permissions
|
||||||
|
this.queueUpdate('permissions')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a valid policy password or
|
||||||
|
* request a valid password if password_policy
|
||||||
|
* is enabled
|
||||||
|
*
|
||||||
|
* @returns {string} a valid password
|
||||||
|
*/
|
||||||
|
async generatePassword() {
|
||||||
|
// password policy is enabled, let's request a pass
|
||||||
|
if (this.config.passwordPolicy.api && this.config.passwordPolicy.api.generate) {
|
||||||
|
try {
|
||||||
|
const request = await axios.get(this.config.passwordPolicy.api.generate)
|
||||||
|
if (request.data.ocs.data.password) {
|
||||||
|
return request.data.ocs.data.password
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.info('Error generating password from password_policy', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate password of 10 length based on passwordSet
|
||||||
|
return Array(10).fill(0)
|
||||||
|
.reduce((prev, curr) => {
|
||||||
|
prev += passwordSet.charAt(Math.floor(Math.random() * passwordSet.length))
|
||||||
|
return prev
|
||||||
|
}, '')
|
||||||
|
},
|
||||||
|
|
||||||
|
async copyLink() {
|
||||||
|
try {
|
||||||
|
await this.$copyText(this.shareLink)
|
||||||
|
// focus and show the tooltip
|
||||||
|
this.$refs.copyButton.$el.focus()
|
||||||
|
this.copySuccess = true
|
||||||
|
this.copied = true
|
||||||
|
} catch (error) {
|
||||||
|
this.copySuccess = false
|
||||||
|
this.copied = true
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.copySuccess = false
|
||||||
|
this.copied = false
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update newPassword values
|
||||||
|
* of share. If password is set but not newPassword
|
||||||
|
* then the user did not changed the password
|
||||||
|
* If both co-exists, the password have changed and
|
||||||
|
* we show it in plain text.
|
||||||
|
* Then on submit (or menu close), we sync it.
|
||||||
|
* @param {string} password the changed password
|
||||||
|
*/
|
||||||
|
onPasswordChange(password) {
|
||||||
|
this.$set(this.share, 'newPassword', password)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uncheck password protection
|
||||||
|
* We need this method because @update:checked
|
||||||
|
* is ran simultaneously as @uncheck, so
|
||||||
|
* so we cannot ensure data is up-to-date
|
||||||
|
*/
|
||||||
|
onPasswordDisable() {
|
||||||
|
this.share.password = ''
|
||||||
|
|
||||||
|
// reset password state after sync
|
||||||
|
this.$delete(this.share, 'newPassword')
|
||||||
|
|
||||||
|
// only update if valid share.
|
||||||
|
if (this.share.id) {
|
||||||
|
this.queueUpdate('password')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu have been closed or password has been submited.
|
||||||
|
* The only property that does not get
|
||||||
|
* synced automatically is the password
|
||||||
|
* So let's check if we have an unsaved
|
||||||
|
* password.
|
||||||
|
* expireDate is saved on datepicker pick
|
||||||
|
* or close.
|
||||||
|
*/
|
||||||
|
onPasswordSubmit() {
|
||||||
|
if (this.hasUnsavedPassword) {
|
||||||
|
this.share.password = this.share.newPassword
|
||||||
|
this.queueUpdate('password')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the share creation
|
||||||
|
* Used in the pending popover
|
||||||
|
*/
|
||||||
|
onCancel() {
|
||||||
|
// this.share already exists at this point,
|
||||||
|
// but is incomplete as not pushed to server
|
||||||
|
// YET. We can safely delete the share :)
|
||||||
|
this.$emit('remove:share', this.share)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sharing-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 44px;
|
||||||
|
&__desc {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.sharing-entry--share) &__actions {
|
||||||
|
.new-share-link {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharing-entry__action--public-upload {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loading {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 14px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// put menus to the left
|
||||||
|
// but only the first one
|
||||||
|
.action-item {
|
||||||
|
margin-left: auto;
|
||||||
|
~ .action-item,
|
||||||
|
~ .sharing-entry__loading {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-checkmark-color {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,97 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @license GNU AGPL version 3 or any later version
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as
|
||||||
|
- published by the Free Software Foundation, either version 3 of the
|
||||||
|
- License, or (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li class="sharing-entry">
|
||||||
|
<slot name="avatar" />
|
||||||
|
<div v-tooltip="tooltip" class="sharing-entry__desc">
|
||||||
|
<h5>{{ title }}</h5>
|
||||||
|
<p v-if="subtitle">
|
||||||
|
{{ subtitle }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Actions v-if="$slots['default']" menu-align="right" class="sharing-entry__actions">
|
||||||
|
<slot />
|
||||||
|
</Actions>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Actions from 'nextcloud-vue/dist/Components/Actions'
|
||||||
|
import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SharingEntrySimple',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Actions
|
||||||
|
},
|
||||||
|
|
||||||
|
directives: {
|
||||||
|
Tooltip
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sharing-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 44px;
|
||||||
|
&__desc {
|
||||||
|
padding: 8px;
|
||||||
|
line-height: 1.2em;
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1;
|
||||||
|
min-width: 0;
|
||||||
|
h5 {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: inherit;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__actions {
|
||||||
|
margin-left: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,444 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @license GNU AGPL version 3 or any later version
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as
|
||||||
|
- published by the Free Software Foundation, either version 3 of the
|
||||||
|
- License, or (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Multiselect ref="multiselect"
|
||||||
|
class="sharing-input"
|
||||||
|
:disabled="!canReshare"
|
||||||
|
:hide-selected="true"
|
||||||
|
:internal-search="false"
|
||||||
|
:loading="loading"
|
||||||
|
:options="options"
|
||||||
|
:placeholder="inputPlaceholder"
|
||||||
|
:preselect-first="true"
|
||||||
|
:preserve-search="true"
|
||||||
|
:searchable="true"
|
||||||
|
:user-select="true"
|
||||||
|
@search-change="asyncFind"
|
||||||
|
@select="addShare">
|
||||||
|
<template #noOptions>
|
||||||
|
{{ t('files_sharing', 'No recommendations. Start typing.') }}
|
||||||
|
</template>
|
||||||
|
<template #noResult>
|
||||||
|
{{ noResultText }}
|
||||||
|
</template>
|
||||||
|
</Multiselect>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { generateOcsUrl } from '@nextcloud/router'
|
||||||
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import debounce from 'debounce'
|
||||||
|
import Multiselect from 'nextcloud-vue/dist/Components/Multiselect'
|
||||||
|
|
||||||
|
import Config from '../services/ConfigService'
|
||||||
|
import Share from '../models/Share'
|
||||||
|
import ShareRequests from '../mixins/ShareRequests'
|
||||||
|
import ShareTypes from '../mixins/ShareTypes'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SharingInput',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Multiselect
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [ShareTypes, ShareRequests],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
shares: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
linkShares: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
fileInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
reshare: {
|
||||||
|
type: Share,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
canReshare: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
config: new Config(),
|
||||||
|
loading: false,
|
||||||
|
query: '',
|
||||||
|
recommendations: [],
|
||||||
|
ShareSearch: OCA.Sharing.ShareSearch.state,
|
||||||
|
suggestions: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Implement ShareSearch
|
||||||
|
* allows external appas to inject new
|
||||||
|
* results into the autocomplete dropdown
|
||||||
|
* Used for the guests app
|
||||||
|
*
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
externalResults() {
|
||||||
|
return this.ShareSearch.results
|
||||||
|
},
|
||||||
|
inputPlaceholder() {
|
||||||
|
const allowRemoteSharing = this.config.isRemoteShareAllowed
|
||||||
|
const allowMailSharing = this.config.isMailShareAllowed
|
||||||
|
|
||||||
|
if (!this.canReshare) {
|
||||||
|
return t('files_sharing', 'Resharing is not allowed')
|
||||||
|
}
|
||||||
|
if (!allowRemoteSharing && allowMailSharing) {
|
||||||
|
return t('files_sharing', 'Name or email address...')
|
||||||
|
}
|
||||||
|
if (allowRemoteSharing && !allowMailSharing) {
|
||||||
|
return t('files_sharing', 'Name or federated cloud ID...')
|
||||||
|
}
|
||||||
|
if (allowRemoteSharing && allowMailSharing) {
|
||||||
|
return t('files_sharing', 'Name, federated cloud ID or email address...')
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('files_sharing', 'Name...')
|
||||||
|
},
|
||||||
|
|
||||||
|
isValidQuery() {
|
||||||
|
return this.query && this.query.trim() !== '' && this.query.length > this.config.minSearchStringLength
|
||||||
|
},
|
||||||
|
|
||||||
|
options() {
|
||||||
|
if (this.isValidQuery) {
|
||||||
|
return this.suggestions
|
||||||
|
}
|
||||||
|
return this.recommendations
|
||||||
|
},
|
||||||
|
|
||||||
|
noResultText() {
|
||||||
|
if (this.loading) {
|
||||||
|
return t('files_sharing', 'Searching...')
|
||||||
|
}
|
||||||
|
return t('files_sharing', 'No elements found.')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.getRecommendations()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async asyncFind(query, id) {
|
||||||
|
// save current query to check if we display
|
||||||
|
// recommendations or search results
|
||||||
|
this.query = query.trim()
|
||||||
|
if (this.isValidQuery) {
|
||||||
|
// start loading now to have proper ux feedback
|
||||||
|
// during the debounce
|
||||||
|
this.loading = true
|
||||||
|
await this.debounceGetSuggestions(query)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get suggestions
|
||||||
|
*
|
||||||
|
* @param {string} search the search query
|
||||||
|
* @param {boolean} [lookup=false] search on lookup server
|
||||||
|
*/
|
||||||
|
async getSuggestions(search, lookup) {
|
||||||
|
this.loading = true
|
||||||
|
lookup = lookup || false
|
||||||
|
console.info(search, lookup)
|
||||||
|
|
||||||
|
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', {
|
||||||
|
params: {
|
||||||
|
format: 'json',
|
||||||
|
itemType: this.fileInfo.type === 'dir' ? 'folder' : 'file',
|
||||||
|
search,
|
||||||
|
lookup,
|
||||||
|
perPage: this.config.maxAutocompleteResults
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (request.data.ocs.meta.statuscode !== 100) {
|
||||||
|
console.error('Error fetching suggestions', request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = request.data.ocs.data
|
||||||
|
const exact = request.data.ocs.data.exact
|
||||||
|
data.exact = [] // removing exact from general results
|
||||||
|
|
||||||
|
// flatten array of arrays
|
||||||
|
const rawExactSuggestions = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
|
||||||
|
const rawSuggestions = Object.values(data).reduce((arr, elem) => arr.concat(elem), [])
|
||||||
|
|
||||||
|
// remove invalid data and format to user-select layout
|
||||||
|
const exactSuggestions = this.filterOutExistingShares(rawExactSuggestions)
|
||||||
|
.map(share => this.formatForMultiselect(share))
|
||||||
|
const suggestions = this.filterOutExistingShares(rawSuggestions)
|
||||||
|
.map(share => this.formatForMultiselect(share))
|
||||||
|
|
||||||
|
// lookup clickable entry
|
||||||
|
const lookupEntry = []
|
||||||
|
if (data.lookupEnabled) {
|
||||||
|
lookupEntry.push({
|
||||||
|
isNoUser: true,
|
||||||
|
displayName: t('files_sharing', 'Search globally'),
|
||||||
|
lookup: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a condition specified, filter it
|
||||||
|
const externalResults = this.externalResults.filter(result => !result.condition || result.condition(this))
|
||||||
|
|
||||||
|
this.suggestions = exactSuggestions.concat(suggestions).concat(externalResults).concat(lookupEntry)
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
console.info('suggestions', this.suggestions)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce getSuggestions
|
||||||
|
*
|
||||||
|
* @param {...*} args the arguments
|
||||||
|
*/
|
||||||
|
debounceGetSuggestions: debounce(function(...args) {
|
||||||
|
this.getSuggestions(...args)
|
||||||
|
}, 300),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the sharing recommendations
|
||||||
|
*/
|
||||||
|
async getRecommendations() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees_recommended', {
|
||||||
|
params: {
|
||||||
|
format: 'json',
|
||||||
|
itemType: this.fileInfo.type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (request.data.ocs.meta.statuscode !== 100) {
|
||||||
|
console.error('Error fetching recommendations', request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const exact = request.data.ocs.data.exact
|
||||||
|
|
||||||
|
// flatten array of arrays
|
||||||
|
const rawRecommendations = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
|
||||||
|
|
||||||
|
// remove invalid data and format to user-select layout
|
||||||
|
this.recommendations = this.filterOutExistingShares(rawRecommendations)
|
||||||
|
.map(share => this.formatForMultiselect(share))
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
console.info('recommendations', this.recommendations)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter out existing shares from
|
||||||
|
* the provided shares search results
|
||||||
|
*
|
||||||
|
* @param {Object[]} shares the array of shares object
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
filterOutExistingShares(shares) {
|
||||||
|
return shares.reduce((arr, share) => {
|
||||||
|
// only check proper objects
|
||||||
|
if (typeof share !== 'object') {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// filter out current user
|
||||||
|
if (share.value.shareWith === getCurrentUser().uid) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out the owner of the share
|
||||||
|
if (this.reshare && share.value.shareWith === this.reshare.owner) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out existing mail shares
|
||||||
|
if (share.value.shareType === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
|
||||||
|
const emails = this.linkShares.map(elem => elem.shareWith)
|
||||||
|
if (emails.indexOf(share.value.shareWith.trim()) !== -1) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
} else { // filter out existing shares
|
||||||
|
// creating an object of uid => type
|
||||||
|
const sharesObj = this.shares.reduce((obj, elem) => {
|
||||||
|
obj[elem.shareWith] = elem.type
|
||||||
|
return obj
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
// if shareWith is the same and the share type too, ignore it
|
||||||
|
const key = share.value.shareWith.trim()
|
||||||
|
if (key in sharesObj
|
||||||
|
&& sharesObj[key] === share.value.shareType) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ALL GOOD
|
||||||
|
// let's add the suggestion
|
||||||
|
arr.push(share)
|
||||||
|
} catch {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}, [])
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the icon based on the share type
|
||||||
|
* @param {number} type the share type
|
||||||
|
* @returns {string} the icon class
|
||||||
|
*/
|
||||||
|
shareTypeToIcon(type) {
|
||||||
|
switch (type) {
|
||||||
|
case this.SHARE_TYPES.SHARE_TYPE_GUEST:
|
||||||
|
// default is a user, other icons are here to differenciate
|
||||||
|
// themselves from it, so let's not display the user icon
|
||||||
|
// case this.SHARE_TYPES.SHARE_TYPE_REMOTE:
|
||||||
|
// case this.SHARE_TYPES.SHARE_TYPE_USER:
|
||||||
|
return 'icon-user'
|
||||||
|
case this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP:
|
||||||
|
case this.SHARE_TYPES.SHARE_TYPE_GROUP:
|
||||||
|
return 'icon-group'
|
||||||
|
case this.SHARE_TYPES.SHARE_TYPE_EMAIL:
|
||||||
|
return 'icon-mail'
|
||||||
|
case this.SHARE_TYPES.SHARE_TYPE_CIRCLE:
|
||||||
|
return 'icon-circle'
|
||||||
|
case this.SHARE_TYPES.SHARE_TYPE_ROOM:
|
||||||
|
return 'icon-room'
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format shares for the multiselect options
|
||||||
|
* @param {Object} result select entry item
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
formatForMultiselect(result) {
|
||||||
|
let desc
|
||||||
|
if ((result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_REMOTE
|
||||||
|
|| result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP
|
||||||
|
) && result.value.server) {
|
||||||
|
desc = t('files_sharing', 'on {server}', { server: result.value.server })
|
||||||
|
} else if (result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
|
||||||
|
desc = result.value.shareWith
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shareWith: result.value.shareWith,
|
||||||
|
shareType: result.value.shareType,
|
||||||
|
user: result.uuid || result.value.shareWith,
|
||||||
|
isNoUser: !result.uuid,
|
||||||
|
displayName: result.name || result.label,
|
||||||
|
desc,
|
||||||
|
icon: this.shareTypeToIcon(result.value.shareType)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the new share request
|
||||||
|
* @param {Object} value the multiselect option
|
||||||
|
*/
|
||||||
|
async addShare(value) {
|
||||||
|
if (value.lookup) {
|
||||||
|
return this.getSuggestions(this.query, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle externalResults from OCA.Sharing.ShareSearch
|
||||||
|
if (value.handler) {
|
||||||
|
const share = await value.handler(this)
|
||||||
|
this.$emit('add:share', new Share(share))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
|
||||||
|
const share = await this.createShare({
|
||||||
|
path,
|
||||||
|
shareType: value.shareType,
|
||||||
|
shareWith: value.shareWith
|
||||||
|
})
|
||||||
|
this.$emit('add:share', share)
|
||||||
|
|
||||||
|
this.getRecommendations()
|
||||||
|
|
||||||
|
} catch (response) {
|
||||||
|
// focus back if any error
|
||||||
|
const input = this.$refs.multiselect.$el.querySelector('input')
|
||||||
|
if (input) {
|
||||||
|
input.focus()
|
||||||
|
}
|
||||||
|
this.query = value.shareWith
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.sharing-input {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
|
||||||
|
// properly style the lookup entry
|
||||||
|
.multiselect__option {
|
||||||
|
span[lookup] {
|
||||||
|
.avatardiv {
|
||||||
|
background-image: var(--icon-search-fff);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-color: var(--color-text-maxcontrast) !important;
|
||||||
|
div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,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/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import SharingTab from './views/SharingTab'
|
||||||
|
import ShareSearch from './services/ShareSearch'
|
||||||
|
import ExternalLinkActions from './services/ExternalLinkActions'
|
||||||
|
|
||||||
|
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() })
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (OCA.Files && OCA.Files.Sidebar) {
|
||||||
|
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab('sharing', SharingTab))
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: remove when ie not supported
|
||||||
|
import 'url-search-params-polyfill'
|
||||||
|
|
||||||
|
import { generateOcsUrl } from '@nextcloud/router'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import Share from '../models/Share'
|
||||||
|
|
||||||
|
const shareUrl = generateOcsUrl('apps/files_sharing/api/v1', 2) + 'shares'
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Create a new share
|
||||||
|
*
|
||||||
|
* @param {Object} data destructuring object
|
||||||
|
* @param {string} data.path path to the file/folder which should be shared
|
||||||
|
* @param {number} data.shareType 0 = user; 1 = group; 3 = public link; 6 = federated cloud share
|
||||||
|
* @param {string} data.shareWith user/group id with which the file should be shared (optional for shareType > 1)
|
||||||
|
* @param {boolean} [data.publicUpload=false] allow public upload to a public shared folder
|
||||||
|
* @param {string} [data.password] password to protect public link Share with
|
||||||
|
* @param {number} [data.permissions=31] 1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1)
|
||||||
|
* @param {boolean} [data.sendPasswordByTalk=false] send the password via a talk conversation
|
||||||
|
* @param {string} [data.expireDate=''] expire the shareautomatically after
|
||||||
|
* @param {string} [data.label=''] custom label
|
||||||
|
* @returns {Share} the new share
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
async createShare({ path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label }) {
|
||||||
|
try {
|
||||||
|
const request = await axios.post(shareUrl, { path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label })
|
||||||
|
if (!('ocs' in request.data)) {
|
||||||
|
throw request
|
||||||
|
}
|
||||||
|
return new Share(request.data.ocs.data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error while creating share', error)
|
||||||
|
OC.Notification.showTemporary(t('files_sharing', 'Error creating the share'), { type: 'error' })
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a share
|
||||||
|
*
|
||||||
|
* @param {number} id share id
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
async deleteShare(id) {
|
||||||
|
try {
|
||||||
|
const request = await axios.delete(shareUrl + `/${id}`)
|
||||||
|
if (!('ocs' in request.data)) {
|
||||||
|
throw request
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error while deleting share', error)
|
||||||
|
OC.Notification.showTemporary(t('files_sharing', 'Error deleting the share'), { type: 'error' })
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a share
|
||||||
|
*
|
||||||
|
* @param {number} id share id
|
||||||
|
* @param {Object} data destructuring object
|
||||||
|
* @param {string} data.property property to update
|
||||||
|
* @param {any} data.value value to set
|
||||||
|
*/
|
||||||
|
async updateShare(id, { property, value }) {
|
||||||
|
try {
|
||||||
|
// ocs api requires x-www-form-urlencoded
|
||||||
|
const data = new URLSearchParams()
|
||||||
|
data.append(property, value)
|
||||||
|
|
||||||
|
const request = await axios.put(shareUrl + `/${id}`, { [property]: value }, headers)
|
||||||
|
if (!('ocs' in request.data)) {
|
||||||
|
throw request
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error while updating share', error)
|
||||||
|
OC.Notification.showTemporary(t('files_sharing', 'Error updating the share'), { type: 'error' })
|
||||||
|
const message = error.response.data.ocs.meta.message
|
||||||
|
throw new Error(`${property}, ${message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
SHARE_TYPES: {
|
||||||
|
SHARE_TYPE_USER: OC.Share.SHARE_TYPE_USER,
|
||||||
|
SHARE_TYPE_GROUP: OC.Share.SHARE_TYPE_GROUP,
|
||||||
|
SHARE_TYPE_LINK: OC.Share.SHARE_TYPE_LINK,
|
||||||
|
SHARE_TYPE_EMAIL: OC.Share.SHARE_TYPE_EMAIL,
|
||||||
|
SHARE_TYPE_REMOTE: OC.Share.SHARE_TYPE_REMOTE,
|
||||||
|
SHARE_TYPE_CIRCLE: OC.Share.SHARE_TYPE_CIRCLE,
|
||||||
|
SHARE_TYPE_GUEST: OC.Share.SHARE_TYPE_GUEST,
|
||||||
|
SHARE_TYPE_REMOTE_GROUP: OC.Share.SHARE_TYPE_REMOTE_GROUP,
|
||||||
|
SHARE_TYPE_ROOM: OC.Share.SHARE_TYPE_ROOM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,303 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import PQueue from 'p-queue'
|
||||||
|
import debounce from 'debounce'
|
||||||
|
|
||||||
|
import Share from '../models/Share'
|
||||||
|
import SharesRequests from './ShareRequests'
|
||||||
|
import ShareTypes from './ShareTypes'
|
||||||
|
import Config from '../services/ConfigService'
|
||||||
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [SharesRequests, ShareTypes],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
fileInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
type: Share,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
config: new Config(),
|
||||||
|
|
||||||
|
// errors helpers
|
||||||
|
errors: {},
|
||||||
|
|
||||||
|
// component status toggles
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
open: false,
|
||||||
|
|
||||||
|
// concurrency management queue
|
||||||
|
// we want one queue per share
|
||||||
|
updateQueue: new PQueue({ concurrency: 1 }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ! This allow vue to make the Share class state reactive
|
||||||
|
* ! do not remove it ot you'll lose all reactivity here
|
||||||
|
*/
|
||||||
|
reactiveState: this.share && this.share.state,
|
||||||
|
|
||||||
|
SHARE_TYPES: {
|
||||||
|
SHARE_TYPE_USER: OC.Share.SHARE_TYPE_USER,
|
||||||
|
SHARE_TYPE_GROUP: OC.Share.SHARE_TYPE_GROUP,
|
||||||
|
SHARE_TYPE_LINK: OC.Share.SHARE_TYPE_LINK,
|
||||||
|
SHARE_TYPE_EMAIL: OC.Share.SHARE_TYPE_EMAIL,
|
||||||
|
SHARE_TYPE_REMOTE: OC.Share.SHARE_TYPE_REMOTE,
|
||||||
|
SHARE_TYPE_CIRCLE: OC.Share.SHARE_TYPE_CIRCLE,
|
||||||
|
SHARE_TYPE_GUEST: OC.Share.SHARE_TYPE_GUEST,
|
||||||
|
SHARE_TYPE_REMOTE_GROUP: OC.Share.SHARE_TYPE_REMOTE_GROUP,
|
||||||
|
SHARE_TYPE_ROOM: OC.Share.SHARE_TYPE_ROOM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the current share have an expiration date
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
hasExpirationDate: {
|
||||||
|
get: function() {
|
||||||
|
return this.config.isDefaultExpireDateEnforced || !!this.share.expireDate
|
||||||
|
},
|
||||||
|
set: function(enabled) {
|
||||||
|
this.share.expireDate = enabled
|
||||||
|
? this.config.defaultExpirationDateString !== ''
|
||||||
|
? this.config.defaultExpirationDateString
|
||||||
|
: moment().format('YYYY-MM-DD')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the current share have a note
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
hasNote: {
|
||||||
|
get: function() {
|
||||||
|
return !!this.share.note
|
||||||
|
},
|
||||||
|
set: function(enabled) {
|
||||||
|
this.share.note = enabled
|
||||||
|
? t('files_sharing', 'Enter a note for the share recipient')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
dateTomorrow() {
|
||||||
|
return moment().add(1, 'days')
|
||||||
|
},
|
||||||
|
|
||||||
|
dateMaxEnforced() {
|
||||||
|
return this.config.isDefaultExpireDateEnforced
|
||||||
|
&& moment().add(1 + this.config.defaultExpireDate, 'days')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datepicker lang values
|
||||||
|
* https://github.com/nextcloud/nextcloud-vue/pull/146
|
||||||
|
* TODO: have this in vue-components
|
||||||
|
*
|
||||||
|
* @returns {int}
|
||||||
|
*/
|
||||||
|
firstDay() {
|
||||||
|
return window.firstDay
|
||||||
|
? window.firstDay
|
||||||
|
: 0 // sunday as default
|
||||||
|
},
|
||||||
|
lang() {
|
||||||
|
// fallback to default in case of unavailable data
|
||||||
|
return {
|
||||||
|
days: window.dayNamesShort
|
||||||
|
? window.dayNamesShort // provided by nextcloud
|
||||||
|
: ['Sun.', 'Mon.', 'Tue.', 'Wed.', 'Thu.', 'Fri.', 'Sat.'],
|
||||||
|
months: window.monthNamesShort
|
||||||
|
? window.monthNamesShort // provided by nextcloud
|
||||||
|
: ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'],
|
||||||
|
placeholder: {
|
||||||
|
date: 'Select Date' // TODO: Translate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isShareOwner() {
|
||||||
|
return this.share && this.share.owner === getCurrentUser().uid
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Check if a share is valid before
|
||||||
|
* firing the request
|
||||||
|
*
|
||||||
|
* @param {Share} share the share to check
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
checkShare(share) {
|
||||||
|
if (share.password) {
|
||||||
|
if (typeof share.password !== 'string' || share.password.trim() === '') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (share.expirationDate) {
|
||||||
|
const date = moment(share.expirationDate)
|
||||||
|
if (!date.isValid()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActionInput can be a little tricky to work with.
|
||||||
|
* Since we expect a string and not a Date,
|
||||||
|
* we need to process the value here
|
||||||
|
*
|
||||||
|
* @param {Date} date js date to be parsed by moment.js
|
||||||
|
*/
|
||||||
|
onExpirationChange(date) {
|
||||||
|
// format to YYYY-MM-DD
|
||||||
|
const value = moment(date).format('YYYY-MM-DD')
|
||||||
|
this.share.expireDate = value
|
||||||
|
this.queueUpdate('expireDate')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uncheck expire date
|
||||||
|
* We need this method because @update:checked
|
||||||
|
* is ran simultaneously as @uncheck, so
|
||||||
|
* so we cannot ensure data is up-to-date
|
||||||
|
*/
|
||||||
|
onExpirationDisable() {
|
||||||
|
this.share.expireDate = ''
|
||||||
|
this.queueUpdate('expireDate')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete share button handler
|
||||||
|
*/
|
||||||
|
async onDelete() {
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
this.open = false
|
||||||
|
await this.deleteShare(this.share.id)
|
||||||
|
console.debug('Share deleted', this.share.id)
|
||||||
|
this.$emit('remove:share', this.share)
|
||||||
|
} catch (error) {
|
||||||
|
// re-open menu if error
|
||||||
|
this.open = true
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an update of the share to the queue
|
||||||
|
*
|
||||||
|
* @param {string} property the property to sync
|
||||||
|
*/
|
||||||
|
queueUpdate(property) {
|
||||||
|
if (this.share.id) {
|
||||||
|
// force value to string because that is what our
|
||||||
|
// share api controller accepts
|
||||||
|
const value = this.share[property].toString()
|
||||||
|
|
||||||
|
this.updateQueue.add(async() => {
|
||||||
|
this.saving = true
|
||||||
|
this.errors = {}
|
||||||
|
try {
|
||||||
|
await this.updateShare(this.share.id, {
|
||||||
|
property,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
|
||||||
|
// clear any previous errors
|
||||||
|
this.$delete(this.errors, property)
|
||||||
|
|
||||||
|
// reset password state after sync
|
||||||
|
this.$delete(this.share, 'newPassword')
|
||||||
|
} catch ({ property, message }) {
|
||||||
|
this.onSyncError(property, message)
|
||||||
|
} finally {
|
||||||
|
this.saving = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.error('Cannot update share.', this.share, 'No valid id')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage sync errors
|
||||||
|
* @param {string} property the errored property, e.g. 'password'
|
||||||
|
* @param {string} message the error message
|
||||||
|
*/
|
||||||
|
onSyncError(property, message) {
|
||||||
|
// re-open menu if closed
|
||||||
|
this.open = true
|
||||||
|
switch (property) {
|
||||||
|
case 'password':
|
||||||
|
case 'pending':
|
||||||
|
case 'expireDate':
|
||||||
|
case 'note': {
|
||||||
|
// show error
|
||||||
|
this.$set(this.errors, property, message)
|
||||||
|
|
||||||
|
let propertyEl = this.$refs[property]
|
||||||
|
if (propertyEl) {
|
||||||
|
if (propertyEl.$el) {
|
||||||
|
propertyEl = propertyEl.$el
|
||||||
|
}
|
||||||
|
// focus if there is a focusable action element
|
||||||
|
const focusable = propertyEl.querySelector('.focusable')
|
||||||
|
if (focusable) {
|
||||||
|
focusable.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce queueUpdate to avoid requests spamming
|
||||||
|
* more importantly for text data
|
||||||
|
*
|
||||||
|
* @param {string} property the property to sync
|
||||||
|
*/
|
||||||
|
debounceQueueUpdate: debounce(function(property) {
|
||||||
|
this.queueUpdate(property)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,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/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: SORT THOSE PROPERTIES
|
||||||
|
get label() {
|
||||||
|
return this.#share.label
|
||||||
|
}
|
||||||
|
|
||||||
|
get parent() {
|
||||||
|
return this.#share.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
get storageId() {
|
||||||
|
return this.#share.storage_id
|
||||||
|
}
|
||||||
|
|
||||||
|
get storage() {
|
||||||
|
return this.#share.storage
|
||||||
|
}
|
||||||
|
|
||||||
|
get itemSource() {
|
||||||
|
return this.#share.item_source
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class Config {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is public upload allowed on link shares ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get isPublicUploadEnabled() {
|
||||||
|
return document.getElementById('filestable')
|
||||||
|
&& document.getElementById('filestable').dataset.allowPublicUpload === 'yes'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are link share allowed ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get isShareWithLinkAllowed() {
|
||||||
|
return document.getElementById('allowShareWithLink')
|
||||||
|
&& document.getElementById('allowShareWithLink').value === 'yes'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the federated sharing documentation link
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get federatedShareDocLink() {
|
||||||
|
return OC.appConfig.core.federatedCloudShareDoc
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default expiration date as string
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get defaultExpirationDateString() {
|
||||||
|
let expireDateString = ''
|
||||||
|
if (this.isDefaultExpireDateEnabled) {
|
||||||
|
const date = window.moment.utc()
|
||||||
|
const expireAfterDays = this.defaultExpireDate
|
||||||
|
date.add(expireAfterDays, 'days')
|
||||||
|
expireDateString = date.format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
return expireDateString
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are link shares password-enforced ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get enforcePasswordForPublicLink() {
|
||||||
|
return OC.appConfig.core.enforcePasswordForPublicLink === true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is password asked by default on link shares ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get enableLinkPasswordByDefault() {
|
||||||
|
return OC.appConfig.core.enableLinkPasswordByDefault === true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is link shares expiration enforced ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get isDefaultExpireDateEnforced() {
|
||||||
|
return OC.appConfig.core.defaultExpireDateEnforced === true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is there a default expiration date for new link shares ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get isDefaultExpireDateEnabled() {
|
||||||
|
return OC.appConfig.core.defaultExpireDateEnabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are users on this server allowed to send shares to other servers ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get isRemoteShareAllowed() {
|
||||||
|
return OC.appConfig.core.remoteShareAllowed === true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is sharing my mail (link share) enabled ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get isMailShareAllowed() {
|
||||||
|
return OC.appConfig.shareByMailEnabled !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default days to expiration
|
||||||
|
*
|
||||||
|
* @returns {int}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get defaultExpireDate() {
|
||||||
|
return OC.appConfig.core.defaultExpireDate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is resharing allowed ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get isResharingAllowed() {
|
||||||
|
return OC.appConfig.core.resharingAllowed === true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is password enforced for mail shares ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get isPasswordForMailSharesRequired() {
|
||||||
|
return (OC.appConfig.shareByMail === undefined) ? false : OC.appConfig.shareByMail.enforcePasswordProtection === true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is sharing with groups allowed ?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get allowGroupSharing() {
|
||||||
|
return OC.appConfig.core.allowGroupSharing === true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum results of a share search
|
||||||
|
*
|
||||||
|
* @returns {int}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get maxAutocompleteResults() {
|
||||||
|
return parseInt(OC.config['sharing.maxAutocompleteResults'], 10) || 200
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the minimal string length
|
||||||
|
* to initiate a share search
|
||||||
|
*
|
||||||
|
* @returns {int}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get minSearchStringLength() {
|
||||||
|
return parseInt(OC.config['sharing.minSearchStringLength'], 10) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the password policy config
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
* @readonly
|
||||||
|
* @memberof Config
|
||||||
|
*/
|
||||||
|
get passwordPolicy() {
|
||||||
|
const capabilities = OC.getCapabilities()
|
||||||
|
return capabilities.password_policy ? capabilities.password_policy : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class ExternalLinkActions {
|
||||||
|
|
||||||
|
#state;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// init empty state
|
||||||
|
this.#state = {}
|
||||||
|
|
||||||
|
// init default values
|
||||||
|
this.#state.actions = []
|
||||||
|
console.debug('OCA.Sharing.ExternalLinkActions initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the state
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @memberof ExternalLinkActions
|
||||||
|
* @returns {Object} the data state
|
||||||
|
*/
|
||||||
|
get state() {
|
||||||
|
return this.#state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new action for the link share
|
||||||
|
* Mostly used by the social sharing app.
|
||||||
|
*
|
||||||
|
* @param {Object} action new action component to register
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
registerAction(action) {
|
||||||
|
if (typeof action === 'object' && action.render && action.components) {
|
||||||
|
this.#state.actions.push(action)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
console.error(`Invalid action component provided`, action)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class ShareSearch {
|
||||||
|
|
||||||
|
#state;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// init empty state
|
||||||
|
this.#state = {}
|
||||||
|
|
||||||
|
// init default values
|
||||||
|
this.#state.results = []
|
||||||
|
console.debug('OCA.Sharing.ShareSearch initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the state
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @memberof ShareSearch
|
||||||
|
* @returns {Object} the data state
|
||||||
|
*/
|
||||||
|
get state() {
|
||||||
|
return this.#state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new result
|
||||||
|
* Mostly used by the guests app.
|
||||||
|
* We should consider deprecation and add results via php ?
|
||||||
|
*
|
||||||
|
* @param {Object} result entry to append
|
||||||
|
* @param {string} [result.user] entry user
|
||||||
|
* @param {string} result.displayName entry first line
|
||||||
|
* @param {string} [result.desc] entry second line
|
||||||
|
* @param {string} [result.icon] entry icon
|
||||||
|
* @param {function} result.handler function to run on entry selection
|
||||||
|
* @param {function} [result.condition] condition to add entry or not
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
addNewResult(result) {
|
||||||
|
if (result.displayName.trim() !== ''
|
||||||
|
&& typeof result.handler === 'function') {
|
||||||
|
this.#state.results.push(result)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
console.error(`Invalid search result provided`, result)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -195,7 +195,7 @@
|
||||||
// do not open sidebar if permission is set and equal to 0
|
// do not open sidebar if permission is set and equal to 0
|
||||||
var permissions = parseInt(context.$file.data('share-permissions'), 10)
|
var permissions = parseInt(context.$file.data('share-permissions'), 10)
|
||||||
if (isNaN(permissions) || permissions > 0) {
|
if (isNaN(permissions) || permissions > 0) {
|
||||||
fileList.showDetailsView(fileName, 'shareTabView')
|
fileList.showDetailsView(fileName, 'sharing')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render: function(actionSpec, isDefault, context) {
|
render: function(actionSpec, isDefault, context) {
|
||||||
|
@ -209,37 +209,37 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
var shareTab = new OCA.Sharing.ShareTabView('shareTabView', { order: -20 })
|
var shareTab = new OCA.Sharing.ShareTabView('sharing', {order: -20})
|
||||||
// detect changes and change the matching list entry
|
// // detect changes and change the matching list entry
|
||||||
shareTab.on('sharesChanged', function(shareModel) {
|
// shareTab.on('sharesChanged', function(shareModel) {
|
||||||
var fileInfoModel = shareModel.fileInfoModel
|
// var fileInfoModel = shareModel.fileInfoModel
|
||||||
var $tr = fileList.findFileEl(fileInfoModel.get('name'))
|
// var $tr = fileList.findFileEl(fileInfoModel.get('name'))
|
||||||
|
|
||||||
// We count email shares as link share
|
// // We count email shares as link share
|
||||||
var hasLinkShares = shareModel.hasLinkShares()
|
// var hasLinkShares = shareModel.hasLinkShares();
|
||||||
shareModel.get('shares').forEach(function(share) {
|
// shareModel.get('shares').forEach(function (share) {
|
||||||
if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
|
// if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
|
||||||
hasLinkShares = true
|
// hasLinkShares = true;
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel)
|
// OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel);
|
||||||
if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) {
|
// if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) {
|
||||||
// remove icon, if applicable
|
// // remove icon, if applicable
|
||||||
OC.Share.markFileAsShared($tr, false, false)
|
// OC.Share.markFileAsShared($tr, false, false)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// FIXME: this is too convoluted. We need to get rid of the above updates
|
// // 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
|
// // and only ever update the model and let the events take care of rerendering
|
||||||
fileInfoModel.set({
|
// fileInfoModel.set({
|
||||||
shareTypes: shareModel.getShareTypes(),
|
// shareTypes: shareModel.getShareTypes(),
|
||||||
// in case markFileAsShared decided to change the icon,
|
// // in case markFileAsShared decided to change the icon,
|
||||||
// we need to modify the model
|
// // we need to modify the model
|
||||||
// (FIXME: yes, this is hacky)
|
// // (FIXME: yes, this is hacky)
|
||||||
icon: $tr.attr('data-icon')
|
// icon: $tr.attr('data-icon')
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
fileList.registerTabView(shareTab)
|
// fileList.registerTabView(shareTab)
|
||||||
|
|
||||||
var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({ shareTab: shareTab })
|
var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({ shareTab: shareTab })
|
||||||
fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView)
|
fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView)
|
||||||
|
|
|
@ -93,7 +93,7 @@
|
||||||
dirInfo: self._dirInfo
|
dirInfo: self._dirInfo
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'shareTabView')
|
OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'sharing')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the shared with me title
|
||||||
|
*
|
||||||
|
* @param {Share} share current share
|
||||||
|
* @returns {string} the title
|
||||||
|
*/
|
||||||
|
const shareWithTitle = function(share) {
|
||||||
|
if (share.type === OC.Share.type_GROUP) {
|
||||||
|
return t(
|
||||||
|
'files_sharing',
|
||||||
|
'Shared with you and the group {group} by {owner}',
|
||||||
|
{
|
||||||
|
group: share.shareWithDisplayName,
|
||||||
|
owner: share.ownerDisplayName
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ escape: false }
|
||||||
|
)
|
||||||
|
} else if (share.type === OC.Share.type_CIRCLE) {
|
||||||
|
return t(
|
||||||
|
'files_sharing',
|
||||||
|
'Shared with you and {circle} by {owner}',
|
||||||
|
{
|
||||||
|
circle: share.shareWithDisplayName,
|
||||||
|
owner: share.ownerDisplayName
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ escape: false }
|
||||||
|
)
|
||||||
|
} else if (share.type === OC.Share.type_ROOM) {
|
||||||
|
if (this.model.get('reshare').share_with_displayname) {
|
||||||
|
return t(
|
||||||
|
'files_sharing',
|
||||||
|
'Shared with you and the conversation {conversation} by {owner}',
|
||||||
|
{
|
||||||
|
conversation: share.shareWithDisplayName,
|
||||||
|
owner: share.ownerDisplayName
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ escape: false }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return t(
|
||||||
|
'files_sharing',
|
||||||
|
'Shared with you in a conversation by {owner}',
|
||||||
|
{
|
||||||
|
owner: share.ownerDisplayName
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ escape: false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return t(
|
||||||
|
'files_sharing',
|
||||||
|
'Shared with you by {owner}',
|
||||||
|
{ owner: share.ownerDisplayName },
|
||||||
|
undefined,
|
||||||
|
{ escape: false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { shareWithTitle }
|
|
@ -0,0 +1,141 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @license GNU AGPL version 3 or any later version
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as
|
||||||
|
- published by the Free Software Foundation, either version 3 of the
|
||||||
|
- License, or (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul class="sharing-link-list">
|
||||||
|
<!-- If no link shares, show the add link default entry -->
|
||||||
|
<SharingEntryLink v-if="!hasLinkShares && canReshare"
|
||||||
|
:can-reshare="canReshare"
|
||||||
|
:file-info="fileInfo"
|
||||||
|
@add:share="addShare" />
|
||||||
|
|
||||||
|
<!-- Else we display the list -->
|
||||||
|
<template v-if="hasShares">
|
||||||
|
<!-- using shares[index] to work with .sync -->
|
||||||
|
<SharingEntryLink v-for="(share, index) in shares"
|
||||||
|
:key="share.id"
|
||||||
|
:can-reshare="canReshare"
|
||||||
|
:share.sync="shares[index]"
|
||||||
|
:file-info="fileInfo"
|
||||||
|
@add:share="addShare(...arguments)"
|
||||||
|
@update:share="awaitForShare(...arguments)"
|
||||||
|
@remove:share="removeShare" />
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import Share from '../models/Share'
|
||||||
|
import ShareTypes from '../mixins/ShareTypes'
|
||||||
|
import SharingEntryLink from '../components/SharingEntryLink'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SharingLinkList',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
SharingEntryLink
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [ShareTypes],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
fileInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
shares: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
canReshare: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Do we have link shares?
|
||||||
|
* Using this to still show the `new link share`
|
||||||
|
* button regardless of mail shares
|
||||||
|
*
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
hasLinkShares() {
|
||||||
|
return this.shares.filter(share => share.type === this.SHARE_TYPES.SHARE_TYPE_LINK).length > 0
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we have any link or email shares?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
hasShares() {
|
||||||
|
return this.shares.length > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Add a new share into the link shares list
|
||||||
|
* and return the newly created share component
|
||||||
|
*
|
||||||
|
* @param {Share} share the share to add to the array
|
||||||
|
* @param {Function} resolve a function to run after the share is added and its component initialized
|
||||||
|
*/
|
||||||
|
addShare(share, resolve) {
|
||||||
|
this.shares.unshift(share)
|
||||||
|
this.awaitForShare(share, resolve)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Await for next tick and render after the list updated
|
||||||
|
* Then resolve with the matched vue component of the
|
||||||
|
* provided share object
|
||||||
|
*
|
||||||
|
* @param {Share} share newly created share
|
||||||
|
* @param {Function} resolve a function to execute after
|
||||||
|
*/
|
||||||
|
awaitForShare(share, resolve) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const newShare = this.$children.find(component => component.share === share)
|
||||||
|
if (newShare) {
|
||||||
|
resolve(newShare)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a share from the shares list
|
||||||
|
*
|
||||||
|
* @param {Share} share the share to remove
|
||||||
|
*/
|
||||||
|
removeShare(share) {
|
||||||
|
const index = this.shares.findIndex(item => item === share)
|
||||||
|
this.shares.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,76 @@
|
||||||
|
<!--
|
||||||
|
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||||
|
-
|
||||||
|
- @license GNU AGPL version 3 or any later version
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as
|
||||||
|
- published by the Free Software Foundation, either version 3 of the
|
||||||
|
- License, or (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul class="sharing-sharee-list">
|
||||||
|
<SharingEntry v-for="share in shares"
|
||||||
|
:key="share.id"
|
||||||
|
:file-info="fileInfo"
|
||||||
|
:share="share"
|
||||||
|
@remove:share="removeShare" />
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import Share from '../models/Share'
|
||||||
|
import SharingEntry from '../components/SharingEntry'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SharingList',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
SharingEntry
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
fileInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
shares: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hasShares() {
|
||||||
|
return this.shares.length === 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Remove a share from the shares list
|
||||||
|
*
|
||||||
|
* @param {Share} share the share to remove
|
||||||
|
*/
|
||||||
|
removeShare(share) {
|
||||||
|
const index = this.shares.findIndex(item => item === share)
|
||||||
|
this.shares.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,318 @@
|
||||||
|
<!--
|
||||||
|
- @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" />
|
||||||
|
</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 { 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,
|
||||||
|
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>
|
|
@ -4,6 +4,7 @@ module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
'additionalScripts': path.join(__dirname, 'src', 'additionalScripts.js'),
|
'additionalScripts': path.join(__dirname, 'src', 'additionalScripts.js'),
|
||||||
'files_sharing': path.join(__dirname, 'src', 'files_sharing.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'),
|
'collaboration': path.join(__dirname, 'src', 'collaborationresourceshandler.js'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|
|
@ -323,6 +323,13 @@
|
||||||
data.isEncrypted = false;
|
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];
|
var contentType = props[Client.PROPERTY_GETCONTENTTYPE];
|
||||||
if (!_.isUndefined(contentType)) {
|
if (!_.isUndefined(contentType)) {
|
||||||
data.mimetype = contentType;
|
data.mimetype = contentType;
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
|
||||||
|
|
||||||
|
if (!Element.prototype.matches) {
|
||||||
|
Element.prototype.matches
|
||||||
|
= Element.prototype.msMatchesSelector
|
||||||
|
|| Element.prototype.webkitMatchesSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Element.prototype.closest) {
|
||||||
|
Element.prototype.closest = function(s) {
|
||||||
|
var el = this
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (el.matches(s)) return el
|
||||||
|
el = el.parentElement || el.parentNode
|
||||||
|
} while (el !== null && el.nodeType === 1)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
/*
|
/**
|
||||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||||
*
|
*
|
||||||
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||||
|
@ -20,4 +20,5 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './console'
|
import './console'
|
||||||
|
import './closest'
|
||||||
import './windows-phone'
|
import './windows-phone'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/*
|
/**
|
||||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||||
*
|
*
|
||||||
* @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
* @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||||
|
@ -20,8 +20,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import '@babel/polyfill'
|
|
||||||
import './Polyfill/index'
|
import './Polyfill/index'
|
||||||
|
import '@babel/polyfill'
|
||||||
|
|
||||||
// If you remove the line below, tests won't pass
|
// If you remove the line below, tests won't pass
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<LoginForm
|
<LoginForm
|
||||||
:username.sync="user"
|
:username.sync="user"
|
||||||
:redirect-url="redirectUrl"
|
:redirect-url="redirectUrl"
|
||||||
:directLogin="directLogin"
|
:direct-login="directLogin"
|
||||||
:messages="messages"
|
:messages="messages"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:throttle-delay="throttleDelay"
|
:throttle-delay="throttleDelay"
|
||||||
|
|
|
@ -359,6 +359,16 @@
|
||||||
"@babel/plugin-syntax-async-generators": "^7.2.0"
|
"@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": {
|
"@babel/plugin-proposal-dynamic-import": {
|
||||||
"version": "7.5.0",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz",
|
"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=",
|
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
||||||
"dev": true
|
"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": {
|
"debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
@ -6085,8 +6100,7 @@
|
||||||
"p-finally": {
|
"p-finally": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
|
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"p-is-promise": {
|
"p-is-promise": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
@ -6110,6 +6124,23 @@
|
||||||
"p-limit": "^2.0.0"
|
"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": {
|
"p-try": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
"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": {
|
"use": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||||
|
|
10
package.json
10
package.json
|
@ -25,6 +25,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/polyfill": "^7.6.0",
|
"@babel/polyfill": "^7.6.0",
|
||||||
"@chenfengyuan/vue-qrcode": "^1.0.1",
|
"@chenfengyuan/vue-qrcode": "^1.0.1",
|
||||||
|
"@nextcloud/auth": "^0.3.1",
|
||||||
"@nextcloud/axios": "^0.5.0",
|
"@nextcloud/axios": "^0.5.0",
|
||||||
"@nextcloud/event-bus": "^0.2.1",
|
"@nextcloud/event-bus": "^0.2.1",
|
||||||
"@nextcloud/initial-state": "^0.2.0",
|
"@nextcloud/initial-state": "^0.2.0",
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
"clipboard": "^2.0.4",
|
"clipboard": "^2.0.4",
|
||||||
"css-vars-ponyfill": "^2.1.2",
|
"css-vars-ponyfill": "^2.1.2",
|
||||||
"davclient.js": "git+https://github.com/owncloud/davclient.js.git#0.2.1",
|
"davclient.js": "git+https://github.com/owncloud/davclient.js.git#0.2.1",
|
||||||
|
"debounce": "^1.2.0",
|
||||||
"dompurify": "^2.0.7",
|
"dompurify": "^2.0.7",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"handlebars": "^4.4.5",
|
"handlebars": "^4.4.5",
|
||||||
|
@ -53,12 +55,14 @@
|
||||||
"nextcloud-router": "0.0.9",
|
"nextcloud-router": "0.0.9",
|
||||||
"nextcloud-vue": "^0.12.7",
|
"nextcloud-vue": "^0.12.7",
|
||||||
"nextcloud-vue-collections": "^0.6.0",
|
"nextcloud-vue-collections": "^0.6.0",
|
||||||
|
"p-queue": "^6.1.0",
|
||||||
"query-string": "^5.1.1",
|
"query-string": "^5.1.1",
|
||||||
"select2": "3.5.1",
|
"select2": "3.5.1",
|
||||||
"snap.js": "^2.0.9",
|
"snap.js": "^2.0.9",
|
||||||
"strengthify": "git+https://github.com/MorrisJobke/strengthify.git#0.5.8",
|
"strengthify": "git+https://github.com/MorrisJobke/strengthify.git#0.5.8",
|
||||||
"toastify-js": "^1.6.1",
|
"toastify-js": "^1.6.1",
|
||||||
"underscore": "^1.9.1",
|
"underscore": "^1.9.1",
|
||||||
|
"url-search-params-polyfill": "^7.0.0",
|
||||||
"v-tooltip": "^2.0.2",
|
"v-tooltip": "^2.0.2",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-click-outside": "^1.0.7",
|
"vue-click-outside": "^1.0.7",
|
||||||
|
@ -72,6 +76,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.6.4",
|
"@babel/core": "^7.6.4",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
"@babel/preset-env": "^7.6.3",
|
"@babel/preset-env": "^7.6.3",
|
||||||
"@nextcloud/browserslist-config": "^1.0.0",
|
"@nextcloud/browserslist-config": "^1.0.0",
|
||||||
|
@ -104,5 +109,8 @@
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"extends @nextcloud/browserslist-config"
|
"extends @nextcloud/browserslist-config"
|
||||||
]
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ const path = require('path')
|
||||||
const merge = require('webpack-merge')
|
const merge = require('webpack-merge')
|
||||||
const { VueLoaderPlugin } = require('vue-loader')
|
const { VueLoaderPlugin } = require('vue-loader')
|
||||||
|
|
||||||
const core = require('./core/webpack')
|
|
||||||
|
|
||||||
const accessibility = require('./apps/accessibility/webpack')
|
const accessibility = require('./apps/accessibility/webpack')
|
||||||
const comments = require('./apps/comments/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_sharing = require('./apps/files_sharing/webpack')
|
||||||
const files_trashbin = require('./apps/files_trashbin/webpack')
|
const files_trashbin = require('./apps/files_trashbin/webpack')
|
||||||
const files_versions = require('./apps/files_versions/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 workflowengine = require('./apps/workflowengine/webpack')
|
||||||
|
|
||||||
const modules = {
|
const modules = {
|
||||||
core,
|
|
||||||
settings,
|
|
||||||
accessibility,
|
accessibility,
|
||||||
comments,
|
comments,
|
||||||
|
core,
|
||||||
|
files,
|
||||||
files_sharing,
|
files_sharing,
|
||||||
files_trashbin,
|
files_trashbin,
|
||||||
files_versions,
|
files_versions,
|
||||||
oauth2,
|
oauth2,
|
||||||
|
settings,
|
||||||
systemtags,
|
systemtags,
|
||||||
twofactor_backupscodes,
|
twofactor_backupscodes,
|
||||||
updatenotifications,
|
updatenotifications,
|
||||||
|
|
Loading…
Reference in New Issue