snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
41
packages/sheets-thread-comment/README.md
Normal file
41
packages/sheets-thread-comment/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/sheets-thread-comment
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets-thread-comment)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-thread-comment)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-thread-comment)
|
||||
|
||||
`@univerjs/sheets-thread-comment` connects shared thread comments to Univer Sheets and provides sheet-specific comment services.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/sheets-thread-comment` | `UniverSheetsThreadComment` | No | No | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/sheets-thread-comment
|
||||
# or
|
||||
npm install @univerjs/sheets-thread-comment
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { UniverSheetsThreadCommentPlugin } from '@univerjs/sheets-thread-comment';
|
||||
|
||||
univer.registerPlugin(UniverSheetsThreadCommentPlugin);
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
Use this package with `@univerjs/sheets-thread-comment-ui` when users need comment UI in sheets.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/sheets-thread-comment)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
92
packages/sheets-thread-comment/package.json
Normal file
92
packages/sheets-thread-comment/package.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"name": "@univerjs/sheets-thread-comment",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Thread comment integration for Univer Sheets.",
|
||||
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
|
||||
"license": "Apache-2.0",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/univer"
|
||||
},
|
||||
"homepage": "https://univer.ai",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dream-num/univer"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/dream-num/univer/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"univer",
|
||||
"sheets",
|
||||
"comment",
|
||||
"thread-comment",
|
||||
"plugin"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./facade": "./src/facade/index.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/es/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./lib/es/index.js",
|
||||
"require": "./lib/cjs/index.js",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./lib/es/*",
|
||||
"require": "./lib/cjs/*",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:bundle": "univer-cli build",
|
||||
"build:types": "tsc -p tsconfig.node.json",
|
||||
"build": "pnpm run build:bundle && pnpm run build:types"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/engine-formula": "workspace:*",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/thread-comment": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"rxjs": "^7.8.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
24
packages/sheets-thread-comment/src/config/config.ts
Normal file
24
packages/sheets-thread-comment/src/config/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const SHEETS_THREAD_COMMENT_PLUGIN_CONFIG_KEY = 'sheets-thread-comment.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_THREAD_COMMENT_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSheetsThreadCommentConfig {
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsThreadCommentConfig = {};
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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 { AddCommentMutation, DeleteCommentMutation, UpdateCommentRefMutation } from '@univerjs/thread-comment';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsThreadCommentRefRangeController } from '../sheets-thread-comment-ref-range.controller';
|
||||
|
||||
const sheetsMocks = vi.hoisted(() => ({
|
||||
handleRangeChange: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@univerjs/sheets', async () => {
|
||||
const actual = await vi.importActual<typeof import('@univerjs/sheets')>('@univerjs/sheets');
|
||||
return {
|
||||
...actual,
|
||||
handleCommonRangeChangeWithEffectRefCommandsSkipNoInterests: sheetsMocks.handleRangeChange,
|
||||
};
|
||||
});
|
||||
|
||||
function createRoot(ref = 'A1', overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'comment-1',
|
||||
threadId: 'thread-1',
|
||||
ref,
|
||||
dT: '2025-01-01T00:00:00.000Z',
|
||||
personId: 'user-1',
|
||||
text: { dataStream: 'hello\r\n', textRuns: [] },
|
||||
attachments: [],
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
...overrides,
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('SheetsThreadCommentRefRangeController', () => {
|
||||
it('should register initial comments and transform ref changes into undo/redo mutations', async () => {
|
||||
const registerDisposes: Array<ReturnType<typeof vi.fn>> = [];
|
||||
const watchDisposes: Array<ReturnType<typeof vi.fn>> = [];
|
||||
const registerCalls: Array<{ callback: (commandInfo: unknown) => unknown }> = [];
|
||||
const watchCalls: Array<{ callback: (before: unknown, after: any) => void }> = [];
|
||||
const executeCommand = vi.fn(async () => true);
|
||||
const root = createRoot('A1', { row: 0, column: 0 });
|
||||
const updates$ = new Subject<any>();
|
||||
|
||||
const controller = new SheetsThreadCommentRefRangeController(
|
||||
{
|
||||
registerRefRange: (_range: unknown, callback: (commandInfo: unknown) => unknown) => {
|
||||
const dispose = vi.fn();
|
||||
registerDisposes.push(dispose);
|
||||
registerCalls.push({ callback });
|
||||
return { dispose };
|
||||
},
|
||||
watchRange: (_unitId: string, _subUnitId: string, _range: unknown, callback: (before: unknown, after: any) => void) => {
|
||||
const dispose = vi.fn();
|
||||
watchDisposes.push(dispose);
|
||||
watchCalls.push({ callback });
|
||||
return { dispose };
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
commentUpdate$: updates$,
|
||||
getComment: vi.fn(),
|
||||
} as any,
|
||||
{
|
||||
getAll: () => [{ threads: [{ unitId: 'unit-1', subUnitId: 'sheet-1', root }] }],
|
||||
} as any,
|
||||
{} as any,
|
||||
{ executeCommand } as any
|
||||
);
|
||||
|
||||
expect(registerCalls).toHaveLength(1);
|
||||
expect(watchCalls).toHaveLength(1);
|
||||
|
||||
sheetsMocks.handleRangeChange.mockReturnValueOnce({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 });
|
||||
expect(registerCalls[0].callback({})).toEqual({ redos: [], undos: [] });
|
||||
|
||||
sheetsMocks.handleRangeChange.mockReturnValueOnce({ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 });
|
||||
expect(registerCalls[0].callback({})).toEqual({
|
||||
redos: [{
|
||||
id: UpdateCommentRefMutation.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
payload: { ref: 'C3', commentId: 'comment-1' },
|
||||
silent: false,
|
||||
},
|
||||
}],
|
||||
undos: [{
|
||||
id: UpdateCommentRefMutation.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
payload: { ref: 'A1', commentId: 'comment-1' },
|
||||
silent: false,
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
sheetsMocks.handleRangeChange.mockReturnValueOnce(null);
|
||||
expect(registerCalls[0].callback({})).toEqual({
|
||||
redos: [{
|
||||
id: DeleteCommentMutation.id,
|
||||
params: { unitId: 'unit-1', subUnitId: 'sheet-1', commentId: 'comment-1' },
|
||||
}],
|
||||
undos: [{
|
||||
id: AddCommentMutation.id,
|
||||
params: expect.objectContaining({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
comment: expect.objectContaining(root),
|
||||
sync: true,
|
||||
}),
|
||||
}],
|
||||
});
|
||||
|
||||
await watchCalls[0].callback(null, { startRow: 4, endRow: 4, startColumn: 1, endColumn: 1 });
|
||||
expect(executeCommand).toHaveBeenCalledWith(UpdateCommentRefMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
payload: { ref: 'B5', commentId: 'comment-1' },
|
||||
silent: true,
|
||||
}, { onlyLocal: true });
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('should react to add, delete and updateRef comment updates by maintaining watchers', () => {
|
||||
const registerDisposes: Array<ReturnType<typeof vi.fn>> = [];
|
||||
const watchDisposes: Array<ReturnType<typeof vi.fn>> = [];
|
||||
const updates$ = new Subject<any>();
|
||||
const movedComment = createRoot('D4');
|
||||
const getComment = vi.fn(() => movedComment);
|
||||
|
||||
const controller = new SheetsThreadCommentRefRangeController(
|
||||
{
|
||||
registerRefRange: vi.fn(() => {
|
||||
const dispose = vi.fn();
|
||||
registerDisposes.push(dispose);
|
||||
return { dispose };
|
||||
}),
|
||||
watchRange: vi.fn(() => {
|
||||
const dispose = vi.fn();
|
||||
watchDisposes.push(dispose);
|
||||
return { dispose };
|
||||
}),
|
||||
} as any,
|
||||
{
|
||||
commentUpdate$: updates$,
|
||||
getComment,
|
||||
} as any,
|
||||
{
|
||||
getAll: () => [],
|
||||
} as any,
|
||||
{} as any,
|
||||
{ executeCommand: vi.fn() } as any
|
||||
);
|
||||
|
||||
updates$.next({
|
||||
type: 'add',
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 0,
|
||||
column: 0,
|
||||
payload: createRoot('A1'),
|
||||
});
|
||||
expect(registerDisposes).toHaveLength(1);
|
||||
expect(watchDisposes).toHaveLength(1);
|
||||
|
||||
updates$.next({
|
||||
type: 'add',
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 0,
|
||||
column: 0,
|
||||
payload: createRoot('A1', { parentId: 'root-1' }),
|
||||
});
|
||||
expect(registerDisposes).toHaveLength(1);
|
||||
|
||||
updates$.next({
|
||||
type: 'updateRef',
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 3,
|
||||
column: 3,
|
||||
silent: false,
|
||||
payload: { commentId: 'comment-1' },
|
||||
});
|
||||
expect(getComment).toHaveBeenCalledWith('unit-1', 'sheet-1', 'comment-1');
|
||||
expect(registerDisposes[0]).toHaveBeenCalledTimes(1);
|
||||
expect(watchDisposes[0]).toHaveBeenCalledTimes(1);
|
||||
expect(registerDisposes).toHaveLength(2);
|
||||
expect(watchDisposes).toHaveLength(2);
|
||||
|
||||
updates$.next({
|
||||
type: 'updateRef',
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 3,
|
||||
column: 3,
|
||||
silent: true,
|
||||
payload: { commentId: 'comment-1' },
|
||||
});
|
||||
expect(watchDisposes[1]).toHaveBeenCalledTimes(0);
|
||||
expect(watchDisposes).toHaveLength(2);
|
||||
expect(registerDisposes).toHaveLength(3);
|
||||
|
||||
updates$.next({
|
||||
type: 'delete',
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
payload: { commentId: 'comment-1' },
|
||||
});
|
||||
expect(registerDisposes[2]).toHaveBeenCalledTimes(1);
|
||||
expect(watchDisposes[1]).toHaveBeenCalledTimes(1);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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 { UniverInstanceType } from '@univerjs/core';
|
||||
import { CopySheetCommand, RemoveSheetCommand } from '@univerjs/sheets';
|
||||
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() {
|
||||
return {
|
||||
id: 'comment-1',
|
||||
threadId: 'thread-1',
|
||||
ref: 'A1',
|
||||
dT: '2025-01-01T00:00:00.000Z',
|
||||
personId: 'user-1',
|
||||
text: { dataStream: 'hello\r\n', textRuns: [] },
|
||||
attachments: [],
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
children: [{ id: 'reply-1', parentId: 'comment-1' }],
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('SheetsThreadCommentResourceController', () => {
|
||||
it('should create undo/redo mutations when removing a sheet', () => {
|
||||
let interceptorConfig: any;
|
||||
const rootComment = createRootComment();
|
||||
|
||||
const controller = new SheetsThreadCommentResourceController(
|
||||
{
|
||||
registerPluginResource: () => ({ dispose: vi.fn() }),
|
||||
} as any,
|
||||
{
|
||||
getCurrentUnitOfType: () => ({
|
||||
getUnitId: () => 'unit-1',
|
||||
getActiveSheet: () => ({ getSheetId: () => 'sheet-1' }),
|
||||
}),
|
||||
} as any,
|
||||
{
|
||||
interceptCommand: (config: any) => {
|
||||
interceptorConfig = config;
|
||||
return { dispose: vi.fn() };
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
ensureMap: () => new Map([
|
||||
[rootComment.id, rootComment],
|
||||
['reply-1', { id: 'reply-1', parentId: rootComment.id }],
|
||||
]),
|
||||
} as any,
|
||||
{
|
||||
syncUpdateMutationToColla: true,
|
||||
} as any
|
||||
);
|
||||
|
||||
const result = interceptorConfig.getMutations({
|
||||
id: RemoveSheetCommand.id,
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(result.redos).toEqual([
|
||||
{
|
||||
id: DeleteCommentMutation.id,
|
||||
params: { unitId: 'unit-1', subUnitId: 'sheet-1', commentId: 'comment-1' },
|
||||
},
|
||||
]);
|
||||
expect(result.undos).toEqual([
|
||||
{
|
||||
id: AddCommentMutation.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
comment: rootComment,
|
||||
sync: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('should clone root comments onto the copied sheet and omit children when sync is disabled', () => {
|
||||
let interceptorConfig: any;
|
||||
const rootComment = createRootComment();
|
||||
|
||||
const controller = new SheetsThreadCommentResourceController(
|
||||
{
|
||||
registerPluginResource: () => ({ dispose: vi.fn() }),
|
||||
} as any,
|
||||
{} as any,
|
||||
{
|
||||
interceptCommand: (config: any) => {
|
||||
interceptorConfig = config;
|
||||
return { dispose: vi.fn() };
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
ensureMap: () => new Map([
|
||||
[rootComment.id, rootComment],
|
||||
['reply-1', { id: 'reply-1', parentId: rootComment.id }],
|
||||
]),
|
||||
} as any,
|
||||
{
|
||||
syncUpdateMutationToColla: false,
|
||||
} as any
|
||||
);
|
||||
|
||||
const result = interceptorConfig.getMutations({
|
||||
id: CopySheetCommand.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
targetSubUnitId: 'sheet-2',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.redos).toHaveLength(1);
|
||||
expect(result.redos[0]).toEqual(expect.objectContaining({
|
||||
id: AddCommentMutation.id,
|
||||
params: expect.objectContaining({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-2',
|
||||
sync: true,
|
||||
}),
|
||||
}));
|
||||
expect(result.redos[0].params.comment).toEqual(expect.objectContaining({
|
||||
subUnitId: 'sheet-2',
|
||||
children: undefined,
|
||||
}));
|
||||
expect(result.redos[0].params.comment.id).not.toBe(rootComment.id);
|
||||
expect(result.redos[0].params.comment.threadId).not.toBe(rootComment.threadId);
|
||||
expect(result.undos).toEqual([
|
||||
{
|
||||
id: DeleteCommentMutation.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-2',
|
||||
commentId: result.redos[0].params.comment.id,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 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 { IDisposable, IRange, Nullable } from '@univerjs/core';
|
||||
import type { EffectRefRangeParams } from '@univerjs/sheets';
|
||||
import type { IAddCommentMutationParams, IUpdateCommentRefMutationParams } from '@univerjs/thread-comment';
|
||||
import type { ISheetThreadComment } from '../types/interfaces/i-sheet-thread-comment';
|
||||
import { Disposable, ICommandService, Inject, sequenceExecuteAsync, toDisposable } from '@univerjs/core';
|
||||
import { serializeRange, singleReferenceToGrid } from '@univerjs/engine-formula';
|
||||
import { handleCommonRangeChangeWithEffectRefCommandsSkipNoInterests, RefRangeService, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { AddCommentMutation, DeleteCommentMutation, deserializeThreadCommentAnchor, ThreadCommentModel, UpdateCommentRefMutation } from '@univerjs/thread-comment';
|
||||
import { SheetsThreadCommentModel } from '../models/sheets-thread-comment.model';
|
||||
|
||||
export class SheetsThreadCommentRefRangeController extends Disposable {
|
||||
private _disposableMap = new Map<string, IDisposable>();
|
||||
private _watcherMap = new Map<string, IDisposable>();
|
||||
|
||||
constructor(
|
||||
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService,
|
||||
@Inject(SheetsThreadCommentModel) private readonly _sheetsThreadCommentModel: SheetsThreadCommentModel,
|
||||
@Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel,
|
||||
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
this._initData();
|
||||
this._initRefRange();
|
||||
}
|
||||
|
||||
private _getIdWithUnitId(unitId: string, subUnitId: string, id: string) {
|
||||
return `${unitId}-${subUnitId}-${id}`;
|
||||
}
|
||||
|
||||
private _handleRangeChange = (unitId: string, subUnitId: string, comment: ISheetThreadComment, resultRange: Nullable<IRange>, silent?: boolean) => {
|
||||
const commentId = comment.id;
|
||||
const oldRange: IRange = {
|
||||
startColumn: comment.column,
|
||||
endColumn: comment.column,
|
||||
startRow: comment.row,
|
||||
endRow: comment.row,
|
||||
};
|
||||
|
||||
if (!resultRange) {
|
||||
return {
|
||||
redos: [{
|
||||
id: DeleteCommentMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
commentId,
|
||||
},
|
||||
}],
|
||||
undos: [{
|
||||
id: AddCommentMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
comment,
|
||||
sync: true,
|
||||
} as IAddCommentMutationParams,
|
||||
}],
|
||||
};
|
||||
}
|
||||
return {
|
||||
redos: [{
|
||||
id: UpdateCommentRefMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
payload: {
|
||||
ref: serializeRange(resultRange),
|
||||
commentId,
|
||||
},
|
||||
silent,
|
||||
} as IUpdateCommentRefMutationParams,
|
||||
}],
|
||||
undos: [{
|
||||
id: UpdateCommentRefMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
payload: {
|
||||
ref: serializeRange(oldRange),
|
||||
commentId,
|
||||
},
|
||||
silent,
|
||||
} as IUpdateCommentRefMutationParams,
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
private _register(unitId: string, subUnitId: string, comment: ISheetThreadComment) {
|
||||
const commentId = comment.id;
|
||||
const oldRange: IRange = {
|
||||
startColumn: comment.column,
|
||||
endColumn: comment.column,
|
||||
startRow: comment.row,
|
||||
endRow: comment.row,
|
||||
};
|
||||
|
||||
this._disposableMap.set(
|
||||
this._getIdWithUnitId(unitId, subUnitId, commentId),
|
||||
this._refRangeService.registerRefRange(oldRange, (commandInfo: EffectRefRangeParams) => {
|
||||
const resultRanges = handleCommonRangeChangeWithEffectRefCommandsSkipNoInterests(oldRange, commandInfo, { selectionManagerService: this._selectionManagerService });
|
||||
const resultRange = Array.isArray(resultRanges) ? resultRanges[0] : resultRanges;
|
||||
if (resultRange && resultRange.startColumn === oldRange.startColumn && resultRange.startRow === oldRange.startRow) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
const res = this._handleRangeChange(unitId, subUnitId, comment, resultRange, false);
|
||||
return res;
|
||||
}, unitId, subUnitId)
|
||||
);
|
||||
}
|
||||
|
||||
private _watch(unitId: string, subUnitId: string, comment: ISheetThreadComment) {
|
||||
const commentId = comment.id;
|
||||
const oldRange: IRange = {
|
||||
startColumn: comment.column,
|
||||
endColumn: comment.column,
|
||||
startRow: comment.row,
|
||||
endRow: comment.row,
|
||||
};
|
||||
this._watcherMap.set(
|
||||
this._getIdWithUnitId(unitId, subUnitId, commentId),
|
||||
this._refRangeService.watchRange(unitId, subUnitId, oldRange, (before, after) => {
|
||||
const { redos } = this._handleRangeChange(unitId, subUnitId, comment, after, true);
|
||||
sequenceExecuteAsync(redos, this._commandService, { onlyLocal: true });
|
||||
}, true)
|
||||
);
|
||||
}
|
||||
|
||||
private _unwatch(unitId: string, subUnitId: string, commentId: string) {
|
||||
const id = this._getIdWithUnitId(unitId, subUnitId, commentId);
|
||||
this._watcherMap.get(id)?.dispose();
|
||||
this._watcherMap.delete(id);
|
||||
}
|
||||
|
||||
private _unregister(unitId: string, subUnitId: string, commentId: string) {
|
||||
const id = this._getIdWithUnitId(unitId, subUnitId, commentId);
|
||||
this._disposableMap.get(id)?.dispose();
|
||||
this._disposableMap.delete(id);
|
||||
}
|
||||
|
||||
private _initData() {
|
||||
const datas = this._threadCommentModel.getAll();
|
||||
|
||||
for (const data of datas) {
|
||||
for (const thread of data.threads) {
|
||||
const { unitId, subUnitId, root } = thread;
|
||||
if (deserializeThreadCommentAnchor(root.ref)) {
|
||||
continue;
|
||||
}
|
||||
const pos = singleReferenceToGrid(root.ref);
|
||||
const sheetComment = {
|
||||
...root,
|
||||
...pos,
|
||||
};
|
||||
this._register(unitId, subUnitId, sheetComment);
|
||||
this._watch(unitId, subUnitId, sheetComment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _initRefRange() {
|
||||
this.disposeWithMe(
|
||||
this._sheetsThreadCommentModel.commentUpdate$.subscribe((option) => {
|
||||
const { unitId, subUnitId } = option;
|
||||
switch (option.type) {
|
||||
case 'add': {
|
||||
if (option.payload.parentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comment = {
|
||||
...option.payload,
|
||||
row: option.row,
|
||||
column: option.column,
|
||||
};
|
||||
|
||||
this._register(option.unitId, option.subUnitId, comment);
|
||||
this._watch(option.unitId, option.subUnitId, comment);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
this._unregister(unitId, subUnitId, option.payload.commentId);
|
||||
this._unwatch(unitId, subUnitId, option.payload.commentId);
|
||||
break;
|
||||
}
|
||||
case 'updateRef': {
|
||||
const comment = this._sheetsThreadCommentModel.getComment(unitId, subUnitId, option.payload.commentId);
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._unregister(unitId, subUnitId, option.payload.commentId);
|
||||
const sheetComment = {
|
||||
...comment,
|
||||
row: option.row,
|
||||
column: option.column,
|
||||
};
|
||||
if (!option.silent) {
|
||||
this._unwatch(unitId, subUnitId, option.payload.commentId);
|
||||
this._watch(unitId, subUnitId, sheetComment);
|
||||
}
|
||||
|
||||
this._register(option.unitId, option.subUnitId, sheetComment);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(toDisposable(() => {
|
||||
this._disposableMap.forEach((item) => {
|
||||
item.dispose();
|
||||
});
|
||||
this._disposableMap.clear();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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 { IMutationInfo, Workbook } from '@univerjs/core';
|
||||
import type { ICopySheetCommandParams, IRemoveSheetCommandParams } from '@univerjs/sheets';
|
||||
import {
|
||||
Disposable,
|
||||
generateRandomId,
|
||||
Inject,
|
||||
IResourceManagerService,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { CopySheetCommand, RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
|
||||
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 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(
|
||||
this._sheetInterceptorService.interceptCommand({
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
getMutations: (commandInfo) => {
|
||||
if (commandInfo.id === RemoveSheetCommand.id) {
|
||||
const params = commandInfo.params as IRemoveSheetCommandParams;
|
||||
const unitId = params.unitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
|
||||
const subUnitId = params.subUnitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
|
||||
|
||||
if (!unitId || !subUnitId) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const commentMap = this._threadCommentModel.ensureMap(unitId, subUnitId);
|
||||
const comments = Array.from(commentMap.values()).filter((comment) => !comment.parentId);
|
||||
const shouldSync = this._threadCommentDataSourceService.syncUpdateMutationToColla;
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
comments.forEach(({ children, ...comment }) => {
|
||||
redos.push({
|
||||
id: DeleteCommentMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
commentId: comment.id,
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: AddCommentMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
comment: {
|
||||
...comment,
|
||||
children: shouldSync ? children : undefined,
|
||||
},
|
||||
sync: !shouldSync,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { redos, undos };
|
||||
} else if (commandInfo.id === CopySheetCommand.id) {
|
||||
const params = commandInfo.params as ICopySheetCommandParams & { targetSubUnitId: string };
|
||||
const { unitId, subUnitId, targetSubUnitId } = params;
|
||||
|
||||
if (!unitId || !subUnitId || !targetSubUnitId) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const commentMap = this._threadCommentModel.ensureMap(unitId, subUnitId);
|
||||
const comments = Array.from(commentMap.values()).map((comment) => {
|
||||
return {
|
||||
...comment,
|
||||
subUnitId: targetSubUnitId,
|
||||
id: generateRandomId(),
|
||||
threadId: generateRandomId(),
|
||||
};
|
||||
}).filter((comment) => !comment.parentId);
|
||||
const shouldSync = this._threadCommentDataSourceService.syncUpdateMutationToColla;
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
comments.forEach(({ children, ...comment }) => {
|
||||
redos.push({
|
||||
id: AddCommentMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId: targetSubUnitId,
|
||||
comment: {
|
||||
...comment,
|
||||
children: shouldSync ? children : undefined,
|
||||
},
|
||||
sync: !shouldSync,
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: DeleteCommentMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId: targetSubUnitId,
|
||||
commentId: comment.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { redos, undos };
|
||||
}
|
||||
return { redos: [], undos: [] };
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { FEventName } from '@univerjs/core/facade';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { FSheetsThreadCommentEventNameMixin } from '../f-event';
|
||||
|
||||
describe('FSheetsThreadCommentEventNameMixin', () => {
|
||||
it('adds thread-comment event names to facade event names', () => {
|
||||
const eventName = Object.create(FSheetsThreadCommentEventNameMixin.prototype) as FSheetsThreadCommentEventNameMixin;
|
||||
const extended = new FEventName();
|
||||
|
||||
expect(eventName.CommentAdded).toBe('CommentAdded');
|
||||
expect(eventName.BeforeCommentAdd).toBe('BeforeCommentAdd');
|
||||
expect(eventName.CommentUpdated).toBe('CommentUpdated');
|
||||
expect(eventName.BeforeCommentUpdate).toBe('BeforeCommentUpdate');
|
||||
expect(eventName.CommentDeleted).toBe('CommentDeleted');
|
||||
expect(eventName.BeforeCommentDelete).toBe('BeforeCommentDelete');
|
||||
expect(eventName.CommentResolved).toBe('CommentResolved');
|
||||
expect(eventName.BeforeCommentResolve).toBe('BeforeCommentResolve');
|
||||
expect(extended.CommentAdded).toBe('CommentAdded');
|
||||
expect(extended.BeforeCommentResolve).toBe('BeforeCommentResolve');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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 {
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
RichTextBuilder,
|
||||
UniverInstanceType,
|
||||
UserManagerService,
|
||||
} from '@univerjs/core';
|
||||
import { FormulaDataModel } from '@univerjs/engine-formula';
|
||||
import { AddCommentCommand, DeleteCommentTreeCommand, ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsThreadCommentModel } from '../../models/sheets-thread-comment.model';
|
||||
import { FRangeSheetsThreadCommentMixin } from '../f-range';
|
||||
import { FTheadCommentBuilder } from '../f-thread-comment';
|
||||
|
||||
function createComment(id: string, ref: string) {
|
||||
return {
|
||||
id,
|
||||
threadId: `thread-${id}`,
|
||||
ref,
|
||||
dT: '2025-01-01T00:00:00.000Z',
|
||||
personId: 'user-1',
|
||||
text: RichTextBuilder.newEmptyData().body!,
|
||||
attachments: [],
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe('FRangeSheetsThreadCommentMixin', () => {
|
||||
it('gets, adds, and clears comments through injected services', async () => {
|
||||
const commandService = { executeCommand: vi.fn(async () => true) };
|
||||
const comments = new Map([
|
||||
['comment-1', createComment('comment-1', 'A1')],
|
||||
['comment-2', createComment('comment-2', 'B1')],
|
||||
]);
|
||||
const locations = new Map([
|
||||
['0:0', 'comment-1'],
|
||||
['0:1', 'comment-2'],
|
||||
]);
|
||||
|
||||
class TestSheetsThreadCommentModel {
|
||||
getByLocation = (_unitId: string, _sheetId: string, row: number, col: number) => locations.get(`${row}:${col}`);
|
||||
getComment = (_unitId: string, _sheetId: string, commentId: string) => comments.get(commentId);
|
||||
getCommentWithChildren = (_unitId: string, _sheetId: string, commentId: string) => comments.get(commentId);
|
||||
}
|
||||
|
||||
class TestUserManagerService {
|
||||
getCurrentUser = () => ({ userID: 'current-user' });
|
||||
}
|
||||
|
||||
const worksheet = {
|
||||
getSheetId: () => 'sheet-1',
|
||||
getRowCount: () => 100,
|
||||
getColumnCount: () => 100,
|
||||
};
|
||||
const workbook = { getUnitId: () => 'book-1', getSheetBySheetId: () => worksheet };
|
||||
|
||||
class TestUniverInstanceService {
|
||||
getUnit = (_unitId: string, type: UniverInstanceType) => (type === UniverInstanceType.UNIVER_SHEET ? workbook : null);
|
||||
}
|
||||
|
||||
class TestThreadCommentModel {
|
||||
getCommentWithChildren = (_unitId: string, _subUnitId: string, commentId: string) => comments.get(commentId);
|
||||
}
|
||||
|
||||
class TestFormulaDataModel {}
|
||||
class TestLogService {}
|
||||
|
||||
const injector = new Injector();
|
||||
injector.add([SheetsThreadCommentModel, { useClass: TestSheetsThreadCommentModel as never }]);
|
||||
injector.add([ICommandService, { useClass: class { executeCommand = commandService.executeCommand; } as never }]);
|
||||
injector.add([UserManagerService, { useClass: TestUserManagerService as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([ThreadCommentModel, { useClass: TestThreadCommentModel as never }]);
|
||||
injector.add([FormulaDataModel, { useClass: TestFormulaDataModel as never }]);
|
||||
injector.add([ILogService, { useClass: TestLogService as never }]);
|
||||
|
||||
const range = injector.createInstance(
|
||||
FRangeSheetsThreadCommentMixin,
|
||||
workbook as never,
|
||||
worksheet as never,
|
||||
{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 1 }
|
||||
);
|
||||
|
||||
expect(range.getComment()?.getCommentData().id).toBe('comment-1');
|
||||
expect(range.getComments().map((comment) => comment.getCommentData().id)).toEqual(['comment-1', 'comment-2']);
|
||||
|
||||
await expect(range.addCommentAsync(FTheadCommentBuilder.create().setId('new-comment'))).resolves.toBe(true);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(AddCommentCommand.id, expect.objectContaining({
|
||||
comment: expect.objectContaining({
|
||||
id: 'new-comment',
|
||||
ref: 'A1',
|
||||
personId: 'current-user',
|
||||
parentId: 'comment-1',
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
}),
|
||||
}));
|
||||
|
||||
await expect(range.addCommentAsync('Agent review', {
|
||||
id: 'agent-comment',
|
||||
personId: 'agent-user',
|
||||
})).resolves.toBe(true);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(AddCommentCommand.id, expect.objectContaining({
|
||||
comment: expect.objectContaining({
|
||||
id: 'agent-comment',
|
||||
personId: 'agent-user',
|
||||
text: expect.objectContaining({ dataStream: 'Agent review\r\n' }),
|
||||
}),
|
||||
}));
|
||||
|
||||
await expect(range.clearCommentAsync()).resolves.toBe(true);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(DeleteCommentTreeCommand.id, {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
threadId: 'thread-comment-1',
|
||||
commentId: 'comment-1',
|
||||
});
|
||||
|
||||
await expect(range.clearCommentsAsync()).resolves.toBe(true);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(DeleteCommentTreeCommand.id, expect.objectContaining({ commentId: 'comment-2' }));
|
||||
});
|
||||
|
||||
it('returns empty comment results and treats clearing an empty cell as successful', async () => {
|
||||
class TestSheetsThreadCommentModel {
|
||||
getByLocation = () => 'missing-comment';
|
||||
getComment = () => null;
|
||||
}
|
||||
|
||||
const injector = new Injector();
|
||||
injector.add([SheetsThreadCommentModel, { useClass: TestSheetsThreadCommentModel as never }]);
|
||||
injector.add([ICommandService, { useClass: class { executeCommand = vi.fn(async () => true); } as never }]);
|
||||
injector.add([UserManagerService, { useClass: class { getCurrentUser = () => ({ userID: 'user' }); } as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: class { getUnit = () => null; } as never }]);
|
||||
injector.add([ThreadCommentModel, { useClass: class { getCommentWithChildren = () => null; } as never }]);
|
||||
injector.add([FormulaDataModel, { useClass: class {} as never }]);
|
||||
injector.add([ILogService, { useClass: class {} as never }]);
|
||||
|
||||
const range = injector.createInstance(
|
||||
FRangeSheetsThreadCommentMixin,
|
||||
{ getUnitId: () => 'book-1' } as never,
|
||||
{ getSheetId: () => 'sheet-1', getRowCount: () => 10, getColumnCount: () => 10 } as never,
|
||||
{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }
|
||||
);
|
||||
|
||||
expect(range.getComment()).toBeNull();
|
||||
expect(range.getComments()).toEqual([]);
|
||||
await expect(range.clearCommentAsync()).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 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 { RichTextValue, UniverInstanceType } from '@univerjs/core';
|
||||
import { AddCommentCommand, DeleteCommentCommand, DeleteCommentTreeCommand, ResolveCommentCommand, UpdateCommentCommand } from '@univerjs/thread-comment';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { FTheadCommentBuilder, FTheadCommentItem, FThreadComment } from '../f-thread-comment';
|
||||
import { FWorksheetCommentMixin } from '../f-worksheet';
|
||||
|
||||
function createBody(text: string) {
|
||||
return RichTextValue.create({
|
||||
id: 'doc-1',
|
||||
body: {
|
||||
dataStream: `${text}\r\n`,
|
||||
textRuns: [],
|
||||
},
|
||||
documentStyle: {},
|
||||
});
|
||||
}
|
||||
|
||||
function createThread(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'comment-1',
|
||||
threadId: 'thread-1',
|
||||
ref: 'A1',
|
||||
dT: '2025-01-01T00:00:00.000Z',
|
||||
personId: 'user-1',
|
||||
text: createBody('hello').getData().body!,
|
||||
attachments: [],
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
resolved: false,
|
||||
children: [],
|
||||
...overrides,
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('thread comment facade', () => {
|
||||
it('should build comment payloads with rich text and keep copies isolated', () => {
|
||||
const richText = createBody('hello univer');
|
||||
const builder = FTheadCommentBuilder.create()
|
||||
.setContent(richText)
|
||||
.setPersonId('user-2')
|
||||
.setDateTime(new Date('2025-02-21T14:22:22.000Z'))
|
||||
.setId('comment-2')
|
||||
.setThreadId('thread-2');
|
||||
|
||||
const cloned = builder.copy().setPersonId('user-3');
|
||||
|
||||
expect(builder.content.getData().body).toEqual(richText.getData().body);
|
||||
expect(builder.personId).toBe('user-2');
|
||||
expect(builder.id).toBe('comment-2');
|
||||
expect(builder.threadId).toBe('thread-2');
|
||||
expect(builder.dateTime).toBe(builder.build().dT);
|
||||
expect(cloned.personId).toBe('user-3');
|
||||
expect(builder.personId).toBe('user-2');
|
||||
|
||||
const item = FTheadCommentItem.create(builder.build());
|
||||
expect(item.content.getData().body).toEqual(builder.build().text);
|
||||
});
|
||||
|
||||
it('should expose thread comment behaviors and execute comment commands with business payloads', async () => {
|
||||
const commandService = {
|
||||
executeCommand: vi.fn(async () => true),
|
||||
};
|
||||
const child = createThread({ id: 'reply-1', parentId: 'comment-1', ref: '' });
|
||||
const root = createThread({ children: [child] });
|
||||
const injector = {
|
||||
createInstance: vi.fn((cls: unknown, ...args: any[]) => {
|
||||
if ((cls as { name?: string }).name === 'FRange') {
|
||||
return { workbook: args[0], worksheet: args[1], range: args[2] };
|
||||
}
|
||||
if (cls === FThreadComment) {
|
||||
return { comment: args[0] };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
const workbook = {
|
||||
getSheetBySheetId: (sheetId: string) => (sheetId === 'sheet-1' ? { getSheetId: () => 'sheet-1' } : null),
|
||||
};
|
||||
const univerInstanceService = {
|
||||
getUnit: (_unitId: string, type: UniverInstanceType) => (type === UniverInstanceType.UNIVER_SHEET ? workbook : null),
|
||||
};
|
||||
const threadCommentModel = {
|
||||
getCommentWithChildren: () => ({ ...root, children: [child] }),
|
||||
};
|
||||
const userManagerService = {
|
||||
getCurrentUser: () => ({ userID: 'current-user' }),
|
||||
};
|
||||
|
||||
const rootComment = new FThreadComment(
|
||||
root,
|
||||
undefined,
|
||||
injector as any,
|
||||
commandService as any,
|
||||
univerInstanceService as any,
|
||||
threadCommentModel as any,
|
||||
userManagerService as any
|
||||
);
|
||||
const replyComment = new FThreadComment(
|
||||
child,
|
||||
root,
|
||||
injector as any,
|
||||
commandService as any,
|
||||
univerInstanceService as any,
|
||||
threadCommentModel as any,
|
||||
userManagerService as any
|
||||
);
|
||||
|
||||
expect(rootComment.getIsRoot()).toBe(true);
|
||||
expect(replyComment.getIsRoot()).toBe(false);
|
||||
expect(rootComment.getCommentData()).not.toHaveProperty('children');
|
||||
expect(rootComment.getReplies()).toEqual([{ comment: child }]);
|
||||
expect(rootComment.getRange()).toEqual(expect.objectContaining({
|
||||
workbook,
|
||||
worksheet: { getSheetId: expect.any(Function) },
|
||||
range: expect.objectContaining({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }),
|
||||
}));
|
||||
expect(rootComment.getRichText().getData().body).toEqual(root.text);
|
||||
|
||||
await rootComment.deleteAsync();
|
||||
await replyComment.deleteAsync();
|
||||
const updatedBody = createBody('updated');
|
||||
await rootComment.updateAsync(updatedBody);
|
||||
await rootComment.resolveAsync();
|
||||
await rootComment.resolveAsync(true);
|
||||
await rootComment.replyAsync(FTheadCommentBuilder.create().setContent(createBody('reply')));
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(DeleteCommentTreeCommand.id, expect.objectContaining({ commentId: root.id }));
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(DeleteCommentCommand.id, expect.objectContaining({ commentId: child.id }));
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(UpdateCommentCommand.id, expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
commentId: root.id,
|
||||
updated: true,
|
||||
text: updatedBody.getData().body,
|
||||
}),
|
||||
}));
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(ResolveCommentCommand.id, expect.objectContaining({ resolved: true }));
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(ResolveCommentCommand.id, expect.objectContaining({ resolved: true }));
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(AddCommentCommand.id, expect.objectContaining({
|
||||
comment: expect.objectContaining({
|
||||
parentId: root.id,
|
||||
threadId: root.threadId,
|
||||
ref: root.ref,
|
||||
personId: 'current-user',
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it('worksheet facade should list, clear and subscribe to sheet comments', async () => {
|
||||
const root = createThread();
|
||||
const deleteAsync = vi.fn(async () => true);
|
||||
const commandListeners: Array<(command: { id: string; params: unknown }) => void> = [];
|
||||
|
||||
const instance = Object.create(FWorksheetCommentMixin.prototype) as any;
|
||||
instance._workbook = { getUnitId: () => 'unit-1' };
|
||||
instance._worksheet = { getSheetId: () => 'sheet-1' };
|
||||
instance._injector = {
|
||||
get: (token: unknown) => {
|
||||
if (typeof token === 'function' && token.name === 'SheetsThreadCommentModel') {
|
||||
return {
|
||||
getSubUnitAll: () => [root],
|
||||
getComment: (_unitId: string, _subUnitId: string, commentId: string) => (commentId === root.id ? root : undefined),
|
||||
};
|
||||
}
|
||||
return {
|
||||
onCommandExecuted: (listener: (command: { id: string; params: unknown }) => void) => {
|
||||
commandListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
},
|
||||
};
|
||||
},
|
||||
createInstance: vi.fn(() => ({ deleteAsync })),
|
||||
};
|
||||
|
||||
expect(instance.getComments()).toHaveLength(1);
|
||||
expect(await instance.clearComments()).toBe(true);
|
||||
expect(deleteAsync).toHaveBeenCalledTimes(1);
|
||||
|
||||
const callback = vi.fn();
|
||||
instance.onCommented(callback);
|
||||
commandListeners.forEach((listener) => listener({ id: AddCommentCommand.id, params: { comment: root } }));
|
||||
commandListeners.forEach((listener) => listener({ id: UpdateCommentCommand.id, params: { comment: root } }));
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(instance.getCommentById(root.id)).toEqual({ deleteAsync });
|
||||
expect(instance.getCommentById('missing')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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 { ICommandService, Injector, RichTextBuilder } from '@univerjs/core';
|
||||
import {
|
||||
AddCommentCommand,
|
||||
DeleteCommentCommand,
|
||||
DeleteCommentTreeCommand,
|
||||
ResolveCommentCommand,
|
||||
UpdateCommentCommand,
|
||||
} from '@univerjs/thread-comment';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { FUniverSheetsThreadCommentMixin } from '../f-univer';
|
||||
import '../f-event';
|
||||
|
||||
function createApi(cancelEvents = new Set<string>()) {
|
||||
const afterListeners: Array<(commandInfo: any) => void> = [];
|
||||
const beforeListeners: Array<(commandInfo: any) => void> = [];
|
||||
const commandService = {
|
||||
onCommandExecuted: (listener: (commandInfo: any) => void) => {
|
||||
afterListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
},
|
||||
beforeCommandExecuted: (listener: (commandInfo: any) => void) => {
|
||||
beforeListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
},
|
||||
};
|
||||
class TestCommandService {
|
||||
onCommandExecuted = commandService.onCommandExecuted;
|
||||
beforeCommandExecuted = commandService.beforeCommandExecuted;
|
||||
}
|
||||
const injector = new Injector();
|
||||
injector.add([ICommandService, { useClass: TestCommandService as never }]);
|
||||
|
||||
const fired: Array<{ name: string; params: any }> = [];
|
||||
const range = { getRow: () => 2, getColumn: () => 3 };
|
||||
const threadComment = {
|
||||
getRange: () => range,
|
||||
};
|
||||
const workbook = {};
|
||||
const worksheet = {
|
||||
getCommentById: (commentId: string) => (commentId === 'missing' ? null : threadComment),
|
||||
};
|
||||
const api = Object.create(FUniverSheetsThreadCommentMixin.prototype) as any;
|
||||
Object.defineProperty(api, 'Event', {
|
||||
value: {
|
||||
CommentAdded: 'CommentAdded',
|
||||
BeforeCommentAdd: 'BeforeCommentAdd',
|
||||
CommentUpdated: 'CommentUpdated',
|
||||
BeforeCommentUpdate: 'BeforeCommentUpdate',
|
||||
CommentDeleted: 'CommentDeleted',
|
||||
BeforeCommentDelete: 'BeforeCommentDelete',
|
||||
CommentResolved: 'CommentResolved',
|
||||
BeforeCommentResolve: 'BeforeCommentResolve',
|
||||
},
|
||||
});
|
||||
api.disposeWithMe = vi.fn((disposable) => disposable);
|
||||
api.registerEventHandler = vi.fn((_name: string, register: () => unknown) => register());
|
||||
api.getSheetCommandTarget = vi.fn(() => ({ workbook, worksheet }));
|
||||
api.fireEvent = vi.fn((name: string, params: any) => {
|
||||
if (cancelEvents.has(name)) {
|
||||
params.cancel = true;
|
||||
}
|
||||
fired.push({ name, params });
|
||||
});
|
||||
api._initialize(injector);
|
||||
|
||||
return { api, fired, afterListeners, beforeListeners, workbook, worksheet, threadComment };
|
||||
}
|
||||
|
||||
describe('FUniverSheetsThreadCommentMixin', () => {
|
||||
it('creates thread comment builders', () => {
|
||||
const { api } = createApi();
|
||||
const builder = api.newTheadComment().setId('comment-1');
|
||||
|
||||
expect(builder.id).toBe('comment-1');
|
||||
});
|
||||
|
||||
it('fires public comment events from executed comment commands', () => {
|
||||
const { api, fired, afterListeners, workbook, worksheet, threadComment } = createApi();
|
||||
|
||||
afterListeners.forEach((listener) => listener({ id: 'unrelated.command', params: {} }));
|
||||
api.getSheetCommandTarget.mockReturnValueOnce(null);
|
||||
afterListeners.forEach((listener) => listener({
|
||||
id: AddCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', comment: { id: 'comment-1' } },
|
||||
}));
|
||||
afterListeners.forEach((listener) => listener({
|
||||
id: AddCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', comment: { id: 'comment-1' } },
|
||||
}));
|
||||
afterListeners.forEach((listener) => listener({
|
||||
id: UpdateCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', payload: { commentId: 'comment-1' } },
|
||||
}));
|
||||
afterListeners.forEach((listener) => listener({
|
||||
id: DeleteCommentTreeCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', commentId: 'comment-1' },
|
||||
}));
|
||||
afterListeners.forEach((listener) => listener({
|
||||
id: ResolveCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', commentId: 'comment-1', resolved: true },
|
||||
}));
|
||||
afterListeners.forEach((listener) => listener({
|
||||
id: AddCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', comment: { id: 'missing' } },
|
||||
}));
|
||||
afterListeners.forEach((listener) => listener({
|
||||
id: UpdateCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', payload: { commentId: 'missing' } },
|
||||
}));
|
||||
afterListeners.forEach((listener) => listener({
|
||||
id: ResolveCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', commentId: 'missing', resolved: true },
|
||||
}));
|
||||
|
||||
expect(fired).toEqual([
|
||||
{ name: 'CommentAdded', params: { workbook, worksheet, row: 2, col: 3, comment: threadComment } },
|
||||
{ name: 'CommentUpdated', params: { workbook, worksheet, row: 2, col: 3, comment: threadComment } },
|
||||
{ name: 'CommentDeleted', params: { workbook, worksheet, commentId: 'comment-1' } },
|
||||
{ name: 'CommentResolved', params: { workbook, worksheet, row: 2, col: 3, comment: threadComment, resolved: true } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('fires before comment events and cancels commands when listeners request it', () => {
|
||||
const body = RichTextBuilder.newEmptyData().body!;
|
||||
const { api, fired, beforeListeners } = createApi();
|
||||
|
||||
beforeListeners.forEach((listener) => listener({ id: 'unrelated.command', params: {} }));
|
||||
api.getSheetCommandTarget.mockReturnValueOnce(null);
|
||||
beforeListeners.forEach((listener) => listener({
|
||||
id: AddCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', comment: { id: 'comment-1', ref: 'B2', text: body } },
|
||||
}));
|
||||
beforeListeners.forEach((listener) => listener({
|
||||
id: AddCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', comment: { id: 'comment-1', ref: 'B2', text: body } },
|
||||
}));
|
||||
beforeListeners.forEach((listener) => listener({
|
||||
id: UpdateCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', payload: { commentId: 'comment-1', text: body } },
|
||||
}));
|
||||
beforeListeners.forEach((listener) => listener({
|
||||
id: DeleteCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', commentId: 'comment-1' },
|
||||
}));
|
||||
beforeListeners.forEach((listener) => listener({
|
||||
id: ResolveCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', commentId: 'comment-1', resolved: false },
|
||||
}));
|
||||
beforeListeners.forEach((listener) => listener({
|
||||
id: UpdateCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', payload: { commentId: 'missing', text: body } },
|
||||
}));
|
||||
beforeListeners.forEach((listener) => listener({
|
||||
id: DeleteCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', commentId: 'missing' },
|
||||
}));
|
||||
beforeListeners.forEach((listener) => listener({
|
||||
id: ResolveCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', commentId: 'missing', resolved: false },
|
||||
}));
|
||||
|
||||
expect(fired.map((event) => event.name)).toEqual([
|
||||
'BeforeCommentAdd',
|
||||
'BeforeCommentUpdate',
|
||||
'BeforeCommentDelete',
|
||||
'BeforeCommentResolve',
|
||||
]);
|
||||
|
||||
const canceled = createApi(new Set(['BeforeCommentAdd']));
|
||||
expect(() => {
|
||||
canceled.beforeListeners.forEach((listener) => listener({
|
||||
id: AddCommentCommand.id,
|
||||
params: { unitId: 'book-1', subUnitId: 'sheet-1', comment: { id: 'comment-1', ref: 'A1', text: body } },
|
||||
}));
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 { describe, expect, it, vi } from 'vitest';
|
||||
import { FWorkbookSheetsThreadCommentMixin } from '../f-workbook';
|
||||
|
||||
describe('FWorkbookSheetsThreadCommentMixin', () => {
|
||||
it('should lazily resolve the thread comment model and create workbook comment facades', async () => {
|
||||
const rootComment = { id: 'comment-1' };
|
||||
const deleteAsync = vi.fn(async () => true);
|
||||
const getUnit = vi.fn(() => [{ root: rootComment }]);
|
||||
const createInstance = vi.fn(() => ({ deleteAsync }));
|
||||
const get = vi.fn(() => ({ getUnit }));
|
||||
|
||||
const instance = Object.create(FWorkbookSheetsThreadCommentMixin.prototype) as any;
|
||||
instance._injector = { get, createInstance };
|
||||
instance._workbook = { getUnitId: () => 'unit-1' };
|
||||
|
||||
instance._initialize();
|
||||
|
||||
expect(instance._threadCommentModel.getUnit('unit-1')).toEqual([{ root: rootComment }]);
|
||||
expect(instance.getComments()).toEqual([{ deleteAsync }]);
|
||||
expect(await instance.clearComments()).toBe(true);
|
||||
expect(deleteAsync).toHaveBeenCalledTimes(1);
|
||||
expect(createInstance).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
346
packages/sheets-thread-comment/src/facade/f-event.ts
Normal file
346
packages/sheets-thread-comment/src/facade/f-event.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* 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 { RichTextValue } from '@univerjs/core';
|
||||
import type { IEventBase } from '@univerjs/core/facade';
|
||||
import type { FWorkbook, FWorksheet } from '@univerjs/sheets/facade';
|
||||
import type { FTheadCommentItem, FThreadComment } from './f-thread-comment';
|
||||
import { FEventName } from '@univerjs/core/facade';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
interface IFSheetsThreadCommentEventNameMixin {
|
||||
/**
|
||||
* Event fired after comment added
|
||||
* @see {@link ISheetCommentAddEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.CommentAdded, (params) => {
|
||||
* const { comment, workbook, worksheet, row, col } = params;
|
||||
* console.log(params);
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly CommentAdded: 'CommentAdded';
|
||||
|
||||
/**
|
||||
* Event fired before comment added
|
||||
* @see {@link IBeforeSheetCommentAddEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeCommentAdd, (params) => {
|
||||
* const { comment, workbook, worksheet, row, col } = params;
|
||||
* console.log(params);
|
||||
*
|
||||
* // Cancel the comment add operation
|
||||
* params.cancel = true;
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeCommentAdd: 'BeforeCommentAdd';
|
||||
|
||||
/**
|
||||
* Event fired after comment updated
|
||||
* @see {@link ISheetCommentUpdateEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.CommentUpdated, (params) => {
|
||||
* const { comment, workbook, worksheet, row, col } = params;
|
||||
* console.log(params);
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly CommentUpdated: 'CommentUpdated';
|
||||
|
||||
/**
|
||||
* Event fired before comment update
|
||||
* @see {@link IBeforeSheetCommentUpdateEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeCommentUpdate, (params) => {
|
||||
* const { comment, workbook, worksheet, row, col, newContent } = params;
|
||||
* console.log(params);
|
||||
*
|
||||
* // Cancel the comment update operation
|
||||
* params.cancel = true;
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeCommentUpdate: 'BeforeCommentUpdate';
|
||||
|
||||
/**
|
||||
* Event fired after comment deleted
|
||||
* @see {@link ISheetCommentDeleteEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.CommentDeleted, (params) => {
|
||||
* const { commentId, workbook, worksheet } = params;
|
||||
* console.log(params);
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly CommentDeleted: 'CommentDeleted';
|
||||
|
||||
/**
|
||||
* Event fired before comment delete
|
||||
* @see {@link IBeforeSheetCommentDeleteEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeCommentDelete, (params) => {
|
||||
* const { comment, workbook, worksheet, row, col } = params;
|
||||
* console.log(params);
|
||||
*
|
||||
* // Cancel the comment delete operation
|
||||
* params.cancel = true;
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeCommentDelete: 'BeforeCommentDelete';
|
||||
|
||||
/**
|
||||
* Event fired after comment resolve
|
||||
* @see {@link ISheetCommentResolveEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.CommentResolved, (params) => {
|
||||
* const { comment, row, col, resolved, workbook, worksheet } = params;
|
||||
* console.log(params);
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly CommentResolved: 'CommentResolved';
|
||||
|
||||
/**
|
||||
* Event fired before comment resolve
|
||||
* @see {@link ISheetCommentResolveEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeCommentResolve, (params) => {
|
||||
* const { comment, row, col, resolved, workbook, worksheet } = params;
|
||||
* console.log(params);
|
||||
*
|
||||
* // Cancel the comment resolve operation
|
||||
* params.cancel = true;
|
||||
* });
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeCommentResolve: 'BeforeCommentResolve';
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class FSheetsThreadCommentEventNameMixin extends FEventName implements IFSheetsThreadCommentEventNameMixin {
|
||||
override get CommentAdded(): 'CommentAdded' {
|
||||
return 'CommentAdded' as const;
|
||||
}
|
||||
|
||||
override get BeforeCommentAdd(): 'BeforeCommentAdd' {
|
||||
return 'BeforeCommentAdd' as const;
|
||||
}
|
||||
|
||||
override get CommentUpdated(): 'CommentUpdated' {
|
||||
return 'CommentUpdated' as const;
|
||||
}
|
||||
|
||||
override get BeforeCommentUpdate(): 'BeforeCommentUpdate' {
|
||||
return 'BeforeCommentUpdate' as const;
|
||||
}
|
||||
|
||||
override get CommentDeleted(): 'CommentDeleted' {
|
||||
return 'CommentDeleted' as const;
|
||||
}
|
||||
|
||||
override get BeforeCommentDelete(): 'BeforeCommentDelete' {
|
||||
return 'BeforeCommentDelete' as const;
|
||||
}
|
||||
|
||||
override get CommentResolved(): 'CommentResolved' {
|
||||
return 'CommentResolved' as const;
|
||||
}
|
||||
|
||||
override get BeforeCommentResolve(): 'BeforeCommentResolve' {
|
||||
return 'BeforeCommentResolve' as const;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event interface triggered after a comment is added to a sheet
|
||||
* @interface ISheetCommentAddEventParams
|
||||
* @augments {IEventBase}
|
||||
*/
|
||||
export interface ISheetCommentAddEventParams extends IEventBase {
|
||||
/** The workbook instance */
|
||||
workbook: FWorkbook;
|
||||
/** The worksheet where the comment is added */
|
||||
worksheet: FWorksheet;
|
||||
/** Row index of the comment */
|
||||
row: number;
|
||||
/** Column index of the comment */
|
||||
col: number;
|
||||
/** The added comment object */
|
||||
comment: FThreadComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event interface triggered before a comment is added to a sheet
|
||||
* @interface IBeforeSheetCommentAddEventParams
|
||||
* @augments {IEventBase}
|
||||
*/
|
||||
export interface IBeforeSheetCommentAddEventParams extends IEventBase {
|
||||
/** The workbook instance */
|
||||
workbook: FWorkbook;
|
||||
/** The worksheet where the comment will be added */
|
||||
worksheet: FWorksheet;
|
||||
/** Row index for the new comment */
|
||||
row: number;
|
||||
/** Column index for the new comment */
|
||||
col: number;
|
||||
/** The comment item to be added */
|
||||
comment: FTheadCommentItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event interface triggered after a comment is updated in a sheet
|
||||
* @interface ISheetCommentUpdateEventParams
|
||||
* @augments {IEventBase}
|
||||
*/
|
||||
export interface ISheetCommentUpdateEventParams extends IEventBase {
|
||||
/** The workbook instance */
|
||||
workbook: FWorkbook;
|
||||
/** The worksheet containing the updated comment */
|
||||
worksheet: FWorksheet;
|
||||
/** Row index of the comment */
|
||||
row: number;
|
||||
/** Column index of the comment */
|
||||
col: number;
|
||||
/** The updated comment object */
|
||||
comment: FThreadComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event interface triggered before a comment is updated in a sheet
|
||||
* @interface IBeforeSheetCommentUpdateEventParams
|
||||
* @augments {IEventBase}
|
||||
*/
|
||||
export interface IBeforeSheetCommentUpdateEventParams extends IEventBase {
|
||||
/** The workbook instance */
|
||||
workbook: FWorkbook;
|
||||
/** The worksheet containing the comment */
|
||||
worksheet: FWorksheet;
|
||||
/** Row index of the comment */
|
||||
row: number;
|
||||
/** Column index of the comment */
|
||||
col: number;
|
||||
/** The current comment object */
|
||||
comment: FThreadComment;
|
||||
/** The new content to replace the existing comment */
|
||||
newContent: RichTextValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event interface triggered before a comment is deleted from a sheet
|
||||
* @interface IBeforeSheetCommentDeleteEventParams
|
||||
* @augments {IEventBase}
|
||||
*/
|
||||
export interface IBeforeSheetCommentDeleteEventParams extends IEventBase {
|
||||
/** The workbook instance */
|
||||
workbook: FWorkbook;
|
||||
/** The worksheet containing the comment */
|
||||
worksheet: FWorksheet;
|
||||
/** Row index of the comment */
|
||||
row: number;
|
||||
/** Column index of the comment */
|
||||
col: number;
|
||||
/** The comment to be deleted */
|
||||
comment: FThreadComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event interface triggered after a comment is deleted from a sheet
|
||||
* @interface ISheetCommentDeleteEventParams
|
||||
* @augments {IEventBase}
|
||||
*/
|
||||
export interface ISheetCommentDeleteEventParams extends IEventBase {
|
||||
/** The workbook instance */
|
||||
workbook: FWorkbook;
|
||||
/** The worksheet that contained the comment */
|
||||
worksheet: FWorksheet;
|
||||
/** The ID of the deleted comment */
|
||||
commentId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event interface triggered when a comment's resolve status changes
|
||||
* @interface ISheetCommentResolveEventParams
|
||||
* @augments {IEventBase}
|
||||
*/
|
||||
export interface ISheetCommentResolveEventParams extends IEventBase {
|
||||
/** The workbook instance */
|
||||
workbook: FWorkbook;
|
||||
/** The worksheet containing the comment */
|
||||
worksheet: FWorksheet;
|
||||
/** Row index of the comment */
|
||||
row: number;
|
||||
/** Column index of the comment */
|
||||
col: number;
|
||||
/** The comment object */
|
||||
comment: FThreadComment;
|
||||
/** The new resolve status */
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface ISheetsThreadCommentEventParamConfig {
|
||||
BeforeCommentAdd: IBeforeSheetCommentAddEventParams;
|
||||
CommentAdded: ISheetCommentAddEventParams;
|
||||
|
||||
BeforeCommentUpdate: IBeforeSheetCommentUpdateEventParams;
|
||||
CommentUpdated: ISheetCommentUpdateEventParams;
|
||||
|
||||
BeforeCommentDelete: IBeforeSheetCommentDeleteEventParams;
|
||||
CommentDeleted: ISheetCommentDeleteEventParams;
|
||||
|
||||
BeforeCommentResolve: ISheetCommentResolveEventParams;
|
||||
CommentResolved: ISheetCommentResolveEventParams;
|
||||
}
|
||||
|
||||
FEventName.extend(FSheetsThreadCommentEventNameMixin);
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FEventName extends IFSheetsThreadCommentEventNameMixin {}
|
||||
|
||||
interface IEventParamConfig extends ISheetsThreadCommentEventParamConfig { }
|
||||
}
|
||||
254
packages/sheets-thread-comment/src/facade/f-range.ts
Normal file
254
packages/sheets-thread-comment/src/facade/f-range.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 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 * as UniverCore from '@univerjs/core';
|
||||
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
|
||||
import { FRange } from '@univerjs/sheets/facade';
|
||||
import * as ThreadComment from '@univerjs/thread-comment';
|
||||
import { FTheadCommentBuilder, FThreadComment } from './f-thread-comment';
|
||||
|
||||
export type ISheetCellCommentCreateOptions = Pick<
|
||||
ThreadComment.ICreateThreadCommentOptions,
|
||||
'attachments' | 'dateTime' | 'id' | 'personId' | 'threadId'
|
||||
>;
|
||||
|
||||
interface IRangeCommentDependencies {
|
||||
commandService: UniverCore.ICommandService;
|
||||
model: SheetsThreadCommentModel;
|
||||
userManagerService: UniverCore.UserManagerService;
|
||||
}
|
||||
|
||||
function firstNonEmpty(...values: Array<string | undefined>): string {
|
||||
for (const value of values) {
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return UniverCore.generateRandomId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFRangeSheetsThreadCommentMixin {
|
||||
/**
|
||||
* Get the comment of the start cell in the current range.
|
||||
* @returns {FThreadComment | null} The comment of the start cell in the current range. If the cell does not have a comment, return `null`.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const range = fWorksheet.getActiveRange();
|
||||
* const comment = range.getComment();
|
||||
* ```
|
||||
*/
|
||||
getComment(): UniverCore.Nullable<FThreadComment>;
|
||||
|
||||
/**
|
||||
* Get the comments in the current range.
|
||||
* @returns {FThreadComment[]} The comments in the current range.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const range = fWorksheet.getActiveRange();
|
||||
* const comments = range.getComments();
|
||||
* comments.forEach((comment) => {
|
||||
* console.log(comment.getRichText());
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getComments(): FThreadComment[];
|
||||
|
||||
/**
|
||||
* Add a comment to the start cell in the current range.
|
||||
* @param content The content of the comment.
|
||||
* @param [options] Optional stable IDs, author, attachments, and creation time.
|
||||
* @returns {Promise<boolean>} Whether the comment is added successfully.
|
||||
* @throws {TypeError} If the content is empty.
|
||||
* @example
|
||||
* ```ts
|
||||
* await univerAPI.getActiveWorkbook()
|
||||
* .getActiveSheet()
|
||||
* .getRange('A1')
|
||||
* .addCommentAsync('Verify this value.', { id: 'review-a1' });
|
||||
*
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText);
|
||||
* console.log(commentBuilder.content.toPlainText());
|
||||
*
|
||||
* // Add the comment to the cell A1
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* const result = await cell.addCommentAsync(commentBuilder);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
addCommentAsync(
|
||||
content: ThreadComment.ThreadCommentContent | FTheadCommentBuilder,
|
||||
options?: ISheetCellCommentCreateOptions
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Clear the comment of the start cell in the current range.
|
||||
* @returns {Promise<boolean>} Whether the comment is cleared successfully.
|
||||
* @example
|
||||
* ```ts
|
||||
* const range = univerAPI.getActiveWorkbook().getActiveSheet().getRange('A1');
|
||||
* const success = await range.clearCommentAsync();
|
||||
* console.log(success);
|
||||
* ```
|
||||
*/
|
||||
clearCommentAsync(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Clear all of the comments in the current range.
|
||||
* @returns {Promise<boolean>} Whether the comments are cleared successfully.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const range = fWorksheet.getActiveRange();
|
||||
* const success = await range.clearCommentsAsync();
|
||||
* ```
|
||||
*/
|
||||
clearCommentsAsync(): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class FRangeSheetsThreadCommentMixin extends FRange implements IFRangeSheetsThreadCommentMixin {
|
||||
declare private _dependencies: IRangeCommentDependencies;
|
||||
|
||||
override _initialize(injector: UniverCore.Injector): void {
|
||||
let dependencies: IRangeCommentDependencies | undefined;
|
||||
Object.defineProperty(this, '_dependencies', {
|
||||
get: () => dependencies ??= {
|
||||
commandService: injector.get(UniverCore.ICommandService),
|
||||
model: injector.get(SheetsThreadCommentModel),
|
||||
userManagerService: injector.get(UniverCore.UserManagerService),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _getCommentDataInRange(): ThreadComment.IThreadComment[] {
|
||||
const model = this._dependencies.model;
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const sheetId = this._worksheet.getSheetId();
|
||||
const comments: ThreadComment.IThreadComment[] = [];
|
||||
UniverCore.Range.foreach(this._range, (row, col) => {
|
||||
const commentId = model.getByLocation(unitId, sheetId, row, col);
|
||||
const comment = commentId ? model.getComment(unitId, sheetId, commentId) : null;
|
||||
if (comment) {
|
||||
comments.push(comment);
|
||||
}
|
||||
});
|
||||
return comments;
|
||||
}
|
||||
|
||||
private _getStartCellCommentData(): ThreadComment.IThreadComment | null {
|
||||
const model = this._dependencies.model;
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const sheetId = this._worksheet.getSheetId();
|
||||
const commentId = model.getByLocation(unitId, sheetId, this._range.startRow, this._range.startColumn);
|
||||
return commentId ? model.getComment(unitId, sheetId, commentId) ?? null : null;
|
||||
}
|
||||
|
||||
override getComment(): UniverCore.Nullable<FThreadComment> {
|
||||
const comment = this._getStartCellCommentData();
|
||||
return comment ? this._injector.createInstance(FThreadComment, comment) : null;
|
||||
}
|
||||
|
||||
override getComments(): FThreadComment[] {
|
||||
return this._getCommentDataInRange().map((comment) => this._injector.createInstance(FThreadComment, comment));
|
||||
}
|
||||
|
||||
override addCommentAsync(
|
||||
content: ThreadComment.ThreadCommentContent | FTheadCommentBuilder,
|
||||
options: ISheetCellCommentCreateOptions = {}
|
||||
): Promise<boolean> {
|
||||
const { commandService, userManagerService } = this._dependencies;
|
||||
const currentComment = this._getStartCellCommentData();
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const sheetId = this._worksheet.getSheetId();
|
||||
const refStr = `${UniverCore.Tools.chatAtABC(this._range.startColumn)}${this._range.startRow + 1}`;
|
||||
const currentUser = userManagerService.getCurrentUser();
|
||||
let commentData: ThreadComment.IThreadComment | null = null;
|
||||
let text: UniverCore.IDocumentBody;
|
||||
if (content instanceof FTheadCommentBuilder) {
|
||||
commentData = content.build();
|
||||
text = commentData.text;
|
||||
} else {
|
||||
text = ThreadComment.normalizeThreadCommentContent(content);
|
||||
}
|
||||
|
||||
return commandService.executeCommand(ThreadComment.AddCommentCommand.id, {
|
||||
unitId,
|
||||
subUnitId: sheetId,
|
||||
comment: {
|
||||
text,
|
||||
dT: options.dateTime ? ThreadComment.getDT(options.dateTime) : commentData?.dT || ThreadComment.getDT(),
|
||||
attachments: options.attachments ?? commentData?.attachments ?? [],
|
||||
id: firstNonEmpty(options.id, commentData?.id),
|
||||
ref: refStr,
|
||||
personId: firstNonEmpty(options.personId, commentData?.personId, currentUser.userID),
|
||||
parentId: currentComment?.id,
|
||||
unitId,
|
||||
subUnitId: sheetId,
|
||||
threadId: firstNonEmpty(currentComment?.threadId, options.threadId, commentData?.threadId),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override clearCommentAsync(): Promise<boolean> {
|
||||
const { commandService } = this._dependencies;
|
||||
const currentComment = this._getStartCellCommentData();
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const sheetId = this._worksheet.getSheetId();
|
||||
|
||||
if (currentComment) {
|
||||
return commandService.executeCommand(ThreadComment.DeleteCommentTreeCommand.id, {
|
||||
unitId,
|
||||
subUnitId: sheetId,
|
||||
threadId: currentComment.threadId,
|
||||
commentId: currentComment.id,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
override async clearCommentsAsync(): Promise<boolean> {
|
||||
const { commandService } = this._dependencies;
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const subUnitId = this._worksheet.getSheetId();
|
||||
const results = await Promise.all(this._getCommentDataInRange().map((comment) => commandService.executeCommand(
|
||||
ThreadComment.DeleteCommentTreeCommand.id,
|
||||
{ unitId, subUnitId, threadId: comment.threadId, commentId: comment.id }
|
||||
)));
|
||||
return results.every(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
FRange.extend(FRangeSheetsThreadCommentMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
interface FRange extends IFRangeSheetsThreadCommentMixin { }
|
||||
}
|
||||
602
packages/sheets-thread-comment/src/facade/f-thread-comment.ts
Normal file
602
packages/sheets-thread-comment/src/facade/f-thread-comment.ts
Normal file
@@ -0,0 +1,602 @@
|
||||
/**
|
||||
* 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, IRange, Workbook } from '@univerjs/core';
|
||||
import type { IAddCommentCommandParams, IBaseComment, IDeleteCommentCommandParams, IResolveCommentCommandParams, IThreadComment, IUpdateCommentCommandParams } from '@univerjs/thread-comment';
|
||||
import { generateRandomId, ICommandService, Inject, Injector, IUniverInstanceService, RichTextBuilder, RichTextValue, Tools, UniverInstanceType, UserManagerService } from '@univerjs/core';
|
||||
import { deserializeRangeWithSheet } from '@univerjs/engine-formula';
|
||||
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
|
||||
import { FRange } from '@univerjs/sheets/facade';
|
||||
import { AddCommentCommand, DeleteCommentCommand, DeleteCommentTreeCommand, getDT, ResolveCommentCommand, UpdateCommentCommand } from '@univerjs/thread-comment';
|
||||
|
||||
/**
|
||||
* An readonly class that represents a comment.
|
||||
* @ignore
|
||||
*/
|
||||
export class FTheadCommentItem {
|
||||
protected _comment: IThreadComment = {
|
||||
id: generateRandomId(),
|
||||
ref: '',
|
||||
threadId: '',
|
||||
dT: '',
|
||||
personId: '',
|
||||
text: RichTextBuilder.newEmptyData().body!,
|
||||
attachments: [],
|
||||
unitId: '',
|
||||
subUnitId: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new FTheadCommentItem
|
||||
* @param {IThreadComment|undefined} [comment] The comment
|
||||
* @returns {FTheadCommentItem} A new instance of FTheadCommentItem
|
||||
* @example
|
||||
* ```ts
|
||||
* const commentBuilder = univerAPI.newTheadComment();
|
||||
* console.log(commentBuilder);
|
||||
* ```
|
||||
*/
|
||||
static create(comment?: IThreadComment): FTheadCommentItem {
|
||||
return new FTheadCommentItem(comment);
|
||||
}
|
||||
|
||||
constructor(comment?: IThreadComment) {
|
||||
if (comment) {
|
||||
this._comment = comment;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the person id of the comment
|
||||
* @returns {string} The person id of the comment
|
||||
* @example
|
||||
* ```ts
|
||||
* const commentBuilder = univerAPI.newTheadComment();
|
||||
* console.log(commentBuilder.personId);
|
||||
* ```
|
||||
*/
|
||||
get personId(): string {
|
||||
return this._comment.personId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the date time of the comment
|
||||
* @returns {string} The date time of the comment
|
||||
* @example
|
||||
* ```ts
|
||||
* const commentBuilder = univerAPI.newTheadComment();
|
||||
* console.log(commentBuilder.dateTime);
|
||||
* ```
|
||||
*/
|
||||
get dateTime(): string {
|
||||
return this._comment.dT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of the comment
|
||||
* @returns {RichTextValue} The content of the comment
|
||||
* @example
|
||||
* ```ts
|
||||
* const commentBuilder = univerAPI.newTheadComment();
|
||||
* console.log(commentBuilder.content);
|
||||
* ```
|
||||
*/
|
||||
get content(): RichTextValue {
|
||||
return RichTextValue.createByBody(this._comment.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id of the comment
|
||||
* @returns {string} The id of the comment
|
||||
* @example
|
||||
* ```ts
|
||||
* const commentBuilder = univerAPI.newTheadComment();
|
||||
* console.log(commentBuilder.id);
|
||||
* ```
|
||||
*/
|
||||
get id(): string {
|
||||
return this._comment.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thread id of the comment
|
||||
* @returns {string} The thread id of the comment
|
||||
* @example
|
||||
* ```ts
|
||||
* const commentBuilder = univerAPI.newTheadComment();
|
||||
* console.log(commentBuilder.threadId);
|
||||
* ```
|
||||
*/
|
||||
get threadId(): string {
|
||||
return this._comment.threadId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the comment
|
||||
* @returns {FTheadCommentBuilder} The comment builder
|
||||
* @example
|
||||
* ```ts
|
||||
* const commentBuilder = univerAPI.newTheadComment();
|
||||
* const newCommentBuilder = commentBuilder.copy();
|
||||
* console.log(newCommentBuilder);
|
||||
* ```
|
||||
*/
|
||||
copy(): FTheadCommentBuilder {
|
||||
return FTheadCommentBuilder.create(Tools.deepClone(this._comment));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for thread comment. use {@link FUniver} `univerAPI.newTheadComment()` to create a new builder.
|
||||
*/
|
||||
export class FTheadCommentBuilder extends FTheadCommentItem {
|
||||
static override create(comment?: IThreadComment): FTheadCommentBuilder {
|
||||
return new FTheadCommentBuilder(comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content of the comment
|
||||
* @param {IDocumentBody | RichTextValue} content The content of the comment
|
||||
* @returns {FTheadCommentBuilder} The comment builder for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText);
|
||||
* console.log(commentBuilder.content);
|
||||
*
|
||||
* // Add the comment to the cell A1
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* const result = await cell.addCommentAsync(commentBuilder);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
setContent(content: IDocumentBody | RichTextValue): FTheadCommentBuilder {
|
||||
if (content instanceof RichTextValue) {
|
||||
this._comment.text = content.getData().body!;
|
||||
} else {
|
||||
this._comment.text = content;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the person id of the comment
|
||||
* @param {string} userId The person id of the comment
|
||||
* @returns {FTheadCommentBuilder} The comment builder for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setPersonId('mock-user-id');
|
||||
* console.log(commentBuilder.personId);
|
||||
*
|
||||
* // Add the comment to the cell A1
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* const result = await cell.addCommentAsync(commentBuilder);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
setPersonId(userId: string): FTheadCommentBuilder {
|
||||
this._comment.personId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the date time of the comment
|
||||
* @param {Date} date The date time of the comment
|
||||
* @returns {FTheadCommentBuilder} The comment builder for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setDateTime(new Date('2025-02-21 14:22:22'));
|
||||
* console.log(commentBuilder.dateTime);
|
||||
*
|
||||
* // Add the comment to the cell A1
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* const result = await cell.addCommentAsync(commentBuilder);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
setDateTime(date: Date): FTheadCommentBuilder {
|
||||
this._comment.dT = getDT(date);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the id of the comment
|
||||
* @param {string} id The id of the comment
|
||||
* @returns {FTheadCommentBuilder} The comment builder for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setId('mock-comment-id');
|
||||
* console.log(commentBuilder.id);
|
||||
*
|
||||
* // Add the comment to the cell A1
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* const result = await cell.addCommentAsync(commentBuilder);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
setId(id: string): FTheadCommentBuilder {
|
||||
this._comment.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the thread id of the comment
|
||||
* @param {string} threadId The thread id of the comment
|
||||
* @returns {FTheadCommentBuilder} The comment builder
|
||||
* @example
|
||||
* ```ts
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setThreadId('mock-thread-id');
|
||||
* console.log(commentBuilder.threadId);
|
||||
*
|
||||
* // Add the comment to the cell A1
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* const result = await cell.addCommentAsync(commentBuilder);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
setThreadId(threadId: string): FTheadCommentBuilder {
|
||||
this._comment.threadId = threadId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the comment
|
||||
* @returns {IThreadComment} The comment
|
||||
* @example
|
||||
* ```ts
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const comment = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setPersonId('mock-user-id')
|
||||
* .setDateTime(new Date('2025-02-21 14:22:22'))
|
||||
* .setId('mock-comment-id')
|
||||
* .setThreadId('mock-thread-id')
|
||||
* .build();
|
||||
* console.log(comment);
|
||||
* ```
|
||||
*/
|
||||
build(): IThreadComment {
|
||||
return this._comment;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that represents a thread comment already in the sheet.
|
||||
*/
|
||||
export class FThreadComment {
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
constructor(
|
||||
private readonly _thread: IThreadComment | IBaseComment,
|
||||
private readonly _parent: IThreadComment | undefined,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(SheetsThreadCommentModel) private readonly _threadCommentModel: SheetsThreadCommentModel,
|
||||
@Inject(UserManagerService) private readonly _userManagerService: UserManagerService
|
||||
) {
|
||||
}
|
||||
|
||||
private _getRef(): IRange {
|
||||
const ref = this._parent?.ref || (this._thread as IThreadComment).ref;
|
||||
const range = deserializeRangeWithSheet(ref);
|
||||
|
||||
return range.range;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the comment is a root comment
|
||||
* @returns {boolean} Whether the comment is a root comment
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const comments = fWorksheet.getComments();
|
||||
* comments.forEach((comment) => {
|
||||
* console.log(comment.getIsRoot());
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getIsRoot(): boolean {
|
||||
return !this._parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comment data
|
||||
* @returns {IBaseComment} The comment data
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const comments = fWorksheet.getComments();
|
||||
* comments.forEach((comment) => {
|
||||
* console.log(comment.getCommentData());
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getCommentData(): IBaseComment {
|
||||
const { children, ...comment } = this._thread;
|
||||
return comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the replies of the comment
|
||||
* @returns {FThreadComment[] | undefined} The loaded replies, or `undefined` when the thread or its replies are unavailable.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const comments = fWorksheet.getComments();
|
||||
* comments.forEach((comment) => {
|
||||
* if (comment.getIsRoot()) {
|
||||
* const replies = comment.getReplies();
|
||||
* replies.forEach((reply) => {
|
||||
* console.log(reply.getCommentData());
|
||||
* });
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getReplies(): FThreadComment[] | undefined {
|
||||
const range = this._getRef();
|
||||
const comments = this._threadCommentModel.getCommentWithChildren(this._thread.unitId, this._thread.subUnitId, range.startRow, range.startColumn);
|
||||
|
||||
return comments?.children?.map((child) => this._injector.createInstance(FThreadComment, child));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the range of the comment
|
||||
* @returns {FRange | null} The range of the comment
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const comments = fWorksheet.getComments();
|
||||
* comments.forEach((comment) => {
|
||||
* console.log(comment.getRange().getA1Notation());
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getRange(): FRange | null {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(this._thread.unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) {
|
||||
return null;
|
||||
}
|
||||
const worksheet = workbook.getSheetBySheetId(this._thread.subUnitId);
|
||||
if (!worksheet) {
|
||||
return null;
|
||||
}
|
||||
const range = this._getRef();
|
||||
return this._injector.createInstance(FRange, workbook, worksheet, range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rich text of the comment
|
||||
* @returns {RichTextValue} The rich text of the comment
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const comments = fWorksheet.getComments();
|
||||
* comments.forEach((comment) => {
|
||||
* console.log(comment.getRichText());
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getRichText(): RichTextValue {
|
||||
const body = this._thread.text;
|
||||
return RichTextValue.create({ body, documentStyle: {}, id: 'd' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the comment and it's replies
|
||||
* @returns {Promise<boolean>} Whether the comment is deleted successfully
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const comments = fWorksheet.getComments();
|
||||
*
|
||||
* // Delete the first comment
|
||||
* const result = await comments[0]?.deleteAsync();
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
deleteAsync(): Promise<boolean> {
|
||||
return this._commandService.executeCommand(
|
||||
this.getIsRoot() ? DeleteCommentTreeCommand.id : DeleteCommentCommand.id,
|
||||
{
|
||||
commentId: this._thread.id,
|
||||
unitId: this._thread.unitId,
|
||||
subUnitId: this._thread.subUnitId,
|
||||
} as IDeleteCommentCommandParams
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the comment content
|
||||
* @param {IDocumentBody | RichTextValue} content The new content of the comment
|
||||
* @returns {Promise<boolean>} Whether the comment is updated successfully
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setId('mock-comment-id');
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* await cell.addCommentAsync(commentBuilder);
|
||||
*
|
||||
* // Update the comment after 3 seconds
|
||||
* setTimeout(async () => {
|
||||
* const comment = fWorksheet.getCommentById('mock-comment-id');
|
||||
* const newRichText = univerAPI.newRichText().insertText('Hello Univer AI');
|
||||
* const result = await comment.updateAsync(newRichText);
|
||||
* console.log(result);
|
||||
* }, 3000);
|
||||
* ```
|
||||
*/
|
||||
async updateAsync(content: IDocumentBody | RichTextValue): Promise<boolean> {
|
||||
const body = content instanceof RichTextValue ? content.getData().body : content;
|
||||
const dt = getDT();
|
||||
const res = await this._commandService.executeCommand(
|
||||
UpdateCommentCommand.id,
|
||||
{
|
||||
unitId: this._thread.unitId,
|
||||
subUnitId: this._thread.subUnitId,
|
||||
payload: {
|
||||
commentId: this._thread.id,
|
||||
text: body,
|
||||
updated: true,
|
||||
updateT: dt,
|
||||
},
|
||||
} as IUpdateCommentCommandParams
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the comment
|
||||
* @param {boolean} [resolved] Whether the comment is resolved
|
||||
* @returns {Promise<boolean>} Set the comment to resolved or not operation result
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setId('mock-comment-id');
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* await cell.addCommentAsync(commentBuilder);
|
||||
*
|
||||
* // Resolve the comment after 3 seconds
|
||||
* setTimeout(async () => {
|
||||
* const comment = fWorksheet.getCommentById('mock-comment-id');
|
||||
* const result = await comment.resolveAsync(true);
|
||||
* console.log(result);
|
||||
* }, 3000);
|
||||
* ```
|
||||
*/
|
||||
resolveAsync(resolved?: boolean): Promise<boolean> {
|
||||
return this._commandService.executeCommand(
|
||||
ResolveCommentCommand.id,
|
||||
{
|
||||
unitId: this._thread.unitId,
|
||||
subUnitId: this._thread.subUnitId,
|
||||
commentId: this._thread.id,
|
||||
resolved: resolved ?? !this._thread.resolved,
|
||||
} as IResolveCommentCommandParams
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to the comment
|
||||
* @param {FTheadCommentBuilder} comment The comment to reply to
|
||||
* @returns {Promise<boolean>} Whether the comment is replied successfully
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setId('mock-comment-id');
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* await cell.addCommentAsync(commentBuilder);
|
||||
*
|
||||
* // Reply to the comment
|
||||
* const replyText = univerAPI.newRichText().insertText('Hello Univer AI');
|
||||
* const reply = univerAPI.newTheadComment().setContent(replyText);
|
||||
* const comment = fWorksheet.getCommentById('mock-comment-id');
|
||||
* const result = await comment.replyAsync(reply);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
replyAsync(comment: FTheadCommentBuilder): Promise<boolean> {
|
||||
const commentData = comment.build();
|
||||
return this._commandService.executeCommand(
|
||||
AddCommentCommand.id,
|
||||
{
|
||||
unitId: this._thread.unitId,
|
||||
subUnitId: this._thread.subUnitId,
|
||||
comment: {
|
||||
id: generateRandomId(),
|
||||
parentId: this._thread.id,
|
||||
threadId: this._thread.threadId,
|
||||
ref: this._parent?.ref || (this._thread as IThreadComment).ref,
|
||||
unitId: this._thread.unitId,
|
||||
subUnitId: this._thread.subUnitId,
|
||||
text: commentData.text,
|
||||
attachments: commentData.attachments,
|
||||
dT: commentData.dT || getDT(),
|
||||
personId: commentData.personId || this._userManagerService.getCurrentUser().userID,
|
||||
},
|
||||
} as IAddCommentCommandParams
|
||||
);
|
||||
}
|
||||
}
|
||||
325
packages/sheets-thread-comment/src/facade/f-univer.ts
Normal file
325
packages/sheets-thread-comment/src/facade/f-univer.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 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 { Injector } from '@univerjs/core';
|
||||
import type { IAddCommentCommandParams, IDeleteCommentCommandParams, IResolveCommentCommandParams, IThreadComment, IUpdateCommentCommandParams } from '@univerjs/thread-comment';
|
||||
import type {
|
||||
IBeforeSheetCommentAddEventParams,
|
||||
IBeforeSheetCommentDeleteEventParams,
|
||||
IBeforeSheetCommentUpdateEventParams,
|
||||
ISheetCommentAddEventParams,
|
||||
ISheetCommentDeleteEventParams,
|
||||
ISheetCommentResolveEventParams,
|
||||
ISheetCommentUpdateEventParams,
|
||||
} from './f-event';
|
||||
import { CanceledError, ICommandService, RichTextValue } from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { deserializeRangeWithSheet } from '@univerjs/engine-formula';
|
||||
import { AddCommentCommand, DeleteCommentCommand, DeleteCommentTreeCommand, ResolveCommentCommand, UpdateCommentCommand } from '@univerjs/thread-comment';
|
||||
import { FTheadCommentBuilder, FTheadCommentItem } from './f-thread-comment';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFUniverSheetsThreadCommentMixin {
|
||||
/**
|
||||
* Create a new thread comment
|
||||
* @returns {FTheadCommentBuilder} The thead comment builder
|
||||
* @example
|
||||
* ```ts
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setPersonId('mock-user-id')
|
||||
* .setDateTime(new Date('2025-02-21 14:22:22'))
|
||||
* .setId('mock-comment-id')
|
||||
* .setThreadId('mock-thread-id');
|
||||
*
|
||||
* // Add the comment to the cell A1
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* const result = await cell.addCommentAsync(commentBuilder);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
newTheadComment(): FTheadCommentBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class FUniverSheetsThreadCommentMixin extends FUniver implements IFUniverSheetsThreadCommentMixin {
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
override _initialize(injector: Injector): void {
|
||||
const commandService = injector.get(ICommandService);
|
||||
|
||||
// After command events
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.CommentAdded,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== AddCommentCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IAddCommentCommandParams;
|
||||
const target = this.getSheetCommandTarget(params);
|
||||
if (!target) return;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const { comment } = params;
|
||||
|
||||
const threadComment = worksheet.getCommentById(comment.id);
|
||||
if (!threadComment) return;
|
||||
|
||||
const eventParams: ISheetCommentAddEventParams = {
|
||||
workbook,
|
||||
worksheet,
|
||||
row: threadComment.getRange()?.getRow() ?? 0,
|
||||
col: threadComment.getRange()?.getColumn() ?? 0,
|
||||
comment: threadComment,
|
||||
};
|
||||
this.fireEvent(this.Event.CommentAdded, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.CommentUpdated,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== UpdateCommentCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IUpdateCommentCommandParams;
|
||||
const target = this.getSheetCommandTarget(params);
|
||||
if (!target) return;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const { payload } = params;
|
||||
|
||||
const threadComment = worksheet.getCommentById(payload.commentId);
|
||||
if (!threadComment) return;
|
||||
|
||||
const eventParams: ISheetCommentUpdateEventParams = {
|
||||
workbook,
|
||||
worksheet,
|
||||
row: threadComment.getRange()?.getRow() ?? 0,
|
||||
col: threadComment.getRange()?.getColumn() ?? 0,
|
||||
comment: threadComment,
|
||||
};
|
||||
this.fireEvent(this.Event.CommentUpdated, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.CommentDeleted,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== DeleteCommentCommand.id && commandInfo.id !== DeleteCommentTreeCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IDeleteCommentCommandParams;
|
||||
const target = this.getSheetCommandTarget(params);
|
||||
if (!target) return;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const { commentId } = params;
|
||||
|
||||
const eventParams: ISheetCommentDeleteEventParams = {
|
||||
workbook,
|
||||
worksheet,
|
||||
commentId,
|
||||
};
|
||||
this.fireEvent(this.Event.CommentDeleted, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.CommentResolved,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== ResolveCommentCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IResolveCommentCommandParams;
|
||||
const target = this.getSheetCommandTarget(params);
|
||||
if (!target) return;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const { commentId, resolved } = params;
|
||||
|
||||
const threadComment = worksheet.getCommentById(commentId);
|
||||
if (!threadComment) return;
|
||||
|
||||
const eventParams: ISheetCommentResolveEventParams = {
|
||||
workbook,
|
||||
worksheet,
|
||||
row: threadComment.getRange()?.getRow() ?? 0,
|
||||
col: threadComment.getRange()?.getColumn() ?? 0,
|
||||
comment: threadComment,
|
||||
resolved,
|
||||
};
|
||||
this.fireEvent(this.Event.CommentResolved, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Before command events
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeCommentAdd,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== AddCommentCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IAddCommentCommandParams;
|
||||
const target = this.getSheetCommandTarget(params);
|
||||
if (!target) return;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const { comment } = params;
|
||||
const { range } = deserializeRangeWithSheet(comment.ref);
|
||||
|
||||
const eventParams: IBeforeSheetCommentAddEventParams = {
|
||||
workbook,
|
||||
worksheet,
|
||||
row: range.startRow,
|
||||
col: range.startColumn,
|
||||
comment: FTheadCommentItem.create(comment),
|
||||
};
|
||||
this.fireEvent(this.Event.BeforeCommentAdd, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeCommentUpdate,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== UpdateCommentCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IUpdateCommentCommandParams;
|
||||
const target = this.getSheetCommandTarget(params);
|
||||
if (!target) return;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const { payload } = params;
|
||||
|
||||
const threadComment = worksheet.getCommentById(payload.commentId);
|
||||
if (!threadComment) return;
|
||||
|
||||
const eventParams: IBeforeSheetCommentUpdateEventParams = {
|
||||
workbook,
|
||||
worksheet,
|
||||
row: threadComment.getRange()?.getRow() ?? 0,
|
||||
col: threadComment.getRange()?.getColumn() ?? 0,
|
||||
comment: threadComment,
|
||||
newContent: RichTextValue.createByBody(payload.text),
|
||||
};
|
||||
this.fireEvent(this.Event.BeforeCommentUpdate, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeCommentDelete,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== DeleteCommentCommand.id && commandInfo.id !== DeleteCommentTreeCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IDeleteCommentCommandParams;
|
||||
const target = this.getSheetCommandTarget(params);
|
||||
if (!target) return;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const { commentId } = params;
|
||||
|
||||
const threadComment = worksheet.getCommentById(commentId);
|
||||
if (!threadComment) return;
|
||||
|
||||
const eventParams: IBeforeSheetCommentDeleteEventParams = {
|
||||
workbook,
|
||||
worksheet,
|
||||
row: threadComment.getRange()?.getRow() ?? 0,
|
||||
col: threadComment.getRange()?.getColumn() ?? 0,
|
||||
comment: threadComment,
|
||||
};
|
||||
this.fireEvent(this.Event.BeforeCommentDelete, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeCommentResolve,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== ResolveCommentCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IResolveCommentCommandParams;
|
||||
const target = this.getSheetCommandTarget(params);
|
||||
if (!target) return;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const { commentId, resolved } = params;
|
||||
|
||||
const threadComment = worksheet.getCommentById(commentId);
|
||||
if (!threadComment) return;
|
||||
|
||||
const eventParams: ISheetCommentResolveEventParams = {
|
||||
workbook,
|
||||
worksheet,
|
||||
row: threadComment.getRange()!.getRow() ?? 0,
|
||||
col: threadComment.getRange()!.getColumn() ?? 0,
|
||||
comment: threadComment,
|
||||
resolved,
|
||||
};
|
||||
this.fireEvent(this.Event.BeforeCommentResolve, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @returns {FTheadCommentBuilder} The thread comment builder.
|
||||
*/
|
||||
override newTheadComment(comment?: IThreadComment): FTheadCommentBuilder {
|
||||
return new FTheadCommentBuilder(comment);
|
||||
}
|
||||
}
|
||||
|
||||
FUniver.extend(FUniverSheetsThreadCommentMixin);
|
||||
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FUniver extends IFUniverSheetsThreadCommentMixin {}
|
||||
}
|
||||
94
packages/sheets-thread-comment/src/facade/f-workbook.ts
Normal file
94
packages/sheets-thread-comment/src/facade/f-workbook.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 { FWorkbook } from '@univerjs/sheets/facade';
|
||||
import { ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { FThreadComment } from './f-thread-comment';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFWorkbookSheetsThreadCommentMixin {
|
||||
/**
|
||||
* Get all comments in the current workbook
|
||||
* @returns {FThreadComment[]} All comments in the current workbook
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const comments = fWorkbook.getComments();
|
||||
* comments.forEach((comment) => {
|
||||
* const isRoot = comment.getIsRoot();
|
||||
*
|
||||
* if (isRoot) {
|
||||
* console.log('root comment:', comment.getCommentData());
|
||||
*
|
||||
* const replies = comment.getReplies();
|
||||
* replies.forEach((reply) => {
|
||||
* console.log('reply comment:', reply.getCommentData());
|
||||
* });
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getComments(): FThreadComment[];
|
||||
|
||||
/**
|
||||
* Clear all comments in the current workbook
|
||||
* @returns {Promise<boolean>} Whether the comments are cleared successfully.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const result = await fWorkbook.clearComments();
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
clearComments(): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class FWorkbookSheetsThreadCommentMixin extends FWorkbook implements IFWorkbookSheetsThreadCommentMixin {
|
||||
declare _threadCommentModel: ThreadCommentModel;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
override _initialize(): void {
|
||||
Object.defineProperty(this, '_threadCommentModel', {
|
||||
get() {
|
||||
return this._injector.get(ThreadCommentModel);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override getComments(): FThreadComment[] {
|
||||
return this._threadCommentModel.getUnit(this._workbook.getUnitId()).map((i) => this._injector.createInstance(FThreadComment, i.root));
|
||||
}
|
||||
|
||||
override clearComments(): Promise<boolean> {
|
||||
const comments = this.getComments();
|
||||
const promises = comments.map((comment) => comment.deleteAsync());
|
||||
|
||||
return Promise.all(promises).then(() => true);
|
||||
}
|
||||
}
|
||||
|
||||
FWorkbook.extend(FWorkbookSheetsThreadCommentMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FWorkbook extends IFWorkbookSheetsThreadCommentMixin {}
|
||||
}
|
||||
136
packages/sheets-thread-comment/src/facade/f-worksheet.ts
Normal file
136
packages/sheets-thread-comment/src/facade/f-worksheet.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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 { IDisposable } from '@univerjs/core';
|
||||
import type { IAddCommentCommandParams } from '@univerjs/thread-comment';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
|
||||
import { FWorksheet } from '@univerjs/sheets/facade';
|
||||
import { AddCommentCommand } from '@univerjs/thread-comment';
|
||||
import { FThreadComment } from './f-thread-comment';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFWorksheetCommentMixin {
|
||||
/**
|
||||
* Get all comments in the current sheet
|
||||
* @returns {FThreadComment[]} All comments in the current sheet
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const comments = fWorksheet.getComments();
|
||||
* comments.forEach((comment) => {
|
||||
* const isRoot = comment.getIsRoot();
|
||||
*
|
||||
* if (isRoot) {
|
||||
* console.log('root comment:', comment.getCommentData());
|
||||
*
|
||||
* const replies = comment.getReplies();
|
||||
* replies.forEach((reply) => {
|
||||
* console.log('reply comment:', reply.getCommentData());
|
||||
* });
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getComments(): FThreadComment[];
|
||||
|
||||
/**
|
||||
* Clear all comments in the current sheet
|
||||
* @returns {Promise<boolean>} Whether the comments are cleared successfully.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const result = await fWorksheet.clearComments();
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
clearComments(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* get comment by comment id
|
||||
* @param {string} commentId comment id
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Create a new comment
|
||||
* const richText = univerAPI.newRichText().insertText('hello univer');
|
||||
* const commentBuilder = univerAPI.newTheadComment()
|
||||
* .setContent(richText)
|
||||
* .setId('mock-comment-id');
|
||||
* const cell = fWorksheet.getRange('A1');
|
||||
* await cell.addCommentAsync(commentBuilder);
|
||||
*
|
||||
* const comment = fWorksheet.getCommentById('mock-comment-id');
|
||||
* console.log(comment, comment?.getCommentData());
|
||||
* ```
|
||||
*/
|
||||
getCommentById(commentId: string): FThreadComment | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class FWorksheetCommentMixin extends FWorksheet implements IFWorksheetCommentMixin {
|
||||
override getComments(): FThreadComment[] {
|
||||
const sheetsTheadCommentModel = this._injector.get(SheetsThreadCommentModel);
|
||||
const comments = sheetsTheadCommentModel.getSubUnitAll(this._workbook.getUnitId(), this._worksheet.getSheetId());
|
||||
return comments.map((comment) => this._injector.createInstance(FThreadComment, comment));
|
||||
}
|
||||
|
||||
override clearComments(): Promise<boolean> {
|
||||
const comments = this.getComments();
|
||||
const promises = comments.map((comment) => comment.deleteAsync());
|
||||
|
||||
return Promise.all(promises).then(() => true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to comment events.
|
||||
* @param callback Callback function, param contains comment info and target cell.
|
||||
* @returns {IDisposable} A disposable used to remove the listener.
|
||||
*/
|
||||
onCommented(callback: (params: IAddCommentCommandParams) => void): IDisposable {
|
||||
const commandService = this._injector.get(ICommandService);
|
||||
return commandService.onCommandExecuted((command) => {
|
||||
if (command.id === AddCommentCommand.id) {
|
||||
const params = command.params as IAddCommentCommandParams;
|
||||
callback(params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override getCommentById(commentId: string): FThreadComment | undefined {
|
||||
const sheetsTheadCommentModel = this._injector.get(SheetsThreadCommentModel);
|
||||
const comment = sheetsTheadCommentModel.getComment(this._workbook.getUnitId(), this._worksheet.getSheetId(), commentId);
|
||||
if (comment) {
|
||||
return this._injector.createInstance(FThreadComment, comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FWorksheet.extend(FWorksheetCommentMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
interface FWorksheet extends IFWorksheetCommentMixin {}
|
||||
}
|
||||
29
packages/sheets-thread-comment/src/facade/index.ts
Normal file
29
packages/sheets-thread-comment/src/facade/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 './f-range';
|
||||
import './f-workbook';
|
||||
import './f-worksheet';
|
||||
import './f-event';
|
||||
import './f-univer';
|
||||
|
||||
export type * from './f-event';
|
||||
export type * from './f-range';
|
||||
export type * from './f-thread-comment';
|
||||
export { FThreadComment } from './f-thread-comment';
|
||||
export type * from './f-univer';
|
||||
export type * from './f-workbook';
|
||||
export type * from './f-worksheet';
|
||||
23
packages/sheets-thread-comment/src/index.ts
Normal file
23
packages/sheets-thread-comment/src/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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';
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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 { UniverInstanceType } from '@univerjs/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SheetsThreadCommentModel } from '../sheets-thread-comment.model';
|
||||
|
||||
function createComment(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'comment-1',
|
||||
threadId: 'thread-1',
|
||||
ref: 'A1',
|
||||
dT: '2025-01-01T00:00:00.000Z',
|
||||
personId: 'user-1',
|
||||
text: { dataStream: 'hello\r\n', textRuns: [] },
|
||||
attachments: [],
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
resolved: false,
|
||||
...overrides,
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('SheetsThreadCommentModel', () => {
|
||||
it('keeps the newer unresolved thread as the default when an older thread is reopened', () => {
|
||||
const olderComment = createComment({ id: 'older-comment', threadId: 'older-thread', resolved: true });
|
||||
const newerComment = createComment({ id: 'newer-comment', threadId: 'newer-thread' });
|
||||
const comments = new Map([
|
||||
[olderComment.id, olderComment],
|
||||
[newerComment.id, newerComment],
|
||||
]);
|
||||
const updates$ = new Subject<unknown>();
|
||||
const threadCommentModel = {
|
||||
getAll: () => [{ threads: [
|
||||
{ unitId: 'unit-1', subUnitId: 'sheet-1', root: olderComment },
|
||||
{ unitId: 'unit-1', subUnitId: 'sheet-1', root: newerComment },
|
||||
] }],
|
||||
commentUpdate$: updates$,
|
||||
getComment: (_unitId: string, _subUnitId: string, commentId: string) => comments.get(commentId),
|
||||
};
|
||||
const univerInstanceService = {
|
||||
getUnitType: () => UniverInstanceType.UNIVER_SHEET,
|
||||
};
|
||||
const model = new SheetsThreadCommentModel(threadCommentModel as never, univerInstanceService as never);
|
||||
|
||||
expect(model.getByLocation('unit-1', 'sheet-1', 0, 0)).toBe(newerComment.id);
|
||||
|
||||
olderComment.resolved = false;
|
||||
updates$.next({
|
||||
type: 'resolve',
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
payload: { commentId: olderComment.id, resolved: false },
|
||||
});
|
||||
|
||||
expect(model.getByLocation('unit-1', 'sheet-1', 0, 0)).toBe(newerComment.id);
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
it('should build location indexes from existing root comments and expose thread data', () => {
|
||||
const root = createComment();
|
||||
const reply = createComment({ id: 'reply-1', parentId: root.id, ref: '', resolved: false });
|
||||
const thread = { ...root, children: [reply] };
|
||||
const updates$ = new Subject<any>();
|
||||
const threadCommentModel = {
|
||||
getAll: () => [{ threads: [{ unitId: 'unit-1', subUnitId: 'sheet-1', root }] }],
|
||||
commentUpdate$: updates$,
|
||||
getComment: (_unitId: string, _subUnitId: string, commentId: string) => {
|
||||
if (commentId === root.id) {
|
||||
return root;
|
||||
}
|
||||
if (commentId === reply.id) {
|
||||
return reply;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getThread: () => thread,
|
||||
getUnit: () => [{ subUnitId: 'sheet-1', root }],
|
||||
};
|
||||
const univerInstanceService = {
|
||||
getUnitType: () => UniverInstanceType.UNIVER_SHEET,
|
||||
};
|
||||
|
||||
const model = new SheetsThreadCommentModel(threadCommentModel as any, univerInstanceService as any);
|
||||
|
||||
expect(model.getByLocation('unit-1', 'sheet-1', 0, 0)).toBe(root.id);
|
||||
expect(model.getAllByLocation('unit-1', 'sheet-1', 0, 0)).toEqual([root]);
|
||||
expect(model.getCommentWithChildren('unit-1', 'sheet-1', 0, 0)).toEqual(thread);
|
||||
expect(model.showCommentMarker('unit-1', 'sheet-1', 0, 0)).toBe(true);
|
||||
expect(model.getSubUnitAll('unit-1', 'sheet-1')).toEqual([root]);
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
it('should transform thread updates into sheet-aware locations and keep indexes in sync', () => {
|
||||
const addComment = createComment();
|
||||
const movedComment = createComment({ ref: 'D4' });
|
||||
const updates$ = new Subject<any>();
|
||||
const commentStore = new Map<string, any>();
|
||||
|
||||
const threadCommentModel = {
|
||||
getAll: () => [],
|
||||
commentUpdate$: updates$,
|
||||
getComment: (_unitId: string, _subUnitId: string, commentId: string) => commentStore.get(commentId),
|
||||
getThread: () => null,
|
||||
getUnit: () => [],
|
||||
};
|
||||
const univerInstanceService = {
|
||||
getUnitType: (unitId: string) => (unitId === 'doc-unit' ? UniverInstanceType.UNIVER_DOC : UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
|
||||
const model = new SheetsThreadCommentModel(threadCommentModel as any, univerInstanceService as any);
|
||||
const received: Array<any> = [];
|
||||
const subscription = model.commentUpdate$.subscribe((update) => received.push(update));
|
||||
|
||||
commentStore.set(addComment.id, addComment);
|
||||
updates$.next({ type: 'add', unitId: 'unit-1', subUnitId: 'sheet-1', payload: addComment });
|
||||
|
||||
expect(model.getByLocation('unit-1', 'sheet-1', 0, 0)).toBe(addComment.id);
|
||||
expect(received.at(-1)).toEqual(expect.objectContaining({ type: 'add', row: 0, column: 0, isRoot: true }));
|
||||
|
||||
updates$.next({ type: 'update', unitId: 'unit-1', subUnitId: 'sheet-1', payload: { commentId: addComment.id } });
|
||||
expect(received.at(-1)).toEqual(expect.objectContaining({ type: 'update', row: 0, column: 0 }));
|
||||
|
||||
commentStore.set(addComment.id, movedComment);
|
||||
updates$.next({ type: 'updateRef', unitId: 'unit-1', subUnitId: 'sheet-1', payload: { commentId: addComment.id, ref: 'D4' } });
|
||||
|
||||
expect(model.getByLocation('unit-1', 'sheet-1', 0, 0)).toBeUndefined();
|
||||
expect(model.getByLocation('unit-1', 'sheet-1', 3, 3)).toBe(addComment.id);
|
||||
expect(received.at(-1)).toEqual(expect.objectContaining({ type: 'updateRef', row: 3, column: 3 }));
|
||||
|
||||
commentStore.set(addComment.id, { ...movedComment, resolved: true });
|
||||
updates$.next({ type: 'resolve', unitId: 'unit-1', subUnitId: 'sheet-1', payload: { commentId: addComment.id, resolved: true } });
|
||||
|
||||
expect(received.at(-1)).toEqual(expect.objectContaining({ type: 'resolve', row: 3, column: 3 }));
|
||||
expect(model.getByLocation('unit-1', 'sheet-1', 3, 3)).toBeUndefined();
|
||||
expect(model.showCommentMarker('unit-1', 'sheet-1', 3, 3)).toBe(false);
|
||||
|
||||
updates$.next({
|
||||
type: 'delete',
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
payload: { isRoot: true, comment: movedComment },
|
||||
});
|
||||
|
||||
expect(model.getAllByLocation('unit-1', 'sheet-1', 3, 3)).toEqual([]);
|
||||
expect(received.at(-1)).toEqual(expect.objectContaining({ type: 'delete', row: 3, column: 3 }));
|
||||
|
||||
updates$.next({ type: 'add', unitId: 'doc-unit', subUnitId: 'sheet-1', payload: createComment({ id: 'doc-comment', ref: 'B2' }) });
|
||||
expect(model.getByLocation('doc-unit', 'sheet-1', 1, 1)).toBeUndefined();
|
||||
|
||||
subscription.unsubscribe();
|
||||
model.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 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 { CommentUpdate, IThreadComment } from '@univerjs/thread-comment';
|
||||
import { Disposable, Inject, IUniverInstanceService, ObjectMatrix, UniverInstanceType } from '@univerjs/core';
|
||||
import { singleReferenceToGrid } from '@univerjs/engine-formula';
|
||||
import { deserializeThreadCommentAnchor, ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export type SheetCommentUpdate = CommentUpdate & {
|
||||
row: number;
|
||||
column: number;
|
||||
};
|
||||
|
||||
export class SheetsThreadCommentModel extends Disposable {
|
||||
private _matrixMap: Map<string, Map<string, ObjectMatrix<Set<string>>>> = new Map();
|
||||
private _locationMap: Map<string, Map<string, Map<string, { row: number; column: number }>>> = new Map();
|
||||
private _commentUpdate$ = new Subject<SheetCommentUpdate>();
|
||||
|
||||
commentUpdate$ = this._commentUpdate$.asObservable();
|
||||
|
||||
constructor(
|
||||
@Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
this._init();
|
||||
this.disposeWithMe(() => {
|
||||
this._commentUpdate$.complete();
|
||||
});
|
||||
}
|
||||
|
||||
private _init() {
|
||||
this._initData();
|
||||
this._initUpdateTransform();
|
||||
}
|
||||
|
||||
private _ensureCommentMatrix(unitId: string, subUnitId: string) {
|
||||
let unitMap = this._matrixMap.get(unitId);
|
||||
|
||||
if (!unitMap) {
|
||||
unitMap = new Map();
|
||||
this._matrixMap.set(unitId, unitMap);
|
||||
}
|
||||
|
||||
let subUnitMap = unitMap.get(subUnitId);
|
||||
if (!subUnitMap) {
|
||||
subUnitMap = new ObjectMatrix();
|
||||
unitMap.set(subUnitId, subUnitMap);
|
||||
}
|
||||
|
||||
return subUnitMap;
|
||||
}
|
||||
|
||||
private _ensureCommentLocationMap(unitId: string, subUnitId: string) {
|
||||
let unitMap = this._locationMap.get(unitId);
|
||||
|
||||
if (!unitMap) {
|
||||
unitMap = new Map();
|
||||
this._locationMap.set(unitId, unitMap);
|
||||
}
|
||||
|
||||
let subUnitMap = unitMap.get(subUnitId);
|
||||
if (!subUnitMap) {
|
||||
subUnitMap = new Map();
|
||||
unitMap.set(subUnitId, subUnitMap);
|
||||
}
|
||||
|
||||
return subUnitMap;
|
||||
}
|
||||
|
||||
private _addCommentToMatrix(matrix: ObjectMatrix<Set<string>>, row: number, column: number, commentId: string) {
|
||||
const current = matrix.getValue(row, column) ?? new Set();
|
||||
current.add(commentId);
|
||||
matrix.setValue(row, column, current);
|
||||
}
|
||||
|
||||
private _deleteCommentFromMatrix(matrix: ObjectMatrix<Set<string>>, row: number, column: number, commentId: string) {
|
||||
if (row >= 0 && column >= 0) {
|
||||
const current = matrix.getValue(row, column);
|
||||
if (current && current.has(commentId)) {
|
||||
current.delete(commentId);
|
||||
if (current.size === 0) {
|
||||
matrix.realDeleteValue(row, column);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _ensure(unitId: string, subUnitId: string) {
|
||||
const matrix = this._ensureCommentMatrix(unitId, subUnitId);
|
||||
const locationMap = this._ensureCommentLocationMap(unitId, subUnitId);
|
||||
return { matrix, locationMap };
|
||||
}
|
||||
|
||||
private _initData() {
|
||||
const datas = this._threadCommentModel.getAll();
|
||||
|
||||
for (const data of datas) {
|
||||
for (const thread of data.threads) {
|
||||
const { unitId, subUnitId, root } = thread;
|
||||
this._addComment(unitId, subUnitId, root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _addComment(unitId: string, subUnitId: string, comment: IThreadComment) {
|
||||
if (deserializeThreadCommentAnchor(comment.ref)) {
|
||||
return;
|
||||
}
|
||||
const location = singleReferenceToGrid(comment.ref);
|
||||
const parentId = comment.parentId;
|
||||
const { row, column } = location;
|
||||
const commentId = comment.id;
|
||||
const { matrix, locationMap } = this._ensure(unitId, subUnitId);
|
||||
if (!parentId && row >= 0 && column >= 0) {
|
||||
this._addCommentToMatrix(matrix, row, column, commentId);
|
||||
locationMap.set(commentId, { row, column });
|
||||
}
|
||||
|
||||
if (!parentId) {
|
||||
this._commentUpdate$.next({
|
||||
unitId,
|
||||
subUnitId,
|
||||
payload: comment,
|
||||
type: 'add',
|
||||
isRoot: !parentId,
|
||||
...location,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initUpdateTransform() {
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
this.disposeWithMe(this._threadCommentModel.commentUpdate$.subscribe((update) => {
|
||||
const { unitId, subUnitId } = update;
|
||||
|
||||
try {
|
||||
const type = this._univerInstanceService.getUnitType(unitId);
|
||||
if (type !== UniverInstanceType.UNIVER_SHEET) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
const { matrix, locationMap } = this._ensure(unitId, subUnitId);
|
||||
switch (update.type) {
|
||||
case 'add': {
|
||||
this._addComment(update.unitId, update.subUnitId, update.payload);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const { isRoot, comment } = update.payload;
|
||||
if (deserializeThreadCommentAnchor(comment.ref)) {
|
||||
return;
|
||||
}
|
||||
if (isRoot) {
|
||||
const location = singleReferenceToGrid(comment.ref);
|
||||
const { row, column } = location;
|
||||
this._deleteCommentFromMatrix(matrix, row, column, comment.id);
|
||||
this._commentUpdate$.next({
|
||||
...update,
|
||||
...location,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
const { commentId } = update.payload;
|
||||
const comment = this._threadCommentModel.getComment(unitId, subUnitId, commentId);
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
if (deserializeThreadCommentAnchor(comment.ref)) {
|
||||
return;
|
||||
}
|
||||
const location = singleReferenceToGrid(comment.ref);
|
||||
this._commentUpdate$.next({
|
||||
...update,
|
||||
...location,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'updateRef': {
|
||||
if (deserializeThreadCommentAnchor(update.payload.ref)) {
|
||||
return;
|
||||
}
|
||||
const location = singleReferenceToGrid(update.payload.ref);
|
||||
const { commentId } = update.payload;
|
||||
const currentLoc = locationMap.get(commentId);
|
||||
if (!currentLoc) {
|
||||
return;
|
||||
}
|
||||
const { row, column } = currentLoc;
|
||||
this._deleteCommentFromMatrix(matrix, row, column, commentId);
|
||||
locationMap.delete(commentId);
|
||||
|
||||
if (location.row >= 0 && location.column >= 0) {
|
||||
this._addCommentToMatrix(matrix, location.row, location.column, commentId);
|
||||
locationMap.set(commentId, { row: location.row, column: location.column });
|
||||
}
|
||||
this._commentUpdate$.next({
|
||||
...update,
|
||||
...location,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'resolve': {
|
||||
const { unitId, subUnitId, payload } = update;
|
||||
const { locationMap } = this._ensure(unitId, subUnitId);
|
||||
const location = locationMap.get(payload.commentId);
|
||||
if (location) {
|
||||
this._commentUpdate$.next({
|
||||
...update,
|
||||
...location,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
getByLocation(unitId: string, subUnitId: string, row: number, column: number): string | undefined {
|
||||
const comments = this.getAllByLocation(unitId, subUnitId, row, column);
|
||||
const activeComments = comments.filter((comment) => !comment.resolved);
|
||||
return activeComments[activeComments.length - 1]?.id;
|
||||
}
|
||||
|
||||
getAllByLocation(unitId: string, subUnitId: string, row: number, column: number): IThreadComment[] {
|
||||
const matrix = this._ensureCommentMatrix(unitId, subUnitId);
|
||||
const current = matrix.getValue(row, column);
|
||||
if (!current) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const comments: IThreadComment[] = [];
|
||||
for (const id of current) {
|
||||
const comment = this.getComment(unitId, subUnitId, id);
|
||||
if (comment) {
|
||||
comments.push(comment);
|
||||
}
|
||||
}
|
||||
return comments;
|
||||
}
|
||||
|
||||
getComment(unitId: string, subUnitId: string, commentId: string) {
|
||||
return this._threadCommentModel.getComment(unitId, subUnitId, commentId);
|
||||
}
|
||||
|
||||
getCommentWithChildren(unitId: string, subUnitId: string, row: number, column: number) {
|
||||
const commentId = this.getByLocation(unitId, subUnitId, row, column);
|
||||
if (!commentId) {
|
||||
return undefined;
|
||||
}
|
||||
const comment = this.getComment(unitId, subUnitId, commentId);
|
||||
if (!comment) {
|
||||
return undefined;
|
||||
}
|
||||
return this._threadCommentModel.getThread(unitId, subUnitId, comment.threadId);
|
||||
}
|
||||
|
||||
showCommentMarker(unitId: string, subUnitId: string, row: number, column: number) {
|
||||
const commentId = this.getByLocation(unitId, subUnitId, row, column);
|
||||
if (!commentId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const comment = this.getComment(unitId, subUnitId, commentId);
|
||||
if (comment && !comment.resolved) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getSubUnitAll(unitId: string, subUnitId: string) {
|
||||
return this._threadCommentModel.getUnit(unitId).filter((i) => i.subUnitId === subUnitId).map((i) => i.root);
|
||||
}
|
||||
}
|
||||
76
packages/sheets-thread-comment/src/plugin.ts
Normal file
76
packages/sheets-thread-comment/src/plugin.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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 { Dependency } from '@univerjs/core';
|
||||
import type { IUniverSheetsThreadCommentConfig } from './config/config';
|
||||
import {
|
||||
DependentOn,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
Inject,
|
||||
Injector,
|
||||
merge,
|
||||
Plugin,
|
||||
touchDependencies,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverThreadCommentPlugin } from '@univerjs/thread-comment';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, SHEETS_THREAD_COMMENT_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { SheetsThreadCommentRefRangeController } from './controllers/sheets-thread-comment-ref-range.controller';
|
||||
import { SheetsThreadCommentResourceController } from './controllers/sheets-thread-comment-resource.controller';
|
||||
import { SheetsThreadCommentModel } from './models/sheets-thread-comment.model';
|
||||
import { SHEET_THREAD_COMMENT_BASE } from './types/const';
|
||||
|
||||
@DependentOn(UniverThreadCommentPlugin, UniverSheetsPlugin)
|
||||
export class UniverSheetsThreadCommentPlugin extends Plugin {
|
||||
static override pluginName = SHEET_THREAD_COMMENT_BASE;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsThreadCommentConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected override _injector: Injector,
|
||||
@Inject(ICommandService) protected _commandService: ICommandService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(SHEETS_THREAD_COMMENT_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
([
|
||||
[SheetsThreadCommentModel],
|
||||
[SheetsThreadCommentRefRangeController],
|
||||
[SheetsThreadCommentResourceController],
|
||||
] as Dependency[]).forEach((dep) => {
|
||||
this._injector.add(dep);
|
||||
});
|
||||
|
||||
touchDependencies(this._injector, [
|
||||
[SheetsThreadCommentRefRangeController],
|
||||
[SheetsThreadCommentResourceController],
|
||||
]);
|
||||
}
|
||||
}
|
||||
20
packages/sheets-thread-comment/src/types/const.ts
Normal file
20
packages/sheets-thread-comment/src/types/const.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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 { 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,22 @@
|
||||
/**
|
||||
* 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 '@univerjs/thread-comment';
|
||||
|
||||
export interface ISheetThreadComment extends IThreadComment {
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
8
packages/sheets-thread-comment/tsconfig.json
Normal file
8
packages/sheets-thread-comment/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/sheets-thread-comment/tsconfig.node.json
Normal file
18
packages/sheets-thread-comment/tsconfig.node.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/node",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/**",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
3
packages/sheets-thread-comment/vitest.config.ts
Normal file
3
packages/sheets-thread-comment/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user