Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> Signed-off-by: npmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com>tags/v21.0.0beta1
@@ -3704,11 +3704,17 @@ | |||
id: tabView.id, | |||
name: tabView.getLabel(), | |||
icon: tabView.getIcon(), | |||
render: function(el, fileInfo) { | |||
mount: function(el, fileInfo) { | |||
tabView.setFileInfo(fileInfo) | |||
el.appendChild(tabView.el) | |||
}, | |||
enabled, | |||
update: function(fileInfo) { | |||
tabView.setFileInfo(fileInfo) | |||
}, | |||
destroy: function() { | |||
tabView.el.remove() | |||
}, | |||
enabled: enabled | |||
})) | |||
} | |||
}, |
@@ -23,21 +23,28 @@ | |||
<template> | |||
<AppSidebarTab | |||
:id="id" | |||
ref="tab" | |||
:name="name" | |||
:icon="icon"> | |||
<!-- Fallback loading --> | |||
<EmptyContent v-if="loading" icon="icon-loading" /> | |||
<!-- Using a dummy div as Vue mount replace the element directly | |||
It does NOT append to the content --> | |||
<div ref="mount"></div> | |||
<div ref="mount" /> | |||
</AppSidebarTab> | |||
</template> | |||
<script> | |||
import AppSidebarTab from '@nextcloud/vue/dist/Components/AppSidebarTab' | |||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' | |||
export default { | |||
name: 'SidebarTab', | |||
components: { | |||
AppSidebarTab, | |||
EmptyContent, | |||
}, | |||
props: { | |||
@@ -58,36 +65,61 @@ export default { | |||
type: String, | |||
required: true, | |||
}, | |||
render: { | |||
/** | |||
* Lifecycle methods. | |||
* They are prefixed with `on` to avoid conflict with Vue | |||
* methods like this.destroy | |||
*/ | |||
onMount: { | |||
type: Function, | |||
required: true, | |||
}, | |||
onUpdate: { | |||
type: Function, | |||
required: true, | |||
}, | |||
onDestroy: { | |||
type: Function, | |||
required: true, | |||
}, | |||
}, | |||
data() { | |||
return { | |||
loading: true, | |||
} | |||
}, | |||
computed: { | |||
// TODO: implement a better way to force pass a prop fromm Sidebar | |||
// TODO: implement a better way to force pass a prop from Sidebar | |||
activeTab() { | |||
return this.$parent.activeTab | |||
}, | |||
}, | |||
watch: { | |||
fileInfo(newFile, oldFile) { | |||
async fileInfo(newFile, oldFile) { | |||
// Update fileInfo on change | |||
if (newFile.id !== oldFile.id) { | |||
this.mountTab() | |||
this.loading = true | |||
await this.onUpdate(this.fileInfo) | |||
this.loading = false | |||
} | |||
}, | |||
}, | |||
mounted() { | |||
this.mountTab() | |||
async mounted() { | |||
this.loading = true | |||
// Mount the tab: mounting point, fileInfo, vue context | |||
await this.onMount(this.$refs.mount, this.fileInfo, this.$refs.tab) | |||
this.loading = false | |||
}, | |||
methods: { | |||
mountTab() { | |||
// Mount the tab into this component | |||
this.render(this.$refs.mount, this.fileInfo) | |||
}, | |||
async beforeDestroy() { | |||
// unmount the tab | |||
await this.onDestroy() | |||
}, | |||
} | |||
</script> |
@@ -25,7 +25,9 @@ export default class Tab { | |||
#id | |||
#name | |||
#icon | |||
#render | |||
#mount | |||
#update | |||
#destroy | |||
#enabled | |||
/** | |||
@@ -35,10 +37,12 @@ export default class Tab { | |||
* @param {string} options.id the unique id of this tab | |||
* @param {string} options.name the translated tab name | |||
* @param {string} options.icon the vue component | |||
* @param {Function} options.render function to render the tab | |||
* @param {Function} options.mount function to mount the tab | |||
* @param {Function} options.update function to update the tab | |||
* @param {Function} options.destroy function to destroy the tab | |||
* @param {Function} [options.enabled] define conditions whether this tab is active. Must returns a boolean | |||
*/ | |||
constructor({ id, name, icon, render, enabled }) { | |||
constructor({ id, name, icon, mount, update, destroy, enabled } = {}) { | |||
if (enabled === undefined) { | |||
enabled = () => true | |||
} | |||
@@ -53,8 +57,14 @@ export default class Tab { | |||
if (typeof icon !== 'string' || icon.trim() === '') { | |||
throw new Error('The icon argument is not a valid string') | |||
} | |||
if (typeof render !== 'function') { | |||
throw new Error('The render argument should be a function') | |||
if (typeof mount !== 'function') { | |||
throw new Error('The mount argument should be a function') | |||
} | |||
if (typeof update !== 'function') { | |||
throw new Error('The update argument should be a function') | |||
} | |||
if (typeof destroy !== 'function') { | |||
throw new Error('The destroy argument should be a function') | |||
} | |||
if (typeof enabled !== 'function') { | |||
throw new Error('The enabled argument should be a function') | |||
@@ -63,7 +73,9 @@ export default class Tab { | |||
this.#id = id | |||
this.#name = name | |||
this.#icon = icon | |||
this.#render = render | |||
this.#mount = mount | |||
this.#update = update | |||
this.#destroy = destroy | |||
this.#enabled = enabled | |||
} | |||
@@ -80,8 +92,16 @@ export default class Tab { | |||
return this.#icon | |||
} | |||
get render() { | |||
return this.#render | |||
get mount() { | |||
return this.#mount | |||
} | |||
get update() { | |||
return this.#update | |||
} | |||
get destroy() { | |||
return this.#destroy | |||
} | |||
get enabled() { |
@@ -21,6 +21,8 @@ | |||
*/ | |||
import Vue from 'vue' | |||
import { translate as t } from '@nextcloud/l10n' | |||
import SidebarView from './views/Sidebar.vue' | |||
import Sidebar from './services/Sidebar' | |||
import Tab from './models/Tab' |
@@ -52,33 +52,38 @@ | |||
</template> | |||
<!-- Error display --> | |||
<div v-if="error" class="emptycontent"> | |||
<div class="icon-error" /> | |||
<h2>{{ error }}</h2> | |||
</div> | |||
<EmptyContent v-if="error" icon="icon-error"> | |||
{{ error }} | |||
</EmptyContent> | |||
<!-- If fileInfo fetch is complete, display tabs --> | |||
<template v-else-if="fileInfo" v-for="tab in tabs"> | |||
<!-- If fileInfo fetch is complete, render tabs --> | |||
<template v-for="tab in tabs" v-else-if="fileInfo"> | |||
<!-- Hide them if we're loading another file but keep them mounted --> | |||
<SidebarTab | |||
v-if="tab.enabled(fileInfo)" | |||
v-show="!loading" | |||
:id="tab.id" | |||
:key="tab.id" | |||
:name="tab.name" | |||
:icon="tab.icon" | |||
:render="tab.render" | |||
:on-mount="tab.mount" | |||
:on-update="tab.update" | |||
:on-destroy="tab.destroy" | |||
:file-info="fileInfo" /> | |||
</template> | |||
</AppSidebar> | |||
</template> | |||
<script> | |||
import { encodePath } from '@nextcloud/paths' | |||
import $ from 'jquery' | |||
import axios from '@nextcloud/axios' | |||
import AppSidebar from '@nextcloud/vue/dist/Components/AppSidebar' | |||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' | |||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' | |||
import FileInfo from '../services/FileInfo' | |||
import SidebarTab from '../components/SidebarTab' | |||
import LegacyView from '../components/LegacyView' | |||
import { encodePath } from '@nextcloud/paths' | |||
export default { | |||
name: 'Sidebar', | |||
@@ -86,8 +91,9 @@ export default { | |||
components: { | |||
ActionButton, | |||
AppSidebar, | |||
SidebarTab, | |||
EmptyContent, | |||
LegacyView, | |||
SidebarTab, | |||
}, | |||
data() { | |||
@@ -95,6 +101,7 @@ export default { | |||
// reactive state | |||
Sidebar: OCA.Files.Sidebar.state, | |||
error: null, | |||
loading: true, | |||
fileInfo: null, | |||
starLoading: false, | |||
} | |||
@@ -185,15 +192,16 @@ export default { | |||
appSidebar() { | |||
if (this.fileInfo) { | |||
return { | |||
background: this.background, | |||
'data-mimetype': this.fileInfo.mimetype, | |||
'star-loading': this.starLoading, | |||
active: this.activeTab, | |||
background: this.background, | |||
class: { 'has-preview': this.fileInfo.hasPreview }, | |||
compact: !this.fileInfo.hasPreview, | |||
'star-loading': this.starLoading, | |||
loading: this.loading, | |||
starred: this.fileInfo.isFavourited, | |||
subtitle: this.subtitle, | |||
title: this.fileInfo.name, | |||
'data-mimetype': this.fileInfo.mimetype, | |||
} | |||
} else if (this.error) { | |||
return { | |||
@@ -201,12 +209,12 @@ export default { | |||
subtitle: '', | |||
title: '', | |||
} | |||
} else { | |||
return { | |||
class: 'icon-loading', | |||
subtitle: '', | |||
title: '', | |||
} | |||
} | |||
// no fileInfo yet, showing empty data | |||
return { | |||
loading: this.loading, | |||
subtitle: '', | |||
title: '', | |||
} | |||
}, | |||
@@ -241,35 +249,6 @@ export default { | |||
}, | |||
}, | |||
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.tabs) { | |||
this.$refs.tabs.updateTabs() | |||
} | |||
}) | |||
} catch (error) { | |||
this.error = t('files', 'Error while loading the file data') | |||
console.error('Error while loading the file data', error) | |||
} | |||
} | |||
}, | |||
}, | |||
methods: { | |||
/** | |||
* Can this tab be displayed ? | |||
@@ -403,9 +382,11 @@ export default { | |||
// update current opened file | |||
this.Sidebar.file = path | |||
// reset previous data | |||
this.resetData() | |||
if (path && path.trim() !== '') { | |||
// reset data, keep old fileInfo to not reload all tabs and just hide them | |||
this.error = null | |||
this.loading = true | |||
try { | |||
this.fileInfo = await FileInfo(this.davPath) | |||
// adding this as fallback because other apps expect it | |||
@@ -427,6 +408,8 @@ export default { | |||
console.error('Error while loading the file data', error) | |||
throw new Error(error) | |||
} finally { | |||
this.loading = false | |||
} | |||
} | |||
}, |
@@ -1,2 +1,2 @@ | |||
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=152)}({152:function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}}); | |||
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=153)}({153:function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}}); | |||
//# sourceMappingURL=collaboration.js.map |
@@ -1,2 +1,2 @@ | |||
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="/js/",r(r.s=277)}({277:function(e,t){Object.assign(OC,{Share:{SHARE_TYPE_USER:0,SHARE_TYPE_GROUP:1,SHARE_TYPE_LINK:3,SHARE_TYPE_EMAIL:4,SHARE_TYPE_REMOTE:6,SHARE_TYPE_CIRCLE:7,SHARE_TYPE_GUEST:8,SHARE_TYPE_REMOTE_GROUP:9,SHARE_TYPE_ROOM:10}})}}); | |||
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="/js/",r(r.s=278)}({278:function(e,t){Object.assign(OC,{Share:{SHARE_TYPE_USER:0,SHARE_TYPE_GROUP:1,SHARE_TYPE_LINK:3,SHARE_TYPE_EMAIL:4,SHARE_TYPE_REMOTE:6,SHARE_TYPE_CIRCLE:7,SHARE_TYPE_GUEST:8,SHARE_TYPE_REMOTE_GROUP:9,SHARE_TYPE_ROOM:10}})}}); | |||
//# sourceMappingURL=main.js.map |
@@ -42,6 +42,7 @@ Vue.use(VueClipboard) | |||
// Init Sharing tab component | |||
const View = Vue.extend(SharingTab) | |||
let TabInstance = null | |||
window.addEventListener('DOMContentLoaded', function() { | |||
if (OCA.Files && OCA.Files.Sidebar) { | |||
@@ -50,13 +51,24 @@ window.addEventListener('DOMContentLoaded', function() { | |||
name: t('files_sharing', 'Sharing'), | |||
icon: 'icon-share', | |||
render: (el, fileInfo) => { | |||
new View({ | |||
propsData: { | |||
fileInfo, | |||
}, | |||
}).$mount(el) | |||
console.info(el) | |||
async mount(el, fileInfo, context) { | |||
if (TabInstance) { | |||
TabInstance.$destroy() | |||
} | |||
TabInstance = new View({ | |||
// Better integration with vue parent component | |||
parent: context, | |||
}) | |||
// Only mount after we have all the info we need | |||
await TabInstance.update(fileInfo) | |||
TabInstance.$mount(el) | |||
}, | |||
update(fileInfo) { | |||
TabInstance.update(fileInfo) | |||
}, | |||
destroy() { | |||
TabInstance.$destroy() | |||
TabInstance = null | |||
}, | |||
})) | |||
} |
@@ -117,24 +117,20 @@ export default { | |||
mixins: [ShareTypes], | |||
props: { | |||
fileInfo: { | |||
type: Object, | |||
default: () => {}, | |||
required: true, | |||
}, | |||
}, | |||
data() { | |||
return { | |||
error: '', | |||
expirationInterval: null, | |||
loading: true, | |||
fileInfo: null, | |||
// reshare Share object | |||
reshare: null, | |||
sharedWithMe: {}, | |||
shares: [], | |||
linkShares: [], | |||
sections: OCA.Sharing.ShareTabSections.getSections(), | |||
} | |||
}, | |||
@@ -155,20 +151,17 @@ export default { | |||
}, | |||
}, | |||
watch: { | |||
fileInfo(newFile, oldFile) { | |||
if (newFile.id !== oldFile.id) { | |||
this.resetState() | |||
this.getShares() | |||
} | |||
methods: { | |||
/** | |||
* Update current fileInfo and fetch new data | |||
* @param {Object} fileInfo the current file FileInfo | |||
*/ | |||
async update(fileInfo) { | |||
this.fileInfo = fileInfo | |||
this.resetState() | |||
this.getShares() | |||
}, | |||
}, | |||
beforeMount() { | |||
this.getShares() | |||
}, | |||
methods: { | |||
/** | |||
* Get the existing shares infos | |||
*/ | |||
@@ -221,6 +214,7 @@ export default { | |||
this.error = '' | |||
this.sharedWithMe = {} | |||
this.shares = [] | |||
this.linkShares = [] | |||
}, | |||
/** |