diff --git a/packages/core/src/services/resource-manager/type.ts b/packages/core/src/services/resource-manager/type.ts index df982f73b2..152f6a96f6 100644 --- a/packages/core/src/services/resource-manager/type.ts +++ b/packages/core/src/services/resource-manager/type.ts @@ -21,7 +21,7 @@ import { createIdentifier } from '../../common/di'; export type IResources = Array<{ id?: string; name: string; data: string }>; -type IBusinessName = 'SHEET' | 'DOC' | 'SLIDE' | 'BASE' | 'UNIVER'; +type IBusinessName = 'SHEET' | 'DOC' | 'SLIDE' | 'BOARD' | 'BASE' | 'UNIVER'; export type IResourceName = `${IBusinessName}_${string}_PLUGIN`; export interface IResourceHook { pluginName: IResourceName; diff --git a/packages/docs-thread-comment/src/controllers/docs-thread-comment-resource.controller.ts b/packages/docs-thread-comment/src/controllers/docs-thread-comment-resource.controller.ts new file mode 100644 index 0000000000..8890456eee --- /dev/null +++ b/packages/docs-thread-comment/src/controllers/docs-thread-comment-resource.controller.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Disposable, Inject, IResourceManagerService, UniverInstanceType } from '@univerjs/core'; +import { + createThreadCommentResourceHook, + IThreadCommentDataSourceService, + TC_PLUGIN_NAME, + ThreadCommentModel, +} from '@univerjs/thread-comment'; + +export const DOC_UNIVER_THREAD_COMMENT_PLUGIN = `DOC_${TC_PLUGIN_NAME}`; + +export class DocsThreadCommentResourceController extends Disposable { + constructor( + @IResourceManagerService private readonly _resourceManagerService: IResourceManagerService, + @Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel, + @IThreadCommentDataSourceService private readonly _threadCommentDataSourceService: IThreadCommentDataSourceService + ) { + super(); + this.disposeWithMe(this._resourceManagerService.registerPluginResource(createThreadCommentResourceHook( + this._threadCommentModel, + this._threadCommentDataSourceService, + DOC_UNIVER_THREAD_COMMENT_PLUGIN, + [UniverInstanceType.UNIVER_DOC] + ))); + } +} diff --git a/packages/docs-thread-comment/src/plugin.ts b/packages/docs-thread-comment/src/plugin.ts index 248de11463..6252caadc4 100644 --- a/packages/docs-thread-comment/src/plugin.ts +++ b/packages/docs-thread-comment/src/plugin.ts @@ -34,6 +34,7 @@ import { } from './commands/commands/create-doc-text-range-comment.command'; import { DOCS_THREAD_COMMENT_PLUGIN_NAME } from './common/const'; import { defaultPluginConfig, DOCS_THREAD_COMMENT_PLUGIN_CONFIG_KEY } from './config/config'; +import { DocsThreadCommentResourceController } from './controllers/docs-thread-comment-resource.controller'; @DependentOn(UniverDocsPlugin, UniverThreadCommentPlugin) export class UniverDocsThreadCommentPlugin extends Plugin { @@ -59,6 +60,8 @@ export class UniverDocsThreadCommentPlugin extends Plugin { } override onStarting(): void { + this._injector.add([DocsThreadCommentResourceController]); + this._injector.get(DocsThreadCommentResourceController); this.disposeWithMe(this._commandService.registerCommand(CreateDocTextRangeCommentCommand)); this.disposeWithMe(this._commandService.registerCommand(AddDocCommentDecorationMutation)); } diff --git a/packages/sheets-thread-comment/src/controllers/__tests__/sheets-thread-comment-resource.controller.spec.ts b/packages/sheets-thread-comment/src/controllers/__tests__/sheets-thread-comment-resource.controller.spec.ts index 6ed04d5f16..db32441f83 100644 --- a/packages/sheets-thread-comment/src/controllers/__tests__/sheets-thread-comment-resource.controller.spec.ts +++ b/packages/sheets-thread-comment/src/controllers/__tests__/sheets-thread-comment-resource.controller.spec.ts @@ -14,9 +14,14 @@ * limitations under the License. */ +import { UniverInstanceType } from '@univerjs/core'; import { CopySheetCommand, RemoveSheetCommand } from '@univerjs/sheets'; -import { AddCommentMutation, DeleteCommentMutation } from '@univerjs/thread-comment'; +import { + AddCommentMutation, + DeleteCommentMutation, +} from '@univerjs/thread-comment'; import { describe, expect, it, vi } from 'vitest'; +import { SHEET_UNIVER_THREAD_COMMENT_PLUGIN } from '../../types/const'; import { SheetsThreadCommentResourceController } from '../sheets-thread-comment-resource.controller'; function createRootComment() { @@ -40,6 +45,9 @@ describe('SheetsThreadCommentResourceController', () => { const rootComment = createRootComment(); const controller = new SheetsThreadCommentResourceController( + { + registerPluginResource: () => ({ dispose: vi.fn() }), + } as any, { getCurrentUnitOfType: () => ({ getUnitId: () => 'unit-1', @@ -94,6 +102,9 @@ describe('SheetsThreadCommentResourceController', () => { const rootComment = createRootComment(); const controller = new SheetsThreadCommentResourceController( + { + registerPluginResource: () => ({ dispose: vi.fn() }), + } as any, {} as any, { interceptCommand: (config: any) => { @@ -149,4 +160,24 @@ describe('SheetsThreadCommentResourceController', () => { controller.dispose(); }); + + it('registers sheet thread comment snapshot resources for sheet units', () => { + let hook: any; + const controller = new SheetsThreadCommentResourceController( + { + registerPluginResource: (nextHook: any) => { + hook = nextHook; + return { dispose: vi.fn() }; + }, + } as any, + {} as any, + { interceptCommand: () => ({ dispose: vi.fn() }) } as any, + { getUnit: () => [] } as any, + { saveToSnapshot: (value: unknown) => value } as any + ); + + expect(hook.pluginName).toBe(SHEET_UNIVER_THREAD_COMMENT_PLUGIN); + expect(hook.businesses).toEqual([UniverInstanceType.UNIVER_SHEET]); + controller.dispose(); + }); }); diff --git a/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-resource.controller.ts b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-resource.controller.ts index b7bf29a588..753ec48890 100644 --- a/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-resource.controller.ts +++ b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-resource.controller.ts @@ -16,21 +16,46 @@ import type { IMutationInfo, Workbook } from '@univerjs/core'; import type { ICopySheetCommandParams, IRemoveSheetCommandParams } from '@univerjs/sheets'; -import { Disposable, generateRandomId, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; +import { + Disposable, + generateRandomId, + Inject, + IResourceManagerService, + IUniverInstanceService, + UniverInstanceType, +} from '@univerjs/core'; import { CopySheetCommand, RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets'; -import { AddCommentMutation, DeleteCommentMutation, IThreadCommentDataSourceService, ThreadCommentModel } from '@univerjs/thread-comment'; +import { + AddCommentMutation, + createThreadCommentResourceHook, + DeleteCommentMutation, + IThreadCommentDataSourceService, + ThreadCommentModel, +} from '@univerjs/thread-comment'; +import { SHEET_UNIVER_THREAD_COMMENT_PLUGIN } from '../types/const'; export class SheetsThreadCommentResourceController extends Disposable { constructor( + @IResourceManagerService private readonly _resourceManagerService: IResourceManagerService, @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, - @Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService, - @Inject(ThreadCommentModel) private _threadCommentModel: ThreadCommentModel, - @IThreadCommentDataSourceService private _threadCommentDataSourceService: IThreadCommentDataSourceService + @Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService, + @Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel, + @IThreadCommentDataSourceService private readonly _threadCommentDataSourceService: IThreadCommentDataSourceService ) { super(); + this._initSnapshot(); this._initSheetChange(); } + private _initSnapshot() { + this.disposeWithMe(this._resourceManagerService.registerPluginResource(createThreadCommentResourceHook( + this._threadCommentModel, + this._threadCommentDataSourceService, + SHEET_UNIVER_THREAD_COMMENT_PLUGIN, + [UniverInstanceType.UNIVER_SHEET] + ))); + } + // eslint-disable-next-line max-lines-per-function private _initSheetChange() { this.disposeWithMe( diff --git a/packages/sheets-thread-comment/src/index.ts b/packages/sheets-thread-comment/src/index.ts index 7bc252067c..a00aaef911 100644 --- a/packages/sheets-thread-comment/src/index.ts +++ b/packages/sheets-thread-comment/src/index.ts @@ -16,6 +16,8 @@ export type { IUniverSheetsThreadCommentConfig } from './config/config'; export { SheetsThreadCommentRefRangeController } from './controllers/sheets-thread-comment-ref-range.controller'; +export { SheetsThreadCommentResourceController } from './controllers/sheets-thread-comment-resource.controller'; export { SheetsThreadCommentModel } from './models/sheets-thread-comment.model'; export { UniverSheetsThreadCommentPlugin } from './plugin'; +export { SHEET_UNIVER_THREAD_COMMENT_PLUGIN } from './types/const'; export type { ISheetThreadComment } from './types/interfaces/i-sheet-thread-comment'; diff --git a/packages/sheets-thread-comment/src/types/const.ts b/packages/sheets-thread-comment/src/types/const.ts index 29f66f2e1b..f916dfd391 100644 --- a/packages/sheets-thread-comment/src/types/const.ts +++ b/packages/sheets-thread-comment/src/types/const.ts @@ -14,4 +14,7 @@ * limitations under the License. */ +import { TC_PLUGIN_NAME } from '@univerjs/thread-comment'; + export const SHEET_THREAD_COMMENT_BASE = 'SHEET_THREAD_COMMENT_BASE_PLUGIN'; +export const SHEET_UNIVER_THREAD_COMMENT_PLUGIN = `SHEET_${TC_PLUGIN_NAME}`; diff --git a/packages/thread-comment/src/common/tc-resource.ts b/packages/thread-comment/src/common/tc-resource.ts new file mode 100644 index 0000000000..cb4e082cd7 --- /dev/null +++ b/packages/thread-comment/src/common/tc-resource.ts @@ -0,0 +1,116 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IResourceHook, UniverInstanceType } from '@univerjs/core'; +import type { ThreadCommentModel } from '../models/thread-comment.model'; +import type { IThreadCommentDataSourceService } from '../services/tc-datasource.service'; +import type { IThreadComment } from '../types/interfaces/i-thread-comment'; +import { isThreadCommentDocumentBody } from '../services/thread-comment-api.service'; + +export type UnitThreadCommentJSON = Record; + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((item) => typeof item === 'string'); +} + +function isSnapshotComment(value: unknown, allowChildren = true): value is IThreadComment { + if (!value || typeof value !== 'object') { + return false; + } + const comment = value as Record; + return typeof comment.id === 'string' + && comment.id.length > 0 + && typeof comment.threadId === 'string' + && comment.threadId.length > 0 + && typeof comment.ref === 'string' + && (comment.text === undefined || isThreadCommentDocumentBody(comment.text)) + && (comment.attachments === undefined || isStringArray(comment.attachments)) + && (comment.mentions === undefined || isStringArray(comment.mentions)) + && (!allowChildren || comment.children === undefined || ( + Array.isArray(comment.children) + && comment.children.every((child) => isSnapshotComment(child, false)) + )); +} + +export function createThreadCommentResourceHook( + threadCommentModel: ThreadCommentModel, + threadCommentDataSourceService: IThreadCommentDataSourceService, + pluginName: IResourceHook['pluginName'], + businesses: UniverInstanceType[] +): IResourceHook { + return { + pluginName, + businesses, + toJson: (unitID) => { + const map = threadCommentModel.getUnit(unitID); + const resultMap: UnitThreadCommentJSON = {}; + map.forEach((info) => { + const subUnitComments = resultMap[info.subUnitId] ?? []; + subUnitComments.push({ + ...info.root, + children: info.children, + }); + resultMap[info.subUnitId] = subUnitComments; + }); + + return JSON.stringify(threadCommentDataSourceService.saveToSnapshot(resultMap, unitID)); + }, + parseJson: (json) => { + if (!json) { + return {}; + } + try { + const value: unknown = JSON.parse(json); + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return {}; + } + return Object.fromEntries(Object.entries(value).flatMap(([subUnitId, comments]) => ( + Array.isArray(comments) + ? [[subUnitId, comments.filter((comment) => isSnapshotComment(comment))]] + : [] + ))); + } catch { + return {}; + } + }, + onUnLoad: (unitID) => { + threadCommentModel.deleteUnit(unitID); + }, + onLoad: (unitID, value) => { + Object.keys(value).forEach((subunitId) => { + const commentList = value[subunitId]; + commentList.forEach((comment: IThreadComment) => { + const seenIds = new Set([comment.id]); + const children = comment.children?.filter((child) => { + if (child.threadId !== comment.threadId || seenIds.has(child.id)) { + return false; + } + seenIds.add(child.id); + return true; + }).map((child) => ({ ...child, unitId: unitID, subUnitId: subunitId })); + threadCommentModel.addComment(unitID, subunitId, { + ...comment, + unitId: unitID, + subUnitId: subunitId, + children, + }); + }); + + threadCommentModel.syncThreadComments(unitID, subunitId, commentList.map((i) => i.threadId)); + }); + }, + }; +} diff --git a/packages/thread-comment/src/controllers/__tests__/tc-resource.controller.spec.ts b/packages/thread-comment/src/controllers/__tests__/tc-resource.controller.spec.ts deleted file mode 100644 index ca45ae194c..0000000000 --- a/packages/thread-comment/src/controllers/__tests__/tc-resource.controller.spec.ts +++ /dev/null @@ -1,247 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { IDocumentBody, Injector, IWorkbookData } from '@univerjs/core'; -import type { IThreadComment } from '../../types/interfaces/i-thread-comment'; -import { - IResourceManagerService, - IUniverInstanceService, - LifecycleService, - LifecycleStages, - LocaleType, - Univer, - UniverInstanceType, -} from '@univerjs/core'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { ThreadCommentModel } from '../../models/thread-comment.model'; -import { UniverThreadCommentPlugin } from '../../plugin'; -import { serializeThreadCommentAnchor, ThreadCommentAnchorKind } from '../../types/comment-anchor'; -import { SHEET_UNIVER_THREAD_COMMENT_PLUGIN } from '../tc-resource.controller'; - -function createWorkbookData(): IWorkbookData { - return { - id: 'unit-1', - appVersion: '3.0.0-alpha', - locale: LocaleType.EN_US, - name: '', - sheetOrder: ['sheet-1'], - styles: {}, - sheets: { - 'sheet-1': { - id: 'sheet-1', - name: 'Sheet1', - cellData: {}, - }, - }, - }; -} - -function createBody(text: string): IDocumentBody { - return { - dataStream: `${text}\r\n`, - }; -} - -function createComment(overrides: Partial = {}): IThreadComment { - const id = overrides.id ?? 'comment-1'; - - return { - id, - threadId: overrides.threadId ?? overrides.parentId ?? id, - ref: overrides.ref ?? 'A1', - dT: overrides.dT ?? '2024-01-01T00:00:00.000Z', - personId: overrides.personId ?? 'user-1', - text: overrides.text ?? createBody(id), - unitId: overrides.unitId ?? 'unit-1', - subUnitId: overrides.subUnitId ?? 'sheet-1', - attachments: overrides.attachments, - children: overrides.children, - mentions: overrides.mentions, - parentId: overrides.parentId, - resolved: overrides.resolved, - updateT: overrides.updateT, - updated: overrides.updated, - }; -} - -describe('ThreadCommentResourceController', () => { - let univer: Univer; - let get: Injector['get']; - let resourceManagerService: IResourceManagerService; - let threadCommentModel: ThreadCommentModel; - - beforeEach(() => { - univer = new Univer(); - univer.registerPlugin(UniverThreadCommentPlugin); - univer.createUnit(UniverInstanceType.UNIVER_SHEET, createWorkbookData()); - - const injector = univer.__getInjector(); - get = injector.get.bind(injector); - get(IUniverInstanceService).focusUnit('unit-1'); - get(LifecycleService).stage = LifecycleStages.Rendered; - - resourceManagerService = get(IResourceManagerService); - threadCommentModel = get(ThreadCommentModel); - }); - - afterEach(() => { - univer.dispose(); - }); - - it('serializes thread comments by sub unit with children preserved', () => { - const root = createComment({ id: 'root-1', ref: 'A1' }); - const reply = createComment({ id: 'reply-1', parentId: root.id, threadId: root.id, ref: '' }); - const sideThread = createComment({ id: 'root-2', subUnitId: 'sheet-2', ref: 'B2' }); - - threadCommentModel.addComment('unit-1', 'sheet-1', root); - threadCommentModel.addComment('unit-1', 'sheet-1', reply); - threadCommentModel.addComment('unit-1', 'sheet-2', sideThread); - - const resource = resourceManagerService.getResourcesByType('unit-1', UniverInstanceType.UNIVER_SHEET) - .find((item) => item.name === SHEET_UNIVER_THREAD_COMMENT_PLUGIN); - - expect(resource).toBeDefined(); - expect(JSON.parse(resource!.data)).toEqual({ - 'sheet-1': [ - { - ...root, - children: [reply], - }, - ], - 'sheet-2': [ - { - ...sideThread, - children: [], - }, - ], - }); - }); - - it.each([ - UniverInstanceType.UNIVER_SLIDE, - UniverInstanceType.UNIVER_BOARD, - UniverInstanceType.UNIVER_BASE, - ])('serializes thread comments for business type %s', (businessType) => { - const root = createComment({ id: 'root-business', ref: 'element-1', subUnitId: 'page-1' }); - threadCommentModel.addComment('unit-1', 'page-1', root); - - const resource = resourceManagerService.getResourcesByType('unit-1', businessType) - .find((item) => item.name === SHEET_UNIVER_THREAD_COMMENT_PLUGIN); - - expect(resource).toBeDefined(); - if (resource === undefined) { - return; - } - - expect(JSON.parse(resource.data)).toEqual({ - 'page-1': [{ - ...root, - children: [], - }], - }); - }); - - it('loads serialized comments and clears them on unload', async () => { - const root = createComment({ - id: 'root-3', - ref: serializeThreadCommentAnchor({ kind: ThreadCommentAnchorKind.SHEET_DRAWING, elementId: 'drawing-1' }), - attachments: ['root.png'], - mentions: ['user-2'], - resolved: true, - updated: true, - updateT: '2024-01-02T00:00:00.000Z', - }); - const reply = createComment({ - id: 'reply-3', - parentId: root.id, - threadId: root.id, - ref: '', - attachments: ['reply.png'], - }); - - resourceManagerService.loadResources('unit-1', [{ - name: SHEET_UNIVER_THREAD_COMMENT_PLUGIN, - data: JSON.stringify({ - 'sheet-1': [ - { - ...root, - children: [reply], - }, - ], - }), - }]); - - await Promise.resolve(); - - expect(threadCommentModel.getThread('unit-1', 'sheet-1', root.id)).toEqual({ - unitId: 'unit-1', - subUnitId: 'sheet-1', - threadId: root.id, - root, - children: [reply], - relativeUsers: new Set(['user-1']), - }); - - resourceManagerService.unloadResources('unit-1', UniverInstanceType.UNIVER_SHEET); - - expect(threadCommentModel.getUnit('unit-1')).toEqual([]); - }); - - it('ignores invalid snapshot payloads', async () => { - resourceManagerService.loadResources('unit-1', [{ - name: SHEET_UNIVER_THREAD_COMMENT_PLUGIN, - data: '{invalid-json', - }]); - - await Promise.resolve(); - - expect(threadCommentModel.getUnit('unit-1')).toEqual([]); - }); - - it('filters malformed snapshot entries and keeps loaded comments inside their resource unit', async () => { - const root = createComment({ - id: 'safe-root', - unitId: 'spoofed-unit', - subUnitId: 'spoofed-sheet', - children: [ - createComment({ id: 'safe-root', threadId: 'safe-root', parentId: 'safe-root' }), - createComment({ id: 'wrong-thread', threadId: 'other-thread', parentId: 'safe-root' }), - createComment({ id: 'safe-reply', threadId: 'safe-root', parentId: 'safe-root' }), - ], - }); - resourceManagerService.loadResources('unit-1', [{ - name: SHEET_UNIVER_THREAD_COMMENT_PLUGIN, - data: JSON.stringify({ - 'not-an-array': {}, - 'sheet-1': [ - null, - { id: '', threadId: 'bad', ref: 'A1' }, - { id: 'bad-body', threadId: 'bad-body', ref: 'A1', text: { dataStream: 1 } }, - root, - ], - }), - }]); - - await Promise.resolve(); - - expect(threadCommentModel.getUnit('spoofed-unit')).toEqual([]); - expect(threadCommentModel.getThread('unit-1', 'sheet-1', root.threadId)).toMatchObject({ - root: { id: root.id, unitId: 'unit-1', subUnitId: 'sheet-1' }, - children: [{ id: 'safe-reply', unitId: 'unit-1', subUnitId: 'sheet-1' }], - }); - expect(threadCommentModel.getComment('unit-1', 'sheet-1', 'wrong-thread')).toBeUndefined(); - }); -}); diff --git a/packages/thread-comment/src/controllers/tc-resource.controller.ts b/packages/thread-comment/src/controllers/tc-resource.controller.ts deleted file mode 100644 index f6906231b7..0000000000 --- a/packages/thread-comment/src/controllers/tc-resource.controller.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright 2023-present DreamNum Co., Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { IThreadComment } from '../types/interfaces/i-thread-comment'; -import { Disposable, Inject, IResourceManagerService, UniverInstanceType } from '@univerjs/core'; -import { ThreadCommentModel } from '../models/thread-comment.model'; -import { IThreadCommentDataSourceService } from '../services/tc-datasource.service'; -import { isThreadCommentDocumentBody } from '../services/thread-comment-api.service'; -import { TC_PLUGIN_NAME } from '../types/const'; - -export type UnitThreadCommentJSON = Record; - -export const SHEET_UNIVER_THREAD_COMMENT_PLUGIN = `SHEET_${TC_PLUGIN_NAME}`; - -function isStringArray(value: unknown): value is string[] { - return Array.isArray(value) && value.every((item) => typeof item === 'string'); -} - -function isSnapshotComment(value: unknown, allowChildren = true): value is IThreadComment { - if (!value || typeof value !== 'object') { - return false; - } - const comment = value as Record; - return typeof comment.id === 'string' - && comment.id.length > 0 - && typeof comment.threadId === 'string' - && comment.threadId.length > 0 - && typeof comment.ref === 'string' - && (comment.text === undefined || isThreadCommentDocumentBody(comment.text)) - && (comment.attachments === undefined || isStringArray(comment.attachments)) - && (comment.mentions === undefined || isStringArray(comment.mentions)) - && (!allowChildren || comment.children === undefined || ( - Array.isArray(comment.children) - && comment.children.every((child) => isSnapshotComment(child, false)) - )); -} - -export class ThreadCommentResourceController extends Disposable { - constructor( - @IResourceManagerService private readonly _resourceManagerService: IResourceManagerService, - @Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel, - @IThreadCommentDataSourceService private readonly _threadCommentDataSourceService: IThreadCommentDataSourceService - ) { - super(); - this._initSnapshot(); - } - - private _initSnapshot() { - const toJson = (unitID: string) => { - const map = this._threadCommentModel.getUnit(unitID); - const resultMap: UnitThreadCommentJSON = {}; - if (map) { - map.forEach((info) => { - const subUnitComments = resultMap[info.subUnitId] ?? []; - subUnitComments.push({ - ...info.root, - children: info.children, - }); - resultMap[info.subUnitId] = subUnitComments; - }); - - return JSON.stringify(this._threadCommentDataSourceService.saveToSnapshot(resultMap, unitID)); - } - return ''; - }; - const parseJson = (json: string): UnitThreadCommentJSON => { - if (!json) { - return {}; - } - try { - const value: unknown = JSON.parse(json); - if (!value || typeof value !== 'object' || Array.isArray(value)) { - return {}; - } - return Object.fromEntries(Object.entries(value).flatMap(([subUnitId, comments]) => ( - Array.isArray(comments) - ? [[subUnitId, comments.filter((comment) => isSnapshotComment(comment))]] - : [] - ))); - } catch { - return {}; - } - }; - - this.disposeWithMe( - this._resourceManagerService.registerPluginResource({ - pluginName: SHEET_UNIVER_THREAD_COMMENT_PLUGIN, - businesses: [ - UniverInstanceType.UNIVER_SHEET, - UniverInstanceType.UNIVER_DOC, - UniverInstanceType.UNIVER_SLIDE, - UniverInstanceType.UNIVER_BOARD, - UniverInstanceType.UNIVER_BASE, - ], - toJson: (unitID) => toJson(unitID), - parseJson: (json) => parseJson(json), - onUnLoad: (unitID) => { - this._threadCommentModel.deleteUnit(unitID); - }, - onLoad: async (unitID, value) => { - Object.keys(value).forEach((subunitId) => { - const commentList = value[subunitId]; - commentList.forEach((comment: IThreadComment) => { - const seenIds = new Set([comment.id]); - const children = comment.children?.filter((child) => { - if (child.threadId !== comment.threadId || seenIds.has(child.id)) { - return false; - } - seenIds.add(child.id); - return true; - }).map((child) => ({ ...child, unitId: unitID, subUnitId: subunitId })); - this._threadCommentModel.addComment(unitID, subunitId, { - ...comment, - unitId: unitID, - subUnitId: subunitId, - children, - }); - }); - - this._threadCommentModel.syncThreadComments(unitID, subunitId, commentList.map((i) => i.threadId)); - }); - }, - }) - ); - } -} diff --git a/packages/thread-comment/src/index.ts b/packages/thread-comment/src/index.ts index a536587bd6..a402ac71ca 100644 --- a/packages/thread-comment/src/index.ts +++ b/packages/thread-comment/src/index.ts @@ -43,12 +43,11 @@ export type { IUpdateCommentPayload, IUpdateCommentRefMutationParams, } from './commands/mutations/comment.mutation'; +export { + createThreadCommentResourceHook, +} from './common/tc-resource'; export { getDT } from './common/utils'; export type { IUniverThreadCommentConfig } from './config/config'; -export { - SHEET_UNIVER_THREAD_COMMENT_PLUGIN, - ThreadCommentResourceController, -} from './controllers/tc-resource.controller'; export { ThreadCommentModel } from './models/thread-comment.model'; export type { CommentUpdate, IThreadCommentQuery, IThreadInfo } from './models/thread-comment.model'; export { UniverThreadCommentPlugin } from './plugin'; diff --git a/packages/thread-comment/src/plugin.ts b/packages/thread-comment/src/plugin.ts index 829b1eae72..4f8840c194 100644 --- a/packages/thread-comment/src/plugin.ts +++ b/packages/thread-comment/src/plugin.ts @@ -42,7 +42,6 @@ import { UpdateCommentRefMutation, } from './commands/mutations/comment.mutation'; import { defaultPluginConfig, THREAD_COMMENT_PLUGIN_CONFIG_KEY } from './config/config'; -import { ThreadCommentResourceController } from './controllers/tc-resource.controller'; import { ThreadCommentModel } from './models/thread-comment.model'; import { IThreadCommentDataSourceService, ThreadCommentDataSourceService } from './services/tc-datasource.service'; import { ThreadCommentFacadeService } from './services/thread-comment-api.service'; @@ -76,7 +75,6 @@ export class UniverThreadCommentPlugin extends Plugin { [IThreadCommentDataSourceService, { useClass: ThreadCommentDataSourceService }], [ThreadCommentModel], [ThreadCommentFacadeService], - [ThreadCommentResourceController], ], this._config?.overrides) as Dependency[]).forEach( (d) => { this._injector.add(d); @@ -97,7 +95,5 @@ export class UniverThreadCommentPlugin extends Plugin { ].forEach((command) => { this._commandService.registerCommand(command); }); - - this._injector.get(ThreadCommentResourceController); } }