mirror of
https://github.com/dream-num/univer.git
synced 2026-08-28 14:56:51 +08:00
refactor(thread-comment): scope resources by host package (#7595)
This commit is contained in:
@@ -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<T = any> {
|
||||
pluginName: IResourceName;
|
||||
|
||||
+41
@@ -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]
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
+32
-1
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+30
-5
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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<string, IThreadComment[]>;
|
||||
|
||||
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<string, unknown>;
|
||||
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<UnitThreadCommentJSON> {
|
||||
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));
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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> = {}): 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();
|
||||
});
|
||||
});
|
||||
@@ -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<string, IThreadComment[]>;
|
||||
|
||||
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<string, unknown>;
|
||||
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));
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user