diff options
Diffstat (limited to 'apps/files/src/views/Sidebar.vue')
-rw-r--r-- | apps/files/src/views/Sidebar.vue | 373 |
1 files changed, 244 insertions, 129 deletions
diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue index c97fb304c32..40a16d42b42 100644 --- a/apps/files/src/views/Sidebar.vue +++ b/apps/files/src/views/Sidebar.vue @@ -1,49 +1,60 @@ <!-- - - @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/>. - - - --> + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> <NcAppSidebar v-if="file" ref="sidebar" + data-cy-sidebar v-bind="appSidebar" :force-menu="true" - tabindex="0" @close="close" @update:active="setActiveTab" - @update:starred="toggleStarred" @[defaultActionListener].stop.prevent="onDefaultAction" @opening="handleOpening" @opened="handleOpened" @closing="handleClosing" @closed="handleClosed"> + <template v-if="fileInfo" #subname> + <div class="sidebar__subname"> + <NcIconSvgWrapper v-if="fileInfo.isFavourited" + :path="mdiStar" + :name="t('files', 'Favorite')" + inline /> + <span>{{ size }}</span> + <span class="sidebar__subname-separator">•</span> + <NcDateTime :timestamp="fileInfo.mtime" /> + <span class="sidebar__subname-separator">•</span> + <span>{{ t('files', 'Owner') }}</span> + <NcUserBubble :user="ownerId" + :display-name="nodeOwnerLabel" /> + </div> + </template> + <!-- TODO: create a standard to allow multiple elements here? --> <template v-if="fileInfo" #description> - <LegacyView v-for="view in views" - :key="view.cid" - :component="view" - :file-info="fileInfo" /> + <div class="sidebar__description"> + <SystemTags v-if="isSystemTagsEnabled && showTagsDefault" + v-show="showTags" + :disabled="!fileInfo?.canEdit()" + :file-id="fileInfo.id" /> + <LegacyView v-for="view in views" + :key="view.cid" + :component="view" + :file-info="fileInfo" /> + </div> </template> <!-- Actions menu --> <template v-if="fileInfo" #secondary-actions> + <NcActionButton :close-after-click="true" + @click="toggleStarred(!fileInfo.isFavourited)"> + <template #icon> + <NcIconSvgWrapper :path="fileInfo.isFavourited ? mdiStarOutline : mdiStar" /> + </template> + {{ fileInfo.isFavourited ? t('files', 'Remove from favorites') : t('files', 'Add to favorites') }} + </NcActionButton> <!-- TODO: create proper api for apps to register actions And inject themselves here. --> <NcActionButton v-if="isSystemTagsEnabled" @@ -81,41 +92,71 @@ </template> </NcAppSidebar> </template> -<script> +<script lang="ts"> +import { davRemoteURL, davRootPath, File, Folder, formatFileSize } from '@nextcloud/files' +import { defineComponent } from 'vue' +import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' import { encodePath } from '@nextcloud/paths' +import { fetchNode } from '../services/WebdavClient.ts' +import { generateUrl } from '@nextcloud/router' +import { getCapabilities } from '@nextcloud/capabilities' +import { getCurrentUser } from '@nextcloud/auth' +import { mdiStar, mdiStarOutline } from '@mdi/js' +import { ShareType } from '@nextcloud/sharing' +import { showError } from '@nextcloud/dialogs' import $ from 'jquery' import axios from '@nextcloud/axios' -import { emit } from '@nextcloud/event-bus' -import moment from '@nextcloud/moment' -import { Type as ShareTypes } from '@nextcloud/sharing' -import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar' -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton' -import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent' +import NcAppSidebar from '@nextcloud/vue/components/NcAppSidebar' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcDateTime from '@nextcloud/vue/components/NcDateTime' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcUserBubble from '@nextcloud/vue/components/NcUserBubble' -import FileInfo from '../services/FileInfo' -import SidebarTab from '../components/SidebarTab' -import LegacyView from '../components/LegacyView' +import FileInfo from '../services/FileInfo.js' +import LegacyView from '../components/LegacyView.vue' +import SidebarTab from '../components/SidebarTab.vue' +import SystemTags from '../../../systemtags/src/components/SystemTags.vue' +import logger from '../logger.ts' -export default { +export default defineComponent({ name: 'Sidebar', components: { + LegacyView, NcActionButton, NcAppSidebar, + NcDateTime, NcEmptyContent, - LegacyView, + NcIconSvgWrapper, SidebarTab, + SystemTags, + NcUserBubble, + }, + + setup() { + const currentUser = getCurrentUser() + + // Non reactive properties + return { + currentUser, + + mdiStar, + mdiStarOutline, + } }, data() { return { // reactive state Sidebar: OCA.Files.Sidebar.state, + showTags: false, + showTagsDefault: true, error: null, loading: true, fileInfo: null, - starLoading: false, + node: null, isFullScreen: false, hasLowHeight: false, } @@ -157,14 +198,12 @@ export default { * @return {string} */ davPath() { - const user = OC.getCurrentUser().uid - return OC.linkToRemote(`dav/files/${user}${encodePath(this.file)}`) + return `${davRemoteURL}${davRootPath}${encodePath(this.file)}` }, /** * Current active tab handler * - * @param {string} id the tab id to set as active * @return {string} the current active tab */ activeTab() { @@ -172,39 +211,12 @@ export default { }, /** - * Sidebar subtitle - * - * @return {string} - */ - subtitle() { - return `${this.size}, ${this.time}` - }, - - /** - * File last modified formatted string - * - * @return {string} - */ - time() { - return OC.Util.relativeModifiedDate(this.fileInfo.mtime) - }, - - /** - * File last modified full string - * - * @return {string} - */ - fullTime() { - return moment(this.fileInfo.mtime).format('LLL') - }, - - /** * File size formatted string * * @return {string} */ size() { - return OC.Util.humanFileSize(this.fileInfo.size) + return formatFileSize(this.fileInfo?.size) }, /** @@ -225,7 +237,6 @@ export default { if (this.fileInfo) { return { 'data-mimetype': this.fileInfo.mimetype, - 'star-loading': this.starLoading, active: this.activeTab, background: this.background, class: { @@ -234,24 +245,27 @@ export default { }, compact: this.hasLowHeight || !this.fileInfo.hasPreview || this.isFullScreen, loading: this.loading, - starred: this.fileInfo.isFavourited, - subtitle: this.subtitle, - subtitleTooltip: this.fullTime, - title: this.fileInfo.name, - titleTooltip: this.fileInfo.name, + name: this.node?.displayname ?? this.fileInfo.name, + title: this.node?.displayname ?? this.fileInfo.name, } } else if (this.error) { return { key: 'error', // force key to re-render - subtitle: '', - title: '', + subname: '', + name: '', + class: { + 'app-sidebar--full': this.isFullScreen, + }, } } // no fileInfo yet, showing empty data return { loading: this.loading, - subtitle: '', - title: '', + subname: '', + name: '', + class: { + 'app-sidebar--full': this.isFullScreen, + }, } }, @@ -282,14 +296,36 @@ export default { }, isSystemTagsEnabled() { - return OCA && 'SystemTags' in OCA + return getCapabilities()?.systemtags?.enabled === true + }, + ownerId() { + return this.node?.attributes?.['owner-id'] ?? this.currentUser.uid + }, + currentUserIsOwner() { + return this.ownerId === this.currentUser.uid + }, + nodeOwnerLabel() { + let ownerDisplayName = this.node?.attributes?.['owner-display-name'] + if (this.currentUserIsOwner) { + ownerDisplayName = `${ownerDisplayName} (${t('files', 'You')})` + } + return ownerDisplayName + }, + sharedMultipleTimes() { + if (Array.isArray(node.attributes?.['share-types']) && node.attributes?.['share-types'].length > 1) { + return t('files', 'Shared multiple times with different people') + } + return null }, }, created() { + subscribe('files:node:deleted', this.onNodeDeleted) + window.addEventListener('resize', this.handleWindowResize) this.handleWindowResize() }, beforeDestroy() { + unsubscribe('file:node:deleted', this.onNodeDeleted) window.removeEventListener('resize', this.handleWindowResize) }, @@ -314,8 +350,9 @@ export default { }, getPreviewIfAny(fileInfo) { - if (fileInfo.hasPreview && !this.isFullScreen) { - return OC.generateUrl(`/core/preview?fileId=${fileInfo.id}&x=${screen.width}&y=${screen.height}&a=true`) + if (fileInfo?.hasPreview && !this.isFullScreen) { + const etag = fileInfo?.etag || '' + return generateUrl(`/core/preview?fileId=${fileInfo.id}&x=${screen.width}&y=${screen.height}&a=true&v=${etag.slice(0, 6)}`) } return this.getIconUrl(fileInfo) }, @@ -328,7 +365,7 @@ export default { * @return {string} Url to the icon for mimeType */ getIconUrl(fileInfo) { - const mimeType = fileInfo.mimetype || 'application/octet-stream' + const mimeType = fileInfo?.mimetype || 'application/octet-stream' if (mimeType === 'httpd/unix-directory') { // use default folder icon if (fileInfo.mountType === 'shared' || fileInfo.mountType === 'shared-root') { @@ -338,8 +375,8 @@ export default { } else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') { return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType) } else if (fileInfo.shareTypes && ( - fileInfo.shareTypes.indexOf(ShareTypes.SHARE_TYPE_LINK) > -1 - || fileInfo.shareTypes.indexOf(ShareTypes.SHARE_TYPE_EMAIL) > -1) + fileInfo.shareTypes.indexOf(ShareType.Link) > -1 + || fileInfo.shareTypes.indexOf(ShareType.Email) > -1) ) { return OC.MimeType.getIconUrl('dir-public') } else if (fileInfo.shareTypes && fileInfo.shareTypes.length > 0) { @@ -357,17 +394,23 @@ export default { */ setActiveTab(id) { OCA.Files.Sidebar.setActiveTab(id) + this.tabs.forEach(tab => { + try { + tab.setIsActive(id === tab.id) + } catch (error) { + logger.error('Error while setting tab active state', { error, id: tab.id, tab }) + } + }) }, /** - * Toggle favourite state + * Toggle favorite state * TODO: better implementation * - * @param {boolean} state favourited or not + * @param {boolean} state is favorite or not */ async toggleStarred(state) { try { - this.starLoading = true await axios({ method: 'PROPPATCH', url: this.davPath, @@ -381,17 +424,28 @@ export default { </d:propertyupdate>`, }) - // TODO: Obliterate as soon as possible and use events with new files app - // Terrible fallback for legacy files: toggle filelist as well - if (OCA.Files && OCA.Files.App && OCA.Files.App.fileList && OCA.Files.App.fileList.fileActions) { - OCA.Files.App.fileList.fileActions.triggerAction('Favorite', OCA.Files.App.fileList.getModelForFile(this.fileInfo.name), OCA.Files.App.fileList) - } + /** + * TODO: adjust this when the Sidebar is finally using File/Folder classes + * @see https://github.com/nextcloud/server/blob/8a75cb6e72acd42712ab9fea22296aa1af863ef5/apps/files/src/views/favorites.ts#L83-L115 + */ + const isDir = this.fileInfo.type === 'dir' + const Node = isDir ? Folder : File + const node = new Node({ + fileid: this.fileInfo.id, + source: `${davRemoteURL}${davRootPath}${this.file}`, + root: davRootPath, + mime: isDir ? undefined : this.fileInfo.mimetype, + attributes: { + favorite: 1, + }, + }) + emit(state ? 'files:favorites:added' : 'files:favorites:removed', node) + this.fileInfo.isFavourited = state } catch (error) { - OC.Notification.showTemporary(t('files', 'Unable to change the favourite state of the file')) - console.error('Unable to change favourite state', error) + showError(t('files', 'Unable to change the favorite state of the file')) + logger.error('Unable to change favorite state', { error }) } - this.starLoading = false }, onDefaultAction() { @@ -410,9 +464,10 @@ export default { * Toggle the tags selector */ toggleTags() { - if (OCA.SystemTags && OCA.SystemTags.View) { - OCA.SystemTags.View.toggle() - } + // toggle + this.showTags = !this.showTags + // save the new state + this.setShowTagsDefault(this.showTags) }, /** @@ -423,38 +478,50 @@ export default { * @throws {Error} loading failure */ async open(path) { + if (!path || path.trim() === '') { + throw new Error(`Invalid path '${path}'`) + } + + // Only focus the tab when the selected file/tab is changed in already opened sidebar + // Focusing the sidebar on first file open is handled by NcAppSidebar + const focusTabAfterLoad = !!this.Sidebar.file + // update current opened file this.Sidebar.file = path - if (path && path.trim() !== '') { - // reset data, keep old fileInfo to not reload all tabs and just hide them - this.error = null - this.loading = true + // 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 - 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) + try { + this.node = await fetchNode(this.file) + this.fileInfo = FileInfo(this.node) + // 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) + }) + + await this.$nextTick() - throw new Error(error) - } finally { - this.loading = false + this.setActiveTab(this.Sidebar.activeTab || this.tabs[0].id) + + this.loading = false + + await this.$nextTick() + + if (focusTabAfterLoad && this.$refs.sidebar) { + this.$refs.sidebar.focusActiveTabContent() } + } catch (error) { + this.loading = false + this.error = t('files', 'Error while loading the file data') + console.error('Error while loading the file data', error) + + throw new Error(error) } }, @@ -463,10 +530,21 @@ export default { */ close() { this.Sidebar.file = '' + this.showTags = false this.resetData() }, /** + * Handle if the current node was deleted + * @param {import('@nextcloud/files').Node} node The deleted node + */ + onNodeDeleted(node) { + if (this.fileInfo && node && this.fileInfo.id === node.fileid) { + this.close() + } + }, + + /** * Allow to set the Sidebar as fullscreen from OCA.Files.Sidebar * * @param {boolean} isFullScreen - Whether or not to render the Sidebar in fullscreen. @@ -483,6 +561,15 @@ export default { }, /** + * Allow to set whether tags should be shown by default from OCA.Files.Sidebar + * + * @param {boolean} showTagsDefault - Whether or not to show the tags by default. + */ + setShowTagsDefault(showTagsDefault) { + this.showTagsDefault = showTagsDefault + }, + + /** * Emit SideBar events. */ handleOpening() { @@ -501,11 +588,11 @@ export default { this.hasLowHeight = document.documentElement.clientHeight < 1024 }, }, -} +}) </script> <style lang="scss" scoped> .app-sidebar { - &--has-preview::v-deep { + &--has-preview:deep { .app-sidebar-header__figure { background-size: cover; } @@ -525,12 +612,40 @@ export default { height: 100% !important; } + :deep { + .app-sidebar-header__description { + margin: 0 16px 4px 16px !important; + } + } + .svg-icon { - ::v-deep svg { + :deep(svg) { width: 20px; height: 20px; fill: currentColor; } } } + +.sidebar__subname { + display: flex; + align-items: center; + gap: 0 8px; + + &-separator { + display: inline-block; + font-weight: bold !important; + } + + .user-bubble__wrapper { + display: inline-flex; + } +} + +.sidebar__description { + display: flex; + flex-direction: column; + width: 100%; + gap: 8px 0; + } </style> |