From c56ebcecb238f90110f41c8f81ce77ba1a63eeeb Mon Sep 17 00:00:00 2001 From: Edward Ly Date: Sun, 15 Feb 2026 18:12:10 -0800 Subject: [PATCH] refactor(comments): migrate to Vue 3 Signed-off-by: Edward Ly --- .../lib/Listener/LoadSidebarScripts.php | 3 +- apps/comments/src/comments-activity-tab.ts | 49 ++++++++--------- apps/comments/src/components/Comment.vue | 4 +- apps/comments/src/files-sidebar.ts | 26 ++++----- .../comments/src/services/CommentsInstance.ts | 54 +++++++++---------- apps/comments/src/views/Comments.vue | 2 +- 6 files changed, 61 insertions(+), 77 deletions(-) diff --git a/apps/comments/lib/Listener/LoadSidebarScripts.php b/apps/comments/lib/Listener/LoadSidebarScripts.php index 906fe40fed2..8629883ba04 100644 --- a/apps/comments/lib/Listener/LoadSidebarScripts.php +++ b/apps/comments/lib/Listener/LoadSidebarScripts.php @@ -34,7 +34,8 @@ class LoadSidebarScripts implements IEventListener { $this->commentsManager->load(); $this->initialState->provideInitialState('activityEnabled', $this->appManager->isEnabledForUser('activity')); - // Add comments sidebar tab script + // Add comments sidebar tab script/style + Util::addStyle(Application::APP_ID, 'comments-tab'); Util::addScript(Application::APP_ID, 'comments-tab', 'files'); } } diff --git a/apps/comments/src/comments-activity-tab.ts b/apps/comments/src/comments-activity-tab.ts index fddd1eb36e3..eda9fe7d55f 100644 --- a/apps/comments/src/comments-activity-tab.ts +++ b/apps/comments/src/comments-activity-tab.ts @@ -4,45 +4,40 @@ */ import type { INode } from '@nextcloud/files' +import type { ComponentPublicInstance } from 'vue' -import { createPinia, PiniaVuePlugin } from 'pinia' -import Vue, { type ComponentPublicInstance } from 'vue' +import { createPinia } from 'pinia' +import { createApp } from 'vue' import logger from './logger.ts' import { getComments } from './services/GetComments.ts' -Vue.use(PiniaVuePlugin) - -let ActivityTabPluginView -let ActivityTabPluginInstance - /** * Register the comments plugins for the Activity sidebar */ export function registerCommentsPlugins() { + let app + window.OCA.Activity.registerSidebarAction({ mount: async (el: HTMLElement, { node, reload }: { node: INode, reload: () => void }) => { const pinia = createPinia() - if (!ActivityTabPluginView) { + if (!app) { const { default: ActivityCommentAction } = await import('./views/ActivityCommentAction.vue') - // @ts-expect-error Types are broken for Vue2 - ActivityTabPluginView = Vue.extend(ActivityCommentAction) + app = createApp( + ActivityCommentAction, + { + reloadCallback: reload, + resourceId: node.fileid, + }, + ) } - ActivityTabPluginInstance = new ActivityTabPluginView({ - el, - pinia, - propsData: { - reloadCallback: reload, - resourceId: node.fileid, - }, - }) + app.use(pinia) + app.mount(el) logger.info('Comments plugin mounted in Activity sidebar action', { node }) }, unmount: () => { // destroy previous instance if available - if (ActivityTabPluginInstance) { - ActivityTabPluginInstance.$destroy() - } + app?.unmount() }, }) @@ -56,7 +51,6 @@ export function registerCommentsPlugins() { ) logger.debug('Loaded comments', { node, comments }) const { default: CommentView } = await import('./views/ActivityCommentEntry.vue') - const CommentsViewObject = Vue.extend(CommentView) return comments.map((comment) => ({ _CommentsViewInstance: undefined as ComponentPublicInstance | undefined, @@ -64,17 +58,18 @@ export function registerCommentsPlugins() { timestamp: Date.parse(comment.props?.creationDateTime as string | undefined ?? ''), mount(element: HTMLElement, { reload }) { - this._CommentsViewInstance = new CommentsViewObject({ - el: element, - propsData: { + this._CommentsViewInstance = createApp( + CommentView, + { comment, resourceId: node.fileid, reloadCallback: reload, }, - }) + ) + this._CommentsViewInstance.mount(el) }, unmount() { - this._CommentsViewInstance?.$destroy() + this._CommentsViewInstance?.unmount() }, })) }) diff --git a/apps/comments/src/components/Comment.vue b/apps/comments/src/components/Comment.vue index 0fdbd99999c..7b7fc4cdd46 100644 --- a/apps/comments/src/components/Comment.vue +++ b/apps/comments/src/components/Comment.vue @@ -75,7 +75,7 @@ :model-value="localMessage" :user-data="userData" aria-describedby="tab-comments__editor-description" - @update:value="updateLocalMessage" + @update:model-value="updateLocalMessage" @submit="onSubmit" />
+ @click="onExpand" />
diff --git a/apps/comments/src/files-sidebar.ts b/apps/comments/src/files-sidebar.ts index 1a39291c48d..999708a840e 100644 --- a/apps/comments/src/files-sidebar.ts +++ b/apps/comments/src/files-sidebar.ts @@ -4,17 +4,13 @@ */ import MessageReplyText from '@mdi/svg/svg/message-reply-text.svg?raw' -import { getCSPNonce } from '@nextcloud/auth' import { registerSidebarTab } from '@nextcloud/files' import { t } from '@nextcloud/l10n' -import wrap from '@vue/web-component-wrapper' -import { createPinia, PiniaVuePlugin } from 'pinia' -import Vue from 'vue' +import { createPinia } from 'pinia' +import { defineCustomElement } from 'vue' import { registerCommentsPlugins } from './comments-activity-tab.ts' import { isUsingActivityIntegration } from './utils/activity.ts' -__webpack_nonce__ = getCSPNonce() - const tagName = 'comments_files-sidebar-tab' if (isUsingActivityIntegration()) { @@ -32,17 +28,15 @@ if (isUsingActivityIntegration()) { async onInit() { const { default: FilesSidebarTab } = await import('./views/FilesSidebarTab.vue') - Vue.use(PiniaVuePlugin) - Vue.mixin({ pinia: createPinia() }) - const webComponent = wrap(Vue, FilesSidebarTab) - // In Vue 2, wrap doesn't support disabling shadow. Disable with a hack - Object.defineProperty(webComponent.prototype, 'attachShadow', { - value() { return this }, + const FilesSidebarTabElement = defineCustomElement(FilesSidebarTab, { + configureApp(app) { + const pinia = createPinia() + app.use(pinia) + }, + shadowRoot: false, }) - Object.defineProperty(webComponent.prototype, 'shadowRoot', { - get() { return this }, - }) - window.customElements.define(tagName, webComponent) + + window.customElements.define(tagName, FilesSidebarTabElement) }, }) } diff --git a/apps/comments/src/services/CommentsInstance.ts b/apps/comments/src/services/CommentsInstance.ts index d43a8581d9c..32a8ed3eeae 100644 --- a/apps/comments/src/services/CommentsInstance.ts +++ b/apps/comments/src/services/CommentsInstance.ts @@ -3,51 +3,45 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { getCSPNonce } from '@nextcloud/auth' import { n, t } from '@nextcloud/l10n' -import { createPinia, PiniaVuePlugin } from 'pinia' -import Vue from 'vue' +import { createPinia } from 'pinia' +import { createApp } from 'vue' import CommentsApp from '../views/Comments.vue' import logger from '../logger.ts' -Vue.use(PiniaVuePlugin) - -__webpack_nonce__ = getCSPNonce() - -// Add translates functions -Vue.mixin({ - data() { - return { - logger, - } - }, - methods: { - t, - n, - }, -}) - export default class CommentInstance { /** * Initialize a new Comments instance for the desired type * * @param {string} resourceType the comments endpoint type - * @param {object} options the vue options (propsData, parent, el...) + * @param {object} options the vue options (propsData, parent, el...) */ constructor(resourceType = 'files', options = {}) { const pinia = createPinia() - // Merge options and set `resourceType` property - options = { - ...options, - propsData: { + const app = createApp( + CommentsApp, + { ...(options.propsData ?? {}), resourceType, }, - pinia, - } - // Init Comments component - const View = Vue.extend(CommentsApp) - return new View(options) + ) + + // Add translates functions + app.mixin({ + data() { + return { + logger, + } + }, + methods: { + t, + n, + }, + }) + + app.use(pinia) + // app.mount(options.el) + return app } } diff --git a/apps/comments/src/views/Comments.vue b/apps/comments/src/views/Comments.vue index 4b5bf5816e4..cdd9621ae19 100644 --- a/apps/comments/src/views/Comments.vue +++ b/apps/comments/src/views/Comments.vue @@ -33,11 +33,11 @@