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:
59
packages/docs-thread-comment-ui/README.md
Normal file
59
packages/docs-thread-comment-ui/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# @univerjs/docs-thread-comment-ui
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/docs-thread-comment-ui)
|
||||
[](https://npmjs.com/package/@univerjs/docs-thread-comment-ui)
|
||||
[](https://npmjs.com/package/@univerjs/docs-thread-comment-ui)
|
||||
|
||||
`@univerjs/docs-thread-comment-ui` adds thread comment UI integration for Univer Docs.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/docs-thread-comment-ui` | `UniverDocsThreadCommentUi` | Yes | Yes | No |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/docs-thread-comment @univerjs/docs-thread-comment-ui @univerjs/thread-comment-ui
|
||||
# or
|
||||
npm install @univerjs/docs-thread-comment @univerjs/docs-thread-comment-ui @univerjs/thread-comment-ui
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { LocaleType, mergeLocales, Univer } from '@univerjs/core';
|
||||
import '@univerjs/docs-thread-comment/facade';
|
||||
import { UniverDocsThreadCommentUIPlugin } from '@univerjs/docs-thread-comment-ui';
|
||||
import DocsThreadCommentUIEnUS from '@univerjs/docs-thread-comment-ui/locale/en-US';
|
||||
import { UniverThreadCommentUIPlugin } from '@univerjs/thread-comment-ui';
|
||||
import ThreadCommentUIEnUS from '@univerjs/thread-comment-ui/locale/en-US';
|
||||
import '@univerjs/docs-thread-comment-ui/lib/index.css';
|
||||
import '@univerjs/thread-comment-ui/lib/index.css';
|
||||
|
||||
const univer = new Univer({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: mergeLocales(DocsThreadCommentUIEnUS, ThreadCommentUIEnUS),
|
||||
},
|
||||
});
|
||||
|
||||
univer.registerPlugin(UniverThreadCommentUIPlugin);
|
||||
univer.registerPlugin(UniverDocsThreadCommentUIPlugin);
|
||||
|
||||
const range = univerAPI.getActiveDocument()?.getTextRange(0, 12);
|
||||
await range?.createCommentAsync('Verify this introduction.', { id: 'review-intro' });
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
Register the shared thread-comment UI plugin before the Docs adapter. Use `@univerjs/docs-thread-comment` for headless model commands and Facade APIs; this UI package adds menus, rendering, and the shared side panel. The underlying `@univerjs/thread-comment` plugin is registered automatically as a dependency.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/docs-thread-comment-ui)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
98
packages/docs-thread-comment-ui/package.json
Normal file
98
packages/docs-thread-comment-ui/package.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"name": "@univerjs/docs-thread-comment-ui",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Thread comment UI integration for Univer Docs.",
|
||||
"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",
|
||||
"docs",
|
||||
"comment",
|
||||
"thread-comment",
|
||||
"ui"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./locale/*": "./src/locale/*.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"
|
||||
},
|
||||
"./locale/*": {
|
||||
"import": "./lib/es/locale/*.js",
|
||||
"require": "./lib/cjs/locale/*.js",
|
||||
"types": "./lib/types/locale/*.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": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/docs-thread-comment": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/drawing": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/icons": "1.38.0",
|
||||
"@univerjs/protocol": "workspace:*",
|
||||
"@univerjs/thread-comment": "workspace:*",
|
||||
"@univerjs/thread-comment-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"postcss": "^8.5.28",
|
||||
"react": "18.3.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "3.4.18",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/docs-thread-comment-ui/postcss.config.mjs
Normal file
1
packages/docs-thread-comment-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, IDocumentData, Injector } from '@univerjs/core';
|
||||
import type { IThreadComment } from '@univerjs/thread-comment';
|
||||
import { ICommandService, IUniverInstanceService, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, DocStateEmitService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import {
|
||||
AddDocCommentDecorationMutation,
|
||||
CreateDocTextRangeCommentCommand,
|
||||
DEFAULT_DOC_SUBUNIT_ID,
|
||||
} from '@univerjs/docs-thread-comment';
|
||||
import { FDocument } from '@univerjs/docs/facade';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
AddCommentMutation,
|
||||
IThreadCommentDataSourceService,
|
||||
ThreadCommentDataSourceService,
|
||||
ThreadCommentFacadeService,
|
||||
ThreadCommentModel,
|
||||
} from '@univerjs/thread-comment';
|
||||
import { SetActiveCommentOperation, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { DesktopSidebarService, ISidebarService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AddDocCommentComment } from '../add-doc-comment.command';
|
||||
import '@univerjs/docs-thread-comment/facade';
|
||||
|
||||
const DOC_ID = 'doc-add-comment-test';
|
||||
|
||||
function createDocData(): IDocumentData {
|
||||
return {
|
||||
id: DOC_ID,
|
||||
body: {
|
||||
dataStream: 'Hello world\r\n',
|
||||
textRuns: [],
|
||||
paragraphs: [],
|
||||
sectionBreaks: [],
|
||||
customBlocks: [],
|
||||
customDecorations: [],
|
||||
},
|
||||
documentStyle: {
|
||||
pageSize: {
|
||||
width: 594.3,
|
||||
height: 840.51,
|
||||
},
|
||||
marginTop: 72,
|
||||
marginBottom: 72,
|
||||
marginRight: 90,
|
||||
marginLeft: 90,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createComment(id = 'comment-1'): IThreadComment {
|
||||
return {
|
||||
id,
|
||||
threadId: '',
|
||||
ref: '',
|
||||
unitId: DOC_ID,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
dT: '2026-06-17T00:00:00.000Z',
|
||||
personId: 'user-1',
|
||||
text: {
|
||||
dataStream: 'Please revise this wording.\r\n',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('AddDocCommentComment', () => {
|
||||
let univer: Univer;
|
||||
let injector: Injector;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
|
||||
function getDocBody() {
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
return univerInstanceService.getUnit<DocumentDataModel>(DOC_ID, UniverInstanceType.UNIVER_DOC)?.getBody();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
univer = new Univer();
|
||||
injector = univer.__getInjector();
|
||||
get = injector.get.bind(injector);
|
||||
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([DocStateEmitService]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
injector.add([IThreadCommentDataSourceService, { useClass: ThreadCommentDataSourceService }]);
|
||||
injector.add([ThreadCommentModel]);
|
||||
injector.add([ThreadCommentFacadeService]);
|
||||
injector.add([ISidebarService, { useClass: DesktopSidebarService }]);
|
||||
injector.add([ThreadCommentPanelService]);
|
||||
|
||||
univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData());
|
||||
get(IUniverInstanceService).focusUnit(DOC_ID);
|
||||
|
||||
commandService = get(ICommandService);
|
||||
commandService.registerCommand(AddDocCommentComment);
|
||||
commandService.registerCommand(CreateDocTextRangeCommentCommand);
|
||||
commandService.registerCommand(AddDocCommentDecorationMutation);
|
||||
commandService.registerCommand(AddCommentMutation);
|
||||
commandService.registerCommand(SetActiveCommentOperation);
|
||||
commandService.registerCommand(RichTextEditingMutation);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('adds a thread comment to an explicit document range and makes it active', async () => {
|
||||
const comment = createComment();
|
||||
const result = await commandService.executeCommand(AddDocCommentComment.id, {
|
||||
unitId: DOC_ID,
|
||||
comment,
|
||||
range: {
|
||||
startOffset: 0,
|
||||
endOffset: 5,
|
||||
collapsed: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
const thread = get(ThreadCommentModel).getThread(DOC_ID, DEFAULT_DOC_SUBUNIT_ID, comment.id);
|
||||
expect(thread?.root).toMatchObject({
|
||||
id: comment.id,
|
||||
threadId: comment.id,
|
||||
unitId: DOC_ID,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
personId: 'user-1',
|
||||
text: {
|
||||
dataStream: 'Please revise this wording.\r\n',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getDocBody()?.customDecorations).toEqual([
|
||||
expect.objectContaining({
|
||||
id: comment.id,
|
||||
startIndex: 0,
|
||||
endIndex: 4,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(get(ThreadCommentPanelService).activeCommentId).toEqual({
|
||||
unitId: DOC_ID,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
commentId: comment.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the server-assigned id for the document anchor', async () => {
|
||||
const selectionManager = get(DocSelectionManagerService);
|
||||
selectionManager.__TEST_ONLY_setCurrentSelection({
|
||||
unitId: DOC_ID,
|
||||
subUnitId: DOC_ID,
|
||||
});
|
||||
selectionManager.__TEST_ONLY_add([{
|
||||
startOffset: 0,
|
||||
endOffset: 5,
|
||||
collapsed: false,
|
||||
isActive: true,
|
||||
segmentId: '',
|
||||
style: null as never,
|
||||
}]);
|
||||
get(IThreadCommentDataSourceService).dataSource = {
|
||||
addComment: vi.fn(async (comment: IThreadComment) => ({
|
||||
...comment,
|
||||
id: 'server-comment-id',
|
||||
threadId: 'server-comment-id',
|
||||
})),
|
||||
updateComment: vi.fn(),
|
||||
resolveComment: vi.fn(),
|
||||
deleteComment: vi.fn(),
|
||||
listComments: vi.fn(),
|
||||
saveCommentToSnapshot: (value) => value,
|
||||
};
|
||||
|
||||
const result = await commandService.executeCommand(AddDocCommentComment.id, {
|
||||
unitId: DOC_ID,
|
||||
comment: createComment('client-comment-id'),
|
||||
range: { startOffset: 0, endOffset: 5, collapsed: false },
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(getDocBody()?.customDecorations).toEqual([
|
||||
expect.objectContaining({ id: 'server-comment-id' }),
|
||||
]);
|
||||
expect(get(ThreadCommentModel).getThread(
|
||||
DOC_ID,
|
||||
DEFAULT_DOC_SUBUNIT_ID,
|
||||
'server-comment-id'
|
||||
)?.root.id).toBe('server-comment-id');
|
||||
expect(get(ThreadCommentPanelService).activeCommentId?.commentId).toBe('server-comment-id');
|
||||
});
|
||||
|
||||
it('rejects a collapsed explicit range before writing to the data source', async () => {
|
||||
const comment = createComment('comment-without-range');
|
||||
const addComment = vi.fn(async (value: IThreadComment) => value);
|
||||
get(IThreadCommentDataSourceService).dataSource = {
|
||||
addComment,
|
||||
updateComment: vi.fn(),
|
||||
resolveComment: vi.fn(),
|
||||
deleteComment: vi.fn(),
|
||||
listComments: vi.fn(),
|
||||
saveCommentToSnapshot: (value) => value,
|
||||
};
|
||||
const result = await commandService.executeCommand(AddDocCommentComment.id, {
|
||||
unitId: DOC_ID,
|
||||
comment,
|
||||
range: {
|
||||
startOffset: 5,
|
||||
endOffset: 5,
|
||||
collapsed: true,
|
||||
},
|
||||
});
|
||||
const outOfBounds = await commandService.executeCommand(AddDocCommentComment.id, {
|
||||
unitId: DOC_ID,
|
||||
comment,
|
||||
range: {
|
||||
startOffset: 0,
|
||||
endOffset: 100,
|
||||
collapsed: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(outOfBounds).toBe(false);
|
||||
expect(get(ThreadCommentModel).getThread(DOC_ID, DEFAULT_DOC_SUBUNIT_ID, comment.id)).toBeUndefined();
|
||||
expect(getDocBody()?.customDecorations).toEqual([]);
|
||||
expect(get(ThreadCommentPanelService).activeCommentId).toBeUndefined();
|
||||
expect(addComment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates an agent-supplied text-range comment with queryable offsets', async () => {
|
||||
const result = await commandService.executeCommand(CreateDocTextRangeCommentCommand.id, {
|
||||
unitId: DOC_ID,
|
||||
range: { startOffset: 0, endOffset: 5, collapsed: false },
|
||||
content: 'Check the greeting.',
|
||||
id: 'agent-comment-1',
|
||||
personId: 'agent-user-1',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(get(ThreadCommentModel).getThread(
|
||||
DOC_ID,
|
||||
DEFAULT_DOC_SUBUNIT_ID,
|
||||
'agent-comment-1'
|
||||
)?.root).toMatchObject({
|
||||
id: 'agent-comment-1',
|
||||
threadId: 'agent-comment-1',
|
||||
personId: 'agent-user-1',
|
||||
ref: 'Hello',
|
||||
startOffset: 0,
|
||||
endOffset: 5,
|
||||
collapsed: false,
|
||||
text: { dataStream: 'Check the greeting.\r\n' },
|
||||
});
|
||||
expect(getDocBody()?.customDecorations).toEqual([
|
||||
expect.objectContaining({ id: 'agent-comment-1', startIndex: 0, endIndex: 4 }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates and queries a comment through FDocumentTextRange', async () => {
|
||||
const documentModel = get(IUniverInstanceService).getUnit<DocumentDataModel>(DOC_ID, UniverInstanceType.UNIVER_DOC);
|
||||
if (!documentModel) {
|
||||
throw new Error('Test document was not created.');
|
||||
}
|
||||
const document = injector.createInstance(FDocument, documentModel);
|
||||
const range = document.getTextRange(6, 11);
|
||||
|
||||
await expect(range.createCommentAsync('Review the noun.', {
|
||||
id: 'facade-comment-1',
|
||||
personId: 'agent-user-2',
|
||||
})).resolves.toBe(true);
|
||||
|
||||
expect(range.getComments()).toHaveLength(1);
|
||||
expect(range.getComments()[0]).toMatchObject({
|
||||
threadId: 'facade-comment-1',
|
||||
root: {
|
||||
ref: 'world',
|
||||
startOffset: 6,
|
||||
endOffset: 11,
|
||||
personId: 'agent-user-2',
|
||||
},
|
||||
});
|
||||
expect(document.getTextRange(0, 5).getComments()).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects an incomplete add-comment request without changing the document', async () => {
|
||||
const result = await commandService.executeCommand(AddDocCommentComment.id);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(get(ThreadCommentModel).getAll()).toEqual([]);
|
||||
expect(getDocBody()?.customDecorations).toEqual([]);
|
||||
expect(get(ThreadCommentPanelService).activeCommentId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, IDocumentData } from '@univerjs/core';
|
||||
import { CustomDecorationType, ICommandService, IUniverInstanceService, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, DocStateEmitService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { DeleteDocCommentComment } from '../delete-doc-comment.command';
|
||||
|
||||
function createDocData(id: string): IDocumentData {
|
||||
return {
|
||||
id,
|
||||
body: {
|
||||
dataStream: 'Hello world\r\n',
|
||||
customDecorations: [{ id: 'c1', startIndex: 0, endIndex: 5, type: CustomDecorationType.COMMENT }],
|
||||
},
|
||||
documentStyle: {
|
||||
pageSize: { width: 100, height: 100 },
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('DeleteDocCommentComment', () => {
|
||||
let univer: Univer;
|
||||
let injector: ReturnType<Univer['__getInjector']>;
|
||||
let commandService: ICommandService;
|
||||
|
||||
beforeEach(() => {
|
||||
univer = new Univer();
|
||||
injector = univer.__getInjector();
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([DocStateEmitService]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(DeleteDocCommentComment);
|
||||
commandService.registerCommand(RichTextEditingMutation);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('removes the comment decoration from the document', async () => {
|
||||
const doc = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, createDocData('doc-1'));
|
||||
injector.get(IUniverInstanceService).focusUnit(doc.getUnitId());
|
||||
|
||||
const ok = await commandService.executeCommand(DeleteDocCommentComment.id, { unitId: 'doc-1', commentId: 'c1' });
|
||||
|
||||
expect(ok).toBe(true);
|
||||
expect(doc.getBody()?.customDecorations).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects an incomplete delete request without changing the document', async () => {
|
||||
const doc = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, createDocData('doc-1'));
|
||||
injector.get(IUniverInstanceService).focusUnit(doc.getUnitId());
|
||||
|
||||
const ok = await commandService.executeCommand(DeleteDocCommentComment.id);
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(doc.getBody()?.customDecorations).toEqual([{ id: 'c1', startIndex: 0, endIndex: 5, type: CustomDecorationType.COMMENT }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import type { IAddDocTextRangeCommentParams } from '@univerjs/docs-thread-comment';
|
||||
import { CommandType, ICommandService, sequenceExecute } from '@univerjs/core';
|
||||
import { RichTextEditingMutation } from '@univerjs/docs';
|
||||
import {
|
||||
|
||||
prepareDocTextRangeComment,
|
||||
} from '@univerjs/docs-thread-comment';
|
||||
import { SetActiveCommentOperation } from '@univerjs/thread-comment-ui';
|
||||
|
||||
export type IAddDocCommentComment = IAddDocTextRangeCommentParams;
|
||||
|
||||
/** Adds a document comment and activates it in the UI comment panel. */
|
||||
export const AddDocCommentComment: ICommand<IAddDocCommentComment> = {
|
||||
id: 'docs.command.add-comment',
|
||||
type: CommandType.COMMAND,
|
||||
async handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prepared = await prepareDocTextRangeComment(accessor, params);
|
||||
if (!prepared) {
|
||||
return false;
|
||||
}
|
||||
const activeOperation = {
|
||||
id: SetActiveCommentOperation.id,
|
||||
params: {
|
||||
unitId: prepared.comment.unitId,
|
||||
subUnitId: prepared.comment.subUnitId,
|
||||
commentId: prepared.comment.id,
|
||||
},
|
||||
};
|
||||
const decorationMutation = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
...prepared.decorationMutationParams,
|
||||
textRanges: null,
|
||||
noNeedSetTextRange: true,
|
||||
},
|
||||
};
|
||||
return (await sequenceExecute([
|
||||
prepared.commentMutation,
|
||||
decorationMutation,
|
||||
activeOperation,
|
||||
], accessor.get(ICommandService))).result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import { CommandType, ICommandService, sequenceExecute } from '@univerjs/core';
|
||||
import { deleteCustomDecorationFactory } from '@univerjs/docs-ui';
|
||||
|
||||
export interface IDeleteDocCommentComment {
|
||||
unitId: string;
|
||||
commentId: string;
|
||||
}
|
||||
|
||||
export const DeleteDocCommentComment: ICommand<IDeleteDocCommentComment> = {
|
||||
id: 'docs.command.delete-comment',
|
||||
type: CommandType.COMMAND,
|
||||
async handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const { commentId, unitId } = params;
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
const doMutation = deleteCustomDecorationFactory(accessor, {
|
||||
id: commentId,
|
||||
unitId,
|
||||
});
|
||||
|
||||
if (doMutation) {
|
||||
return (await sequenceExecute([doMutation], commandService)).result;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, IDisposable, IDocumentData } from '@univerjs/core';
|
||||
import type { IRender } from '@univerjs/engine-render';
|
||||
import type { ISidebarMethodOptions } from '@univerjs/ui';
|
||||
import {
|
||||
Disposable,
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
toDisposable,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '@univerjs/docs-thread-comment';
|
||||
import { DrawingManagerService, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { Subject } from 'rxjs';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { DOCS_THREAD_COMMENT_PANEL } from '../../../common/const';
|
||||
import { DocThreadCommentService } from '../../../services/doc-thread-comment.service';
|
||||
import {
|
||||
AddDocDrawingCommentOperation,
|
||||
ShowCommentPanelOperation,
|
||||
StartAddCommentOperation,
|
||||
ToggleCommentPanelOperation,
|
||||
} from '../show-comment-panel.operation';
|
||||
|
||||
const unitId = 'doc-thread-comment-operation-doc';
|
||||
|
||||
function createDocData(): IDocumentData {
|
||||
return {
|
||||
id: unitId,
|
||||
body: {
|
||||
dataStream: 'Hello world\r\n',
|
||||
},
|
||||
documentStyle: {
|
||||
pageSize: {
|
||||
width: 594.3,
|
||||
height: 840.51,
|
||||
},
|
||||
marginTop: 72,
|
||||
marginBottom: 72,
|
||||
marginRight: 90,
|
||||
marginLeft: 90,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class TestSidebarService extends Disposable implements ISidebarService {
|
||||
readonly sidebarOptions$ = new Subject<ISidebarMethodOptions>();
|
||||
readonly scrollEvent$ = new Subject<Event>();
|
||||
private _options: ISidebarMethodOptions = {};
|
||||
private _container?: HTMLElement;
|
||||
private _width?: number;
|
||||
|
||||
get visible(): boolean {
|
||||
return this._options.visible ?? false;
|
||||
}
|
||||
|
||||
get options(): ISidebarMethodOptions {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
get width(): number | undefined {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
open(params: ISidebarMethodOptions): IDisposable {
|
||||
this._options = { ...params, visible: true };
|
||||
this.sidebarOptions$.next(this._options);
|
||||
return toDisposable(() => this.close());
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this._options = { ...this._options, visible: false };
|
||||
this.sidebarOptions$.next(this._options);
|
||||
this._options.onClose?.();
|
||||
}
|
||||
|
||||
getContainer(): HTMLElement | undefined {
|
||||
return this._container;
|
||||
}
|
||||
|
||||
setContainer(element?: HTMLElement): void {
|
||||
this._container = element;
|
||||
}
|
||||
|
||||
setWidth(value: number): void {
|
||||
this._width = value;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.sidebarOptions$.complete();
|
||||
this.scrollEvent$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService implements Pick<IRenderManagerService, 'getRenderUnitById'> {
|
||||
getRenderUnitById(): IRender | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
describe('doc thread comment panel operations', () => {
|
||||
let univer: Univer;
|
||||
let commandService: ICommandService;
|
||||
let sidebarService: ISidebarService;
|
||||
let panelService: ThreadCommentPanelService;
|
||||
let selectionManager: DocSelectionManagerService;
|
||||
let docThreadCommentService: DocThreadCommentService;
|
||||
|
||||
beforeEach(() => {
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([ISidebarService, { useClass: TestSidebarService }]);
|
||||
injector.add([ThreadCommentPanelService]);
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([DocThreadCommentService]);
|
||||
injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
|
||||
|
||||
univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, createDocData());
|
||||
injector.get(IUniverInstanceService).focusUnit(unitId);
|
||||
|
||||
commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(ShowCommentPanelOperation);
|
||||
commandService.registerCommand(ToggleCommentPanelOperation);
|
||||
commandService.registerCommand(StartAddCommentOperation);
|
||||
|
||||
sidebarService = injector.get(ISidebarService);
|
||||
panelService = injector.get(ThreadCommentPanelService);
|
||||
selectionManager = injector.get(DocSelectionManagerService);
|
||||
docThreadCommentService = injector.get(DocThreadCommentService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('opens the docs comment panel and activates the requested comment', async () => {
|
||||
const activeComment = { unitId, subUnitId: 'doc', commentId: 'comment-1' };
|
||||
|
||||
const result = await commandService.executeCommand(ShowCommentPanelOperation.id, {
|
||||
activeComment,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(panelService.panelVisible).toBe(true);
|
||||
expect(panelService.activeCommentId).toEqual(activeComment);
|
||||
expect(sidebarService.visible).toBe(true);
|
||||
expect(sidebarService.options.children?.label).toBe(DOCS_THREAD_COMMENT_PANEL);
|
||||
});
|
||||
|
||||
it('toggles an open comment panel closed and clears the active comment', async () => {
|
||||
const activeComment = { unitId, subUnitId: 'doc', commentId: 'comment-2' };
|
||||
|
||||
await commandService.executeCommand(ShowCommentPanelOperation.id, {
|
||||
activeComment,
|
||||
});
|
||||
const result = await commandService.executeCommand(ToggleCommentPanelOperation.id);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(panelService.panelVisible).toBe(false);
|
||||
expect(panelService.activeCommentId).toBeNull();
|
||||
expect(sidebarService.visible).toBe(false);
|
||||
});
|
||||
|
||||
it('starts a new comment draft from the selected document text', async () => {
|
||||
selectionManager.__TEST_ONLY_setCurrentSelection({
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
});
|
||||
selectionManager.__TEST_ONLY_add([{
|
||||
startOffset: 0,
|
||||
endOffset: 5,
|
||||
collapsed: false,
|
||||
isActive: true,
|
||||
segmentId: '',
|
||||
style: null as never,
|
||||
}]);
|
||||
|
||||
const result = await commandService.executeCommand(StartAddCommentOperation.id);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(panelService.panelVisible).toBe(true);
|
||||
expect(docThreadCommentService.addingComment).toMatchObject({
|
||||
unitId,
|
||||
ref: 'Hello',
|
||||
startOffset: 0,
|
||||
endOffset: 5,
|
||||
collapsed: true,
|
||||
});
|
||||
expect(panelService.activeCommentId).toEqual({
|
||||
unitId,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
commentId: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not start a new comment draft without selected document text', async () => {
|
||||
const result = await commandService.executeCommand(StartAddCommentOperation.id);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(docThreadCommentService.addingComment).toBeUndefined();
|
||||
expect(panelService.panelVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a focused drawing left over from another document', () => {
|
||||
const injector = univer.__getInjector();
|
||||
const drawing = {
|
||||
unitId: 'other-doc',
|
||||
subUnitId: 'other-doc',
|
||||
drawingId: 'drawing-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_SHAPE,
|
||||
};
|
||||
const drawingManager = new DrawingManagerService();
|
||||
drawingManager.registerDrawingData(drawing.unitId, {
|
||||
[drawing.subUnitId]: { data: { [drawing.drawingId]: drawing }, order: [drawing.drawingId] },
|
||||
});
|
||||
drawingManager.focusDrawing([drawing]);
|
||||
injector.add([IDrawingManagerService, {
|
||||
useValue: drawingManager,
|
||||
}]);
|
||||
|
||||
expect(AddDocDrawingCommentOperation.handler(injector)).toBe(false);
|
||||
drawingManager.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, ICommand } from '@univerjs/core';
|
||||
import type { ActiveCommentInfo } from '@univerjs/thread-comment-ui';
|
||||
import { BuildTextUtils, CommandType, ICommandService, IUniverInstanceService, UniverInstanceType, UserManagerService } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '@univerjs/docs-thread-comment';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { getDT, ThreadCommentAnchorKind } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentDraftService, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { DOCS_THREAD_COMMENT_PANEL } from '../../common/const';
|
||||
import { DocThreadCommentService } from '../../services/doc-thread-comment.service';
|
||||
|
||||
export interface IShowCommentPanelOperationParams {
|
||||
activeComment: ActiveCommentInfo;
|
||||
}
|
||||
|
||||
export const ShowCommentPanelOperation: ICommand<IShowCommentPanelOperationParams> = {
|
||||
id: 'docs.operation.show-comment-panel',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor, params) {
|
||||
const panelService = accessor.get(ThreadCommentPanelService);
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
|
||||
if (!panelService.panelVisible || sidebarService.options.children?.label !== DOCS_THREAD_COMMENT_PANEL) {
|
||||
sidebarService.open({
|
||||
header: { title: 'docs-thread-comment-ui.panel.title' },
|
||||
children: { label: DOCS_THREAD_COMMENT_PANEL },
|
||||
width: 320,
|
||||
onClose: () => panelService.setPanelVisible(false),
|
||||
});
|
||||
panelService.setPanelVisible(true);
|
||||
}
|
||||
|
||||
if (params) {
|
||||
panelService.setActiveComment(params?.activeComment);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const ToggleCommentPanelOperation: ICommand = {
|
||||
id: 'docs.operation.toggle-comment-panel',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor) {
|
||||
const panelService = accessor.get(ThreadCommentPanelService);
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
|
||||
if (!panelService.panelVisible || sidebarService.options.children?.label !== DOCS_THREAD_COMMENT_PANEL) {
|
||||
sidebarService.open({
|
||||
header: { title: 'docs-thread-comment-ui.panel.title' },
|
||||
children: { label: DOCS_THREAD_COMMENT_PANEL },
|
||||
width: 320,
|
||||
onClose: () => panelService.setPanelVisible(false),
|
||||
});
|
||||
panelService.setPanelVisible(true);
|
||||
} else {
|
||||
sidebarService.close();
|
||||
panelService.setPanelVisible(false);
|
||||
panelService.setActiveComment(null);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const StartAddCommentOperation: ICommand = {
|
||||
id: 'docs.operation.start-add-comment',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor) {
|
||||
const panelService = accessor.get(ThreadCommentPanelService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const doc = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const userManagerService = accessor.get(UserManagerService);
|
||||
const docCommentService = accessor.get(DocThreadCommentService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
const textRange = docSelectionManagerService.getActiveTextRange();
|
||||
if (!doc || !textRange) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const docSelectionRenderManager = renderManagerService.getRenderUnitById(doc.getUnitId())?.with(DocSelectionRenderService);
|
||||
docSelectionRenderManager?.setReserveRangesStatus(true);
|
||||
if (textRange.collapsed) {
|
||||
if (panelService.panelVisible) {
|
||||
panelService.setPanelVisible(false);
|
||||
sidebarService.close();
|
||||
} else {
|
||||
commandService.executeCommand(ShowCommentPanelOperation.id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
commandService.executeCommand(ShowCommentPanelOperation.id);
|
||||
const unitId = doc.getUnitId();
|
||||
const dataStream = (doc.getBody()?.dataStream ?? '').slice(textRange.startOffset, textRange.endOffset);
|
||||
const text = BuildTextUtils.transform.getPlainText(dataStream);
|
||||
const subUnitId = DEFAULT_DOC_SUBUNIT_ID;
|
||||
const commentId = '';
|
||||
const comment = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
id: commentId,
|
||||
ref: text,
|
||||
dT: getDT(),
|
||||
personId: userManagerService.getCurrentUser().userID,
|
||||
text: {
|
||||
dataStream: '\r\n',
|
||||
},
|
||||
startOffset: textRange.startOffset!,
|
||||
endOffset: textRange.endOffset!,
|
||||
collapsed: true,
|
||||
threadId: commentId,
|
||||
};
|
||||
|
||||
docSelectionRenderManager?.blur();
|
||||
docCommentService.startAdd(comment);
|
||||
panelService.setActiveComment({
|
||||
unitId,
|
||||
subUnitId,
|
||||
commentId,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const AddDocDrawingCommentOperation: ICommand = {
|
||||
id: 'docs.operation.add-drawing-comment',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor) {
|
||||
const drawing = accessor.get(IDrawingManagerService).getFocusDrawings()[0];
|
||||
const doc = accessor.get(IUniverInstanceService)
|
||||
.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
if (!drawing || !doc || drawing.unitId !== doc.getUnitId()) {
|
||||
return false;
|
||||
}
|
||||
accessor.get(ThreadCommentDraftService).place({
|
||||
unitId: drawing.unitId,
|
||||
subUnitId: drawing.subUnitId,
|
||||
anchor: {
|
||||
kind: ThreadCommentAnchorKind.DOC_DRAWING,
|
||||
pageId: drawing.subUnitId,
|
||||
elementId: drawing.drawingId,
|
||||
},
|
||||
});
|
||||
const panelService = accessor.get(ThreadCommentPanelService);
|
||||
accessor.get(ISidebarService).open({
|
||||
header: { title: 'docs-thread-comment-ui.panel.title' },
|
||||
children: { label: DOCS_THREAD_COMMENT_PANEL },
|
||||
width: 320,
|
||||
onClose: () => panelService.setPanelVisible(false),
|
||||
});
|
||||
panelService.setPanelVisible(true);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
21
packages/docs-thread-comment-ui/src/common/const.ts
Normal file
21
packages/docs-thread-comment-ui/src/common/const.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 DOCS_THREAD_COMMENT_PANEL = 'univer.doc.thread-comment-panel';
|
||||
|
||||
export const PLUGIN_NAME = 'DOC_THREAD_COMMENT_UI_PLUGIN';
|
||||
|
||||
export const DEFAULT_TEMP_COMMENT_ID = 'default_comment';
|
||||
27
packages/docs-thread-comment-ui/src/config/config.ts
Normal file
27
packages/docs-thread-comment-ui/src/config/config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
export const DOCS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY = 'docs-thread-comment-ui.config';
|
||||
|
||||
export const configSymbol = Symbol(DOCS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverDocsThreadCommentUIConfig {
|
||||
menu?: MenuConfig;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverDocsThreadCommentUIConfig = {};
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 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 { CustomDecorationType } from '@univerjs/core';
|
||||
import { SetTextSelectionsOperation } from '@univerjs/docs';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '@univerjs/docs-thread-comment';
|
||||
import { SetActiveCommentOperation } from '@univerjs/thread-comment-ui';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ShowCommentPanelOperation } from '../../commands/operations/show-comment-panel.operation';
|
||||
import { DocThreadCommentSelectionController } from '../doc-thread-comment-selection.controller';
|
||||
|
||||
describe('DocThreadCommentSelectionController', () => {
|
||||
it('should show comment panel when selection enters a comment decoration', () => {
|
||||
const activeCommentId$ = new Subject<any>();
|
||||
const threadCommentPanelService = {
|
||||
activeCommentId: null,
|
||||
activeCommentId$,
|
||||
};
|
||||
|
||||
const doc = {
|
||||
getBody: () => ({
|
||||
customDecorations: [
|
||||
{ id: 'outer', type: CustomDecorationType.COMMENT, startIndex: 0, endIndex: 10 },
|
||||
{ id: 'older', type: CustomDecorationType.COMMENT, startIndex: 0, endIndex: 5 },
|
||||
{ id: 'not-comment', type: CustomDecorationType.DELETED, startIndex: 0, endIndex: 5 },
|
||||
{ id: 'newest', type: CustomDecorationType.COMMENT, startIndex: 0, endIndex: 5 },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const univerInstanceService = {
|
||||
getUnit: vi.fn(() => doc),
|
||||
};
|
||||
|
||||
let onExecutedHandler: any;
|
||||
const executeCommand = vi.fn();
|
||||
const commandService = {
|
||||
onCommandExecuted: vi.fn((fn) => {
|
||||
onExecutedHandler = fn;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
executeCommand,
|
||||
};
|
||||
|
||||
const docThreadCommentService = { addingComment: null, endAdd: vi.fn() };
|
||||
const renderManagerService = { getRenderUnitById: vi.fn(() => null) };
|
||||
const threadCommentModel = { getComment: vi.fn(() => ({ resolved: false })) };
|
||||
|
||||
const controller = new DocThreadCommentSelectionController(
|
||||
threadCommentPanelService as any,
|
||||
univerInstanceService as any,
|
||||
commandService as any,
|
||||
docThreadCommentService as any,
|
||||
renderManagerService as any,
|
||||
threadCommentModel as any
|
||||
);
|
||||
|
||||
onExecutedHandler({
|
||||
id: SetTextSelectionsOperation.id,
|
||||
params: {
|
||||
unitId: 'doc-1',
|
||||
ranges: [{ startOffset: 1, endOffset: 2, collapsed: true }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledWith(ShowCommentPanelOperation.id, {
|
||||
activeComment: { unitId: 'doc-1', subUnitId: DEFAULT_DOC_SUBUNIT_ID, commentId: 'newest' },
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('should reset active comment when selection moves out', () => {
|
||||
const activeCommentId$ = new Subject<any>();
|
||||
const threadCommentPanelService = {
|
||||
activeCommentId: { unitId: 'doc-1', subUnitId: DEFAULT_DOC_SUBUNIT_ID, commentId: 'c1' },
|
||||
activeCommentId$,
|
||||
};
|
||||
|
||||
const doc = {
|
||||
getBody: () => ({ customDecorations: [{ id: 'c1', type: CustomDecorationType.COMMENT, startIndex: 0, endIndex: 5 }] }),
|
||||
};
|
||||
|
||||
const univerInstanceService = { getUnit: vi.fn(() => doc) };
|
||||
|
||||
let onExecutedHandler: any;
|
||||
const executeCommand = vi.fn();
|
||||
const commandService = {
|
||||
onCommandExecuted: vi.fn((fn) => {
|
||||
onExecutedHandler = fn;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
executeCommand,
|
||||
};
|
||||
|
||||
const docThreadCommentService = { addingComment: null, endAdd: vi.fn() };
|
||||
const renderManagerService = { getRenderUnitById: vi.fn(() => null) };
|
||||
const threadCommentModel = { getComment: vi.fn(() => ({ resolved: false })) };
|
||||
|
||||
const controller = new DocThreadCommentSelectionController(
|
||||
threadCommentPanelService as any,
|
||||
univerInstanceService as any,
|
||||
commandService as any,
|
||||
docThreadCommentService as any,
|
||||
renderManagerService as any,
|
||||
threadCommentModel as any
|
||||
);
|
||||
|
||||
onExecutedHandler({
|
||||
id: SetTextSelectionsOperation.id,
|
||||
params: {
|
||||
unitId: 'doc-1',
|
||||
ranges: [{ startOffset: 10, endOffset: 10, collapsed: true }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledWith(SetActiveCommentOperation.id);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('should keep the temporary adding comment active when selection changes before the comment is saved', () => {
|
||||
const activeCommentId$ = new Subject<any>();
|
||||
const threadCommentPanelService = {
|
||||
activeCommentId: { unitId: 'doc-1', subUnitId: DEFAULT_DOC_SUBUNIT_ID, commentId: '' },
|
||||
activeCommentId$,
|
||||
};
|
||||
|
||||
const doc = {
|
||||
getBody: () => ({ customDecorations: [] }),
|
||||
};
|
||||
|
||||
const univerInstanceService = { getUnit: vi.fn(() => doc) };
|
||||
|
||||
let onExecutedHandler: any;
|
||||
const executeCommand = vi.fn();
|
||||
const commandService = {
|
||||
onCommandExecuted: vi.fn((fn) => {
|
||||
onExecutedHandler = fn;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
executeCommand,
|
||||
};
|
||||
|
||||
const docThreadCommentService = {
|
||||
addingComment: {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
id: '',
|
||||
},
|
||||
endAdd: vi.fn(),
|
||||
};
|
||||
const renderManagerService = { getRenderUnitById: vi.fn(() => null) };
|
||||
const threadCommentModel = { getComment: vi.fn(() => null) };
|
||||
|
||||
const controller = new DocThreadCommentSelectionController(
|
||||
threadCommentPanelService as any,
|
||||
univerInstanceService as any,
|
||||
commandService as any,
|
||||
docThreadCommentService as any,
|
||||
renderManagerService as any,
|
||||
threadCommentModel as any
|
||||
);
|
||||
|
||||
onExecutedHandler({
|
||||
id: SetTextSelectionsOperation.id,
|
||||
params: {
|
||||
unitId: 'doc-1',
|
||||
ranges: [{ startOffset: 10, endOffset: 10, collapsed: true }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(executeCommand).not.toHaveBeenCalledWith(SetActiveCommentOperation.id);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { CommentIcon, InsertCommentDoubleIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { DOCS_THREAD_COMMENT_PANEL } from '../common/const';
|
||||
import { DocThreadCommentPanel } from '../views/DocThreadCommentPanel';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerComponents();
|
||||
this._registerIcons();
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
([
|
||||
[DOCS_THREAD_COMMENT_PANEL, DocThreadCommentPanel],
|
||||
] as const).forEach(([id, comp]) => {
|
||||
this.disposeWithMe(
|
||||
this._componentManager.register(id, comp)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
CommentIcon,
|
||||
InsertCommentDoubleIcon,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, ICustomDecoration, ITextRange } from '@univerjs/core';
|
||||
import type { ISetTextSelectionsOperationParams } from '@univerjs/docs';
|
||||
import type { ITextRangeWithStyle } from '@univerjs/engine-render';
|
||||
import {
|
||||
CustomDecorationType,
|
||||
Disposable,
|
||||
ICommandService,
|
||||
Inject,
|
||||
isInternalEditorID,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { SetTextSelectionsOperation } from '@univerjs/docs';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '@univerjs/docs-thread-comment';
|
||||
import { DocBackScrollRenderController } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { SetActiveCommentOperation, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { ShowCommentPanelOperation } from '../commands/operations/show-comment-panel.operation';
|
||||
import { DocThreadCommentService } from '../services/doc-thread-comment.service';
|
||||
|
||||
export class DocThreadCommentSelectionController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ThreadCommentPanelService) private readonly _threadCommentPanelService: ThreadCommentPanelService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(DocThreadCommentService) private readonly _docThreadCommentService: DocThreadCommentService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initSelectionChange();
|
||||
this._initActiveCommandChange();
|
||||
}
|
||||
|
||||
private _initSelectionChange() {
|
||||
let lastSelection: ITextRange | undefined;
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === SetTextSelectionsOperation.id) {
|
||||
const params = commandInfo.params as ISetTextSelectionsOperationParams;
|
||||
const { unitId, ranges } = params;
|
||||
if (isInternalEditorID(unitId)) return;
|
||||
const doc = this._univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
const primary = ranges[0] as ITextRangeWithStyle | undefined;
|
||||
if (lastSelection?.startOffset === primary?.startOffset && lastSelection?.endOffset === primary?.endOffset) {
|
||||
return;
|
||||
}
|
||||
lastSelection = primary;
|
||||
if (primary && doc) {
|
||||
const { startOffset, endOffset, collapsed } = primary;
|
||||
const selectionEnd = collapsed ? startOffset : endOffset - 1;
|
||||
const customRange = this._findActiveCommentDecoration(doc, unitId, startOffset, selectionEnd);
|
||||
|
||||
if (customRange) {
|
||||
this._commandService.executeCommand(ShowCommentPanelOperation.id, {
|
||||
activeComment: {
|
||||
unitId,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
commentId: customRange.id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._threadCommentPanelService.activeCommentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addingComment = this._docThreadCommentService.addingComment;
|
||||
const activeComment = this._threadCommentPanelService.activeCommentId;
|
||||
if (
|
||||
addingComment &&
|
||||
activeComment?.unitId === addingComment.unitId &&
|
||||
activeComment?.subUnitId === DEFAULT_DOC_SUBUNIT_ID &&
|
||||
activeComment?.commentId === addingComment.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._commandService.executeCommand(SetActiveCommentOperation.id);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _findActiveCommentDecoration(
|
||||
doc: DocumentDataModel,
|
||||
unitId: string,
|
||||
selectionStart: number,
|
||||
selectionEnd: number
|
||||
): ICustomDecoration | undefined {
|
||||
return [...(doc.getBody()?.customDecorations ?? [])]
|
||||
.reverse()
|
||||
.filter((decoration) => decoration.type === CustomDecorationType.COMMENT
|
||||
&& decoration.startIndex <= selectionStart
|
||||
&& decoration.endIndex >= selectionEnd)
|
||||
.sort((left, right) => (left.endIndex - left.startIndex) - (right.endIndex - right.startIndex))
|
||||
.find((decoration) => {
|
||||
const comment = this._threadCommentModel.getComment(unitId, DEFAULT_DOC_SUBUNIT_ID, decoration.id);
|
||||
return comment && !comment.resolved;
|
||||
});
|
||||
}
|
||||
|
||||
private _initActiveCommandChange() {
|
||||
this.disposeWithMe(this._threadCommentPanelService.activeCommentId$.subscribe((activeComment) => {
|
||||
if (activeComment) {
|
||||
const doc = this._univerInstanceService.getUnit<DocumentDataModel>(activeComment.unitId);
|
||||
if (doc) {
|
||||
const backScrollController = this._renderManagerService.getRenderUnitById(activeComment.unitId)?.with(DocBackScrollRenderController);
|
||||
const customRange = doc.getBody()?.customDecorations?.find((range) => range.id === activeComment.commentId);
|
||||
if (customRange && backScrollController) {
|
||||
backScrollController.scrollToRange({
|
||||
startOffset: customRange.startIndex,
|
||||
endOffset: customRange.endIndex,
|
||||
collapsed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeComment || activeComment.commentId !== this._docThreadCommentService.addingComment?.id) {
|
||||
this._docThreadCommentService.endAdd();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* 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 { CustomDecorationType } from '@univerjs/core';
|
||||
import { DOC_INTERCEPTOR_POINT, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '@univerjs/docs-thread-comment';
|
||||
import { getDrawingShapeKeyByDrawingSearch } from '@univerjs/drawing';
|
||||
import { Vector2 } from '@univerjs/engine-render';
|
||||
import { serializeThreadCommentAnchor, ThreadCommentAnchorKind } from '@univerjs/thread-comment';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DocThreadCommentRenderController } from '../render.controller';
|
||||
|
||||
describe('DocThreadCommentRenderController', () => {
|
||||
it('should intercept decoration view-model based on active comment and resolved status', () => {
|
||||
let handler: any;
|
||||
const docInterceptorService = {
|
||||
intercept: vi.fn((point, config) => {
|
||||
if (point === DOC_INTERCEPTOR_POINT.CUSTOM_DECORATION) {
|
||||
handler = config.handler;
|
||||
}
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
};
|
||||
|
||||
const makeDirty = vi.fn();
|
||||
|
||||
const activeCommentId$ = new BehaviorSubject<any>(undefined);
|
||||
const hoveredCommentId$ = new BehaviorSubject<any>(undefined);
|
||||
const threadCommentPanelService = {
|
||||
activeCommentId: { unitId: 'doc-1', subUnitId: DEFAULT_DOC_SUBUNIT_ID, commentId: 'c2' },
|
||||
activeCommentId$,
|
||||
hoveredCommentId: undefined as any,
|
||||
hoveredCommentId$,
|
||||
};
|
||||
|
||||
const univerInstanceService = {
|
||||
getCurrentUnitOfType: vi.fn(() => ({ getUnitId: () => 'doc-1' })),
|
||||
};
|
||||
|
||||
const commentUpdate$ = new Subject<any>();
|
||||
const threadCommentModel = {
|
||||
commentUpdate$,
|
||||
getComment: vi.fn((_unitId: string, _subUnitId: string, id: string) => (id === 'c1' ? null : { id, ref: 'text', resolved: false })),
|
||||
query: vi.fn(() => []),
|
||||
addComment: vi.fn(),
|
||||
syncThreadComments: vi.fn(),
|
||||
};
|
||||
|
||||
let onCommandExecuted: any;
|
||||
const commandService = {
|
||||
onCommandExecuted: vi.fn((fn) => {
|
||||
onCommandExecuted = fn;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
};
|
||||
|
||||
const unit = {
|
||||
getUnitId: () => 'doc-1',
|
||||
getBody: () => ({ customDecorations: [{ id: 'c1', type: CustomDecorationType.COMMENT }, { id: 'c2', type: CustomDecorationType.COMMENT }] }),
|
||||
};
|
||||
const context = {
|
||||
unit,
|
||||
unitId: 'doc-1',
|
||||
mainComponent: { makeDirty },
|
||||
scene: {
|
||||
addObject: vi.fn(),
|
||||
getObject: vi.fn(),
|
||||
getObjectIncludeInGroup: vi.fn(),
|
||||
},
|
||||
engine: {
|
||||
onTransformChange$: {
|
||||
subscribeEvent: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
},
|
||||
},
|
||||
};
|
||||
const drawingManagerService = {
|
||||
add$: new Subject<never[]>(),
|
||||
update$: new Subject<never[]>(),
|
||||
remove$: new Subject<never[]>(),
|
||||
};
|
||||
const themeService = {
|
||||
currentTheme$: new Subject<void>(),
|
||||
getColorFromTheme: vi.fn((token: string) => token),
|
||||
};
|
||||
|
||||
const controller = new DocThreadCommentRenderController(
|
||||
context as any,
|
||||
docInterceptorService as any,
|
||||
threadCommentPanelService as any,
|
||||
univerInstanceService as any,
|
||||
threadCommentModel as any,
|
||||
commandService as any,
|
||||
drawingManagerService as any,
|
||||
themeService as any
|
||||
);
|
||||
|
||||
expect(makeDirty).not.toHaveBeenCalled();
|
||||
expect(threadCommentModel.addComment).not.toHaveBeenCalled();
|
||||
|
||||
const next = (v: any) => v;
|
||||
const outActive = handler(
|
||||
{ id: 'c2' },
|
||||
{
|
||||
unitId: 'doc-1',
|
||||
index: 3,
|
||||
customDecorations: [{ id: 'c2', startIndex: 0, endIndex: 5 }],
|
||||
},
|
||||
next
|
||||
);
|
||||
expect(outActive.active).toBe(true);
|
||||
expect(outActive.show).toBe(true);
|
||||
|
||||
const outResolved = handler(
|
||||
{ id: 'c1' },
|
||||
{
|
||||
unitId: 'doc-1',
|
||||
index: 3,
|
||||
customDecorations: [{ id: 'c2', startIndex: 0, endIndex: 5 }],
|
||||
},
|
||||
next
|
||||
);
|
||||
expect(outResolved.show).toBe(false);
|
||||
|
||||
threadCommentPanelService.hoveredCommentId = {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
commentId: 'c3',
|
||||
};
|
||||
const outOverlapping = handler(
|
||||
{ id: 'c2' },
|
||||
{
|
||||
unitId: 'doc-1',
|
||||
index: 3,
|
||||
customDecorations: [
|
||||
{ id: 'c2', startIndex: 0, endIndex: 5 },
|
||||
{ id: 'c3', startIndex: 2, endIndex: 4 },
|
||||
],
|
||||
},
|
||||
next
|
||||
);
|
||||
expect(outOverlapping.active).toBe(true);
|
||||
const outHovered = handler(
|
||||
{ id: 'c3' },
|
||||
{
|
||||
unitId: 'doc-1',
|
||||
index: 3,
|
||||
customDecorations: [
|
||||
{ id: 'c2', startIndex: 0, endIndex: 5 },
|
||||
{ id: 'c3', startIndex: 2, endIndex: 4 },
|
||||
],
|
||||
},
|
||||
next
|
||||
);
|
||||
expect(outHovered.active).toBe(true);
|
||||
|
||||
threadCommentPanelService.hoveredCommentId = {
|
||||
unitId: 'other-doc',
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
commentId: 'c3',
|
||||
};
|
||||
const outActiveWithForeignHover = handler({ id: 'c2' }, { unitId: 'doc-1' }, next);
|
||||
expect(outActiveWithForeignHover.active).toBe(true);
|
||||
hoveredCommentId$.next(threadCommentPanelService.hoveredCommentId);
|
||||
expect(makeDirty).toHaveBeenCalledWith(true);
|
||||
|
||||
// resolved branch triggers rerender
|
||||
threadCommentModel.getComment.mockImplementation((_unitId: string, _subUnitId: string, id: string) => {
|
||||
if (id === 'c1') return null;
|
||||
return { id, ref: 'text', resolved: id === 'c2' };
|
||||
});
|
||||
const outResolvedComment = handler(
|
||||
{ id: 'c2' },
|
||||
{
|
||||
unitId: 'doc-1',
|
||||
index: 3,
|
||||
customDecorations: [{ id: 'c2', startIndex: 0, endIndex: 5 }],
|
||||
},
|
||||
next
|
||||
);
|
||||
expect(outResolvedComment.show).toBe(false);
|
||||
|
||||
// rerender on active comment changes
|
||||
activeCommentId$.next({ unitId: 'doc-1', subUnitId: DEFAULT_DOC_SUBUNIT_ID, commentId: 'c1' });
|
||||
expect(makeDirty).toHaveBeenCalledWith(true);
|
||||
|
||||
// rerender on resolve updates
|
||||
commentUpdate$.next({ type: 'resolve', unitId: 'doc-1' });
|
||||
expect(makeDirty).toHaveBeenCalledWith(true);
|
||||
|
||||
// trigger sync on rich text mutations (add new decoration)
|
||||
// NOTE: `_initSyncComments` compares sorted thread ids. We emulate that the doc body returns a stable
|
||||
// list that changes when mutation happens.
|
||||
unit.getBody = () => ({
|
||||
customDecorations: [
|
||||
{ id: 'c1', type: CustomDecorationType.COMMENT },
|
||||
{ id: 'c2', type: CustomDecorationType.COMMENT },
|
||||
{ id: 'c3', type: CustomDecorationType.COMMENT },
|
||||
],
|
||||
});
|
||||
|
||||
onCommandExecuted({ id: RichTextEditingMutation.id, params: { unitId: 'doc-1' } });
|
||||
expect(threadCommentModel.addComment).not.toHaveBeenCalled();
|
||||
expect(threadCommentModel.syncThreadComments).toHaveBeenCalledWith('doc-1', DEFAULT_DOC_SUBUNIT_ID, ['c3']);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('keeps drawing comment underlines attached to the rendered object', () => {
|
||||
const ref = serializeThreadCommentAnchor({
|
||||
kind: ThreadCommentAnchorKind.DOC_DRAWING,
|
||||
pageId: 'doc-1',
|
||||
elementId: 'shape-1',
|
||||
});
|
||||
const drawingKey = getDrawingShapeKeyByDrawingSearch({
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'shape-1',
|
||||
});
|
||||
let bounds = { left: 20, top: 40, width: 80, height: 60 };
|
||||
let roots = ['first-comment', 'newest-comment'];
|
||||
const scene = {
|
||||
addObject: vi.fn(),
|
||||
getObjectIncludeInGroup: vi.fn((key: string) => key === drawingKey ? { getRealBound: () => bounds } : null),
|
||||
getObject: vi.fn(),
|
||||
};
|
||||
const context = {
|
||||
unitId: 'doc-1',
|
||||
unit: { getUnitId: () => 'doc-1', getBody: () => ({ customDecorations: [] }) },
|
||||
mainComponent: { makeDirty: vi.fn() },
|
||||
scene,
|
||||
engine: { onTransformChange$: { subscribeEvent: vi.fn(() => ({ dispose: vi.fn() })) } },
|
||||
};
|
||||
const activeCommentId$ = new BehaviorSubject({
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
commentId: 'first-comment',
|
||||
});
|
||||
const hoveredCommentId$ = new BehaviorSubject<undefined>(undefined);
|
||||
const panelService = {
|
||||
activeCommentId: activeCommentId$.value,
|
||||
hoveredCommentId: hoveredCommentId$.value,
|
||||
activeCommentId$,
|
||||
hoveredCommentId$,
|
||||
};
|
||||
const commentUpdate$ = new Subject<{ type: string; unitId: string }>();
|
||||
const commentModel = {
|
||||
commentUpdate$,
|
||||
query: vi.fn(() => roots.map((id) => ({ root: { id, ref }, subUnitId: 'doc-1' }))),
|
||||
getComment: vi.fn((_unitId: string, _subUnitId: string, id: string) => ({ id, ref, resolved: false })),
|
||||
syncThreadComments: vi.fn(),
|
||||
};
|
||||
const drawingManager = { add$: new Subject(), update$: new Subject(), remove$: new Subject() };
|
||||
const controller = new DocThreadCommentRenderController(
|
||||
context as never,
|
||||
{ intercept: vi.fn(() => ({ dispose: vi.fn() })) } as never,
|
||||
panelService as never,
|
||||
{ getCurrentUnitOfType: vi.fn(() => context.unit) } as never,
|
||||
commentModel as never,
|
||||
{ executeCommand: vi.fn(() => Promise.resolve(true)), onCommandExecuted: vi.fn(() => ({ dispose: vi.fn() })) } as never,
|
||||
drawingManager as never,
|
||||
{ currentTheme$: new Subject(), getColorFromTheme: vi.fn((token: string) => token) } as never
|
||||
);
|
||||
const overlay = scene.addObject.mock.calls[0][0];
|
||||
|
||||
expect(overlay.isHit(new Vector2(60, 102))).toBe(true);
|
||||
expect(overlay.hitCommentId).toBe('newest-comment');
|
||||
|
||||
roots = ['first-comment'];
|
||||
commentUpdate$.next({ type: 'delete', unitId: 'doc-1' });
|
||||
expect(overlay.isHit(new Vector2(60, 102))).toBe(true);
|
||||
expect(overlay.hitCommentId).toBe('first-comment');
|
||||
|
||||
bounds = { left: 120, top: 140, width: 100, height: 70 };
|
||||
drawingManager.update$.next([{ unitId: 'doc-1' }]);
|
||||
expect(overlay.isHit(new Vector2(60, 102))).toBe(false);
|
||||
expect(overlay.isHit(new Vector2(170, 212))).toBe(true);
|
||||
|
||||
roots = [];
|
||||
commentUpdate$.next({ type: 'resolve', unitId: 'doc-1' });
|
||||
expect(overlay.isHit(new Vector2(170, 212))).toBe(false);
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, EventState } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { IMouseEvent, IPointerEvent, IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { IThreadCommentCanvasOutline, IThreadCommentCanvasUnderline } from '@univerjs/thread-comment-ui';
|
||||
import {
|
||||
CustomDecorationType,
|
||||
Disposable,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
ThemeService,
|
||||
toDisposable,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DOC_INTERCEPTOR_POINT, DocInterceptorService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '@univerjs/docs-thread-comment';
|
||||
import { getDrawingShapeKeyByDrawingSearch, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { deserializeThreadCommentAnchor, ThreadCommentAnchorKind, ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentCanvasOverlay, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { pairwise, skip, startWith } from 'rxjs';
|
||||
import { ShowCommentPanelOperation } from '../../commands/operations/show-comment-panel.operation';
|
||||
|
||||
const DOC_COMMENT_DRAWING_OVERLAY_KEY = 'doc-thread-comment-drawing-overlay';
|
||||
const DOC_COMMENT_DRAWING_OVERLAY_LAYER_INDEX = 10_100;
|
||||
|
||||
export class DocThreadCommentRenderController extends Disposable implements IRenderModule {
|
||||
private readonly _drawingOverlay: ThreadCommentCanvasOverlay;
|
||||
private readonly _drawingCommentSubUnits = new Map<string, string>();
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<DocumentDataModel>,
|
||||
@Inject(DocInterceptorService) private readonly _docInterceptorService: DocInterceptorService,
|
||||
@Inject(ThreadCommentPanelService) private readonly _threadCommentPanelService: ThreadCommentPanelService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(ThemeService) private readonly _themeService: ThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._drawingOverlay = new ThreadCommentCanvasOverlay(DOC_COMMENT_DRAWING_OVERLAY_KEY, {
|
||||
...this._getDrawingOverlayColors(),
|
||||
zoomRatio: 1,
|
||||
markers: [],
|
||||
underlines: [],
|
||||
});
|
||||
this._context.scene.addObject(this._drawingOverlay, DOC_COMMENT_DRAWING_OVERLAY_LAYER_INDEX);
|
||||
|
||||
this._interceptorViewModel();
|
||||
this._initReRender();
|
||||
this._initSyncComments();
|
||||
this._initDrawingOverlay();
|
||||
}
|
||||
|
||||
private _initReRender() {
|
||||
[
|
||||
this._threadCommentPanelService.activeCommentId$,
|
||||
this._threadCommentPanelService.hoveredCommentId$,
|
||||
].forEach((observable) => this.disposeWithMe(observable.pipe(
|
||||
skip(1),
|
||||
startWith(undefined),
|
||||
pairwise()
|
||||
).subscribe(([previous, current]) => {
|
||||
const currentUnitId = this._univerInstanceService
|
||||
.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC)
|
||||
?.getUnitId();
|
||||
if (new Set([previous?.unitId, current?.unitId, currentUnitId]).has(this._context.unitId)) {
|
||||
this._context.mainComponent?.makeDirty(true);
|
||||
}
|
||||
})));
|
||||
|
||||
this.disposeWithMe(this._threadCommentModel.commentUpdate$.subscribe((update) => {
|
||||
if (update.type === 'resolve' && update.unitId === this._context.unitId) {
|
||||
this._context.mainComponent?.makeDirty(true);
|
||||
}
|
||||
if (update.unitId === this._context.unitId) {
|
||||
this._syncDrawingOverlay();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _initDrawingOverlay(): void {
|
||||
this.disposeWithMe(toDisposable(this._drawingOverlay.onPointerDown$.subscribeEvent(
|
||||
(_event: IPointerEvent | IMouseEvent, state: EventState) => {
|
||||
const commentId = this._drawingOverlay.hitCommentId;
|
||||
const subUnitId = commentId && this._drawingCommentSubUnits.get(commentId);
|
||||
if (!commentId || !subUnitId) {
|
||||
return;
|
||||
}
|
||||
state.stopPropagation();
|
||||
this._commandService.executeCommand(ShowCommentPanelOperation.id, {
|
||||
activeComment: { unitId: this._context.unitId, subUnitId, commentId, trigger: 'doc-canvas' },
|
||||
});
|
||||
}
|
||||
)));
|
||||
this.disposeWithMe(toDisposable(
|
||||
this._context.engine.onTransformChange$.subscribeEvent(() => this._syncDrawingOverlay())
|
||||
));
|
||||
[this._threadCommentPanelService.activeCommentId$, this._threadCommentPanelService.hoveredCommentId$]
|
||||
.forEach((observable) => this.disposeWithMe(observable.subscribe(() => this._syncDrawingOverlay())));
|
||||
[this._drawingManagerService.add$, this._drawingManagerService.update$, this._drawingManagerService.remove$]
|
||||
.forEach((observable) => this.disposeWithMe(observable.subscribe((drawings) => {
|
||||
if (drawings.some((drawing) => drawing.unitId === this._context.unitId)) {
|
||||
this._syncDrawingOverlay();
|
||||
}
|
||||
})));
|
||||
this.disposeWithMe(this._themeService.currentTheme$.subscribe(() => this._syncDrawingOverlay()));
|
||||
this._syncDrawingOverlay();
|
||||
}
|
||||
|
||||
private _syncDrawingOverlay(): void {
|
||||
const underlines = new Map<string, IThreadCommentCanvasUnderline>();
|
||||
this._drawingCommentSubUnits.clear();
|
||||
this._threadCommentModel.query({
|
||||
unitIds: [this._context.unitId],
|
||||
anchorKinds: [ThreadCommentAnchorKind.DOC_DRAWING],
|
||||
resolved: false,
|
||||
}).forEach(({ root, subUnitId }) => {
|
||||
const anchor = deserializeThreadCommentAnchor(root.ref);
|
||||
if (anchor?.kind !== ThreadCommentAnchorKind.DOC_DRAWING) {
|
||||
return;
|
||||
}
|
||||
const outline = this._getDrawingOutline(anchor.pageId ?? subUnitId, anchor.elementId);
|
||||
if (!outline) {
|
||||
return;
|
||||
}
|
||||
this._drawingCommentSubUnits.set(root.id, subUnitId);
|
||||
underlines.set(`${anchor.pageId ?? subUnitId}\0${anchor.elementId}`, {
|
||||
commentId: root.id,
|
||||
left: outline.left,
|
||||
top: outline.top + outline.height + 2,
|
||||
width: outline.width,
|
||||
});
|
||||
});
|
||||
const focusedCommentIds: string[] = [];
|
||||
const focusOutlines = new Map<string, IThreadCommentCanvasOutline>();
|
||||
[this._threadCommentPanelService.activeCommentId, this._threadCommentPanelService.hoveredCommentId]
|
||||
.forEach((target) => {
|
||||
if (!target || target.unitId !== this._context.unitId) {
|
||||
return;
|
||||
}
|
||||
const comment = this._threadCommentModel.getComment(target.unitId, target.subUnitId, target.commentId);
|
||||
const anchor = comment && deserializeThreadCommentAnchor(comment.ref);
|
||||
if (anchor?.kind !== ThreadCommentAnchorKind.DOC_DRAWING) {
|
||||
return;
|
||||
}
|
||||
focusedCommentIds.push(target.commentId);
|
||||
const outline = this._getDrawingOutline(anchor.pageId ?? target.subUnitId, anchor.elementId);
|
||||
if (outline) {
|
||||
focusOutlines.set(`${anchor.pageId ?? target.subUnitId}\0${anchor.elementId}`, outline);
|
||||
}
|
||||
});
|
||||
this._drawingOverlay.updateState({
|
||||
...this._getDrawingOverlayColors(),
|
||||
markers: [],
|
||||
underlines: Array.from(underlines.values()),
|
||||
focusedCommentIds,
|
||||
focusOutlines: Array.from(focusOutlines.values()),
|
||||
});
|
||||
}
|
||||
|
||||
private _getDrawingOutline(subUnitId: string, drawingId: string): IThreadCommentCanvasOutline | null {
|
||||
const objectKey = getDrawingShapeKeyByDrawingSearch({
|
||||
unitId: this._context.unitId,
|
||||
subUnitId,
|
||||
drawingId,
|
||||
});
|
||||
const object = this._context.scene.getObjectIncludeInGroup?.(objectKey)
|
||||
?? this._context.scene.getObject(objectKey);
|
||||
if (!object) {
|
||||
return null;
|
||||
}
|
||||
const bounds = object.getRealBound();
|
||||
return { left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height };
|
||||
}
|
||||
|
||||
private _getDrawingOverlayColors(): { accentColor: string; foregroundColor: string; outlineColor: string } {
|
||||
return {
|
||||
accentColor: this._themeService.getColorFromTheme('yellow.400'),
|
||||
foregroundColor: this._themeService.getColorFromTheme('gray.900'),
|
||||
outlineColor: this._themeService.getColorFromTheme('white'),
|
||||
};
|
||||
}
|
||||
|
||||
private _interceptorViewModel() {
|
||||
this._docInterceptorService.intercept(DOC_INTERCEPTOR_POINT.CUSTOM_DECORATION, {
|
||||
handler: (data, pos, next) => {
|
||||
if (!data) {
|
||||
return next(data);
|
||||
}
|
||||
const { unitId } = pos;
|
||||
const focusedComments = [
|
||||
this._threadCommentPanelService.activeCommentId,
|
||||
this._threadCommentPanelService.hoveredCommentId,
|
||||
];
|
||||
const comment = this._threadCommentModel.getComment(unitId, DEFAULT_DOC_SUBUNIT_ID, data.id);
|
||||
if (!comment) {
|
||||
return next({
|
||||
...data,
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
const isActive = focusedComments.some((focusedComment) => (
|
||||
focusedComment?.unitId === unitId
|
||||
&& focusedComment.subUnitId === DEFAULT_DOC_SUBUNIT_ID
|
||||
&& focusedComment.commentId === data.id
|
||||
));
|
||||
return next({
|
||||
...data,
|
||||
active: isActive,
|
||||
show: !comment.resolved,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _initSyncComments() {
|
||||
const unitId = this._context.unit.getUnitId();
|
||||
const subUnitId = DEFAULT_DOC_SUBUNIT_ID;
|
||||
const threadIds = this._context.unit.getBody()?.customDecorations?.filter((i) => i.type === CustomDecorationType.COMMENT).map((i) => i.id) ?? [];
|
||||
threadIds.length && this._threadCommentModel.syncThreadComments(this._context.unit.getUnitId(), DEFAULT_DOC_SUBUNIT_ID, threadIds);
|
||||
|
||||
let prevThreadIds: string[] = threadIds.sort();
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === RichTextEditingMutation.id) {
|
||||
const params = commandInfo.params as IRichTextEditingMutationParams;
|
||||
if (params.unitId !== this._context.unit.getUnitId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentThreadIds = this._context.unit.getBody()?.customDecorations?.filter((i) => i.type === CustomDecorationType.COMMENT).map((i) => i.id) ?? [];
|
||||
const currentThreadIdsSorted = currentThreadIds.sort();
|
||||
if (JSON.stringify(prevThreadIds) !== JSON.stringify(currentThreadIdsSorted)) {
|
||||
const preIds = new Set(prevThreadIds);
|
||||
const addIds = new Set<string>();
|
||||
|
||||
currentThreadIds.forEach((id) => {
|
||||
if (!preIds.has(id)) {
|
||||
addIds.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
prevThreadIds = currentThreadIdsSorted;
|
||||
this._threadCommentModel.syncThreadComments(unitId, subUnitId, [...addIds]);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, ICommandService } from '@univerjs/core';
|
||||
import { FLOAT_TOOLBAR_MENU_POSITION } from '@univerjs/docs-ui';
|
||||
import { IMenuManagerService } from '@univerjs/ui';
|
||||
import { AddDocCommentComment } from '../commands/commands/add-doc-comment.command';
|
||||
import { DeleteDocCommentComment } from '../commands/commands/delete-doc-comment.command';
|
||||
import {
|
||||
AddDocDrawingCommentOperation,
|
||||
ShowCommentPanelOperation,
|
||||
StartAddCommentOperation,
|
||||
ToggleCommentPanelOperation,
|
||||
} from '../commands/operations/show-comment-panel.operation';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
|
||||
export class DocThreadCommentUIController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService
|
||||
) {
|
||||
super();
|
||||
this._initCommands();
|
||||
this._initMenus();
|
||||
}
|
||||
|
||||
private _initCommands() {
|
||||
[
|
||||
AddDocDrawingCommentOperation,
|
||||
AddDocCommentComment,
|
||||
DeleteDocCommentComment,
|
||||
ShowCommentPanelOperation,
|
||||
StartAddCommentOperation,
|
||||
ToggleCommentPanelOperation,
|
||||
].forEach((command) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(command));
|
||||
});
|
||||
}
|
||||
|
||||
private _initMenus() {
|
||||
this._menuManagerService.appendRootMenu({ [FLOAT_TOOLBAR_MENU_POSITION]: {} });
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
}
|
||||
2
packages/docs-thread-comment-ui/src/global.css
Normal file
2
packages/docs-thread-comment-ui/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
28
packages/docs-thread-comment-ui/src/index.ts
Normal file
28
packages/docs-thread-comment-ui/src/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 './global.css';
|
||||
|
||||
export { AddDocCommentComment } from './commands/commands/add-doc-comment.command';
|
||||
export type { IAddDocCommentComment } from './commands/commands/add-doc-comment.command';
|
||||
export { DeleteDocCommentComment } from './commands/commands/delete-doc-comment.command';
|
||||
export type { IDeleteDocCommentComment } from './commands/commands/delete-doc-comment.command';
|
||||
export { ShowCommentPanelOperation, StartAddCommentOperation } from './commands/operations/show-comment-panel.operation';
|
||||
export type { IShowCommentPanelOperationParams } from './commands/operations/show-comment-panel.operation';
|
||||
export type { IUniverDocsThreadCommentUIConfig } from './config/config';
|
||||
export { menuSchema as DocsThreadCommentUIMenuSchema } from './menu/schema';
|
||||
export { UniverDocsThreadCommentMobileUIPlugin } from './mobile-plugin';
|
||||
export { UniverDocsThreadCommentUIPlugin } from './plugin';
|
||||
29
packages/docs-thread-comment-ui/src/locale/ar-SA.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/ar-SA.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'إدارة التعليقات',
|
||||
addComment: 'إضافة تعليق',
|
||||
openComments: 'فتح التعليقات',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/ca-ES.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/ca-ES.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Gestió de comentaris',
|
||||
addComment: 'Afegeix un comentari',
|
||||
openComments: 'Obre els comentaris',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/de-DE.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/de-DE.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Kommentarverwaltung',
|
||||
addComment: 'Kommentar hinzufügen',
|
||||
openComments: 'Kommentare öffnen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
27
packages/docs-thread-comment-ui/src/locale/en-US.ts
Normal file
27
packages/docs-thread-comment-ui/src/locale/en-US.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Comment Management',
|
||||
addComment: 'Add Comment',
|
||||
openComments: 'Open Comments',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/es-ES.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/es-ES.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Gestión de comentarios',
|
||||
addComment: 'Añadir comentario',
|
||||
openComments: 'Abrir comentarios',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/fa-IR.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/fa-IR.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'مدیریت نظرات',
|
||||
addComment: 'افزودن نظر',
|
||||
openComments: 'باز کردن نظرات',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/fr-FR.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/fr-FR.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Gestion des commentaires',
|
||||
addComment: 'Ajouter un commentaire',
|
||||
openComments: 'Ouvrir les commentaires',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/id-ID.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/id-ID.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Manajemen Komentar',
|
||||
addComment: 'Tambah Komentar',
|
||||
openComments: 'Buka Komentar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/it-IT.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/it-IT.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Gestione Commenti',
|
||||
addComment: 'Aggiungi Commento',
|
||||
openComments: 'Apri commenti',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/ja-JP.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/ja-JP.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'コメント管理',
|
||||
addComment: 'コメントを追加',
|
||||
openComments: 'コメントを開く',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/ko-KR.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/ko-KR.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: '댓글 관리',
|
||||
addComment: '댓글 추가',
|
||||
openComments: '댓글 열기',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/pl-PL.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/pl-PL.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Zarządzanie komentarzami',
|
||||
addComment: 'Dodaj komentarz',
|
||||
openComments: 'Otwórz komentarze',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/pt-BR.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/pt-BR.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Gerenciamento de Comentários',
|
||||
addComment: 'Adicionar Comentário',
|
||||
openComments: 'Abrir comentários',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/ru-RU.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/ru-RU.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Управление комментариями',
|
||||
addComment: 'Добавить комментарий',
|
||||
openComments: 'Открыть комментарии',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/sk-SK.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/sk-SK.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Správa komentárov',
|
||||
addComment: 'Pridať komentár',
|
||||
openComments: 'Otvoriť komentáre',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/docs-thread-comment-ui/src/locale/types.ts
Normal file
20
packages/docs-thread-comment-ui/src/locale/types.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 type { LocaleLeafKeys } from '@univerjs/core';
|
||||
import type enUS from './en-US';
|
||||
|
||||
export type LocaleKey = LocaleLeafKeys<typeof enUS>;
|
||||
29
packages/docs-thread-comment-ui/src/locale/vi-VN.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/vi-VN.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: 'Quản lý Bình luận',
|
||||
addComment: 'Thêm bình luận',
|
||||
openComments: 'Mở bình luận',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/zh-CN.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/zh-CN.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: '评论管理',
|
||||
addComment: '添加评论',
|
||||
openComments: '打开评论',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/zh-HK.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/zh-HK.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: '評論管理',
|
||||
addComment: '新增評論',
|
||||
openComments: '開啟評論',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
29
packages/docs-thread-comment-ui/src/locale/zh-TW.ts
Normal file
29
packages/docs-thread-comment-ui/src/locale/zh-TW.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 type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-thread-comment-ui': {
|
||||
panel: {
|
||||
title: '評論管理',
|
||||
addComment: '新增評論',
|
||||
openComments: '開啟評論',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
121
packages/docs-thread-comment-ui/src/menu/menu.ts
Normal file
121
packages/docs-thread-comment-ui/src/menu/menu.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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 { IPermissionService, IUniverInstanceService, SHEET_EDITOR_UNITS, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, DocSkeletonManagerService, getDocumentPermissionValue } from '@univerjs/docs';
|
||||
import { DocumentEditArea, IRenderManagerService, withCurrentTypeOfRenderer } from '@univerjs/engine-render';
|
||||
import { UnitAction } from '@univerjs/protocol';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { combineLatest, debounceTime, map, startWith } from 'rxjs';
|
||||
import {
|
||||
AddDocDrawingCommentOperation,
|
||||
StartAddCommentOperation,
|
||||
ToggleCommentPanelOperation,
|
||||
} from '../commands/operations/show-comment-panel.operation';
|
||||
|
||||
type MenuAccessor = Parameters<typeof getMenuHiddenObservable>[0];
|
||||
|
||||
function getCommentPermissionDisabled$(accessor: MenuAccessor) {
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const permissionService = accessor.get(IPermissionService);
|
||||
return combineLatest([
|
||||
instanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_DOC),
|
||||
permissionService.permissionPointUpdate$.pipe(startWith(undefined)),
|
||||
]).pipe(map(([document]) => !document || !getDocumentPermissionValue(
|
||||
permissionService,
|
||||
document.getUnitId(),
|
||||
document.getUnitId(),
|
||||
UnitAction.Comment
|
||||
)));
|
||||
}
|
||||
|
||||
export function AddDocDrawingCommentMenuItemFactory(accessor: MenuAccessor) {
|
||||
return {
|
||||
id: AddDocDrawingCommentOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'InsertCommentDoubleIcon',
|
||||
title: 'docs-thread-comment-ui.panel.addComment',
|
||||
tooltip: 'docs-thread-comment-ui.panel.addComment',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
disabled$: getCommentPermissionDisabled$(accessor),
|
||||
};
|
||||
}
|
||||
|
||||
export const shouldDisableAddComment = (accessor: MenuAccessor) => {
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const permissionService = accessor.get(IPermissionService);
|
||||
const document = instanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC);
|
||||
if (!document || !getDocumentPermissionValue(
|
||||
permissionService,
|
||||
document.getUnitId(),
|
||||
document.getUnitId(),
|
||||
UnitAction.Comment
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
const skeleton = withCurrentTypeOfRenderer(
|
||||
UniverInstanceType.UNIVER_DOC,
|
||||
DocSkeletonManagerService,
|
||||
instanceService,
|
||||
renderManagerService
|
||||
)?.getSkeleton();
|
||||
|
||||
const editArea = skeleton?.getViewModel().getEditArea();
|
||||
if (editArea === DocumentEditArea.FOOTER || editArea === DocumentEditArea.HEADER) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const range = docSelectionManagerService.getActiveTextRange();
|
||||
|
||||
if (range == null || range.collapsed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export function AddDocCommentMenuItemFactory(accessor: MenuAccessor) {
|
||||
return {
|
||||
id: StartAddCommentOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'InsertCommentDoubleIcon',
|
||||
title: 'docs-thread-comment-ui.panel.addComment',
|
||||
tooltip: 'docs-thread-comment-ui.panel.addComment',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC, undefined, SHEET_EDITOR_UNITS),
|
||||
disabled$: combineLatest([
|
||||
accessor.get(DocSelectionManagerService).textSelection$.pipe(
|
||||
debounceTime(16),
|
||||
map(() => shouldDisableAddComment(accessor)),
|
||||
startWith(shouldDisableAddComment(accessor))
|
||||
),
|
||||
getCommentPermissionDisabled$(accessor),
|
||||
]).pipe(map(([selectionDisabled, permissionDisabled]) =>
|
||||
selectionDisabled || permissionDisabled)),
|
||||
};
|
||||
}
|
||||
|
||||
export function ToolbarDocCommentMenuItemFactory(accessor: MenuAccessor) {
|
||||
return {
|
||||
id: ToggleCommentPanelOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'CommentIcon',
|
||||
title: 'docs-thread-comment-ui.panel.openComments',
|
||||
tooltip: 'docs-thread-comment-ui.panel.openComments',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
68
packages/docs-thread-comment-ui/src/menu/schema.ts
Normal file
68
packages/docs-thread-comment-ui/src/menu/schema.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 { MenuSchemaType } from '@univerjs/ui';
|
||||
import { FLOAT_TOOLBAR_MENU_POSITION } from '@univerjs/docs-ui';
|
||||
import { ContextMenuGroup, ContextMenuPosition, FloatingObjectToolbarPosition, RibbonInsertGroup } from '@univerjs/ui';
|
||||
import {
|
||||
AddDocDrawingCommentOperation,
|
||||
StartAddCommentOperation,
|
||||
ToggleCommentPanelOperation,
|
||||
} from '../commands/operations/show-comment-panel.operation';
|
||||
import { AddDocCommentMenuItemFactory, AddDocDrawingCommentMenuItemFactory, ToolbarDocCommentMenuItemFactory } from './menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonInsertGroup.MEDIA]: {
|
||||
[ToggleCommentPanelOperation.id]: {
|
||||
order: 3,
|
||||
gridLayout: { row: 1, column: 4, showLabel: true },
|
||||
menuItemFactory: ToolbarDocCommentMenuItemFactory,
|
||||
},
|
||||
[StartAddCommentOperation.id]: {
|
||||
order: 3.1,
|
||||
gridLayout: { row: 2, column: 4, showLabel: true },
|
||||
menuItemFactory: AddDocCommentMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[FLOAT_TOOLBAR_MENU_POSITION]: {
|
||||
[StartAddCommentOperation.id]: {
|
||||
order: 21,
|
||||
menuItemFactory: AddDocCommentMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[FloatingObjectToolbarPosition.DOC]: {
|
||||
[AddDocDrawingCommentOperation.id]: {
|
||||
order: 10,
|
||||
menuItemFactory: AddDocDrawingCommentMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.MAIN_AREA]: {
|
||||
[ContextMenuGroup.DATA]: {
|
||||
[StartAddCommentOperation.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: AddDocCommentMenuItemFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.DRAWING]: {
|
||||
[ContextMenuGroup.DATA]: {
|
||||
[AddDocDrawingCommentOperation.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: AddDocDrawingCommentMenuItemFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
93
packages/docs-thread-comment-ui/src/mobile-plugin.ts
Normal file
93
packages/docs-thread-comment-ui/src/mobile-plugin.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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 { IUniverDocsThreadCommentUIConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsThreadCommentPlugin } from '@univerjs/docs-thread-comment';
|
||||
import { UniverDocsMobileUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverThreadCommentPlugin } from '@univerjs/thread-comment';
|
||||
import { UniverThreadCommentMobileUIPlugin } from '@univerjs/thread-comment-ui';
|
||||
import pkg from '../package.json';
|
||||
import { PLUGIN_NAME } from './common/const';
|
||||
import { defaultPluginConfig, DOCS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { DocThreadCommentSelectionController } from './controllers/doc-thread-comment-selection.controller';
|
||||
import { DocThreadCommentRenderController } from './controllers/render-controllers/render.controller';
|
||||
import { DocThreadCommentUIController } from './controllers/ui.controller';
|
||||
import { DocThreadCommentService } from './services/doc-thread-comment.service';
|
||||
|
||||
@DependentOn(
|
||||
UniverDocsPlugin,
|
||||
UniverDocsThreadCommentPlugin,
|
||||
UniverThreadCommentPlugin,
|
||||
UniverDrawingPlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverDocsMobileUIPlugin,
|
||||
UniverThreadCommentMobileUIPlugin
|
||||
)
|
||||
export class UniverDocsThreadCommentMobileUIPlugin extends Plugin {
|
||||
static override pluginName = PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_DOC;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverDocsThreadCommentUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected override _injector: Injector,
|
||||
@IRenderManagerService private readonly _renderManagerSrv: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(DOCS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
([
|
||||
[ComponentsController],
|
||||
[DocThreadCommentUIController],
|
||||
[DocThreadCommentSelectionController],
|
||||
[DocThreadCommentService],
|
||||
] as Dependency[]).forEach((dep) => {
|
||||
this._injector.add(dep);
|
||||
});
|
||||
|
||||
this._injector.get(ComponentsController);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._initRenderModule();
|
||||
|
||||
this._injector.get(DocThreadCommentSelectionController);
|
||||
this._injector.get(DocThreadCommentUIController);
|
||||
}
|
||||
|
||||
private _initRenderModule() {
|
||||
this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, [DocThreadCommentRenderController]);
|
||||
}
|
||||
}
|
||||
94
packages/docs-thread-comment-ui/src/plugin.ts
Normal file
94
packages/docs-thread-comment-ui/src/plugin.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 type { IUniverDocsThreadCommentUIConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsThreadCommentPlugin } from '@univerjs/docs-thread-comment';
|
||||
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverThreadCommentPlugin } from '@univerjs/thread-comment';
|
||||
import { UniverThreadCommentUIPlugin } from '@univerjs/thread-comment-ui';
|
||||
import pkg from '../package.json';
|
||||
import { PLUGIN_NAME } from './common/const';
|
||||
import { defaultPluginConfig, DOCS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { DocThreadCommentSelectionController } from './controllers/doc-thread-comment-selection.controller';
|
||||
import { DocThreadCommentRenderController } from './controllers/render-controllers/render.controller';
|
||||
import { DocThreadCommentUIController } from './controllers/ui.controller';
|
||||
import { DocThreadCommentService } from './services/doc-thread-comment.service';
|
||||
|
||||
const STARTING_DEPENDENCIES: Array<Parameters<Injector['add']>[0]> = [
|
||||
[DocThreadCommentUIController],
|
||||
[DocThreadCommentSelectionController],
|
||||
[DocThreadCommentService],
|
||||
];
|
||||
|
||||
@DependentOn(
|
||||
UniverDocsPlugin,
|
||||
UniverDocsThreadCommentPlugin,
|
||||
UniverThreadCommentPlugin,
|
||||
UniverDrawingPlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverDocsUIPlugin,
|
||||
UniverThreadCommentUIPlugin
|
||||
)
|
||||
export class UniverDocsThreadCommentUIPlugin extends Plugin {
|
||||
static override pluginName = PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_DOC;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverDocsThreadCommentUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected _injector: Injector,
|
||||
@IRenderManagerService private readonly _renderManagerSrv: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(DOCS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ComponentsController]);
|
||||
this._injector.get(ComponentsController);
|
||||
STARTING_DEPENDENCIES.forEach((dep) => {
|
||||
this._injector.add(dep);
|
||||
});
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._initRenderModule();
|
||||
|
||||
this._injector.get(DocThreadCommentSelectionController);
|
||||
this._injector.get(DocThreadCommentUIController);
|
||||
}
|
||||
|
||||
private _initRenderModule() {
|
||||
this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, [DocThreadCommentRenderController]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { Injector } from '@univerjs/core';
|
||||
import { ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DocThreadCommentService } from '../doc-thread-comment.service';
|
||||
|
||||
class TestSidebarService {}
|
||||
|
||||
class TestThreadCommentPanelService {}
|
||||
|
||||
describe('DocThreadCommentService', () => {
|
||||
it('should track adding comment lifecycle', () => {
|
||||
const injector = new Injector();
|
||||
injector.add([ISidebarService, { useClass: TestSidebarService }]);
|
||||
injector.add([ThreadCommentPanelService, { useClass: TestThreadCommentPanelService }]);
|
||||
injector.add([DocThreadCommentService]);
|
||||
const service = injector.get(DocThreadCommentService);
|
||||
|
||||
expect(service.addingComment).toBeUndefined();
|
||||
|
||||
const comment = { id: 'c1', startOffset: 1, endOffset: 2 };
|
||||
service.startAdd(comment as any);
|
||||
expect(service.addingComment).toEqual(comment);
|
||||
|
||||
service.endAdd();
|
||||
expect(service.addingComment).toBeUndefined();
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 { ITextRange, Nullable } from '@univerjs/core';
|
||||
import type { IThreadComment } from '@univerjs/thread-comment';
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export class DocThreadCommentService extends Disposable {
|
||||
private _addingComment$ = new BehaviorSubject<Nullable<IThreadComment & ITextRange>>(undefined);
|
||||
readonly addingComment$ = this._addingComment$.asObservable();
|
||||
|
||||
get addingComment() {
|
||||
return this._addingComment$.getValue();
|
||||
}
|
||||
|
||||
constructor(
|
||||
@ISidebarService private readonly _sidebarService: ISidebarService,
|
||||
@Inject(ThreadCommentPanelService) private readonly _threadCommentPanelService: ThreadCommentPanelService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(() => {
|
||||
this._addingComment$.complete();
|
||||
});
|
||||
}
|
||||
|
||||
startAdd(comment: IThreadComment & ITextRange) {
|
||||
this._addingComment$.next(comment);
|
||||
}
|
||||
|
||||
endAdd() {
|
||||
this._addingComment$.next(undefined);
|
||||
}
|
||||
}
|
||||
@@ -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 type { DocumentDataModel } from '@univerjs/core';
|
||||
import type { IAddDocCommentComment } from '../commands/commands/add-doc-comment.command';
|
||||
import type { IDeleteDocCommentComment } from '../commands/commands/delete-doc-comment.command';
|
||||
import { ICommandService, Injector, IPermissionService, isInternalEditorID, IUniverInstanceService, UniverInstanceType, UserManagerService } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '@univerjs/docs-thread-comment';
|
||||
import { deserializeThreadCommentAnchor, serializeThreadCommentAnchor, ThreadCommentAnchorKind, ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentDraftService, ThreadCommentPanel } from '@univerjs/thread-comment-ui';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { debounceTime, filter, map, merge, Observable } from 'rxjs';
|
||||
import { AddDocCommentComment } from '../commands/commands/add-doc-comment.command';
|
||||
import { DeleteDocCommentComment } from '../commands/commands/delete-doc-comment.command';
|
||||
import { StartAddCommentOperation } from '../commands/operations/show-comment-panel.operation';
|
||||
import { shouldDisableAddComment } from '../menu/menu';
|
||||
import { DocThreadCommentService } from '../services/doc-thread-comment.service';
|
||||
|
||||
export function getDocCommentPanelSubUnitId(draft: { unitId: string; subUnitId: string } | null, unitId: string | undefined): string {
|
||||
return draft && draft.unitId === unitId ? draft.subUnitId : DEFAULT_DOC_SUBUNIT_ID;
|
||||
}
|
||||
|
||||
export const DocThreadCommentPanel = () => {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const injector = useDependency(Injector);
|
||||
const doc$ = useMemo(() => univerInstanceService.getCurrentTypeOfUnit$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC).pipe(filter((doc) => !!doc && !isInternalEditorID(doc.getUnitId()))), [univerInstanceService]);
|
||||
const doc = useObservable(doc$);
|
||||
const draftService = useDependency(ThreadCommentDraftService);
|
||||
const drawingDraft = useObservable(draftService.draft$, draftService.draft);
|
||||
const subUnitId$ = useMemo(
|
||||
() => new Observable<string>((sub) => sub.next(getDocCommentPanelSubUnitId(drawingDraft, doc?.getUnitId()))),
|
||||
[doc, drawingDraft]
|
||||
);
|
||||
const docSelectionManagerService = useDependency(DocSelectionManagerService);
|
||||
const selectionChange$ = useMemo(
|
||||
() => docSelectionManagerService.textSelection$.pipe(debounceTime(16)),
|
||||
[docSelectionManagerService.textSelection$]
|
||||
);
|
||||
const permissionService = useDependency(IPermissionService);
|
||||
const disableAddChange$ = useMemo(
|
||||
() => merge(selectionChange$, permissionService.permissionPointUpdate$),
|
||||
[permissionService.permissionPointUpdate$, selectionChange$]
|
||||
);
|
||||
const disableAdd = useObservable(
|
||||
() => disableAddChange$.pipe(map(() => shouldDisableAddComment(injector))),
|
||||
shouldDisableAddComment(injector),
|
||||
false,
|
||||
[disableAddChange$, injector]
|
||||
);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const threadCommentModel = useDependency(ThreadCommentModel);
|
||||
useObservable(threadCommentModel.commentUpdate$);
|
||||
const docCommentService = useDependency(DocThreadCommentService);
|
||||
const textTempComment = useObservable(docCommentService.addingComment$);
|
||||
const userManagerService = useDependency(UserManagerService);
|
||||
const drawingTempComment = drawingDraft?.anchor.kind === ThreadCommentAnchorKind.DOC_DRAWING && drawingDraft.unitId === doc?.getUnitId()
|
||||
? {
|
||||
id: '',
|
||||
threadId: '',
|
||||
unitId: drawingDraft.unitId,
|
||||
subUnitId: drawingDraft.subUnitId,
|
||||
ref: serializeThreadCommentAnchor(drawingDraft.anchor),
|
||||
dT: '',
|
||||
personId: userManagerService.getCurrentUser().userID,
|
||||
text: { dataStream: '\r\n' },
|
||||
}
|
||||
: null;
|
||||
const tempComment = drawingTempComment ?? textTempComment;
|
||||
const drawingIds = new Set(Object.keys(doc?.getSnapshot().drawings ?? {}));
|
||||
const isDrawingComment = (comment: { ref: string }) => {
|
||||
const anchor = deserializeThreadCommentAnchor(comment.ref);
|
||||
return anchor?.kind === ThreadCommentAnchorKind.DOC_DRAWING
|
||||
|| (comment.ref.startsWith('#') && drawingIds.has(comment.ref.slice(1)));
|
||||
};
|
||||
const drawingCommentIds = doc
|
||||
? threadCommentModel.getUnit(doc.getUnitId())
|
||||
.filter((thread) => isDrawingComment(thread.root))
|
||||
.map((thread) => thread.root.id)
|
||||
: [];
|
||||
const [commentIds, setCommentIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const set = new Set<string>();
|
||||
const customRanges = doc?.getCustomDecorations();
|
||||
setCommentIds(customRanges?.map((r) => r.id).filter((i) => {
|
||||
const hasRepeat = set.has(i);
|
||||
set.add(i);
|
||||
return !hasRepeat;
|
||||
}) ?? []);
|
||||
|
||||
const dispose = commandService.onCommandExecuted((command) => {
|
||||
if (command.id === RichTextEditingMutation.id) {
|
||||
const set = new Set<string>();
|
||||
const customRanges = doc?.getCustomDecorations();
|
||||
setCommentIds(customRanges?.map((r) => r.id).filter((i) => {
|
||||
const hasRepeat = set.has(i);
|
||||
set.add(i);
|
||||
return !hasRepeat;
|
||||
}) ?? []);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
}, [commandService, doc]);
|
||||
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unitId = doc.getUnitId();
|
||||
|
||||
return (
|
||||
<ThreadCommentPanel
|
||||
unitId={unitId}
|
||||
subUnitId$={subUnitId$}
|
||||
type={UniverInstanceType.UNIVER_DOC}
|
||||
onAdd={() => {
|
||||
commandService.executeCommand(StartAddCommentOperation.id);
|
||||
}}
|
||||
getSubUnitName={() => ''}
|
||||
disableAdd={disableAdd}
|
||||
tempComment={tempComment}
|
||||
onAddComment={async (comment) => {
|
||||
if (drawingTempComment && !comment.parentId) {
|
||||
return true;
|
||||
}
|
||||
// attach an comment to an custom-range
|
||||
if (!comment.parentId) {
|
||||
const params: IAddDocCommentComment = {
|
||||
unitId,
|
||||
range: textTempComment!,
|
||||
comment,
|
||||
};
|
||||
const success = await commandService.executeCommand(AddDocCommentComment.id, params);
|
||||
if (!success) {
|
||||
throw new Error('Failed to add document comment.');
|
||||
}
|
||||
docCommentService.endAdd();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
onAfterDeleteComment={async (comment) => {
|
||||
if (!comment.parentId) {
|
||||
if (isDrawingComment(comment)) {
|
||||
return;
|
||||
}
|
||||
const params: IDeleteDocCommentComment = {
|
||||
unitId,
|
||||
commentId: comment.id,
|
||||
};
|
||||
await commandService.executeCommand(DeleteDocCommentComment.id, params);
|
||||
}
|
||||
}}
|
||||
showComments={[
|
||||
...commentIds,
|
||||
...drawingCommentIds,
|
||||
]}
|
||||
onTempCommentClose={() => draftService.cancel()}
|
||||
formatRef={(comment) => {
|
||||
const anchor = deserializeThreadCommentAnchor(comment.ref);
|
||||
return anchor?.kind === ThreadCommentAnchorKind.DOC_DRAWING ? `#${anchor.elementId}` : comment.ref;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, IDisposable, IDocumentBody, IDocumentData, Injector } from '@univerjs/core';
|
||||
import type { IRender } from '@univerjs/engine-render';
|
||||
import type { IThreadComment } from '@univerjs/thread-comment';
|
||||
import type { ISidebarMethodOptions } from '@univerjs/ui';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import {
|
||||
CustomDecorationType,
|
||||
Disposable,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleType,
|
||||
toDisposable,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
UserManagerService,
|
||||
} from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '@univerjs/docs-thread-comment';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { IThreadCommentDataSourceService, ThreadCommentDataSourceService, ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { SetActiveCommentOperation, ThreadCommentDraftService, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import threadCommentEnUS from '@univerjs/thread-comment-ui/locale/en-US';
|
||||
import { ISidebarService, RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Subject } from 'rxjs';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { DocThreadCommentService } from '../../services/doc-thread-comment.service';
|
||||
import { DocThreadCommentPanel, getDocCommentPanelSubUnitId } from '../DocThreadCommentPanel';
|
||||
|
||||
const DOC_ID = 'doc-thread-comment-panel-test';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
class TestSidebarService extends Disposable implements ISidebarService {
|
||||
readonly sidebarOptions$ = new Subject<ISidebarMethodOptions>();
|
||||
readonly scrollEvent$ = new Subject<Event>();
|
||||
private _options: ISidebarMethodOptions = {};
|
||||
private _container?: HTMLElement;
|
||||
|
||||
open(params: ISidebarMethodOptions): IDisposable {
|
||||
this._options = { ...params, visible: true };
|
||||
this.sidebarOptions$.next(this._options);
|
||||
return toDisposable(() => this.close());
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this._options = { ...this._options, visible: false };
|
||||
this.sidebarOptions$.next(this._options);
|
||||
this._options.onClose?.();
|
||||
}
|
||||
|
||||
get visible(): boolean {
|
||||
return this._options.visible ?? false;
|
||||
}
|
||||
|
||||
get options(): ISidebarMethodOptions {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
get width(): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setWidth(): void {}
|
||||
|
||||
getContainer(): HTMLElement | undefined {
|
||||
return this._container;
|
||||
}
|
||||
|
||||
setContainer(element?: HTMLElement): void {
|
||||
this._container = element;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.sidebarOptions$.complete();
|
||||
this.scrollEvent$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService implements Pick<IRenderManagerService, 'getRenderUnitById'> {
|
||||
getRenderUnitById(): IRender | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createDocData(decorationIds: string[]): IDocumentData {
|
||||
return {
|
||||
id: DOC_ID,
|
||||
body: {
|
||||
dataStream: 'Alpha beta gamma\r\n',
|
||||
textRuns: [],
|
||||
paragraphs: [],
|
||||
sectionBreaks: [],
|
||||
customBlocks: [],
|
||||
customDecorations: decorationIds.map((id) => ({
|
||||
id,
|
||||
type: CustomDecorationType.COMMENT,
|
||||
startIndex: 0,
|
||||
endIndex: 4,
|
||||
})),
|
||||
},
|
||||
documentStyle: {
|
||||
pageSize: {
|
||||
width: 594.3,
|
||||
height: 840.51,
|
||||
},
|
||||
marginTop: 72,
|
||||
marginBottom: 72,
|
||||
marginRight: 90,
|
||||
marginLeft: 90,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTextBody(text: string): IDocumentBody {
|
||||
const dataStream = `${text}\r\n`;
|
||||
return {
|
||||
dataStream,
|
||||
textRuns: [],
|
||||
paragraphs: [
|
||||
{
|
||||
startIndex: dataStream.length - 2,
|
||||
paragraphId: `paragraph-${text}`,
|
||||
},
|
||||
],
|
||||
sectionBreaks: [
|
||||
{
|
||||
sectionId: `section-${text}`,
|
||||
startIndex: dataStream.length - 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function createThread(id: string, text: string, resolved = false): IThreadComment {
|
||||
return {
|
||||
id,
|
||||
threadId: id,
|
||||
ref: `Range ${id}`,
|
||||
unitId: DOC_ID,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
dT: '2026-06-17T00:00:00.000Z',
|
||||
personId: 'user-1',
|
||||
resolved,
|
||||
text: createTextBody(text),
|
||||
};
|
||||
}
|
||||
|
||||
function createPanelTestBed(decorationIds: string[]) {
|
||||
const univer = new Univer({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: threadCommentEnUS,
|
||||
},
|
||||
});
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector) as Injector['get'];
|
||||
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
|
||||
injector.add([IThreadCommentDataSourceService, { useClass: ThreadCommentDataSourceService }]);
|
||||
injector.add([ThreadCommentModel]);
|
||||
injector.add([ISidebarService, { useClass: TestSidebarService as never }]);
|
||||
injector.add([ThreadCommentDraftService]);
|
||||
injector.add([ThreadCommentPanelService]);
|
||||
injector.add([DocThreadCommentService]);
|
||||
|
||||
univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, createDocData(decorationIds));
|
||||
get(IUniverInstanceService).focusUnit(DOC_ID);
|
||||
get(UserManagerService).setCurrentUser({ userID: 'user-1', name: 'Ada' });
|
||||
get(ICommandService).registerCommand(SetActiveCommentOperation);
|
||||
|
||||
return {
|
||||
univer,
|
||||
injector,
|
||||
get,
|
||||
threadCommentModel: get(ThreadCommentModel),
|
||||
panelService: get(ThreadCommentPanelService),
|
||||
};
|
||||
}
|
||||
|
||||
function renderPanel(injector: Injector) {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
<DocThreadCommentPanel />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
return { container, root };
|
||||
}
|
||||
|
||||
describe('DocThreadCommentPanel', () => {
|
||||
let univer: Univer | undefined;
|
||||
let root: Root | undefined;
|
||||
let container: HTMLElement | undefined;
|
||||
let scrollIntoViewDescriptor: PropertyDescriptor | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
scrollIntoViewDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollIntoView');
|
||||
Object.defineProperty(Element.prototype, 'scrollIntoView', {
|
||||
configurable: true,
|
||||
value() {},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (root) {
|
||||
act(() => root!.unmount());
|
||||
}
|
||||
container?.remove();
|
||||
univer?.dispose();
|
||||
if (scrollIntoViewDescriptor) {
|
||||
Object.defineProperty(Element.prototype, 'scrollIntoView', scrollIntoViewDescriptor);
|
||||
} else {
|
||||
delete (Element.prototype as { scrollIntoView?: unknown }).scrollIntoView;
|
||||
}
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
univer = undefined;
|
||||
});
|
||||
|
||||
it('renders only thread comments attached to document custom ranges', () => {
|
||||
const testBed = createPanelTestBed(['visible-thread', 'visible-thread']);
|
||||
univer = testBed.univer;
|
||||
testBed.threadCommentModel.addComment(DOC_ID, DEFAULT_DOC_SUBUNIT_ID, createThread('visible-thread', 'Visible document feedback'));
|
||||
testBed.threadCommentModel.addComment(DOC_ID, DEFAULT_DOC_SUBUNIT_ID, createThread('detached-thread', 'Detached feedback'));
|
||||
|
||||
const rendered = renderPanel(testBed.injector);
|
||||
root = rendered.root;
|
||||
container = rendered.container;
|
||||
|
||||
expect(container.textContent).toContain('Visible document feedback');
|
||||
expect(container.textContent).not.toContain('Detached feedback');
|
||||
expect(container.textContent?.match(/Visible document feedback/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('uses the drawing subunit while a drawing comment draft is active', () => {
|
||||
expect(getDocCommentPanelSubUnitId({ unitId: DOC_ID, subUnitId: DOC_ID }, DOC_ID)).toBe(DOC_ID);
|
||||
expect(getDocCommentPanelSubUnitId({ unitId: 'other-doc', subUnitId: 'other-doc' }, DOC_ID)).toBe(DEFAULT_DOC_SUBUNIT_ID);
|
||||
expect(getDocCommentPanelSubUnitId(null, DOC_ID)).toBe(DEFAULT_DOC_SUBUNIT_ID);
|
||||
expect(getDocCommentPanelSubUnitId(null, undefined)).toBe(DEFAULT_DOC_SUBUNIT_ID);
|
||||
});
|
||||
});
|
||||
27
packages/docs-thread-comment-ui/tailwind.config.ts
Normal file
27
packages/docs-thread-comment-ui/tailwind.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 { Config } from 'tailwindcss';
|
||||
import preset from '@univerjs-infra/shared/tailwind';
|
||||
|
||||
const config: Config = {
|
||||
presets: [preset],
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
packages/docs-thread-comment-ui/tsconfig.json
Normal file
8
packages/docs-thread-comment-ui/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/docs-thread-comment-ui/tsconfig.node.json
Normal file
18
packages/docs-thread-comment-ui/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/docs-thread-comment-ui/vitest.config.ts
Normal file
3
packages/docs-thread-comment-ui/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