From 7077685bf88420c7085189d48d136d684f00c82c Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Sun, 28 Dec 2025 19:01:18 +0100 Subject: [PATCH] refactor(files_sharing): migrate to new Files Sidebar API Signed-off-by: Ferdinand Thiessen --- .../src/files_actions/sharingStatusAction.ts | 16 ++-- apps/files_sharing/src/files_sharing_tab.js | 73 +++++++++---------- .../src/services/FileInfo.ts | 15 ++-- .../src/views/FilesSidebarTab.vue | 21 ++++++ apps/files_sharing/src/views/SharingTab.vue | 30 +++++--- .../e2e/files_sharing/FilesSharingUtils.ts | 8 +- cypress/e2e/files_sharing/expiry-date.cy.ts | 4 +- 7 files changed, 98 insertions(+), 69 deletions(-) rename apps/{files => files_sharing}/src/services/FileInfo.ts (88%) create mode 100644 apps/files_sharing/src/views/FilesSidebarTab.vue diff --git a/apps/files_sharing/src/files_actions/sharingStatusAction.ts b/apps/files_sharing/src/files_actions/sharingStatusAction.ts index e81ef8fd8af..9d24a3082a8 100644 --- a/apps/files_sharing/src/files_actions/sharingStatusAction.ts +++ b/apps/files_sharing/src/files_actions/sharingStatusAction.ts @@ -3,28 +3,28 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Node } from '@nextcloud/files' +import type { INode } from '@nextcloud/files' import AccountGroupSvg from '@mdi/svg/svg/account-group-outline.svg?raw' import AccountPlusSvg from '@mdi/svg/svg/account-plus-outline.svg?raw' import LinkSvg from '@mdi/svg/svg/link.svg?raw' import { getCurrentUser } from '@nextcloud/auth' import { showError } from '@nextcloud/dialogs' -import { FileAction, Permission, registerFileAction } from '@nextcloud/files' +import { FileAction, getSidebar, Permission, registerFileAction } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import { ShareType } from '@nextcloud/sharing' import { isPublicShare } from '@nextcloud/sharing/public' import CircleSvg from '../../../../core/img/apps/circles.svg?raw' -import { action as sidebarAction } from '../../../files/src/actions/sidebarAction.ts' import { generateAvatarSvg } from '../utils/AccountIcon.ts' import './sharingStatusAction.scss' /** + * Check if the node is external (federated) * - * @param node + * @param node - The node to check */ -function isExternal(node: Node) { +function isExternal(node: INode) { return node.attributes?.['is-federated'] ?? false } @@ -136,12 +136,12 @@ export const action = new FileAction({ && (node.permissions & Permission.READ) !== 0 }, - async exec({ nodes, view, folder, contents }) { + async exec({ nodes }) { // You need read permissions to see the sidebar const node = nodes[0] if ((node.permissions & Permission.READ) !== 0) { - window.OCA?.Files?.Sidebar?.setActiveTab?.('sharing') - sidebarAction.exec({ nodes, view, folder, contents }) + const sidebar = getSidebar() + sidebar.open(node, 'sharing') return null } diff --git a/apps/files_sharing/src/files_sharing_tab.js b/apps/files_sharing/src/files_sharing_tab.js index 9e0bc799f49..98191054e82 100644 --- a/apps/files_sharing/src/files_sharing_tab.js +++ b/apps/files_sharing/src/files_sharing_tab.js @@ -5,8 +5,11 @@ import ShareVariant from '@mdi/svg/svg/share-variant.svg?raw' import { getCSPNonce } from '@nextcloud/auth' +import { registerSidebarTab } from '@nextcloud/files' import { n, t } from '@nextcloud/l10n' +import wrap from '@vue/web-component-wrapper' import Vue from 'vue' +import FilesSidebarTab from './views/FilesSidebarTab.vue' import ExternalShareActions from './services/ExternalShareActions.js' import ShareSearch from './services/ShareSearch.js' import TabSections from './services/TabSections.js' @@ -14,9 +17,7 @@ import TabSections from './services/TabSections.js' __webpack_nonce__ = getCSPNonce() // Init Sharing Tab Service -if (!window.OCA.Sharing) { - window.OCA.Sharing = {} -} +window.OCA.Sharing ??= {} Object.assign(window.OCA.Sharing, { ShareSearch: new ShareSearch() }) Object.assign(window.OCA.Sharing, { ExternalShareActions: new ExternalShareActions() }) Object.assign(window.OCA.Sharing, { ShareTabSections: new TabSections() }) @@ -24,42 +25,34 @@ Object.assign(window.OCA.Sharing, { ShareTabSections: new TabSections() }) Vue.prototype.t = t Vue.prototype.n = n -// Init Sharing tab component -let TabInstance = null +const tagName = 'files_sharing-sidebar-tab' -window.addEventListener('DOMContentLoaded', function() { - if (OCA.Files && OCA.Files.Sidebar) { - OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab({ - id: 'sharing', - name: t('files_sharing', 'Sharing'), - iconSvg: ShareVariant, - - async mount(el, fileInfo, context) { - const SharingTab = (await import('./views/SharingTab.vue')).default - const View = Vue.extend(SharingTab) - - 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() { - if (TabInstance) { - TabInstance.$destroy() - TabInstance = null - } - }, - })) - } +registerSidebarTab({ + id: 'sharing', + displayName: t('files_sharing', 'Sharing'), + iconSvgInline: ShareVariant, + order: 10, + tagName, + enabled() { + if (!window.customElements.get(tagName)) { + setupSidebarTab() + } + return true + }, }) + +/** + * Setup the sidebar tab as a web component + */ +function setupSidebarTab() { + const webComponent = wrap(Vue, FilesSidebarTab) + // In Vue 2, wrap doesn't support diseabling shadow. Disable with a hack + Object.defineProperty(webComponent.prototype, 'attachShadow', { + value() { return this }, + }) + Object.defineProperty(webComponent.prototype, 'shadowRoot', { + get() { return this }, + }) + + window.customElements.define(tagName, webComponent) +} diff --git a/apps/files/src/services/FileInfo.ts b/apps/files_sharing/src/services/FileInfo.ts similarity index 88% rename from apps/files/src/services/FileInfo.ts rename to apps/files_sharing/src/services/FileInfo.ts index f146b662fe8..f8ea6b37310 100644 --- a/apps/files/src/services/FileInfo.ts +++ b/apps/files_sharing/src/services/FileInfo.ts @@ -1,11 +1,9 @@ -/** +/*! * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -/* eslint-disable jsdoc/require-jsdoc */ - -import type { Attribute, Node } from '@nextcloud/files' +import type { Attribute, INode } from '@nextcloud/files' interface RawLegacyFileInfo { id: number @@ -30,11 +28,16 @@ export type LegacyFileInfo = RawLegacyFileInfo & { get: (key: keyof RawLegacyFileInfo) => unknown isDirectory: () => boolean canEdit: () => boolean - node: Node + node: INode canDownload: () => boolean } -export default function(node: Node): LegacyFileInfo { +/** + * Convert Node to legacy file info + * + * @param node - The Node to convert + */ +export default function(node: INode): LegacyFileInfo { const rawFileInfo: RawLegacyFileInfo = { id: node.fileid!, path: node.dirname, diff --git a/apps/files_sharing/src/views/FilesSidebarTab.vue b/apps/files_sharing/src/views/FilesSidebarTab.vue new file mode 100644 index 00000000000..e46e78722c0 --- /dev/null +++ b/apps/files_sharing/src/views/FilesSidebarTab.vue @@ -0,0 +1,21 @@ + + + diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue index 17398fbcf14..784958c5aed 100644 --- a/apps/files_sharing/src/views/SharingTab.vue +++ b/apps/files_sharing/src/views/SharingTab.vue @@ -230,6 +230,13 @@ export default { mixins: [ShareDetails], + props: { + fileInfo: { + type: Object, + required: true, + }, + }, + data() { return { config: new Config(), @@ -238,8 +245,6 @@ export default { expirationInterval: null, loading: true, - fileInfo: null, - // reshare Share object reshare: null, sharedWithMe: {}, @@ -328,18 +333,19 @@ export default { }, }, - 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() + watch: { + fileInfo: { + immediate: true, + handler(newValue, oldValue) { + if (oldValue?.id === undefined || oldValue?.id !== newValue?.id) { + this.resetState() + this.getShares() + } + }, }, + }, + methods: { /** * Get the existing shares infos */ diff --git a/cypress/e2e/files_sharing/FilesSharingUtils.ts b/cypress/e2e/files_sharing/FilesSharingUtils.ts index fb3b3cb72b9..f83f0c9c782 100644 --- a/cypress/e2e/files_sharing/FilesSharingUtils.ts +++ b/cypress/e2e/files_sharing/FilesSharingUtils.ts @@ -35,8 +35,12 @@ export function createShare(fileName: string, username: string, shareSettings: P export function openSharingDetails(index: number) { cy.get('#app-sidebar-vue').within(() => { - cy.get('[data-cy-files-sharing-share-actions]').eq(index).click({ force: true }) - cy.get('[data-cy-files-sharing-share-permissions-bundle="custom"]').click() + cy.findAllByRole('button', { name: /open sharing details/i }) + .should('have.length.at.least', index + 1) + .eq(index) + .click({ force: true }) + cy.get('[data-cy-files-sharing-share-permissions-bundle="custom"]') + .click() }) } diff --git a/cypress/e2e/files_sharing/expiry-date.cy.ts b/cypress/e2e/files_sharing/expiry-date.cy.ts index 32b5239e3a1..0055b499d38 100644 --- a/cypress/e2e/files_sharing/expiry-date.cy.ts +++ b/cypress/e2e/files_sharing/expiry-date.cy.ts @@ -90,11 +90,13 @@ describe('files_sharing: Expiry date', () => { prepareDirectory(dir) updateShare(dir, 0, { expiryDate: fortnight }) validateExpiryDate(dir, fortnightString) - closeSidebar() + + cy.log('Upadate share and validate expiry date is kept') updateShare(dir, 0, { note: 'Only note changed' }) validateExpiryDate(dir, fortnightString) + cy.log('Reload page and validate expiry date is kept') cy.visit('/apps/files') validateExpiryDate(dir, fortnightString) })