Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>pull/15719/head
parent
ea6f423e2c
commit
fd90af50d9
@ -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' |
||||
} |
||||
} |
||||
@ -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); |
||||
} |
||||
@ -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 |
||||
} |
||||
|
||||
} |
||||
@ -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> |
||||
@ -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 |
||||
} |
||||
} |
||||
Loading…
Reference in new issue