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 = {
|
||||
plugins: ['@babel/plugin-syntax-dynamic-import'],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }]
|
||||
],
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
|
|
2
Makefile
2
Makefile
|
@ -30,6 +30,7 @@ lint-fix-watch:
|
|||
clean:
|
||||
rm -rf apps/accessibility/js/
|
||||
rm -rf apps/comments/js/
|
||||
rm -rf apps/files/js/dist/
|
||||
rm -rf apps/files_sharing/js/dist/
|
||||
rm -rf apps/files_trashbin/js/
|
||||
rm -rf apps/files_versions/js/
|
||||
|
@ -47,6 +48,7 @@ clean-dev:
|
|||
clean-git: clean
|
||||
git checkout -- apps/accessibility/js/
|
||||
git checkout -- apps/comments/js/
|
||||
git checkout -- apps/files/js/dist/
|
||||
git checkout -- apps/files_sharing/js/dist/
|
||||
git checkout -- apps/files_trashbin/js/
|
||||
git checkout -- apps/files_versions/js/
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
actionHandler: function(fileName, context) {
|
||||
context.$file.find('.action-comment').tooltip('hide')
|
||||
// open sidebar in comments section
|
||||
context.fileList.showDetailsView(fileName, 'commentsTabView')
|
||||
context.fileList.showDetailsView(fileName, 'comments')
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -85,8 +85,9 @@
|
|||
}
|
||||
|
||||
/* fit app list view heights */
|
||||
.app-files #app-content>.viewcontainer {
|
||||
.app-files #app-content > .viewcontainer {
|
||||
min-height: 0%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-files #app-content {
|
||||
|
|
|
@ -704,6 +704,12 @@
|
|||
}
|
||||
context.fileList.do_delete(fileName, context.dir);
|
||||
$('.tipsy').remove();
|
||||
|
||||
// close sidebar on delete
|
||||
const path = context.dir + '/' + fileName
|
||||
if (OCA.Files.Sidebar && OCA.Files.Sidebar.file === path) {
|
||||
OCA.Files.Sidebar.file = undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -610,11 +610,11 @@
|
|||
* @param {string} [tabId] optional tab id to select
|
||||
*/
|
||||
showDetailsView: function(fileName, tabId) {
|
||||
console.warn('showDetailsView is deprecated! Use OCA.Files.Sidebar.activeTab. It will be removed in nextcloud 20.');
|
||||
this._updateDetailsView(fileName);
|
||||
if (tabId) {
|
||||
this._detailsView.selectTab(tabId);
|
||||
OCA.Files.Sidebar.activeTab = tabId;
|
||||
}
|
||||
OC.Apps.showAppSidebar(this._detailsView.$el);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -623,48 +623,23 @@
|
|||
* @param {string|OCA.Files.FileInfoModel} fileName file name from the current list or a FileInfoModel object
|
||||
* @param {boolean} [show=true] whether to open the sidebar if it was closed
|
||||
*/
|
||||
_updateDetailsView: function(fileName, show) {
|
||||
if (!this._detailsView) {
|
||||
_updateDetailsView: function(fileName) {
|
||||
if (!(OCA.Files && OCA.Files.Sidebar)) {
|
||||
console.error('No sidebar available');
|
||||
return;
|
||||
}
|
||||
|
||||
// show defaults to true
|
||||
show = _.isUndefined(show) || !!show;
|
||||
var oldFileInfo = this._detailsView.getFileInfo();
|
||||
if (oldFileInfo) {
|
||||
// TODO: use more efficient way, maybe track the highlight
|
||||
this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.get('id')).removeClass('highlighted');
|
||||
oldFileInfo.off('change', this._onSelectedModelChanged, this);
|
||||
}
|
||||
|
||||
if (!fileName) {
|
||||
this._detailsView.setFileInfo(null);
|
||||
if (this._currentFileModel) {
|
||||
this._currentFileModel.off();
|
||||
}
|
||||
this._currentFileModel = null;
|
||||
OC.Apps.hideAppSidebar(this._detailsView.$el);
|
||||
return;
|
||||
OCA.Files.Sidebar.file = null
|
||||
return
|
||||
} else if (typeof fileName !== 'string') {
|
||||
fileName = ''
|
||||
}
|
||||
|
||||
if (show && this._detailsView.$el.hasClass('disappear')) {
|
||||
OC.Apps.showAppSidebar(this._detailsView.$el);
|
||||
}
|
||||
|
||||
if (fileName instanceof OCA.Files.FileInfoModel) {
|
||||
var model = fileName;
|
||||
} else {
|
||||
var $tr = this.findFileEl(fileName);
|
||||
var model = this.getModelForFile($tr);
|
||||
$tr.addClass('highlighted');
|
||||
}
|
||||
|
||||
this._currentFileModel = model;
|
||||
|
||||
this._replaceDetailsViewElementIfNeeded();
|
||||
|
||||
this._detailsView.setFileInfo(model);
|
||||
this._detailsView.$el.scrollTop(0);
|
||||
// open sidebar and set file
|
||||
const dir = `${this.dirInfo.path}/${this.dirInfo.name}`
|
||||
const path = `${dir}/${fileName}`
|
||||
OCA.Files.Sidebar.file = path.replace('//', '/')
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1404,6 +1379,13 @@
|
|||
return OC.MimeType.getIconUrl('dir-external');
|
||||
} else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
|
||||
return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType);
|
||||
} else if (fileInfo.shareTypes && (
|
||||
fileInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_LINK) > -1
|
||||
|| fileInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_EMAIL) > -1)
|
||||
) {
|
||||
return OC.MimeType.getIconUrl('dir-public')
|
||||
} else if (fileInfo.shareTypes && fileInfo.shareTypes.length > 0) {
|
||||
return OC.MimeType.getIconUrl('dir-shared')
|
||||
}
|
||||
return OC.MimeType.getIconUrl('dir');
|
||||
}
|
||||
|
@ -3654,8 +3636,10 @@
|
|||
* Register a tab view to be added to all views
|
||||
*/
|
||||
registerTabView: function(tabView) {
|
||||
if (this._detailsView) {
|
||||
this._detailsView.addTabView(tabView);
|
||||
console.warn('registerTabView is deprecated! It will be removed in nextcloud 20.');
|
||||
const name = tabView.getLabel()
|
||||
if (name) {
|
||||
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab(name, tabView, true))
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -3663,8 +3647,9 @@
|
|||
* Register a detail view to be added to all views
|
||||
*/
|
||||
registerDetailView: function(detailView) {
|
||||
if (this._detailsView) {
|
||||
this._detailsView.addDetailView(detailView);
|
||||
console.warn('registerDetailView is deprecated! It will be removed in nextcloud 20.');
|
||||
if (detailView.el) {
|
||||
OCA.Files.Sidebar.registerSecondaryView(detailView)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,33 +1,34 @@
|
|||
[
|
||||
"dist/sidebar.js",
|
||||
"app.js",
|
||||
"templates.js",
|
||||
"file-upload.js",
|
||||
"newfilemenu.js",
|
||||
"jquery.fileupload.js",
|
||||
"jquery-visibility.js",
|
||||
"fileinfomodel.js",
|
||||
"filesummary.js",
|
||||
"filemultiselectmenu.js",
|
||||
"breadcrumb.js",
|
||||
"filelist.js",
|
||||
"search.js",
|
||||
"favoritesfilelist.js",
|
||||
"recentfilelist.js",
|
||||
"tagsplugin.js",
|
||||
"gotoplugin.js",
|
||||
"favoritesplugin.js",
|
||||
"recentplugin.js",
|
||||
"detailfileinfoview.js",
|
||||
"sidebarpreviewmanager.js",
|
||||
"sidebarpreviewtext.js",
|
||||
"detailtabview.js",
|
||||
"semaphore.js",
|
||||
"mainfileinfodetailview.js",
|
||||
"operationprogressbar.js",
|
||||
"detailsview.js",
|
||||
"detailtabview.js",
|
||||
"favoritesfilelist.js",
|
||||
"favoritesplugin.js",
|
||||
"file-upload.js",
|
||||
"fileactions.js",
|
||||
"fileactionsmenu.js",
|
||||
"fileinfomodel.js",
|
||||
"filelist.js",
|
||||
"filemultiselectmenu.js",
|
||||
"files.js",
|
||||
"filesummary.js",
|
||||
"gotoplugin.js",
|
||||
"jquery-visibility.js",
|
||||
"jquery.fileupload.js",
|
||||
"keyboardshortcuts.js",
|
||||
"navigation.js"
|
||||
"mainfileinfodetailview.js",
|
||||
"navigation.js",
|
||||
"newfilemenu.js",
|
||||
"operationprogressbar.js",
|
||||
"recentfilelist.js",
|
||||
"recentplugin.js",
|
||||
"search.js",
|
||||
"semaphore.js",
|
||||
"sidebarpreviewmanager.js",
|
||||
"sidebarpreviewtext.js",
|
||||
"tagsplugin.js",
|
||||
"templates.js"
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
function() {
|
||||
\OCP\Util::addScript('files_sharing', 'dist/additionalScripts');
|
||||
\OCP\Util::addStyle('files_sharing', 'icons');
|
||||
}
|
||||
);
|
||||
\OC::$server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', function () {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// This is the icons used in the sharing ui (multiselect)
|
||||
.icon-room {
|
||||
@include icon-color('app', 'spreed', $color-black);
|
||||
}
|
||||
.icon-circle {
|
||||
@include icon-color('circles', 'circles', $color-black, 3, false);
|
||||
}
|
||||
.icon-guests {
|
||||
@include icon-color('app', 'guests', $color-black);
|
||||
}
|
|
@ -33,6 +33,7 @@ $tmpl = new OCP\Template('files_sharing', 'list', '');
|
|||
$tmpl->assign('showgridview', $showgridview && !$isIE);
|
||||
|
||||
OCP\Util::addScript('files_sharing', 'dist/files_sharing');
|
||||
OCP\Util::addScript('files_sharing', 'dist/files_sharing_tab');
|
||||
\OC::$server->getEventDispatcher()->dispatch('\OCP\Collaboration\Resources::loadAdditionalScripts');
|
||||
|
||||
$tmpl->printPage();
|
||||
|
|
|
@ -0,0 +1,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
|
||||
var permissions = parseInt(context.$file.data('share-permissions'), 10)
|
||||
if (isNaN(permissions) || permissions > 0) {
|
||||
fileList.showDetailsView(fileName, 'shareTabView')
|
||||
fileList.showDetailsView(fileName, 'sharing')
|
||||
}
|
||||
},
|
||||
render: function(actionSpec, isDefault, context) {
|
||||
|
@ -209,37 +209,37 @@
|
|||
}
|
||||
})
|
||||
|
||||
var shareTab = new OCA.Sharing.ShareTabView('shareTabView', { order: -20 })
|
||||
// detect changes and change the matching list entry
|
||||
shareTab.on('sharesChanged', function(shareModel) {
|
||||
var fileInfoModel = shareModel.fileInfoModel
|
||||
var $tr = fileList.findFileEl(fileInfoModel.get('name'))
|
||||
var shareTab = new OCA.Sharing.ShareTabView('sharing', {order: -20})
|
||||
// // detect changes and change the matching list entry
|
||||
// shareTab.on('sharesChanged', function(shareModel) {
|
||||
// var fileInfoModel = shareModel.fileInfoModel
|
||||
// var $tr = fileList.findFileEl(fileInfoModel.get('name'))
|
||||
|
||||
// We count email shares as link share
|
||||
var hasLinkShares = shareModel.hasLinkShares()
|
||||
shareModel.get('shares').forEach(function(share) {
|
||||
if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
|
||||
hasLinkShares = true
|
||||
}
|
||||
})
|
||||
// // We count email shares as link share
|
||||
// var hasLinkShares = shareModel.hasLinkShares();
|
||||
// shareModel.get('shares').forEach(function (share) {
|
||||
// if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
|
||||
// hasLinkShares = true;
|
||||
// }
|
||||
// })
|
||||
|
||||
OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel)
|
||||
if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) {
|
||||
// remove icon, if applicable
|
||||
OC.Share.markFileAsShared($tr, false, false)
|
||||
}
|
||||
// OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel);
|
||||
// if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) {
|
||||
// // remove icon, if applicable
|
||||
// OC.Share.markFileAsShared($tr, false, false)
|
||||
// }
|
||||
|
||||
// FIXME: this is too convoluted. We need to get rid of the above updates
|
||||
// and only ever update the model and let the events take care of rerendering
|
||||
fileInfoModel.set({
|
||||
shareTypes: shareModel.getShareTypes(),
|
||||
// in case markFileAsShared decided to change the icon,
|
||||
// we need to modify the model
|
||||
// (FIXME: yes, this is hacky)
|
||||
icon: $tr.attr('data-icon')
|
||||
})
|
||||
})
|
||||
fileList.registerTabView(shareTab)
|
||||
// // FIXME: this is too convoluted. We need to get rid of the above updates
|
||||
// // and only ever update the model and let the events take care of rerendering
|
||||
// fileInfoModel.set({
|
||||
// shareTypes: shareModel.getShareTypes(),
|
||||
// // in case markFileAsShared decided to change the icon,
|
||||
// // we need to modify the model
|
||||
// // (FIXME: yes, this is hacky)
|
||||
// icon: $tr.attr('data-icon')
|
||||
// })
|
||||
// })
|
||||
// fileList.registerTabView(shareTab)
|
||||
|
||||
var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({ shareTab: shareTab })
|
||||
fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView)
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
dirInfo: self._dirInfo
|
||||
})
|
||||
})
|
||||
OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'shareTabView')
|
||||
OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'sharing')
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the shared with me title
|
||||
*
|
||||
* @param {Share} share current share
|
||||
* @returns {string} the title
|
||||
*/
|
||||
const shareWithTitle = function(share) {
|
||||
if (share.type === OC.Share.type_GROUP) {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you and the group {group} by {owner}',
|
||||
{
|
||||
group: share.shareWithDisplayName,
|
||||
owner: share.ownerDisplayName
|
||||
},
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
} else if (share.type === OC.Share.type_CIRCLE) {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you and {circle} by {owner}',
|
||||
{
|
||||
circle: share.shareWithDisplayName,
|
||||
owner: share.ownerDisplayName
|
||||
},
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
} else if (share.type === OC.Share.type_ROOM) {
|
||||
if (this.model.get('reshare').share_with_displayname) {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you and the conversation {conversation} by {owner}',
|
||||
{
|
||||
conversation: share.shareWithDisplayName,
|
||||
owner: share.ownerDisplayName
|
||||
},
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
} else {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you in a conversation by {owner}',
|
||||
{
|
||||
owner: share.ownerDisplayName
|
||||
},
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return t(
|
||||
'files_sharing',
|
||||
'Shared with you by {owner}',
|
||||
{ owner: share.ownerDisplayName },
|
||||
undefined,
|
||||
{ escape: false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export { shareWithTitle }
|
|
@ -0,0 +1,141 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ul class="sharing-link-list">
|
||||
<!-- If no link shares, show the add link default entry -->
|
||||
<SharingEntryLink v-if="!hasLinkShares && canReshare"
|
||||
:can-reshare="canReshare"
|
||||
:file-info="fileInfo"
|
||||
@add:share="addShare" />
|
||||
|
||||
<!-- Else we display the list -->
|
||||
<template v-if="hasShares">
|
||||
<!-- using shares[index] to work with .sync -->
|
||||
<SharingEntryLink v-for="(share, index) in shares"
|
||||
:key="share.id"
|
||||
:can-reshare="canReshare"
|
||||
:share.sync="shares[index]"
|
||||
:file-info="fileInfo"
|
||||
@add:share="addShare(...arguments)"
|
||||
@update:share="awaitForShare(...arguments)"
|
||||
@remove:share="removeShare" />
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Share from '../models/Share'
|
||||
import ShareTypes from '../mixins/ShareTypes'
|
||||
import SharingEntryLink from '../components/SharingEntryLink'
|
||||
|
||||
export default {
|
||||
name: 'SharingLinkList',
|
||||
|
||||
components: {
|
||||
SharingEntryLink
|
||||
},
|
||||
|
||||
mixins: [ShareTypes],
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
},
|
||||
shares: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true
|
||||
},
|
||||
canReshare: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Do we have link shares?
|
||||
* Using this to still show the `new link share`
|
||||
* button regardless of mail shares
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
hasLinkShares() {
|
||||
return this.shares.filter(share => share.type === this.SHARE_TYPES.SHARE_TYPE_LINK).length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Do we have any link or email shares?
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasShares() {
|
||||
return this.shares.length > 0
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Add a new share into the link shares list
|
||||
* and return the newly created share component
|
||||
*
|
||||
* @param {Share} share the share to add to the array
|
||||
* @param {Function} resolve a function to run after the share is added and its component initialized
|
||||
*/
|
||||
addShare(share, resolve) {
|
||||
this.shares.unshift(share)
|
||||
this.awaitForShare(share, resolve)
|
||||
},
|
||||
|
||||
/**
|
||||
* Await for next tick and render after the list updated
|
||||
* Then resolve with the matched vue component of the
|
||||
* provided share object
|
||||
*
|
||||
* @param {Share} share newly created share
|
||||
* @param {Function} resolve a function to execute after
|
||||
*/
|
||||
awaitForShare(share, resolve) {
|
||||
this.$nextTick(() => {
|
||||
const newShare = this.$children.find(component => component.share === share)
|
||||
if (newShare) {
|
||||
resolve(newShare)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a share from the shares list
|
||||
*
|
||||
* @param {Share} share the share to remove
|
||||
*/
|
||||
removeShare(share) {
|
||||
const index = this.shares.findIndex(item => item === share)
|
||||
this.shares.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,76 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ul class="sharing-sharee-list">
|
||||
<SharingEntry v-for="share in shares"
|
||||
:key="share.id"
|
||||
:file-info="fileInfo"
|
||||
:share="share"
|
||||
@remove:share="removeShare" />
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Share from '../models/Share'
|
||||
import SharingEntry from '../components/SharingEntry'
|
||||
|
||||
export default {
|
||||
name: 'SharingList',
|
||||
|
||||
components: {
|
||||
SharingEntry
|
||||
},
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true
|
||||
},
|
||||
shares: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasShares() {
|
||||
return this.shares.length === 0
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Remove a share from the shares list
|
||||
*
|
||||
* @param {Share} share the share to remove
|
||||
*/
|
||||
removeShare(share) {
|
||||
const index = this.shares.findIndex(item => item === share)
|
||||
this.shares.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,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: {
|
||||
'additionalScripts': path.join(__dirname, 'src', 'additionalScripts.js'),
|
||||
'files_sharing': path.join(__dirname, 'src', 'files_sharing.js'),
|
||||
'files_sharing_tab': path.join(__dirname, 'src', 'files_sharing_tab.js'),
|
||||
'collaboration': path.join(__dirname, 'src', 'collaborationresourceshandler.js'),
|
||||
},
|
||||
output: {
|
||||
|
|
|
@ -323,6 +323,13 @@
|
|||
data.isEncrypted = false;
|
||||
}
|
||||
|
||||
var isFavouritedProp = props['{' + Client.NS_OWNCLOUD + '}favorite'];
|
||||
if (!_.isUndefined(isFavouritedProp)) {
|
||||
data.isFavourited = isFavouritedProp === '1';
|
||||
} else {
|
||||
data.isFavourited = false;
|
||||
}
|
||||
|
||||
var contentType = props[Client.PROPERTY_GETCONTENTTYPE];
|
||||
if (!_.isUndefined(contentType)) {
|
||||
data.mimetype = contentType;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
|
||||
|
||||
if (!Element.prototype.matches) {
|
||||
Element.prototype.matches
|
||||
= Element.prototype.msMatchesSelector
|
||||
|| Element.prototype.webkitMatchesSelector
|
||||
}
|
||||
|
||||
if (!Element.prototype.closest) {
|
||||
Element.prototype.closest = function(s) {
|
||||
var el = this
|
||||
|
||||
do {
|
||||
if (el.matches(s)) return el
|
||||
el = el.parentElement || el.parentNode
|
||||
} while (el !== null && el.nodeType === 1)
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
|
@ -20,4 +20,5 @@
|
|||
*/
|
||||
|
||||
import './console'
|
||||
import './closest'
|
||||
import './windows-phone'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
|
@ -20,8 +20,8 @@
|
|||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import '@babel/polyfill'
|
||||
import './Polyfill/index'
|
||||
import '@babel/polyfill'
|
||||
|
||||
// If you remove the line below, tests won't pass
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<LoginForm
|
||||
:username.sync="user"
|
||||
:redirect-url="redirectUrl"
|
||||
:directLogin="directLogin"
|
||||
:direct-login="directLogin"
|
||||
:messages="messages"
|
||||
:errors="errors"
|
||||
:throttle-delay="throttleDelay"
|
||||
|
|
|
@ -359,6 +359,16 @@
|
|||
"@babel/plugin-syntax-async-generators": "^7.2.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-class-properties": {
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz",
|
||||
"integrity": "sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.5.5",
|
||||
"@babel/helper-plugin-utils": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-dynamic-import": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz",
|
||||
|
@ -2384,6 +2394,11 @@
|
|||
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
||||
"dev": true
|
||||
},
|
||||
"debounce": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
|
||||
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -6085,8 +6100,7 @@
|
|||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
|
||||
"dev": true
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"p-is-promise": {
|
||||
"version": "2.1.0",
|
||||
|
@ -6110,6 +6124,23 @@
|
|||
"p-limit": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-queue": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.2.0.tgz",
|
||||
"integrity": "sha512-B2LXNONcyn/G6uz2UBFsGjmSa0e/br3jznlzhEyCXg56c7VhEpiT2pZxGOfv32Q3FSyugAdys9KGpsv3kV+Sbg==",
|
||||
"requires": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"p-timeout": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"requires": {
|
||||
"p-finally": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
|
@ -8184,6 +8215,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"url-search-params-polyfill": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-search-params-polyfill/-/url-search-params-polyfill-7.0.0.tgz",
|
||||
"integrity": "sha512-0SEH3s+wCNbxEE/rWUalN004ICNi23Q74Ksc0gS2kG8EXnbayxGOrV97JdwnIVPKZ75Xk0hvKXvtIC4xReLMgg=="
|
||||
},
|
||||
"use": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
|
|
10
package.json
10
package.json
|
@ -25,6 +25,7 @@
|
|||
"dependencies": {
|
||||
"@babel/polyfill": "^7.6.0",
|
||||
"@chenfengyuan/vue-qrcode": "^1.0.1",
|
||||
"@nextcloud/auth": "^0.3.1",
|
||||
"@nextcloud/axios": "^0.5.0",
|
||||
"@nextcloud/event-bus": "^0.2.1",
|
||||
"@nextcloud/initial-state": "^0.2.0",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"clipboard": "^2.0.4",
|
||||
"css-vars-ponyfill": "^2.1.2",
|
||||
"davclient.js": "git+https://github.com/owncloud/davclient.js.git#0.2.1",
|
||||
"debounce": "^1.2.0",
|
||||
"dompurify": "^2.0.7",
|
||||
"escape-html": "^1.0.3",
|
||||
"handlebars": "^4.4.5",
|
||||
|
@ -53,12 +55,14 @@
|
|||
"nextcloud-router": "0.0.9",
|
||||
"nextcloud-vue": "^0.12.7",
|
||||
"nextcloud-vue-collections": "^0.6.0",
|
||||
"p-queue": "^6.1.0",
|
||||
"query-string": "^5.1.1",
|
||||
"select2": "3.5.1",
|
||||
"snap.js": "^2.0.9",
|
||||
"strengthify": "git+https://github.com/MorrisJobke/strengthify.git#0.5.8",
|
||||
"toastify-js": "^1.6.1",
|
||||
"underscore": "^1.9.1",
|
||||
"url-search-params-polyfill": "^7.0.0",
|
||||
"v-tooltip": "^2.0.2",
|
||||
"vue": "^2.6.10",
|
||||
"vue-click-outside": "^1.0.7",
|
||||
|
@ -72,6 +76,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"@nextcloud/browserslist-config": "^1.0.0",
|
||||
|
@ -104,5 +109,8 @@
|
|||
},
|
||||
"browserslist": [
|
||||
"extends @nextcloud/browserslist-config"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ const path = require('path')
|
|||
const merge = require('webpack-merge')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
|
||||
const core = require('./core/webpack')
|
||||
|
||||
const accessibility = require('./apps/accessibility/webpack')
|
||||
const comments = require('./apps/comments/webpack')
|
||||
const core = require('./core/webpack')
|
||||
const files = require('./apps/files/webpack')
|
||||
const files_sharing = require('./apps/files_sharing/webpack')
|
||||
const files_trashbin = require('./apps/files_trashbin/webpack')
|
||||
const files_versions = require('./apps/files_versions/webpack')
|
||||
|
@ -18,14 +18,15 @@ const updatenotifications = require('./apps/updatenotification/webpack')
|
|||
const workflowengine = require('./apps/workflowengine/webpack')
|
||||
|
||||
const modules = {
|
||||
core,
|
||||
settings,
|
||||
accessibility,
|
||||
comments,
|
||||
core,
|
||||
files,
|
||||
files_sharing,
|
||||
files_trashbin,
|
||||
files_versions,
|
||||
oauth2,
|
||||
settings,
|
||||
systemtags,
|
||||
twofactor_backupscodes,
|
||||
updatenotifications,
|
||||
|
|
Loading…
Reference in New Issue