mirror of
https://github.com/dream-num/univer.git
synced 2026-08-28 23:01:30 +08:00
feat(thread-comment): support unified comment anchors and facade APIs (#7585)
This commit is contained in:
@@ -875,6 +875,13 @@ export type BaseHitTestResult =
|
||||
viewId: ViewId;
|
||||
recordId: RecordId;
|
||||
}
|
||||
| {
|
||||
type: 'base-record-action' | 'base-record-action-badge';
|
||||
tableId: TableId;
|
||||
viewId: ViewId;
|
||||
recordId: RecordId;
|
||||
actionId: string;
|
||||
}
|
||||
| {
|
||||
type: 'grid-hierarchy-toggle' | 'grid-hierarchy-add-child';
|
||||
tableId: TableId;
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
OpenImageCropOperation,
|
||||
} from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { FloatingObjectToolbarPosition, IMenuManagerService, MenuItemType } from '@univerjs/ui';
|
||||
import { takeUntil } from 'rxjs';
|
||||
import { EditDocDrawingOperation } from '../commands/operations/edit-doc-drawing.operation';
|
||||
import { SidebarDocDrawingOperation } from '../commands/operations/open-drawing-panel.operation';
|
||||
@@ -59,7 +60,8 @@ export class DocDrawingPopupMenuController extends RxDisposable {
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IDocDrawingAdapterService private readonly _drawingAdapterService: IDocDrawingAdapterService,
|
||||
@Inject(DocDrawingFloatingToolbarAdapterService) private readonly _floatingToolbarAdapterService: DocDrawingFloatingToolbarAdapterService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService
|
||||
|
||||
) {
|
||||
super();
|
||||
@@ -265,15 +267,11 @@ export class DocDrawingPopupMenuController extends RxDisposable {
|
||||
const floatingToolbarMenuItems = drawing
|
||||
? this._floatingToolbarAdapterService.getItems({ unitId, subUnitId, drawing })
|
||||
: null;
|
||||
if (floatingToolbarMenuItems) {
|
||||
return floatingToolbarMenuItems;
|
||||
}
|
||||
|
||||
const editCommandInfo = drawing
|
||||
? this._drawingAdapterService.getEditDrawingCommandInfo({ unitId, subUnitId, drawing })
|
||||
: null;
|
||||
|
||||
return [
|
||||
const defaultItems = [
|
||||
{
|
||||
label: editCommandInfo?.label ?? 'docs-drawing-ui.image-popup.edit',
|
||||
index: 0,
|
||||
@@ -307,5 +305,30 @@ export class DocDrawingPopupMenuController extends RxDisposable {
|
||||
disable: true, // TODO: @JOCS, feature is not ready.
|
||||
},
|
||||
];
|
||||
|
||||
return [
|
||||
...(floatingToolbarMenuItems ?? defaultItems),
|
||||
...this._getFloatingObjectMenuItems(),
|
||||
];
|
||||
}
|
||||
|
||||
private _getFloatingObjectMenuItems() {
|
||||
return this._menuManagerService
|
||||
.getFlatMenuByPositionKey(FloatingObjectToolbarPosition.DOC)
|
||||
.flatMap(({ item }, index) => {
|
||||
if (!item || item.type !== MenuItemType.BUTTON || !item.title || typeof item.icon !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
type: 'button' as const,
|
||||
label: item.title,
|
||||
index: 100 + index,
|
||||
commandId: item.commandId ?? item.id,
|
||||
commandParams: typeof item.params === 'function' ? item.params() : item.params,
|
||||
disable: false,
|
||||
icon: item.icon,
|
||||
}];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
} from '@univerjs/core';
|
||||
import { FBase } from '@univerjs/core/facade';
|
||||
import {
|
||||
RemoveDocDrawingCommand,
|
||||
SetDocDrawingArrangeCommand,
|
||||
@@ -33,12 +34,14 @@ import {
|
||||
* Facade API for an image in a document.
|
||||
* @hideconstructor
|
||||
*/
|
||||
export class FDocumentImage {
|
||||
export class FDocumentImage extends FBase {
|
||||
constructor(
|
||||
private readonly _document: FDocument,
|
||||
private readonly _imageId: string,
|
||||
private readonly _injector: Injector
|
||||
) {}
|
||||
protected readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the id of the document containing the image.
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/docs-thread-comment-ui
|
||||
pnpm add @univerjs/docs-thread-comment @univerjs/docs-thread-comment-ui
|
||||
# or
|
||||
npm install @univerjs/docs-thread-comment-ui
|
||||
npm install @univerjs/docs-thread-comment @univerjs/docs-thread-comment-ui
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
@@ -26,14 +26,18 @@ Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
```ts
|
||||
import '@univerjs/docs-thread-comment-ui/lib/index.css';
|
||||
import '@univerjs/docs-thread-comment/facade';
|
||||
import { UniverDocsThreadCommentUIPlugin } from '@univerjs/docs-thread-comment-ui';
|
||||
|
||||
univer.registerPlugin(UniverDocsThreadCommentUIPlugin);
|
||||
|
||||
const range = univerAPI.getActiveDocument()?.getTextRange(0, 12);
|
||||
await range?.createCommentAsync('Verify this introduction.', { id: 'review-intro' });
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
Use this package with `@univerjs/thread-comment` and `@univerjs/thread-comment-ui` for shared comment behavior.
|
||||
Use `@univerjs/docs-thread-comment` for headless model commands and Facade APIs. This UI package adds menus, rendering, and the shared side panel.
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -76,7 +76,9 @@
|
||||
"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/thread-comment": "workspace:*",
|
||||
|
||||
+142
-25
@@ -14,22 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, ICommand, IDocumentData, Injector } from '@univerjs/core';
|
||||
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 } from 'vitest';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '../../../common/const';
|
||||
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';
|
||||
|
||||
@@ -74,6 +81,7 @@ function createComment(id = 'comment-1'): IThreadComment {
|
||||
|
||||
describe('AddDocCommentComment', () => {
|
||||
let univer: Univer;
|
||||
let injector: Injector;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
|
||||
@@ -84,7 +92,7 @@ describe('AddDocCommentComment', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
injector = univer.__getInjector();
|
||||
get = injector.get.bind(injector);
|
||||
|
||||
injector.add([DocSelectionManagerService]);
|
||||
@@ -92,6 +100,7 @@ describe('AddDocCommentComment', () => {
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
injector.add([IThreadCommentDataSourceService, { useClass: ThreadCommentDataSourceService }]);
|
||||
injector.add([ThreadCommentModel]);
|
||||
injector.add([ThreadCommentFacadeService]);
|
||||
injector.add([ISidebarService, { useClass: DesktopSidebarService }]);
|
||||
injector.add([ThreadCommentPanelService]);
|
||||
|
||||
@@ -100,32 +109,18 @@ describe('AddDocCommentComment', () => {
|
||||
|
||||
commandService = get(ICommandService);
|
||||
commandService.registerCommand(AddDocCommentComment);
|
||||
commandService.registerCommand(CreateDocTextRangeCommentCommand);
|
||||
commandService.registerCommand(AddDocCommentDecorationMutation);
|
||||
commandService.registerCommand(AddCommentMutation);
|
||||
commandService.registerCommand(SetActiveCommentOperation);
|
||||
commandService.registerCommand(RichTextEditingMutation as unknown as ICommand);
|
||||
commandService.registerCommand(RichTextEditingMutation);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('adds a thread comment to the selected document text and makes it active', 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,
|
||||
},
|
||||
]);
|
||||
|
||||
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,
|
||||
@@ -133,6 +128,7 @@ describe('AddDocCommentComment', () => {
|
||||
range: {
|
||||
startOffset: 0,
|
||||
endOffset: 5,
|
||||
collapsed: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -165,21 +161,142 @@ describe('AddDocCommentComment', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not attach a comment when the document has no selected text', async () => {
|
||||
const comment = createComment('comment-without-selection');
|
||||
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: 0,
|
||||
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 () => {
|
||||
|
||||
@@ -14,20 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand, ITextRange } from '@univerjs/core';
|
||||
import type { IThreadComment } from '@univerjs/thread-comment';
|
||||
import { CommandType, CustomDecorationType, ICommandService, sequenceExecute } from '@univerjs/core';
|
||||
import { addCustomDecorationBySelectionFactory } from '@univerjs/docs-ui';
|
||||
import { AddCommentMutation, IThreadCommentDataSourceService } from '@univerjs/thread-comment';
|
||||
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';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '../../common/const';
|
||||
|
||||
export interface IAddDocCommentComment {
|
||||
unitId: string;
|
||||
comment: IThreadComment;
|
||||
range: ITextRange;
|
||||
}
|
||||
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,
|
||||
@@ -35,41 +34,31 @@ export const AddDocCommentComment: ICommand<IAddDocCommentComment> = {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const { comment: originComment, unitId } = params;
|
||||
const dataSourceService = accessor.get(IThreadCommentDataSourceService);
|
||||
const comment = await dataSourceService.addComment(originComment);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
const doMutation = addCustomDecorationBySelectionFactory(
|
||||
accessor,
|
||||
{
|
||||
id: comment.threadId,
|
||||
type: CustomDecorationType.COMMENT,
|
||||
unitId,
|
||||
}
|
||||
);
|
||||
if (doMutation) {
|
||||
const addComment = {
|
||||
id: AddCommentMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
comment,
|
||||
},
|
||||
};
|
||||
|
||||
const activeOperation = {
|
||||
id: SetActiveCommentOperation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
commentId: comment.id,
|
||||
},
|
||||
};
|
||||
|
||||
return (await sequenceExecute([addComment, doMutation, activeOperation], commandService)).result;
|
||||
const prepared = await prepareDocTextRangeComment(accessor, params);
|
||||
if (!prepared) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
+26
-1
@@ -19,6 +19,7 @@ import type { IRender } from '@univerjs/engine-render';
|
||||
import type { ISidebarMethodOptions } from '@univerjs/ui';
|
||||
import {
|
||||
Disposable,
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
toDisposable,
|
||||
@@ -26,14 +27,17 @@ import {
|
||||
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 { DEFAULT_DOC_SUBUNIT_ID, DOCS_THREAD_COMMENT_PANEL } from '../../../common/const';
|
||||
import { DOCS_THREAD_COMMENT_PANEL } from '../../../common/const';
|
||||
import { DocThreadCommentService } from '../../../services/doc-thread-comment.service';
|
||||
import {
|
||||
AddDocDrawingCommentOperation,
|
||||
ShowCommentPanelOperation,
|
||||
StartAddCommentOperation,
|
||||
ToggleCommentPanelOperation,
|
||||
@@ -218,4 +222,25 @@ describe('doc thread comment panel operations', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
+36
-3
@@ -18,12 +18,14 @@ 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 } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { getDT, ThreadCommentAnchorKind } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentDraftService, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID, DOCS_THREAD_COMMENT_PANEL } from '../../common/const';
|
||||
import { DOCS_THREAD_COMMENT_PANEL } from '../../common/const';
|
||||
import { DocThreadCommentService } from '../../services/doc-thread-comment.service';
|
||||
|
||||
export interface IShowCommentPanelOperationParams {
|
||||
@@ -142,3 +144,34 @@ export const StartAddCommentOperation: ICommand = {
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,6 +18,4 @@ export const DOCS_THREAD_COMMENT_PANEL = 'univer.doc.thread-comment-panel';
|
||||
|
||||
export const PLUGIN_NAME = 'DOC_THREAD_COMMENT_UI_PLUGIN';
|
||||
|
||||
export const DEFAULT_DOC_SUBUNIT_ID = 'default_doc';
|
||||
|
||||
export const DEFAULT_TEMP_COMMENT_ID = 'default_comment';
|
||||
|
||||
+12
-4
@@ -14,12 +14,13 @@
|
||||
* 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 { DEFAULT_DOC_SUBUNIT_ID } from '../../common/const';
|
||||
import { DocThreadCommentSelectionController } from '../doc-thread-comment-selection.controller';
|
||||
|
||||
describe('DocThreadCommentSelectionController', () => {
|
||||
@@ -31,7 +32,14 @@ describe('DocThreadCommentSelectionController', () => {
|
||||
};
|
||||
|
||||
const doc = {
|
||||
getBody: () => ({ customDecorations: [{ id: 'c1', startIndex: 0, endIndex: 5 }] }),
|
||||
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 = {
|
||||
@@ -70,7 +78,7 @@ describe('DocThreadCommentSelectionController', () => {
|
||||
});
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledWith(ShowCommentPanelOperation.id, {
|
||||
activeComment: { unitId: 'doc-1', subUnitId: DEFAULT_DOC_SUBUNIT_ID, commentId: 'c1' },
|
||||
activeComment: { unitId: 'doc-1', subUnitId: DEFAULT_DOC_SUBUNIT_ID, commentId: 'newest' },
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
@@ -84,7 +92,7 @@ describe('DocThreadCommentSelectionController', () => {
|
||||
};
|
||||
|
||||
const doc = {
|
||||
getBody: () => ({ customDecorations: [{ id: 'c1', startIndex: 0, endIndex: 5 }] }),
|
||||
getBody: () => ({ customDecorations: [{ id: 'c1', type: CustomDecorationType.COMMENT, startIndex: 0, endIndex: 5 }] }),
|
||||
};
|
||||
|
||||
const univerInstanceService = { getUnit: vi.fn(() => doc) };
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { CommentIcon } from '@univerjs/icons';
|
||||
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';
|
||||
@@ -44,6 +44,7 @@ export class ComponentsController extends Disposable {
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
CommentIcon,
|
||||
InsertCommentDoubleIcon,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
+30
-19
@@ -14,10 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, ITextRange } from '@univerjs/core';
|
||||
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,
|
||||
@@ -26,12 +27,12 @@ import {
|
||||
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 { DEFAULT_DOC_SUBUNIT_ID } from '../common/const';
|
||||
import { DocThreadCommentService } from '../services/doc-thread-comment.service';
|
||||
|
||||
export class DocThreadCommentSelectionController extends Disposable {
|
||||
@@ -65,25 +66,17 @@ export class DocThreadCommentSelectionController extends Disposable {
|
||||
lastSelection = primary;
|
||||
if (primary && doc) {
|
||||
const { startOffset, endOffset, collapsed } = primary;
|
||||
let customRange;
|
||||
|
||||
if (collapsed) { // cursor
|
||||
customRange = doc.getBody()?.customDecorations?.find((value) => value.startIndex <= startOffset && value.endIndex >= (endOffset - 1));
|
||||
} else { // range
|
||||
customRange = doc.getBody()?.customDecorations?.find((value) => value.startIndex <= startOffset && value.endIndex >= (endOffset - 1));
|
||||
}
|
||||
const selectionEnd = collapsed ? startOffset : endOffset - 1;
|
||||
const customRange = this._findActiveCommentDecoration(doc, unitId, startOffset, selectionEnd);
|
||||
|
||||
if (customRange) {
|
||||
const comment = this._threadCommentModel.getComment(unitId, DEFAULT_DOC_SUBUNIT_ID, customRange.id);
|
||||
if (comment && !comment.resolved) {
|
||||
this._commandService.executeCommand(ShowCommentPanelOperation.id, {
|
||||
activeComment: {
|
||||
unitId,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
commentId: customRange.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
this._commandService.executeCommand(ShowCommentPanelOperation.id, {
|
||||
activeComment: {
|
||||
unitId,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
commentId: customRange.id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -109,6 +102,24 @@ export class DocThreadCommentSelectionController extends Disposable {
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
+155
-6
@@ -16,9 +16,12 @@
|
||||
|
||||
import { CustomDecorationType } from '@univerjs/core';
|
||||
import { DOC_INTERCEPTOR_POINT, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { Subject } from 'rxjs';
|
||||
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 { DEFAULT_DOC_SUBUNIT_ID } from '../../../common/const';
|
||||
import { DocThreadCommentRenderController } from '../render.controller';
|
||||
|
||||
describe('DocThreadCommentRenderController', () => {
|
||||
@@ -37,9 +40,12 @@ describe('DocThreadCommentRenderController', () => {
|
||||
const docRenderController = { reRender };
|
||||
|
||||
const activeCommentId$ = new Subject<any>();
|
||||
const hoveredCommentId$ = new Subject<any>();
|
||||
const threadCommentPanelService = {
|
||||
activeCommentId: { unitId: 'doc-1', subUnitId: DEFAULT_DOC_SUBUNIT_ID, commentId: 'c2' },
|
||||
activeCommentId$,
|
||||
hoveredCommentId: undefined as any,
|
||||
hoveredCommentId$,
|
||||
};
|
||||
|
||||
const univerInstanceService = {
|
||||
@@ -49,7 +55,8 @@ describe('DocThreadCommentRenderController', () => {
|
||||
const commentUpdate$ = new Subject<any>();
|
||||
const threadCommentModel = {
|
||||
commentUpdate$,
|
||||
getComment: vi.fn((_unitId: string, _subUnitId: string, id: string) => (id === 'c1' ? null : { id, resolved: false })),
|
||||
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(),
|
||||
};
|
||||
@@ -66,7 +73,29 @@ describe('DocThreadCommentRenderController', () => {
|
||||
getUnitId: () => 'doc-1',
|
||||
getBody: () => ({ customDecorations: [{ id: 'c1', type: CustomDecorationType.COMMENT }, { id: 'c2', type: CustomDecorationType.COMMENT }] }),
|
||||
};
|
||||
const context = { unit };
|
||||
const context = {
|
||||
unit,
|
||||
unitId: 'doc-1',
|
||||
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,
|
||||
@@ -75,7 +104,9 @@ describe('DocThreadCommentRenderController', () => {
|
||||
docRenderController as any,
|
||||
univerInstanceService as any,
|
||||
threadCommentModel as any,
|
||||
commandService as any
|
||||
commandService as any,
|
||||
drawingManagerService as any,
|
||||
themeService as any
|
||||
);
|
||||
|
||||
expect(threadCommentModel.addComment).not.toHaveBeenCalled();
|
||||
@@ -104,10 +135,52 @@ describe('DocThreadCommentRenderController', () => {
|
||||
);
|
||||
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(reRender).toHaveBeenCalledWith('doc-1');
|
||||
|
||||
// resolved branch triggers rerender
|
||||
threadCommentModel.getComment.mockImplementation((_unitId: string, _subUnitId: string, id: string) => {
|
||||
if (id === 'c1') return null;
|
||||
return { id, resolved: id === 'c2' };
|
||||
return { id, ref: 'text', resolved: id === 'c2' };
|
||||
});
|
||||
const outResolvedComment = handler(
|
||||
{ id: 'c2' },
|
||||
@@ -145,4 +218,80 @@ describe('DocThreadCommentRenderController', () => {
|
||||
|
||||
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: [] }) },
|
||||
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,
|
||||
{ reRender: vi.fn() } 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();
|
||||
});
|
||||
});
|
||||
|
||||
+162
-24
@@ -14,24 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel } from '@univerjs/core';
|
||||
import type { DocumentDataModel, EventState } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
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 { DocRenderController } from '@univerjs/docs-ui';
|
||||
import { ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '../../common/const';
|
||||
import { getDrawingShapeKeyByDrawingSearch, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { deserializeThreadCommentAnchor, ThreadCommentAnchorKind, ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentCanvasOverlay, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { pairwise, 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,
|
||||
@@ -39,45 +51,168 @@ export class DocThreadCommentRenderController extends Disposable implements IRen
|
||||
@Inject(DocRenderController) private readonly _docRenderController: DocRenderController,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
@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.disposeWithMe(this._threadCommentPanelService.activeCommentId$.subscribe((activeComment) => {
|
||||
if (activeComment) {
|
||||
this._docRenderController.reRender(activeComment.unitId);
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = this._univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC)?.getUnitId();
|
||||
if (unitId) {
|
||||
this._docRenderController.reRender(unitId);
|
||||
}
|
||||
}));
|
||||
[
|
||||
this._threadCommentPanelService.activeCommentId$,
|
||||
this._threadCommentPanelService.hoveredCommentId$,
|
||||
].forEach((observable) => this.disposeWithMe(observable.pipe(
|
||||
startWith(undefined),
|
||||
pairwise()
|
||||
).subscribe(([previous, current]) => {
|
||||
const currentUnitId = this._univerInstanceService
|
||||
.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC)
|
||||
?.getUnitId();
|
||||
new Set([previous?.unitId, current?.unitId, currentUnitId]).forEach((unitId) => {
|
||||
if (unitId) {
|
||||
this._docRenderController.reRender(unitId);
|
||||
}
|
||||
});
|
||||
})));
|
||||
|
||||
this.disposeWithMe(this._threadCommentModel.commentUpdate$.subscribe((update) => {
|
||||
if (update.type === 'resolve') {
|
||||
this._docRenderController.reRender(update.unitId);
|
||||
}
|
||||
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, index, customDecorations } = pos;
|
||||
const activeComment = this._threadCommentPanelService.activeCommentId;
|
||||
const { commentId, unitId: commentUnitID } = activeComment || {};
|
||||
const activeCustomDecoration = customDecorations.find((i) => i.id === commentId);
|
||||
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({
|
||||
@@ -86,11 +221,14 @@ export class DocThreadCommentRenderController extends Disposable implements IRen
|
||||
});
|
||||
}
|
||||
|
||||
const isActiveIndex = activeCustomDecoration && index >= activeCustomDecoration.startIndex && index <= activeCustomDecoration.endIndex;
|
||||
const isActive = commentUnitID === unitId && data.id === commentId;
|
||||
const isActive = focusedComments.some((focusedComment) => (
|
||||
focusedComment?.unitId === unitId
|
||||
&& focusedComment.subUnitId === DEFAULT_DOC_SUBUNIT_ID
|
||||
&& focusedComment.commentId === data.id
|
||||
));
|
||||
return next({
|
||||
...data,
|
||||
active: isActive || isActiveIndex,
|
||||
active: isActive,
|
||||
show: !comment.resolved,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ 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,
|
||||
@@ -38,6 +39,7 @@ export class DocThreadCommentUIController extends Disposable {
|
||||
|
||||
private _initCommands() {
|
||||
[
|
||||
AddDocDrawingCommentOperation,
|
||||
AddDocCommentComment,
|
||||
DeleteDocCommentComment,
|
||||
ShowCommentPanelOperation,
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'إدارة التعليقات',
|
||||
addComment: 'إضافة تعليق',
|
||||
openComments: 'فتح التعليقات',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Gestió de comentaris',
|
||||
addComment: 'Afegeix un comentari',
|
||||
openComments: 'Obre els comentaris',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Kommentarverwaltung',
|
||||
addComment: 'Kommentar hinzufügen',
|
||||
openComments: 'Kommentare öffnen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ const locale = {
|
||||
panel: {
|
||||
title: 'Comment Management',
|
||||
addComment: 'Add Comment',
|
||||
openComments: 'Open Comments',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Gestión de comentarios',
|
||||
addComment: 'Añadir comentario',
|
||||
openComments: 'Abrir comentarios',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'مدیریت نظرات',
|
||||
addComment: 'افزودن نظر',
|
||||
openComments: 'باز کردن نظرات',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Gestion des commentaires',
|
||||
addComment: 'Ajouter un commentaire',
|
||||
openComments: 'Ouvrir les commentaires',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Manajemen Komentar',
|
||||
addComment: 'Tambah Komentar',
|
||||
openComments: 'Buka Komentar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Gestione Commenti',
|
||||
addComment: 'Aggiungi Commento',
|
||||
openComments: 'Apri commenti',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'コメント管理',
|
||||
addComment: 'コメントを追加',
|
||||
openComments: 'コメントを開く',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: '댓글 관리',
|
||||
addComment: '댓글 추가',
|
||||
openComments: '댓글 열기',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Zarządzanie komentarzami',
|
||||
addComment: 'Dodaj komentarz',
|
||||
openComments: 'Otwórz komentarze',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Gerenciamento de Comentários',
|
||||
addComment: 'Adicionar Comentário',
|
||||
openComments: 'Abrir comentários',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Управление комментариями',
|
||||
addComment: 'Добавить комментарий',
|
||||
openComments: 'Открыть комментарии',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Správa komentárov',
|
||||
addComment: 'Pridať komentár',
|
||||
openComments: 'Otvoriť komentáre',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Quản lý Bình luận',
|
||||
addComment: 'Thêm bình luận',
|
||||
openComments: 'Mở bình luận',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: '评论管理',
|
||||
addComment: '添加评论',
|
||||
openComments: '打开评论',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: '評論管理',
|
||||
addComment: '新增評論',
|
||||
openComments: '開啟評論',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: '評論管理',
|
||||
addComment: '新增評論',
|
||||
openComments: '開啟評論',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,10 +23,22 @@ import { DocumentEditArea, IRenderManagerService, withCurrentTypeOfRenderer } fr
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { debounceTime, Observable } from 'rxjs';
|
||||
import {
|
||||
AddDocDrawingCommentOperation,
|
||||
StartAddCommentOperation,
|
||||
ToggleCommentPanelOperation,
|
||||
} from '../commands/operations/show-comment-panel.operation';
|
||||
|
||||
export function AddDocDrawingCommentMenuItemFactory(accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
export const shouldDisableAddComment = (accessor: IAccessor) => {
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
@@ -55,7 +67,7 @@ export function AddDocCommentMenuItemFactory(accessor: IAccessor): IMenuButtonIt
|
||||
return {
|
||||
id: StartAddCommentOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'CommentIcon',
|
||||
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),
|
||||
@@ -77,8 +89,8 @@ export function ToolbarDocCommentMenuItemFactory(accessor: IAccessor): IMenuButt
|
||||
id: ToggleCommentPanelOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'CommentIcon',
|
||||
title: 'docs-thread-comment-ui.panel.addComment',
|
||||
tooltip: 'docs-thread-comment-ui.panel.addComment',
|
||||
title: 'docs-thread-comment-ui.panel.openComments',
|
||||
tooltip: 'docs-thread-comment-ui.panel.openComments',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,19 +16,26 @@
|
||||
|
||||
import type { MenuSchemaType } from '@univerjs/ui';
|
||||
import { FLOAT_TOOLBAR_MENU_POSITION } from '@univerjs/docs-ui';
|
||||
import { ContextMenuGroup, ContextMenuPosition, RibbonInsertGroup } from '@univerjs/ui';
|
||||
import { ContextMenuGroup, ContextMenuPosition, FloatingObjectToolbarPosition, RibbonInsertGroup } from '@univerjs/ui';
|
||||
import {
|
||||
AddDocDrawingCommentOperation,
|
||||
StartAddCommentOperation,
|
||||
ToggleCommentPanelOperation,
|
||||
} from '../commands/operations/show-comment-panel.operation';
|
||||
import { AddDocCommentMenuItemFactory, ToolbarDocCommentMenuItemFactory } from './menu';
|
||||
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]: {
|
||||
@@ -36,6 +43,12 @@ export const menuSchema: MenuSchemaType = {
|
||||
menuItemFactory: AddDocCommentMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[FloatingObjectToolbarPosition.DOC]: {
|
||||
[AddDocDrawingCommentOperation.id]: {
|
||||
order: 10,
|
||||
menuItemFactory: AddDocDrawingCommentMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.MAIN_AREA]: {
|
||||
[ContextMenuGroup.DATA]: {
|
||||
[StartAddCommentOperation.id]: {
|
||||
@@ -44,4 +57,12 @@ export const menuSchema: MenuSchemaType = {
|
||||
},
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.DRAWING]: {
|
||||
[ContextMenuGroup.DATA]: {
|
||||
[AddDocDrawingCommentOperation.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: AddDocDrawingCommentMenuItemFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,11 +14,12 @@
|
||||
* 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 { 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';
|
||||
@@ -31,9 +32,17 @@ import { DocThreadCommentRenderController } from './controllers/render-controlle
|
||||
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
|
||||
@@ -67,11 +76,7 @@ export class UniverDocsThreadCommentUIPlugin extends Plugin {
|
||||
override onStarting(): void {
|
||||
this._injector.add([ComponentsController]);
|
||||
this._injector.get(ComponentsController);
|
||||
([
|
||||
[DocThreadCommentUIController],
|
||||
[DocThreadCommentSelectionController],
|
||||
[DocThreadCommentService],
|
||||
] as Dependency[]).forEach((dep) => {
|
||||
STARTING_DEPENDENCIES.forEach((dep) => {
|
||||
this._injector.add(dep);
|
||||
});
|
||||
}
|
||||
@@ -84,8 +89,6 @@ export class UniverDocsThreadCommentUIPlugin extends Plugin {
|
||||
}
|
||||
|
||||
private _initRenderModule() {
|
||||
[DocThreadCommentRenderController].forEach((dep) => {
|
||||
this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, dep as unknown as Dependency);
|
||||
});
|
||||
this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, [DocThreadCommentRenderController]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,25 +17,35 @@
|
||||
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, isInternalEditorID, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { ICommandService, Injector, isInternalEditorID, IUniverInstanceService, UniverInstanceType, UserManagerService } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { ThreadCommentPanel } from '@univerjs/thread-comment-ui';
|
||||
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, 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 { DEFAULT_DOC_SUBUNIT_ID } from '../common/const';
|
||||
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 subUnitId$ = useMemo(() => new Observable<string>((sub) => sub.next(DEFAULT_DOC_SUBUNIT_ID)), []);
|
||||
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)),
|
||||
@@ -48,8 +58,35 @@ export const DocThreadCommentPanel = () => {
|
||||
[injector, selectionChange$]
|
||||
);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const threadCommentModel = useDependency(ThreadCommentModel);
|
||||
useObservable(threadCommentModel.commentUpdate$);
|
||||
const docCommentService = useDependency(DocThreadCommentService);
|
||||
const tempComment = useObservable(docCommentService.addingComment$);
|
||||
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(() => {
|
||||
@@ -94,33 +131,48 @@ export const DocThreadCommentPanel = () => {
|
||||
getSubUnitName={() => ''}
|
||||
disableAdd={disableAdd}
|
||||
tempComment={tempComment}
|
||||
onAddComment={(comment) => {
|
||||
onAddComment={async (comment) => {
|
||||
if (drawingTempComment && !comment.parentId) {
|
||||
return true;
|
||||
}
|
||||
// attach an comment to an custom-range
|
||||
if (!comment.parentId) {
|
||||
const params: IAddDocCommentComment = {
|
||||
unitId,
|
||||
range: tempComment!,
|
||||
range: textTempComment!,
|
||||
comment,
|
||||
};
|
||||
commandService.executeCommand(AddDocCommentComment.id, params);
|
||||
const success = await commandService.executeCommand(AddDocCommentComment.id, params);
|
||||
if (!success) {
|
||||
throw new Error('Failed to add document comment.');
|
||||
}
|
||||
docCommentService.endAdd();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
onDeleteComment={(comment) => {
|
||||
onAfterDeleteComment={async (comment) => {
|
||||
if (!comment.parentId) {
|
||||
if (isDrawingComment(comment)) {
|
||||
return;
|
||||
}
|
||||
const params: IDeleteDocCommentComment = {
|
||||
unitId,
|
||||
commentId: comment.id,
|
||||
};
|
||||
commandService.executeCommand(DeleteDocCommentComment.id, params);
|
||||
return false;
|
||||
await commandService.executeCommand(DeleteDocCommentComment.id, params);
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
showComments={commentIds}
|
||||
showComments={[
|
||||
...commentIds,
|
||||
...drawingCommentIds,
|
||||
]}
|
||||
onTempCommentClose={() => draftService.cancel()}
|
||||
formatRef={(comment) => {
|
||||
const anchor = deserializeThreadCommentAnchor(comment.ref);
|
||||
return anchor?.kind === ThreadCommentAnchorKind.DOC_DRAWING ? `#${anchor.elementId}` : comment.ref;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,18 +31,18 @@ import {
|
||||
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, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
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 { DEFAULT_DOC_SUBUNIT_ID } from '../../common/const';
|
||||
import { DocThreadCommentService } from '../../services/doc-thread-comment.service';
|
||||
import { DocThreadCommentPanel } from '../DocThreadCommentPanel';
|
||||
import { DocThreadCommentPanel, getDocCommentPanelSubUnitId } from '../DocThreadCommentPanel';
|
||||
|
||||
const DOC_ID = 'doc-thread-comment-panel-test';
|
||||
|
||||
@@ -179,6 +179,7 @@ function createPanelTestBed(decorationIds: string[]) {
|
||||
injector.add([IThreadCommentDataSourceService, { useClass: ThreadCommentDataSourceService }]);
|
||||
injector.add([ThreadCommentModel]);
|
||||
injector.add([ISidebarService, { useClass: TestSidebarService as never }]);
|
||||
injector.add([ThreadCommentDraftService]);
|
||||
injector.add([ThreadCommentPanelService]);
|
||||
injector.add([DocThreadCommentService]);
|
||||
|
||||
@@ -256,4 +257,11 @@ describe('DocThreadCommentPanel', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# @univerjs/docs-thread-comment
|
||||
|
||||
`@univerjs/docs-thread-comment` provides model commands and Facade APIs for comments anchored to fixed document text ranges. It has no UI or rendering dependency and can be used in Node environments.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/docs-thread-comment` | `UniverDocsThreadComment` | No | No | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/docs-thread-comment
|
||||
# or
|
||||
npm install @univerjs/docs-thread-comment
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { UniverDocsThreadCommentPlugin } from '@univerjs/docs-thread-comment';
|
||||
import '@univerjs/docs-thread-comment/facade';
|
||||
|
||||
univer.registerPlugin(UniverDocsThreadCommentPlugin);
|
||||
|
||||
const range = univerAPI.getActiveDocument()?.getTextRange(0, 12);
|
||||
await range?.createCommentAsync('Verify this introduction.', { id: 'review-intro' });
|
||||
```
|
||||
|
||||
Use `@univerjs/docs-thread-comment-ui` separately when menus, canvas decorations, or the shared side panel are required.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/docs-thread-comment)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "@univerjs/docs-thread-comment",
|
||||
"version": "1.0.0-beta.2",
|
||||
"private": false,
|
||||
"description": "Thread comment model integration and Facade APIs 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"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./facade": "./src/facade/index.ts",
|
||||
"./*": "./src/*"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/es/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./lib/es/index.js",
|
||||
"require": "./lib/cjs/index.js",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./lib/es/*",
|
||||
"require": "./lib/cjs/*",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:bundle": "univer-cli build",
|
||||
"build:types": "tsc -p tsconfig.node.json",
|
||||
"build": "pnpm run build:bundle && pnpm run build:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/thread-comment": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"rxjs": "^7.8.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
ICommandService,
|
||||
|
||||
IUniverInstanceService,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FDocument } from '@univerjs/docs/facade';
|
||||
import {
|
||||
AddCommentMutation,
|
||||
IThreadCommentDataSourceService,
|
||||
ThreadCommentDataSourceService,
|
||||
ThreadCommentFacadeService,
|
||||
ThreadCommentModel,
|
||||
} from '@univerjs/thread-comment';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '../../../common/const';
|
||||
import {
|
||||
AddDocCommentDecorationMutation,
|
||||
CreateDocTextRangeCommentCommand,
|
||||
} from '../create-doc-text-range-comment.command';
|
||||
import '@univerjs/docs-thread-comment/facade';
|
||||
|
||||
const DOC_ID = 'headless-doc-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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('CreateDocTextRangeCommentCommand', () => {
|
||||
let univer: Univer;
|
||||
|
||||
beforeEach(() => {
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([IThreadCommentDataSourceService, { useClass: ThreadCommentDataSourceService }]);
|
||||
injector.add([ThreadCommentModel]);
|
||||
injector.add([ThreadCommentFacadeService]);
|
||||
univer.createUnit(UniverInstanceType.UNIVER_DOC, createDocData());
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(CreateDocTextRangeCommentCommand);
|
||||
commandService.registerCommand(AddCommentMutation);
|
||||
commandService.registerCommand(AddDocCommentDecorationMutation);
|
||||
});
|
||||
|
||||
afterEach(() => univer.dispose());
|
||||
|
||||
it('creates and queries a fixed text comment without UI services', async () => {
|
||||
const injector = univer.__getInjector();
|
||||
const commandService = injector.get(ICommandService);
|
||||
await expect(commandService.executeCommand(CreateDocTextRangeCommentCommand.id, {
|
||||
unitId: DOC_ID,
|
||||
range: { startOffset: 0, endOffset: 5 },
|
||||
content: 'Check the greeting.',
|
||||
id: 'agent-comment-1',
|
||||
personId: 'agent-user-1',
|
||||
})).resolves.toBe(true);
|
||||
|
||||
const documentModel = injector.get(IUniverInstanceService)
|
||||
.getUnit<DocumentDataModel>(DOC_ID, UniverInstanceType.UNIVER_DOC);
|
||||
if (!documentModel) {
|
||||
throw new Error('Test document was not created.');
|
||||
}
|
||||
const range = injector.createInstance(FDocument, documentModel).getTextRange(0, 5);
|
||||
expect(range.getComments()).toHaveLength(1);
|
||||
expect(injector.get(ThreadCommentModel).getThread(
|
||||
DOC_ID,
|
||||
DEFAULT_DOC_SUBUNIT_ID,
|
||||
'agent-comment-1'
|
||||
)?.root).toMatchObject({
|
||||
ref: 'Hello',
|
||||
startOffset: 0,
|
||||
endOffset: 5,
|
||||
personId: 'agent-user-1',
|
||||
});
|
||||
});
|
||||
});
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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, IAccessor, ICommand, IMutation, IMutationInfo, ITextRangeParam, JSONXActions } from '@univerjs/core';
|
||||
import type { IThreadComment, ThreadCommentContent } from '@univerjs/thread-comment';
|
||||
import {
|
||||
BuildTextUtils,
|
||||
CommandType,
|
||||
CustomDecorationType,
|
||||
|
||||
generateRandomId,
|
||||
|
||||
ICommandService,
|
||||
|
||||
IUniverInstanceService,
|
||||
JSONX,
|
||||
|
||||
sequenceExecute,
|
||||
UniverInstanceType,
|
||||
UserManagerService,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
AddCommentMutation,
|
||||
getDT,
|
||||
|
||||
IThreadCommentDataSourceService,
|
||||
normalizeThreadCommentContent,
|
||||
|
||||
} from '@univerjs/thread-comment';
|
||||
import { DEFAULT_DOC_SUBUNIT_ID } from '../../common/const';
|
||||
|
||||
export interface IAddDocTextRangeCommentParams {
|
||||
unitId: string;
|
||||
comment: IThreadComment;
|
||||
range: ITextRangeParam;
|
||||
}
|
||||
|
||||
/** Parameters accepted by the document text-range Facade. */
|
||||
export interface ICreateDocTextRangeCommentParams {
|
||||
unitId: string;
|
||||
range: ITextRangeParam;
|
||||
content: ThreadCommentContent;
|
||||
attachments?: string[];
|
||||
id?: string;
|
||||
threadId?: string;
|
||||
personId?: string;
|
||||
dateTime?: Date;
|
||||
}
|
||||
|
||||
interface IValidTextRange extends ITextRangeParam {
|
||||
startOffset: number;
|
||||
endOffset: number;
|
||||
}
|
||||
|
||||
export interface IPreparedDocTextRangeComment {
|
||||
comment: IThreadComment;
|
||||
commentMutation: IMutationInfo;
|
||||
decorationMutationParams: IDocCommentDecorationMutationParams;
|
||||
}
|
||||
|
||||
export interface IDocCommentDecorationMutationParams {
|
||||
unitId: string;
|
||||
actions: JSONXActions;
|
||||
segmentId?: string;
|
||||
}
|
||||
|
||||
/** Applies document comment decoration actions without render or selection services. */
|
||||
export const AddDocCommentDecorationMutation: IMutation<
|
||||
IDocCommentDecorationMutationParams,
|
||||
IDocCommentDecorationMutationParams | false
|
||||
> = {
|
||||
id: 'docs-thread-comment.mutation.add-decoration',
|
||||
type: CommandType.MUTATION,
|
||||
handler(accessor, params) {
|
||||
if (!params || JSONX.isNoop(params.actions)) {
|
||||
return false;
|
||||
}
|
||||
const document = accessor.get(IUniverInstanceService)
|
||||
.getUnit<DocumentDataModel>(params.unitId, UniverInstanceType.UNIVER_DOC);
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
const undoActions = JSONX.invertWithDoc(params.actions, document.getSnapshot());
|
||||
document.apply(params.actions);
|
||||
return { ...params, actions: undoActions };
|
||||
},
|
||||
};
|
||||
|
||||
function getValidRange(accessor: IAccessor, unitId: string, range: ITextRangeParam): IValidTextRange | null {
|
||||
const { startOffset, endOffset } = range;
|
||||
if (!Number.isInteger(startOffset) || !Number.isInteger(endOffset) || startOffset < 0 || endOffset <= startOffset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const document = accessor.get(IUniverInstanceService).getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
const body = document?.getSelfOrHeaderFooterModel(range.segmentId)?.getBody();
|
||||
if (!body || endOffset > body.dataStream.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { ...range, startOffset, endOffset, collapsed: false };
|
||||
}
|
||||
|
||||
/** Builds the model mutations shared by headless and UI document comment commands. */
|
||||
export async function prepareDocTextRangeComment(
|
||||
accessor: IAccessor,
|
||||
params: IAddDocTextRangeCommentParams
|
||||
): Promise<IPreparedDocTextRangeComment | null> {
|
||||
const range = getValidRange(accessor, params.unitId, params.range);
|
||||
if (!range) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const savedComment = await accessor.get(IThreadCommentDataSourceService).addComment(params.comment);
|
||||
const comment: IThreadComment = {
|
||||
...params.comment,
|
||||
...savedComment,
|
||||
unitId: params.unitId,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
startOffset: range.startOffset,
|
||||
endOffset: range.endOffset,
|
||||
segmentId: range.segmentId,
|
||||
collapsed: false,
|
||||
};
|
||||
const textX = BuildTextUtils.customDecoration.add({
|
||||
ranges: [range],
|
||||
id: comment.id,
|
||||
type: CustomDecorationType.COMMENT,
|
||||
});
|
||||
const decorationMutationParams: IDocCommentDecorationMutationParams = {
|
||||
unitId: params.unitId,
|
||||
actions: JSONX.getInstance().editOp(textX.serialize()),
|
||||
segmentId: range.segmentId,
|
||||
};
|
||||
|
||||
return {
|
||||
comment,
|
||||
commentMutation: {
|
||||
id: AddCommentMutation.id,
|
||||
params: { unitId: params.unitId, subUnitId: DEFAULT_DOC_SUBUNIT_ID, comment },
|
||||
},
|
||||
decorationMutationParams,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a root comment and document decoration for an explicit text range.
|
||||
* This model command is safe to execute without loading document UI packages.
|
||||
*/
|
||||
export const CreateDocTextRangeCommentCommand: ICommand<ICreateDocTextRangeCommentParams> = {
|
||||
id: 'docs.command.create-text-range-comment',
|
||||
type: CommandType.COMMAND,
|
||||
async handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const range = getValidRange(accessor, params.unitId, params.range);
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
const document = accessor.get(IUniverInstanceService).getUnit<DocumentDataModel>(params.unitId, UniverInstanceType.UNIVER_DOC);
|
||||
const body = document?.getSelfOrHeaderFooterModel(range.segmentId)?.getBody();
|
||||
if (!body) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = params.id ?? generateRandomId();
|
||||
const comment: IThreadComment = {
|
||||
id,
|
||||
threadId: params.threadId ?? id,
|
||||
unitId: params.unitId,
|
||||
subUnitId: DEFAULT_DOC_SUBUNIT_ID,
|
||||
ref: BuildTextUtils.transform.getPlainText(body.dataStream.slice(range.startOffset, range.endOffset)),
|
||||
text: normalizeThreadCommentContent(params.content),
|
||||
attachments: params.attachments ?? [],
|
||||
dT: getDT(params.dateTime),
|
||||
personId: params.personId ?? accessor.get(UserManagerService).getCurrentUser().userID,
|
||||
startOffset: range.startOffset,
|
||||
endOffset: range.endOffset,
|
||||
segmentId: range.segmentId,
|
||||
collapsed: false,
|
||||
};
|
||||
const prepared = await prepareDocTextRangeComment(accessor, { unitId: params.unitId, range, comment });
|
||||
if (!prepared) {
|
||||
return false;
|
||||
}
|
||||
return (await sequenceExecute([
|
||||
prepared.commentMutation,
|
||||
{ id: AddDocCommentDecorationMutation.id, params: prepared.decorationMutationParams },
|
||||
], accessor.get(ICommandService))).result;
|
||||
},
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/** Stable subunit used by document text comments. */
|
||||
export const DEFAULT_DOC_SUBUNIT_ID = 'default_doc';
|
||||
|
||||
export const DOCS_THREAD_COMMENT_PLUGIN_NAME = 'DOC_THREAD_COMMENT_PLUGIN';
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const DOCS_THREAD_COMMENT_PLUGIN_CONFIG_KEY = 'docs-thread-comment.config';
|
||||
|
||||
export const configSymbol = Symbol(DOCS_THREAD_COMMENT_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverDocsThreadCommentConfig {
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverDocsThreadCommentConfig = {};
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Injector } from '@univerjs/core';
|
||||
import { CustomDecorationType, ICommandService } from '@univerjs/core';
|
||||
import * as DocsThreadComment from '@univerjs/docs-thread-comment';
|
||||
import * as DocsFacade from '@univerjs/docs/facade';
|
||||
import * as ThreadComment from '@univerjs/thread-comment';
|
||||
|
||||
export type IDocumentTextRangeCommentCreateOptions = Omit<
|
||||
DocsThreadComment.ICreateDocTextRangeCommentParams,
|
||||
'unitId' | 'range' | 'content'
|
||||
>;
|
||||
|
||||
/** Comment methods added to a fixed document text range. */
|
||||
export interface IFDocumentTextRangeThreadCommentMixin {
|
||||
/**
|
||||
* Creates a comment on this text range.
|
||||
* @param content Plain text or a Univer document body for rich comment content.
|
||||
* @param options Optional stable IDs, author, attachments, and creation time.
|
||||
* @returns `true` when the create command succeeds; otherwise, `false`.
|
||||
* @throws {TypeError} If the content is empty.
|
||||
* @example
|
||||
* ```ts
|
||||
* const range = univerAPI.getActiveDocument()?.getTextRange(0, 12);
|
||||
* await range?.createCommentAsync('Verify this introduction.', { id: 'review-intro' });
|
||||
* ```
|
||||
*/
|
||||
createCommentAsync(
|
||||
content: ThreadComment.ThreadCommentContent,
|
||||
options?: IDocumentTextRangeCommentCreateOptions
|
||||
): Promise<boolean>;
|
||||
/**
|
||||
* Returns locally loaded comments whose comment decorations overlap this text range.
|
||||
* @returns Matching comment threads. The returned anchors use `DOC_TEXT_RANGE`.
|
||||
* @example
|
||||
* ```ts
|
||||
* const range = univerAPI.getActiveDocument()?.getTextRange(0, 12);
|
||||
* const comments = range?.getComments() ?? [];
|
||||
* comments.forEach(({ root, children }) => console.log(root.id, children.length));
|
||||
* ```
|
||||
*/
|
||||
getComments(): ThreadComment.IFacadeThreadCommentInfo[];
|
||||
/**
|
||||
* Synchronizes known document threads and returns comments whose decorations overlap this text range.
|
||||
* @returns A promise resolving to the synchronized matching comment threads.
|
||||
* @example
|
||||
* ```ts
|
||||
* const range = univerAPI.getActiveDocument()?.getTextRange(0, 12);
|
||||
* const comments = range ? await range.listCommentsAsync() : [];
|
||||
* console.log(comments.length);
|
||||
* ```
|
||||
*/
|
||||
listCommentsAsync(): Promise<ThreadComment.IFacadeThreadCommentInfo[]>;
|
||||
}
|
||||
|
||||
export class FDocumentTextRangeThreadCommentMixin extends DocsFacade.FDocumentTextRange implements IFDocumentTextRangeThreadCommentMixin {
|
||||
declare private _threadCommentCommandService: ICommandService;
|
||||
declare private _threadCommentFacadeService: ThreadComment.ThreadCommentFacadeService;
|
||||
|
||||
override _initialize(injector: Injector): void {
|
||||
this._threadCommentCommandService = injector.get(ICommandService);
|
||||
let commentService: ThreadComment.ThreadCommentFacadeService | undefined;
|
||||
Object.defineProperty(this, '_threadCommentFacadeService', {
|
||||
get: () => commentService ??= injector.get(ThreadComment.ThreadCommentFacadeService),
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override createCommentAsync(
|
||||
content: ThreadComment.ThreadCommentContent,
|
||||
options: IDocumentTextRangeCommentCreateOptions = {}
|
||||
): Promise<boolean> {
|
||||
return this._threadCommentCommandService.executeCommand(DocsThreadComment.CreateDocTextRangeCommentCommand.id, {
|
||||
...options,
|
||||
unitId: this._document.getId(),
|
||||
range: { ...this.getRange(), collapsed: false },
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override getComments(): ThreadComment.IFacadeThreadCommentInfo[] {
|
||||
const range = this.getRange();
|
||||
const commentIds = this._getOverlappingCommentIds(range);
|
||||
return this._threadCommentFacadeService.getComments({
|
||||
unitIds: [this._document.getId()],
|
||||
subUnitIds: [DocsThreadComment.DEFAULT_DOC_SUBUNIT_ID],
|
||||
anchorKinds: [ThreadComment.ThreadCommentAnchorKind.DOC_TEXT_RANGE],
|
||||
}).filter((comment) => commentIds.has(comment.root.id));
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override async listCommentsAsync(): Promise<ThreadComment.IFacadeThreadCommentInfo[]> {
|
||||
const range = this.getRange();
|
||||
const commentIds = this._getOverlappingCommentIds(range);
|
||||
const comments = await this._threadCommentFacadeService.listCommentsAsync({
|
||||
unitIds: [this._document.getId()],
|
||||
subUnitIds: [DocsThreadComment.DEFAULT_DOC_SUBUNIT_ID],
|
||||
anchorKinds: [ThreadComment.ThreadCommentAnchorKind.DOC_TEXT_RANGE],
|
||||
});
|
||||
return comments.filter((comment) => commentIds.has(comment.root.id));
|
||||
}
|
||||
|
||||
private _getOverlappingCommentIds(range: DocsFacade.IFDocumentTextRange): Set<string> {
|
||||
const decorations = this._document.getBody(range.segmentId).customDecorations ?? [];
|
||||
return new Set(decorations.filter((decoration) => decoration.type === CustomDecorationType.COMMENT
|
||||
&& decoration.startIndex < range.endOffset
|
||||
&& decoration.endIndex >= range.startOffset).map((decoration) => decoration.id));
|
||||
}
|
||||
}
|
||||
|
||||
DocsFacade.FDocumentTextRange.extend(FDocumentTextRangeThreadCommentMixin);
|
||||
|
||||
declare module '@univerjs/docs/facade' {
|
||||
interface FDocumentTextRange extends IFDocumentTextRangeThreadCommentMixin {}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './f-document-text-range';
|
||||
|
||||
export type {
|
||||
IDocumentTextRangeCommentCreateOptions,
|
||||
IFDocumentTextRangeThreadCommentMixin,
|
||||
} from './f-document-text-range';
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 {
|
||||
AddDocCommentDecorationMutation,
|
||||
CreateDocTextRangeCommentCommand,
|
||||
prepareDocTextRangeComment,
|
||||
} from './commands/commands/create-doc-text-range-comment.command';
|
||||
export type {
|
||||
IAddDocTextRangeCommentParams,
|
||||
ICreateDocTextRangeCommentParams,
|
||||
IDocCommentDecorationMutationParams,
|
||||
IPreparedDocTextRangeComment,
|
||||
} from './commands/commands/create-doc-text-range-comment.command';
|
||||
export { DEFAULT_DOC_SUBUNIT_ID } from './common/const';
|
||||
export type { IUniverDocsThreadCommentConfig } from './config/config';
|
||||
export { UniverDocsThreadCommentPlugin } from './plugin';
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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 { IUniverDocsThreadCommentConfig } from './config/config';
|
||||
import {
|
||||
DependentOn,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
Inject,
|
||||
Injector,
|
||||
merge,
|
||||
Plugin,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverThreadCommentPlugin } from '@univerjs/thread-comment';
|
||||
import pkg from '../package.json';
|
||||
import {
|
||||
AddDocCommentDecorationMutation,
|
||||
CreateDocTextRangeCommentCommand,
|
||||
} from './commands/commands/create-doc-text-range-comment.command';
|
||||
import { DOCS_THREAD_COMMENT_PLUGIN_NAME } from './common/const';
|
||||
import { defaultPluginConfig, DOCS_THREAD_COMMENT_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
|
||||
@DependentOn(UniverDocsPlugin, UniverThreadCommentPlugin)
|
||||
export class UniverDocsThreadCommentPlugin extends Plugin {
|
||||
static override pluginName = DOCS_THREAD_COMMENT_PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_DOC;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverDocsThreadCommentConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected override _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(DOCS_THREAD_COMMENT_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this.disposeWithMe(this._commandService.registerCommand(CreateDocTextRangeCommentCommand));
|
||||
this.disposeWithMe(this._commandService.registerCommand(AddDocCommentDecorationMutation));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
+15
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DocumentBlockRangeType, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocumentBlockRangeType, FOCUSING_COMMON_DRAWINGS, UniverInstanceType } from '@univerjs/core';
|
||||
import { RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { ContextMenuPosition } from '@univerjs/ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
@@ -38,6 +38,7 @@ function createController(options?: {
|
||||
textRanges?: Array<{ startOffset: number; endOffset: number }>;
|
||||
blockRanges?: Array<{ startIndex: number; endIndex: number; blockType: DocumentBlockRangeType }>;
|
||||
menuVisible?: boolean;
|
||||
focusingDrawing?: boolean;
|
||||
}) {
|
||||
const onPointerDown$ = createEventSubject();
|
||||
let commandHandler: ((command: { id: string }) => void) | undefined;
|
||||
@@ -60,6 +61,9 @@ function createController(options?: {
|
||||
} as never,
|
||||
{ isPointerOnNonChecklistBullet: vi.fn(() => options?.pointerOnBullet ?? false) } as never,
|
||||
{ getTextRanges: vi.fn(() => options?.textRanges ?? []) } as never,
|
||||
{
|
||||
getContextValue: vi.fn((key) => key === FOCUSING_COMMON_DRAWINGS && (options?.focusingDrawing ?? false)),
|
||||
} as never,
|
||||
{
|
||||
getCurrentUnitOfType: vi.fn((type) => type === UniverInstanceType.UNIVER_DOC
|
||||
? { getBody: () => ({ blockRanges: options?.blockRanges ?? [] }) }
|
||||
@@ -81,6 +85,16 @@ describe('DocContextMenuRenderController', () => {
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('opens the drawing context menu when a drawing is focused', () => {
|
||||
const { controller, onPointerDown$, contextMenuService } = createController({ focusingDrawing: true });
|
||||
const event = { button: 2, offsetX: 10, offsetY: 20 };
|
||||
|
||||
onPointerDown$.emit(event);
|
||||
|
||||
expect(contextMenuService.triggerContextMenu).toHaveBeenCalledWith(event, ContextMenuPosition.DRAWING);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('does not open context menu for checklist bullets or selections inside code blocks', () => {
|
||||
const bullet = createController({ pointerOnBullet: true });
|
||||
bullet.onPointerDown$.emit({ button: 2, offsetX: 10, offsetY: 20 });
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 { EventSubject } from '@univerjs/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DocResizeRenderController } from '../doc-resize.render-controller';
|
||||
|
||||
describe('DocResizeRenderController', () => {
|
||||
it('refreshes page layout and text selection after sidebar layout changes', async () => {
|
||||
const calculatePagePosition = vi.fn();
|
||||
const refreshRanges = vi.fn();
|
||||
const refreshSelection = vi.fn();
|
||||
const sidebarOptions$ = new Subject();
|
||||
const controller = new DocResizeRenderController(
|
||||
{
|
||||
unitId: 'doc-1',
|
||||
engine: {
|
||||
onTransformChange$: new EventSubject(),
|
||||
},
|
||||
} as never,
|
||||
{ calculatePagePosition } as never,
|
||||
{ refreshSelection } as never,
|
||||
{ refreshRanges } as never,
|
||||
{ sidebarOptions$ } as never
|
||||
);
|
||||
|
||||
sidebarOptions$.next({ visible: true });
|
||||
expect(calculatePagePosition).not.toHaveBeenCalled();
|
||||
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||
|
||||
expect(calculatePagePosition).toHaveBeenCalledOnce();
|
||||
expect(refreshRanges).toHaveBeenCalledOnce();
|
||||
expect(refreshSelection).toHaveBeenCalledOnce();
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
+7
-1
@@ -22,7 +22,9 @@ import {
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
DocumentBlockRangeType,
|
||||
FOCUSING_COMMON_DRAWINGS,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
@@ -48,6 +50,7 @@ export class DocContextMenuRenderController extends Disposable implements IRende
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(DocEventManagerService) private readonly _docEventManagerService: DocEventManagerService,
|
||||
@Inject(DocSelectionManagerService) private readonly _docSelectionManagerService: DocSelectionManagerService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
@@ -70,7 +73,10 @@ export class DocContextMenuRenderController extends Disposable implements IRende
|
||||
return;
|
||||
}
|
||||
|
||||
this._contextMenuService.triggerContextMenu(event, ContextMenuPosition.MAIN_AREA);
|
||||
const position = this._contextService.getContextValue(FOCUSING_COMMON_DRAWINGS)
|
||||
? ContextMenuPosition.DRAWING
|
||||
: ContextMenuPosition.MAIN_AREA;
|
||||
this._contextMenuService.triggerContextMenu(event, position);
|
||||
}
|
||||
});
|
||||
this.disposeWithMe(documentsSubscription);
|
||||
|
||||
+22
-11
@@ -17,16 +17,19 @@
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import { Disposable, fromEventSubject, Inject, isInternalEditorID } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { TRANSFORM_CHANGE_OBSERVABLE_TYPE } from '@univerjs/engine-render';
|
||||
import { animationFrameScheduler, filter, throttleTime } from 'rxjs';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { animationFrameScheduler, observeOn, throttleTime } from 'rxjs';
|
||||
import { DocPageLayoutService } from '../../services/doc-page-layout.service';
|
||||
import { DocSelectionRenderService } from '../../services/selection/doc-selection-render.service';
|
||||
|
||||
// REFACTOR: @JOCS, move to new-docs package.
|
||||
export class DocResizeRenderController extends Disposable implements IRenderModule {
|
||||
constructor(
|
||||
private _context: IRenderContext,
|
||||
@Inject(DocPageLayoutService) private readonly _docPageLayoutService: DocPageLayoutService,
|
||||
@Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService
|
||||
@Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService,
|
||||
@Inject(DocSelectionRenderService) private readonly _docSelectionRenderService: DocSelectionRenderService,
|
||||
@ISidebarService private readonly _sidebarService: ISidebarService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -39,16 +42,24 @@ export class DocResizeRenderController extends Disposable implements IRenderModu
|
||||
private _initResize() {
|
||||
this.disposeWithMe(
|
||||
fromEventSubject(this._context.engine.onTransformChange$).pipe(
|
||||
filter((evt) => evt.type === TRANSFORM_CHANGE_OBSERVABLE_TYPE.resize),
|
||||
throttleTime(0, animationFrameScheduler)
|
||||
).subscribe(() => {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
).subscribe(() => this._refreshLayoutAndSelection())
|
||||
);
|
||||
|
||||
this._docPageLayoutService.calculatePagePosition();
|
||||
this._textSelectionManagerService.refreshSelection();
|
||||
})
|
||||
this.disposeWithMe(
|
||||
this._sidebarService.sidebarOptions$.pipe(
|
||||
observeOn(animationFrameScheduler)
|
||||
).subscribe(() => this._refreshLayoutAndSelection())
|
||||
);
|
||||
}
|
||||
|
||||
private _refreshLayoutAndSelection() {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._docPageLayoutService.calculatePagePosition();
|
||||
this._docSelectionRenderService.refreshRanges();
|
||||
this._textSelectionManagerService.refreshSelection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +232,11 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
|
||||
this._reserveRanges = status;
|
||||
}
|
||||
|
||||
refreshRanges() {
|
||||
this._rangeList.forEach((range) => range.refresh());
|
||||
this._rectRangeList.forEach((range) => range.refresh());
|
||||
}
|
||||
|
||||
private _setRangeStyle(style: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE) {
|
||||
this._selectionStyle = style;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface IRenderManagerService extends IDisposable {
|
||||
* @param dep
|
||||
*/
|
||||
registerRenderModule<T extends UnitModel>(type: UniverInstanceType, dep: Dependency<T>): IDisposable;
|
||||
registerRenderModule(type: UniverInstanceType, dep: Dependency): IDisposable;
|
||||
}
|
||||
|
||||
const DEFAULT_SCENE_SIZE = { width: 1500, height: 1000 };
|
||||
@@ -148,6 +149,8 @@ export class RenderManagerService extends Disposable implements IRenderManagerSe
|
||||
* @param type
|
||||
* @param depCtor
|
||||
*/
|
||||
registerRenderModule<T extends UnitModel>(type: UniverInstanceType, depCtor: Dependency<T>): IDisposable;
|
||||
registerRenderModule(type: UniverInstanceType, depCtor: Dependency): IDisposable;
|
||||
registerRenderModule(type: UniverInstanceType, depCtor: Dependency): IDisposable {
|
||||
if (!this._renderDependencies.has(type)) {
|
||||
this._renderDependencies.set(type, []);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency, Workbook } from '@univerjs/core';
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import type { IUniverSheetsDataValidationUIConfig } from './config/config';
|
||||
import { DependentOn, ICommandService, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
|
||||
@@ -117,7 +117,7 @@ export class UniverSheetsDataValidationMobileUIPlugin extends Plugin {
|
||||
this._injector.get(DataValidationPermissionController);
|
||||
|
||||
const renderManager = this._injector.get(IRenderManagerService);
|
||||
renderManager.registerRenderModule<Workbook>(
|
||||
renderManager.registerRenderModule(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
[SheetsDataValidationReRenderController] as Dependency
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency, Workbook } from '@univerjs/core';
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import type { IUniverSheetsDataValidationUIConfig } from './config/config';
|
||||
import { DependentOn, ICommandService, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
|
||||
@@ -119,7 +119,7 @@ export class UniverSheetsDataValidationUIPlugin extends Plugin {
|
||||
this._injector.get(DataValidationAlertController);
|
||||
|
||||
const renderManager = this._injector.get(IRenderManagerService);
|
||||
renderManager.registerRenderModule<Workbook>(
|
||||
renderManager.registerRenderModule(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
[SheetsDataValidationReRenderController] as Dependency
|
||||
);
|
||||
|
||||
+28
-5
@@ -27,7 +27,7 @@ import {
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import { IMenuManagerService, IMessageService, MenuItemType } from '@univerjs/ui';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DrawingPopupMenuController } from '../drawing-popup-menu.controller';
|
||||
@@ -46,10 +46,14 @@ describe('DrawingPopupMenuController', () => {
|
||||
const currentWorkbook$ = new Subject<never>();
|
||||
const disposedWorkbook$ = new Subject<never>();
|
||||
const imageObject = { oKey: 'image-1' };
|
||||
const attachPopupToObject = vi.fn(() => ({
|
||||
dispose: vi.fn(),
|
||||
canDispose: () => true,
|
||||
}));
|
||||
let attachedPopup: { extraProps?: Record<string, unknown> } | undefined;
|
||||
const attachPopupToObject = vi.fn((_targetObject: unknown, popup: { extraProps?: Record<string, unknown> }) => {
|
||||
attachedPopup = popup;
|
||||
return {
|
||||
dispose: vi.fn(),
|
||||
canDispose: () => true,
|
||||
};
|
||||
});
|
||||
|
||||
injector.add([LocaleService]);
|
||||
injector.add([IDrawingManagerService, {
|
||||
@@ -92,6 +96,19 @@ describe('DrawingPopupMenuController', () => {
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IMessageService, { useValue: { show: () => toDisposable(() => {}) } as never }]);
|
||||
injector.add([IMenuManagerService, {
|
||||
useValue: {
|
||||
getFlatMenuByPositionKey: () => [{
|
||||
key: 'add-comment',
|
||||
order: 0,
|
||||
item: {
|
||||
id: 'sheet.operation.add-drawing-comment',
|
||||
type: MenuItemType.BUTTON,
|
||||
title: 'sheets-thread-comment-ui.menu.addComment',
|
||||
},
|
||||
}],
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IContextService, {
|
||||
useValue: {
|
||||
contextChanged$,
|
||||
@@ -113,6 +130,12 @@ describe('DrawingPopupMenuController', () => {
|
||||
direction: 'left',
|
||||
})
|
||||
);
|
||||
expect(attachedPopup?.extraProps?.menuItems).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
commandId: 'sheet.operation.add-drawing-comment',
|
||||
label: 'sheets-thread-comment-ui.menu.addComment',
|
||||
}),
|
||||
]));
|
||||
|
||||
controller.dispose();
|
||||
injector.dispose();
|
||||
|
||||
@@ -43,7 +43,12 @@ import {
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { RemoveSheetDrawingCommand } from '@univerjs/sheets-drawing';
|
||||
import { SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import {
|
||||
FloatingObjectToolbarPosition,
|
||||
IMenuManagerService,
|
||||
IMessageService,
|
||||
MenuItemType,
|
||||
} from '@univerjs/ui';
|
||||
import { FlipSheetDrawingCommand } from '../commands/commands/flip-drawings.command';
|
||||
import { EditSheetDrawingOperation } from '../commands/operations/edit-sheet-drawing.operation';
|
||||
|
||||
@@ -58,6 +63,7 @@ export class DrawingPopupMenuController extends RxDisposable {
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IMessageService private readonly _messageService: IMessageService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IImageIoService private readonly _ioService: ImageIoService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
@@ -176,7 +182,10 @@ export class DrawingPopupMenuController extends RxDisposable {
|
||||
direction: this._localeService.getDirection() === 'rtl' ? 'left' : 'horizontal',
|
||||
offset: [2, 0],
|
||||
extraProps: {
|
||||
menuItems: menus || this._getImageMenuItems(unitId, subUnitId, drawingId, drawingType),
|
||||
menuItems: [
|
||||
...(menus || this._getImageMenuItems(unitId, subUnitId, drawingId, drawingType)),
|
||||
...this._getFloatingObjectMenuItems(),
|
||||
],
|
||||
},
|
||||
}));
|
||||
})
|
||||
@@ -248,4 +257,22 @@ export class DrawingPopupMenuController extends RxDisposable {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private _getFloatingObjectMenuItems() {
|
||||
return this._menuManagerService
|
||||
.getFlatMenuByPositionKey(FloatingObjectToolbarPosition.SHEET)
|
||||
.flatMap(({ item }, index) => {
|
||||
if (!item || item.type !== MenuItemType.BUTTON || !item.title) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
label: item.title,
|
||||
index: 100 + index,
|
||||
commandId: item.commandId ?? item.id,
|
||||
commandParams: typeof item.params === 'function' ? item.params() : item.params,
|
||||
disable: false,
|
||||
}];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,6 +832,16 @@ export class FOverGridImage extends FBase {
|
||||
return this._image.drawingId;
|
||||
}
|
||||
|
||||
/** Returns the workbook unit id that owns this image. */
|
||||
getUnitId(): string {
|
||||
return this._image.unitId;
|
||||
}
|
||||
|
||||
/** Returns the worksheet id that owns this image. */
|
||||
getSubUnitId(): string {
|
||||
return this._image.subUnitId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the drawing type of the image
|
||||
* @returns {DrawingTypeEnum} The drawing type of the image
|
||||
|
||||
@@ -22,6 +22,6 @@ import './f-univer';
|
||||
|
||||
export type * from './f-enum';
|
||||
export type * from './f-event';
|
||||
export type * from './f-over-grid-image';
|
||||
export { FOverGridImage, type FOverGridImageBuilder, type IFOverGridImage } from './f-over-grid-image';
|
||||
export type * from './f-univer';
|
||||
export type * from './f-worksheet';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency, Workbook } from '@univerjs/core';
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import type { IUniverSheetsHyperLinkUIConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
@@ -94,7 +94,7 @@ export class UniverSheetsHyperLinkUIPlugin extends Plugin {
|
||||
|
||||
override onReady(): void {
|
||||
const renderManager = this._injector.get(IRenderManagerService);
|
||||
renderManager.registerRenderModule<Workbook>(UniverInstanceType.UNIVER_SHEET, [SheetsHyperLinkRenderController] as Dependency);
|
||||
renderManager.registerRenderModule(UniverInstanceType.UNIVER_SHEET, [SheetsHyperLinkRenderController] as Dependency);
|
||||
|
||||
this._injector.get(SheetsHyperLinkAutoFillController);
|
||||
this._injector.get(SheetsHyperLinkCopyPasteController);
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/drawing": "workspace:*",
|
||||
"@univerjs/engine-formula": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/icons": "1.38.0",
|
||||
|
||||
+24
-1
@@ -18,6 +18,7 @@ import type { IWorkbookData } from '@univerjs/core';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import type { IThreadComment } from '@univerjs/thread-comment';
|
||||
import {
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LifecycleService,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DrawingManagerService, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
|
||||
import { CellPopupManagerService, SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
|
||||
@@ -41,7 +43,7 @@ import { DesktopSidebarService, ISidebarService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SheetsThreadCommentPopupService } from '../../../services/sheets-thread-comment-popup.service';
|
||||
import { SHEETS_THREAD_COMMENT_PANEL } from '../../../types/const';
|
||||
import { ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../comment.operation';
|
||||
import { AddSheetDrawingCommentOperation, ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../comment.operation';
|
||||
|
||||
const unitId = 'comment-workbook';
|
||||
const subUnitId = 'sheet-1';
|
||||
@@ -193,4 +195,25 @@ describe('sheet thread comment operations', () => {
|
||||
expect(panelService.panelVisible).toBe(false);
|
||||
expect(sidebarService.visible).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a focused drawing from another workbook or worksheet', () => {
|
||||
const injector = univer.__getInjector();
|
||||
const drawing = {
|
||||
unitId: 'other-book',
|
||||
subUnitId,
|
||||
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(AddSheetDrawingCommentOperation.handler(injector, {})).toBe(false);
|
||||
drawingManager.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IOperation } from '@univerjs/core';
|
||||
import type { IOperation, Workbook } from '@univerjs/core';
|
||||
import type { ISheetLocation } from '@univerjs/sheets';
|
||||
import { CommandType, IUniverInstanceService } from '@univerjs/core';
|
||||
import { CommandType, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
|
||||
import { ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { ThreadCommentAnchorKind } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentDraftService, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { SheetsThreadCommentPopupService } from '../../services/sheets-thread-comment-popup.service';
|
||||
import { SHEETS_THREAD_COMMENT_PANEL } from '../../types/const';
|
||||
@@ -80,14 +82,72 @@ export const ToggleSheetCommentPanelOperation: IOperation = {
|
||||
sidebarService.close();
|
||||
panelService.setPanelVisible(false);
|
||||
} else {
|
||||
sidebarService.open({
|
||||
header: { title: 'sheets-thread-comment-ui.panel.title' },
|
||||
children: { label: SHEETS_THREAD_COMMENT_PANEL },
|
||||
width: 360,
|
||||
});
|
||||
panelService.setPanelVisible(true);
|
||||
openSheetCommentPanel(sidebarService, panelService);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const OpenSheetCommentPanelOperation: IOperation = {
|
||||
id: 'sheet.operation.open-comment-panel',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor) {
|
||||
openSheetCommentPanel(
|
||||
accessor.get(ISidebarService),
|
||||
accessor.get(ThreadCommentPanelService)
|
||||
);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
function openSheetCommentPanel(
|
||||
sidebarService: ISidebarService,
|
||||
panelService: ThreadCommentPanelService
|
||||
): void {
|
||||
if (!panelService.panelVisible || sidebarService.options.children?.label !== SHEETS_THREAD_COMMENT_PANEL) {
|
||||
sidebarService.open({
|
||||
header: { title: 'sheets-thread-comment-ui.panel.title' },
|
||||
children: { label: SHEETS_THREAD_COMMENT_PANEL },
|
||||
width: 360,
|
||||
onClose: () => panelService.setPanelVisible(false),
|
||||
});
|
||||
}
|
||||
panelService.setPanelVisible(true);
|
||||
}
|
||||
|
||||
export const AddSheetDrawingCommentOperation: IOperation = {
|
||||
id: 'sheet.operation.add-drawing-comment',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor) {
|
||||
const drawing = accessor.get(IDrawingManagerService).getFocusDrawings()[0];
|
||||
const workbook = accessor.get(IUniverInstanceService)
|
||||
.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (
|
||||
!drawing
|
||||
|| !workbook
|
||||
|| drawing.unitId !== workbook.getUnitId()
|
||||
|| drawing.subUnitId !== workbook.getActiveSheet()?.getSheetId()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
accessor.get(ThreadCommentDraftService).place({
|
||||
unitId: drawing.unitId,
|
||||
subUnitId: drawing.subUnitId,
|
||||
anchor: {
|
||||
kind: ThreadCommentAnchorKind.SHEET_DRAWING,
|
||||
pageId: drawing.subUnitId,
|
||||
elementId: drawing.drawingId,
|
||||
},
|
||||
});
|
||||
const panelService = accessor.get(ThreadCommentPanelService);
|
||||
accessor.get(ISidebarService).open({
|
||||
header: { title: 'sheets-thread-comment-ui.panel.title' },
|
||||
children: { label: SHEETS_THREAD_COMMENT_PANEL },
|
||||
width: 360,
|
||||
onClose: () => panelService.setPanelVisible(false),
|
||||
});
|
||||
panelService.setPanelVisible(true);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
+25
@@ -129,4 +129,29 @@ describe('SheetsThreadCommentCopyPasteController', () => {
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('does not implicitly clone a comment during a normal cell copy', async () => {
|
||||
await testBed.commandService.executeCommand(AddCommentMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
comment: createRootComment(),
|
||||
});
|
||||
const controller = testBed.injector.createInstance(SheetsThreadCommentCopyPasteController);
|
||||
const hook = testBed.getClipboardHook()!;
|
||||
hook.onBeforeCopy('test', 'sheet1', {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
});
|
||||
|
||||
expect(hook.onPasteCells(
|
||||
null,
|
||||
{ unitId: 'test', subUnitId: 'sheet2', range: { rows: [2], cols: [3] } },
|
||||
null,
|
||||
{ copyType: COPY_TYPE.COPY }
|
||||
)).toEqual({ redos: [], undos: [] });
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
+75
-12
@@ -14,32 +14,57 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AddCommentCommand, DeleteCommentCommand, UpdateCommentCommand } from '@univerjs/thread-comment';
|
||||
import { ICommandService, Injector, LocaleService } from '@univerjs/core';
|
||||
import { SheetPermissionCheckController } from '@univerjs/sheets';
|
||||
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
|
||||
import {
|
||||
AddCommentCommand,
|
||||
DeleteCommentCommand,
|
||||
serializeThreadCommentAnchor,
|
||||
ThreadCommentAnchorKind,
|
||||
UpdateCommentCommand,
|
||||
} from '@univerjs/thread-comment';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ShowAddSheetCommentModalOperation } from '../../commands/operations/comment.operation';
|
||||
import { SheetsThreadCommentPermissionController } from '../sheets-thread-comment-permission.controller';
|
||||
|
||||
type BeforeCommandHandler = (command: { id: string; params?: unknown }) => void;
|
||||
|
||||
function createTestBed(permissionCheck: object, commentModel: object = { getComment: vi.fn() }) {
|
||||
let beforeCommandHandler: BeforeCommandHandler | undefined;
|
||||
const injector = new Injector([
|
||||
[ICommandService, { useValue: {
|
||||
beforeCommandExecuted: vi.fn((handler: BeforeCommandHandler) => {
|
||||
beforeCommandHandler = handler;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} }],
|
||||
[LocaleService, { useValue: { t: (key: string) => key } }],
|
||||
[SheetPermissionCheckController, { useValue: permissionCheck }],
|
||||
[SheetsThreadCommentModel, { useValue: commentModel }],
|
||||
[SheetsThreadCommentPermissionController],
|
||||
]);
|
||||
|
||||
return {
|
||||
controller: injector.get(SheetsThreadCommentPermissionController),
|
||||
getBeforeCommandHandler: () => beforeCommandHandler,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SheetsThreadCommentPermissionController', () => {
|
||||
it('blocks comment panel, add, update and delete actions when comment permissions are denied', () => {
|
||||
let beforeCommandHandler: ((command: { id: string; params?: unknown }) => void) | undefined;
|
||||
const permissionCheck = {
|
||||
permissionCheckWithoutRange: vi.fn(() => false),
|
||||
permissionCheckWithRanges: vi.fn(() => false),
|
||||
blockExecuteWithoutPermission: vi.fn(),
|
||||
};
|
||||
const controller = new SheetsThreadCommentPermissionController(
|
||||
{ t: (key: string) => key } as never,
|
||||
{
|
||||
beforeCommandExecuted: vi.fn((handler) => {
|
||||
beforeCommandHandler = handler;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as never,
|
||||
permissionCheck as never,
|
||||
const { controller, getBeforeCommandHandler } = createTestBed(
|
||||
permissionCheck,
|
||||
{
|
||||
getComment: vi.fn(() => ({ id: 'comment-1', ref: 'C4' })),
|
||||
} as never
|
||||
}
|
||||
);
|
||||
const beforeCommandHandler = getBeforeCommandHandler();
|
||||
|
||||
beforeCommandHandler?.({ id: ShowAddSheetCommentModalOperation.id });
|
||||
beforeCommandHandler?.({
|
||||
@@ -85,4 +110,42 @@ describe('SheetsThreadCommentPermissionController', () => {
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('rechecks drawing comment permissions without creating an invalid cell range', () => {
|
||||
const permissionCheck = {
|
||||
permissionCheckWithoutRange: vi.fn()
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(false),
|
||||
permissionCheckWithRanges: vi.fn(),
|
||||
blockExecuteWithoutPermission: vi.fn(),
|
||||
};
|
||||
const { controller, getBeforeCommandHandler } = createTestBed(permissionCheck);
|
||||
const beforeCommandHandler = getBeforeCommandHandler();
|
||||
const command = {
|
||||
id: AddCommentCommand.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
comment: {
|
||||
ref: serializeThreadCommentAnchor({
|
||||
kind: ThreadCommentAnchorKind.SHEET_DRAWING,
|
||||
elementId: 'drawing-1',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeCommandHandler?.(command);
|
||||
beforeCommandHandler?.(command);
|
||||
|
||||
expect(permissionCheck.permissionCheckWithoutRange).toHaveBeenCalledTimes(2);
|
||||
expect(permissionCheck.permissionCheckWithoutRange).toHaveBeenLastCalledWith(
|
||||
expect.any(Object),
|
||||
'unit-1',
|
||||
'sheet-1'
|
||||
);
|
||||
expect(permissionCheck.permissionCheckWithRanges).not.toHaveBeenCalled();
|
||||
expect(permissionCheck.blockExecuteWithoutPermission).toHaveBeenCalledTimes(1);
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveFocusedSheetComment } from '../sheets-thread-comment-popup.controller';
|
||||
|
||||
describe('resolveFocusedSheetComment', () => {
|
||||
const activeComment = {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
commentId: 'active-comment',
|
||||
};
|
||||
|
||||
it('prefers a hovered comment from the current sheet', () => {
|
||||
const hoveredComment = {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
commentId: 'hovered-comment',
|
||||
};
|
||||
|
||||
expect(resolveFocusedSheetComment(activeComment, hoveredComment, 'book-1', 'sheet-1'))
|
||||
.toEqual(hoveredComment);
|
||||
});
|
||||
|
||||
it('keeps the active highlight when hover belongs to another sheet or workbook', () => {
|
||||
expect(resolveFocusedSheetComment(activeComment, {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-2',
|
||||
commentId: 'foreign-hover',
|
||||
}, 'book-1', 'sheet-1')).toEqual(activeComment);
|
||||
expect(resolveFocusedSheetComment(activeComment, {
|
||||
unitId: 'book-2',
|
||||
subUnitId: 'sheet-1',
|
||||
commentId: 'foreign-hover',
|
||||
}, 'book-1', 'sheet-1')).toEqual(activeComment);
|
||||
});
|
||||
|
||||
it('does not draw a stale highlight outside the current sheet context', () => {
|
||||
expect(resolveFocusedSheetComment(activeComment, undefined, 'book-1', 'sheet-2')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { CommentIcon } from '@univerjs/icons';
|
||||
import { CommentIcon, InsertCommentDoubleIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { SHEETS_THREAD_COMMENT_MODAL, SHEETS_THREAD_COMMENT_PANEL } from '../types/const';
|
||||
import { SheetsThreadCommentCell } from '../views/SheetsThreadCommentCell';
|
||||
@@ -46,6 +46,7 @@ export class ComponentsController extends Disposable {
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
CommentIcon,
|
||||
InsertCommentDoubleIcon,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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 { 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 { SheetsThreadCommentDrawingRenderController } from '../drawing.render-controller';
|
||||
|
||||
describe('SheetsThreadCommentDrawingRenderController', () => {
|
||||
it('aggregates drawing roots, follows transforms, and retains the remaining root after deletion', () => {
|
||||
const worksheet = { getSheetId: () => 'sheet-1', getZoomRatio: () => 2 };
|
||||
const activeSheet$ = new BehaviorSubject(worksheet);
|
||||
const workbook = { activeSheet$, getActiveSheet: () => worksheet };
|
||||
const drawingKey = getDrawingShapeKeyByDrawingSearch({
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-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: 'book-1',
|
||||
unit: workbook,
|
||||
scene,
|
||||
engine: { onTransformChange$: { subscribeEvent: vi.fn(() => ({ dispose: vi.fn() })) } },
|
||||
};
|
||||
const commentUpdate$ = new Subject<{ unitId: string }>();
|
||||
const ref = serializeThreadCommentAnchor({
|
||||
kind: ThreadCommentAnchorKind.SHEET_DRAWING,
|
||||
pageId: 'sheet-1',
|
||||
elementId: 'shape-1',
|
||||
});
|
||||
const commentModel = {
|
||||
commentUpdate$,
|
||||
query: vi.fn(() => roots.map((id) => ({ root: { id, ref } }))),
|
||||
getComment: vi.fn((_unitId: string, _subUnitId: string, commentId: string) => ({ id: commentId, ref })),
|
||||
};
|
||||
const activeCommentId$ = new BehaviorSubject({
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
commentId: 'first-comment',
|
||||
});
|
||||
const hoveredCommentId$ = new BehaviorSubject<undefined>(undefined);
|
||||
const panelService = {
|
||||
activeCommentId: activeCommentId$.value,
|
||||
hoveredCommentId: hoveredCommentId$.value,
|
||||
activeCommentId$,
|
||||
hoveredCommentId$,
|
||||
setActiveComment: vi.fn(),
|
||||
};
|
||||
const drawingManager = { add$: new Subject(), update$: new Subject(), remove$: new Subject() };
|
||||
const themeService = {
|
||||
currentTheme$: new Subject(),
|
||||
getColorFromTheme: vi.fn((token: string) => token),
|
||||
};
|
||||
const controller = new SheetsThreadCommentDrawingRenderController(
|
||||
context as never,
|
||||
{ executeCommand: vi.fn(() => Promise.resolve(true)) } as never,
|
||||
drawingManager as never,
|
||||
commentModel as never,
|
||||
panelService as never,
|
||||
themeService as never
|
||||
);
|
||||
const overlay = scene.addObject.mock.calls[0][0];
|
||||
|
||||
expect(overlay.isHit(new Vector2(60, 101))).toBe(true);
|
||||
expect(overlay.hitCommentId).toBe('newest-comment');
|
||||
|
||||
roots = ['first-comment'];
|
||||
commentUpdate$.next({ unitId: 'book-1' });
|
||||
expect(overlay.isHit(new Vector2(60, 101))).toBe(true);
|
||||
expect(overlay.hitCommentId).toBe('first-comment');
|
||||
|
||||
bounds = { left: 120, top: 140, width: 100, height: 70 };
|
||||
drawingManager.update$.next([{ unitId: 'book-1' }]);
|
||||
expect(overlay.isHit(new Vector2(60, 101))).toBe(false);
|
||||
expect(overlay.isHit(new Vector2(170, 211))).toBe(true);
|
||||
|
||||
roots = [];
|
||||
commentUpdate$.next({ unitId: 'book-1' });
|
||||
expect(overlay.isHit(new Vector2(170, 211))).toBe(false);
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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 { EventState, Workbook } from '@univerjs/core';
|
||||
import type { IMouseEvent, IPointerEvent, IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { IThreadCommentCanvasOutline, IThreadCommentCanvasUnderline } from '@univerjs/thread-comment-ui';
|
||||
import { ICommandService, Inject, RxDisposable, ThemeService, toDisposable } from '@univerjs/core';
|
||||
import { getDrawingShapeKeyByDrawingSearch, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { deserializeThreadCommentAnchor, ThreadCommentAnchorKind, ThreadCommentModel } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentCanvasOverlay, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { OpenSheetCommentPanelOperation } from '../../commands/operations/comment.operation';
|
||||
|
||||
const SHEET_COMMENT_DRAWING_OVERLAY_KEY = 'sheet-thread-comment-drawing-overlay';
|
||||
const SHEET_COMMENT_DRAWING_OVERLAY_LAYER_INDEX = 10_100;
|
||||
|
||||
export class SheetsThreadCommentDrawingRenderController extends RxDisposable implements IRenderModule {
|
||||
private readonly _overlay: ThreadCommentCanvasOverlay;
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(ThreadCommentModel) private readonly _commentModel: ThreadCommentModel,
|
||||
@Inject(ThreadCommentPanelService) private readonly _panelService: ThreadCommentPanelService,
|
||||
@Inject(ThemeService) private readonly _themeService: ThemeService
|
||||
) {
|
||||
super();
|
||||
this._overlay = new ThreadCommentCanvasOverlay(SHEET_COMMENT_DRAWING_OVERLAY_KEY, {
|
||||
...this._getColors(),
|
||||
zoomRatio: 1,
|
||||
markers: [],
|
||||
underlines: [],
|
||||
});
|
||||
this._context.scene.addObject(this._overlay, SHEET_COMMENT_DRAWING_OVERLAY_LAYER_INDEX);
|
||||
this.disposeWithMe(toDisposable(this._overlay.onPointerDown$.subscribeEvent(
|
||||
(_event: IPointerEvent | IMouseEvent, state: EventState) => this._onOverlayPointerDown(state)
|
||||
)));
|
||||
this.disposeWithMe(toDisposable(
|
||||
this._context.engine.onTransformChange$.subscribeEvent(() => this._syncOverlay())
|
||||
));
|
||||
this.disposeWithMe(this._context.unit.activeSheet$.subscribe(() => this._syncOverlay()));
|
||||
this.disposeWithMe(this._commentModel.commentUpdate$.subscribe((update) => {
|
||||
if (update.unitId === this._context.unitId) {
|
||||
this._syncOverlay();
|
||||
}
|
||||
}));
|
||||
[this._panelService.activeCommentId$, this._panelService.hoveredCommentId$]
|
||||
.forEach((observable) => this.disposeWithMe(observable.subscribe(() => this._syncOverlay())));
|
||||
[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._syncOverlay();
|
||||
}
|
||||
})));
|
||||
this.disposeWithMe(this._themeService.currentTheme$.subscribe(() => this._syncOverlay()));
|
||||
this._syncOverlay();
|
||||
}
|
||||
|
||||
private _onOverlayPointerDown(state: EventState): void {
|
||||
const commentId = this._overlay.hitCommentId;
|
||||
const subUnitId = this._context.unit.getActiveSheet()?.getSheetId();
|
||||
if (!commentId || !subUnitId) {
|
||||
return;
|
||||
}
|
||||
state.stopPropagation();
|
||||
this._panelService.setActiveComment({
|
||||
unitId: this._context.unitId,
|
||||
subUnitId,
|
||||
commentId,
|
||||
trigger: 'sheet-canvas',
|
||||
});
|
||||
this._commandService.executeCommand(OpenSheetCommentPanelOperation.id).catch(() => undefined);
|
||||
}
|
||||
|
||||
private _syncOverlay(): void {
|
||||
const worksheet = this._context.unit.getActiveSheet();
|
||||
const subUnitId = worksheet?.getSheetId();
|
||||
if (!worksheet || !subUnitId) {
|
||||
this._overlay.updateState({ markers: [], underlines: [], focusedCommentIds: [], focusOutlines: [] });
|
||||
return;
|
||||
}
|
||||
const underlines = new Map<string, IThreadCommentCanvasUnderline>();
|
||||
this._commentModel.query({
|
||||
unitIds: [this._context.unitId],
|
||||
subUnitIds: [subUnitId],
|
||||
anchorKinds: [ThreadCommentAnchorKind.SHEET_DRAWING],
|
||||
resolved: false,
|
||||
}).forEach(({ root }) => {
|
||||
const anchor = deserializeThreadCommentAnchor(root.ref);
|
||||
if (anchor?.kind !== ThreadCommentAnchorKind.SHEET_DRAWING) {
|
||||
return;
|
||||
}
|
||||
const outline = this._getDrawingOutline(subUnitId, anchor.elementId);
|
||||
if (outline) {
|
||||
underlines.set(anchor.elementId, {
|
||||
commentId: root.id,
|
||||
left: outline.left,
|
||||
top: outline.top + outline.height + 2 / worksheet.getZoomRatio(),
|
||||
width: outline.width,
|
||||
});
|
||||
}
|
||||
});
|
||||
const focusedCommentIds: string[] = [];
|
||||
const focusOutlines = new Map<string, IThreadCommentCanvasOutline>();
|
||||
[this._panelService.activeCommentId, this._panelService.hoveredCommentId].forEach((target) => {
|
||||
if (!target || target.unitId !== this._context.unitId || target.subUnitId !== subUnitId) {
|
||||
return;
|
||||
}
|
||||
const comment = this._commentModel.getComment(target.unitId, target.subUnitId, target.commentId);
|
||||
const anchor = comment && deserializeThreadCommentAnchor(comment.ref);
|
||||
if (anchor?.kind !== ThreadCommentAnchorKind.SHEET_DRAWING) {
|
||||
return;
|
||||
}
|
||||
focusedCommentIds.push(target.commentId);
|
||||
const outline = this._getDrawingOutline(subUnitId, anchor.elementId);
|
||||
if (outline) {
|
||||
focusOutlines.set(anchor.elementId, outline);
|
||||
}
|
||||
});
|
||||
this._overlay.updateState({
|
||||
...this._getColors(),
|
||||
zoomRatio: worksheet.getZoomRatio(),
|
||||
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 _getColors(): { accentColor: string; foregroundColor: string; outlineColor: string } {
|
||||
return {
|
||||
accentColor: this._themeService.getColorFromTheme('yellow.400'),
|
||||
foregroundColor: this._themeService.getColorFromTheme('gray.900'),
|
||||
outlineColor: this._themeService.getColorFromTheme('white'),
|
||||
};
|
||||
}
|
||||
}
|
||||
+31
-33
@@ -36,7 +36,9 @@ import {
|
||||
AddCommentCommand,
|
||||
DeleteCommentCommand,
|
||||
DeleteCommentTreeCommand,
|
||||
deserializeThreadCommentAnchor,
|
||||
ResolveCommentCommand,
|
||||
ThreadCommentAnchorKind,
|
||||
UpdateCommentCommand,
|
||||
} from '@univerjs/thread-comment';
|
||||
import { ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../commands/operations/comment.operation';
|
||||
@@ -69,15 +71,7 @@ export class SheetsThreadCommentPermissionController extends Disposable {
|
||||
} else if (id === AddCommentCommand.id) {
|
||||
const params = command.params as IAddCommentCommandParams;
|
||||
const { unitId, subUnitId, comment } = params;
|
||||
const location = singleReferenceToGrid(comment.ref);
|
||||
const { row, column } = location;
|
||||
|
||||
const permission = this._sheetPermissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookCommentPermission],
|
||||
worksheetTypes: [WorksheetViewPermission],
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
}, [{ startRow: row, startColumn: column, endRow: row, endColumn: column }], unitId, subUnitId);
|
||||
if (!permission) {
|
||||
if (!this._hasCommentPermission(unitId, subUnitId, comment.ref)) {
|
||||
this._sheetPermissionCheckController.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-thread-comment-ui.permission.commentErr'));
|
||||
}
|
||||
} else if (id === UpdateCommentCommand.id) {
|
||||
@@ -86,39 +80,43 @@ export class SheetsThreadCommentPermissionController extends Disposable {
|
||||
const { commentId } = payload;
|
||||
const comment = this._sheetsThreadCommentModel.getComment(unitId, subUnitId, commentId);
|
||||
|
||||
if (comment) {
|
||||
const location = singleReferenceToGrid(comment.ref);
|
||||
const { row, column } = location;
|
||||
|
||||
const permission = this._sheetPermissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookCommentPermission],
|
||||
worksheetTypes: [WorksheetViewPermission],
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
}, [{ startRow: row, startColumn: column, endRow: row, endColumn: column }], unitId, subUnitId);
|
||||
if (!permission) {
|
||||
this._sheetPermissionCheckController.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-thread-comment-ui.permission.commentErr'));
|
||||
}
|
||||
if (comment && !this._hasCommentPermission(unitId, subUnitId, comment.ref)) {
|
||||
this._sheetPermissionCheckController.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-thread-comment-ui.permission.commentErr'));
|
||||
}
|
||||
} else if (id === ResolveCommentCommand.id || id === DeleteCommentCommand.id || id === DeleteCommentTreeCommand.id) {
|
||||
const params = command.params as IResolveCommentCommandParams | IDeleteCommentCommandParams | IDeleteCommentTreeCommandParams;
|
||||
const { unitId, subUnitId, commentId } = params;
|
||||
const comment = this._sheetsThreadCommentModel.getComment(unitId, subUnitId, commentId);
|
||||
|
||||
if (comment) {
|
||||
const location = singleReferenceToGrid(comment.ref);
|
||||
const { row, column } = location;
|
||||
|
||||
const permission = this._sheetPermissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookCommentPermission],
|
||||
worksheetTypes: [WorksheetViewPermission],
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
}, [{ startRow: row, startColumn: column, endRow: row, endColumn: column }], unitId, subUnitId);
|
||||
if (!permission) {
|
||||
this._sheetPermissionCheckController.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-thread-comment-ui.permission.commentErr'));
|
||||
}
|
||||
if (comment && !this._hasCommentPermission(unitId, subUnitId, comment.ref)) {
|
||||
this._sheetPermissionCheckController.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-thread-comment-ui.permission.commentErr'));
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _hasCommentPermission(unitId: string, subUnitId: string, ref: string): boolean {
|
||||
const permissionTypes = {
|
||||
workbookTypes: [WorkbookCommentPermission],
|
||||
worksheetTypes: [WorksheetViewPermission],
|
||||
};
|
||||
const anchor = deserializeThreadCommentAnchor(ref);
|
||||
if (anchor?.kind === ThreadCommentAnchorKind.SHEET_DRAWING) {
|
||||
return this._sheetPermissionCheckController.permissionCheckWithoutRange(
|
||||
permissionTypes,
|
||||
unitId,
|
||||
subUnitId
|
||||
);
|
||||
}
|
||||
|
||||
const { row, column } = singleReferenceToGrid(ref);
|
||||
if (!Number.isFinite(row) || !Number.isFinite(column)) {
|
||||
return false;
|
||||
}
|
||||
return this._sheetPermissionCheckController.permissionCheckWithRanges({
|
||||
...permissionTypes,
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
}, [{ startRow: row, startColumn: column, endRow: row, endColumn: column }], unitId, subUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
+37
-6
@@ -17,15 +17,16 @@
|
||||
import type { Nullable, Workbook } from '@univerjs/core';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import type { IDeleteCommentMutationParams } from '@univerjs/thread-comment';
|
||||
import type { ActiveCommentInfo } from '@univerjs/thread-comment-ui';
|
||||
import { Disposable, ICommandService, Inject, IUniverInstanceService, RANGE_TYPE, Rectangle, UniverInstanceType } from '@univerjs/core';
|
||||
import { singleReferenceToGrid } from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { RangeProtectionPermissionViewPoint, SetWorksheetActiveOperation, SheetPermissionCheckController, SheetsSelectionsService, WorkbookCommentPermission, WorksheetViewPermission } from '@univerjs/sheets';
|
||||
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
|
||||
import { IEditorBridgeService, IMarkSelectionService, ScrollToRangeOperation, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { DeleteCommentMutation } from '@univerjs/thread-comment';
|
||||
import { DeleteCommentMutation, deserializeThreadCommentAnchor, ThreadCommentAnchorKind } from '@univerjs/thread-comment';
|
||||
import { SetActiveCommentOperation, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import { combineLatest, debounceTime } from 'rxjs';
|
||||
import { SheetsThreadCommentPopupService } from '../services/sheets-thread-comment-popup.service';
|
||||
|
||||
interface ISelectionShapeInfo {
|
||||
@@ -35,6 +36,20 @@ interface ISelectionShapeInfo {
|
||||
commentId: string;
|
||||
}
|
||||
|
||||
export function resolveFocusedSheetComment(
|
||||
activeComment: ActiveCommentInfo,
|
||||
hoveredComment: ActiveCommentInfo,
|
||||
unitId: string | undefined,
|
||||
subUnitId: string | undefined
|
||||
): Exclude<ActiveCommentInfo, null | undefined> | undefined {
|
||||
for (const comment of [hoveredComment, activeComment]) {
|
||||
if (comment && comment.unitId === unitId && comment.subUnitId === subUnitId) {
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export class SheetsThreadCommentPopupController extends Disposable {
|
||||
private _isSwitchToCommenting = false;
|
||||
private _selectionShapeInfo: Nullable<ISelectionShapeInfo> = null;
|
||||
@@ -149,6 +164,10 @@ export class SheetsThreadCommentPopupController extends Disposable {
|
||||
if (!comment || comment.resolved) {
|
||||
return;
|
||||
}
|
||||
const anchor = deserializeThreadCommentAnchor(comment.ref);
|
||||
if (anchor?.kind === ThreadCommentAnchorKind.SHEET_DRAWING) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUnit = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!currentUnit) {
|
||||
@@ -209,8 +228,20 @@ export class SheetsThreadCommentPopupController extends Disposable {
|
||||
}
|
||||
|
||||
private _initMarkSelection() {
|
||||
this.disposeWithMe(this._threadCommentPanelService.activeCommentId$.pipe(debounceTime(100)).subscribe((activeComment) => {
|
||||
if (!activeComment) {
|
||||
this.disposeWithMe(combineLatest([
|
||||
this._threadCommentPanelService.activeCommentId$,
|
||||
this._threadCommentPanelService.hoveredCommentId$,
|
||||
]).pipe(debounceTime(100)).subscribe(([activeComment, hoveredComment]) => {
|
||||
const currentUnit = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const currentUnitId = currentUnit?.getUnitId();
|
||||
const currentSheetId = currentUnit?.getActiveSheet()?.getSheetId();
|
||||
const focusedComment = resolveFocusedSheetComment(
|
||||
activeComment,
|
||||
hoveredComment,
|
||||
currentUnitId,
|
||||
currentSheetId
|
||||
);
|
||||
if (!focusedComment) {
|
||||
if (this._selectionShapeInfo) {
|
||||
this._markSelectionService.removeShape(this._selectionShapeInfo.shapeId);
|
||||
this._selectionShapeInfo = null;
|
||||
@@ -218,7 +249,7 @@ export class SheetsThreadCommentPopupController extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, commentId } = activeComment;
|
||||
const { unitId, subUnitId, commentId } = focusedComment;
|
||||
if (this._selectionShapeInfo) {
|
||||
this._markSelectionService.removeShape(this._selectionShapeInfo.shapeId);
|
||||
this._selectionShapeInfo = null;
|
||||
@@ -266,7 +297,7 @@ export class SheetsThreadCommentPopupController extends Disposable {
|
||||
}
|
||||
|
||||
this._selectionShapeInfo = {
|
||||
...activeComment,
|
||||
...focusedComment,
|
||||
shapeId,
|
||||
};
|
||||
}));
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'إضافة تعليق',
|
||||
openComments: 'فتح التعليقات',
|
||||
commentManagement: 'إدارة التعليقات',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Afegeix un comentari',
|
||||
openComments: 'Obre els comentaris',
|
||||
commentManagement: 'Gestió de comentaris',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Kommentar hinzufügen',
|
||||
openComments: 'Kommentare öffnen',
|
||||
commentManagement: 'Kommentarverwaltung',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ const locale = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Add Comment',
|
||||
openComments: 'Open Comments',
|
||||
commentManagement: 'Comment Management',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Añadir comentario',
|
||||
openComments: 'Abrir comentarios',
|
||||
commentManagement: 'Gestión de comentarios',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'افزودن نظر',
|
||||
openComments: 'باز کردن نظرات',
|
||||
commentManagement: 'مدیریت نظر',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Ajouter un commentaire',
|
||||
openComments: 'Ouvrir les commentaires',
|
||||
commentManagement: 'Gestion des commentaires',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Tambah Komentar',
|
||||
openComments: 'Buka Komentar',
|
||||
commentManagement: 'Manajemen Komentar',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Aggiungi commento',
|
||||
openComments: 'Apri commenti',
|
||||
commentManagement: 'Gestione commenti',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'コメントを追加',
|
||||
openComments: 'コメントを開く',
|
||||
commentManagement: 'コメント管理',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: '댓글 추가',
|
||||
openComments: '댓글 열기',
|
||||
commentManagement: '댓글 관리',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Dodaj komentarz',
|
||||
openComments: 'Otwórz komentarze',
|
||||
commentManagement: 'Zarządzanie komentarzami',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Adicionar Comentário',
|
||||
openComments: 'Abrir comentários',
|
||||
commentManagement: 'Gerenciamento de Comentários',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Добавить комментарий',
|
||||
openComments: 'Открыть комментарии',
|
||||
commentManagement: 'Управление комментариями',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Pridať komentár',
|
||||
openComments: 'Otvoriť komentáre',
|
||||
commentManagement: 'Správa komentárov',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: 'Thêm bình luận',
|
||||
openComments: 'Mở bình luận',
|
||||
commentManagement: 'Quản lý bình luận',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: '添加评论',
|
||||
openComments: '打开评论',
|
||||
commentManagement: '评论管理',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: '新增評論',
|
||||
openComments: '開啟評論',
|
||||
commentManagement: '評論管理',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const locale: typeof enUS = {
|
||||
},
|
||||
menu: {
|
||||
addComment: '新增評論',
|
||||
openComments: '開啟評論',
|
||||
commentManagement: '評論管理',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -20,13 +20,22 @@ import { UniverInstanceType } from '@univerjs/core';
|
||||
import { RangeProtectionPermissionViewPoint, WorkbookCommentPermission, WorksheetViewPermission } from '@univerjs/sheets';
|
||||
import { getCurrentRangeDisable$, whenSheetEditorFocused } from '@univerjs/sheets-ui';
|
||||
import { getMenuHiddenObservable, KeyCode, MenuItemType, MetaKeys } from '@univerjs/ui';
|
||||
import { ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../commands/operations/comment.operation';
|
||||
import { AddSheetDrawingCommentOperation, ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../commands/operations/comment.operation';
|
||||
|
||||
export const drawingCommentMenuFactory = (accessor: IAccessor) => ({
|
||||
id: AddSheetDrawingCommentOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'InsertCommentDoubleIcon',
|
||||
title: 'sheets-thread-comment-ui.menu.addComment',
|
||||
tooltip: 'sheets-thread-comment-ui.menu.addComment',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
});
|
||||
|
||||
export const threadCommentMenuFactory = (accessor: IAccessor) => {
|
||||
return {
|
||||
id: ShowAddSheetCommentModalOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'CommentIcon',
|
||||
icon: 'InsertCommentDoubleIcon',
|
||||
title: 'sheets-thread-comment-ui.menu.addComment',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
disabled$: getCurrentRangeDisable$(accessor, {
|
||||
@@ -42,7 +51,8 @@ export const threadPanelMenuFactory = (accessor: IAccessor) => {
|
||||
id: ToggleSheetCommentPanelOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'CommentIcon',
|
||||
tooltip: 'sheets-thread-comment-ui.menu.commentManagement',
|
||||
title: 'sheets-thread-comment-ui.menu.openComments',
|
||||
tooltip: 'sheets-thread-comment-ui.menu.openComments',
|
||||
disabled$: getCurrentRangeDisable$(accessor, {
|
||||
workbookTypes: [WorkbookCommentPermission],
|
||||
worksheetTypes: [WorksheetViewPermission],
|
||||
|
||||
@@ -15,17 +15,22 @@
|
||||
*/
|
||||
|
||||
import type { MenuSchemaType } from '@univerjs/ui';
|
||||
import { ContextMenuGroup, ContextMenuPosition, RibbonInsertGroup } from '@univerjs/ui';
|
||||
import { ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../commands/operations/comment.operation';
|
||||
import { threadCommentMenuFactory, threadPanelMenuFactory } from './menu';
|
||||
import { ContextMenuGroup, ContextMenuPosition, FloatingObjectToolbarPosition, RibbonInsertGroup } from '@univerjs/ui';
|
||||
import { AddSheetDrawingCommentOperation, ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../commands/operations/comment.operation';
|
||||
import { drawingCommentMenuFactory, threadCommentMenuFactory, threadPanelMenuFactory } from './menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonInsertGroup.MEDIA]: {
|
||||
[ToggleSheetCommentPanelOperation.id]: {
|
||||
order: 2,
|
||||
gridLayout: { row: 1, column: 3, rowSpan: 2, showLabel: true },
|
||||
gridLayout: { row: 1, column: 3, showLabel: true },
|
||||
menuItemFactory: threadPanelMenuFactory,
|
||||
},
|
||||
[ShowAddSheetCommentModalOperation.id]: {
|
||||
order: 2.1,
|
||||
gridLayout: { row: 2, column: 3, showLabel: true },
|
||||
menuItemFactory: threadCommentMenuFactory,
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.MAIN_AREA]: {
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
@@ -35,4 +40,18 @@ export const menuSchema: MenuSchemaType = {
|
||||
},
|
||||
},
|
||||
},
|
||||
[FloatingObjectToolbarPosition.SHEET]: {
|
||||
[AddSheetDrawingCommentOperation.id]: {
|
||||
order: 10,
|
||||
menuItemFactory: drawingCommentMenuFactory,
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.DRAWING]: {
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
[AddSheetDrawingCommentOperation.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: drawingCommentMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,16 +17,18 @@
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import type { IUniverSheetsThreadCommentUIConfig } from './config/config';
|
||||
import { DependentOn, ICommandService, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsThreadCommentPlugin } from '@univerjs/sheets-thread-comment';
|
||||
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
||||
import { UniverThreadCommentPlugin } from '@univerjs/thread-comment';
|
||||
import { UniverThreadCommentUIPlugin } from '@univerjs/thread-comment-ui';
|
||||
import pkg from '../package.json';
|
||||
import { ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from './commands/operations/comment.operation';
|
||||
import { AddSheetDrawingCommentOperation, OpenSheetCommentPanelOperation, ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from './commands/operations/comment.operation';
|
||||
import { defaultPluginConfig, SHEETS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { SheetsThreadCommentDrawingRenderController } from './controllers/render-controllers/drawing.render-controller';
|
||||
import { SheetsThreadCommentRenderController } from './controllers/render-controllers/render.controller';
|
||||
import { SheetsThreadCommentCopyPasteController } from './controllers/sheets-thread-comment-copy-paste.controller';
|
||||
import { SheetsThreadCommentHoverController } from './controllers/sheets-thread-comment-hover.controller';
|
||||
@@ -39,6 +41,7 @@ import { PLUGIN_NAME } from './types/const';
|
||||
@DependentOn(
|
||||
UniverRenderEnginePlugin,
|
||||
UniverThreadCommentPlugin,
|
||||
UniverDrawingPlugin,
|
||||
UniverSheetsPlugin,
|
||||
UniverThreadCommentUIPlugin,
|
||||
UniverSheetsThreadCommentPlugin,
|
||||
@@ -54,6 +57,7 @@ export class UniverSheetsThreadCommentUIPlugin extends Plugin {
|
||||
private readonly _config: Partial<IUniverSheetsThreadCommentUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected override _injector: Injector,
|
||||
@Inject(ICommandService) protected _commandService: ICommandService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
@@ -86,6 +90,8 @@ export class UniverSheetsThreadCommentUIPlugin extends Plugin {
|
||||
});
|
||||
|
||||
[
|
||||
AddSheetDrawingCommentOperation,
|
||||
OpenSheetCommentPanelOperation,
|
||||
ShowAddSheetCommentModalOperation,
|
||||
ToggleSheetCommentPanelOperation,
|
||||
].forEach((command) => {
|
||||
@@ -100,6 +106,10 @@ export class UniverSheetsThreadCommentUIPlugin extends Plugin {
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._renderManagerService.registerRenderModule(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
[SheetsThreadCommentDrawingRenderController]
|
||||
);
|
||||
this._injector.get(SheetsThreadCommentCopyPasteController);
|
||||
this._injector.get(SheetsThreadCommentHoverController);
|
||||
this._injector.get(SheetsThreadCommentPopupController);
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
|
||||
import type { Workbook } from '@univerjs/core';
|
||||
import type { IThreadComment } from '@univerjs/thread-comment';
|
||||
import { ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { ICommandService, IUniverInstanceService, UniverInstanceType, UserManagerService } from '@univerjs/core';
|
||||
import { singleReferenceToGrid } from '@univerjs/engine-formula';
|
||||
import { IMarkSelectionService } from '@univerjs/sheets-ui';
|
||||
import { ThreadCommentPanel, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { deserializeThreadCommentAnchor, serializeThreadCommentAnchor, ThreadCommentAnchorKind } from '@univerjs/thread-comment';
|
||||
import { ThreadCommentDraftService, ThreadCommentPanel, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { map } from 'rxjs';
|
||||
@@ -33,6 +34,21 @@ export const SheetsThreadCommentPanel = () => {
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const unitId = workbook.getUnitId();
|
||||
const commandService = useDependency(ICommandService);
|
||||
const draftService = useDependency(ThreadCommentDraftService);
|
||||
const userManagerService = useDependency(UserManagerService);
|
||||
const draft = useObservable(draftService.draft$, draftService.draft);
|
||||
const tempDrawingComment = draft?.anchor.kind === ThreadCommentAnchorKind.SHEET_DRAWING && draft.unitId === unitId
|
||||
? {
|
||||
id: '',
|
||||
threadId: '',
|
||||
unitId: draft.unitId,
|
||||
subUnitId: draft.subUnitId,
|
||||
ref: serializeThreadCommentAnchor(draft.anchor),
|
||||
dT: '',
|
||||
personId: userManagerService.getCurrentUser().userID,
|
||||
text: { dataStream: '\r\n' },
|
||||
}
|
||||
: null;
|
||||
const subUnitId$ = useMemo(() => workbook.activeSheet$.pipe(map((i) => i?.getSheetId())), [workbook.activeSheet$]);
|
||||
const subUnitId = useObservable(subUnitId$, workbook.getActiveSheet()?.getSheetId());
|
||||
const hoverShapeId = useRef<string | null>(null);
|
||||
@@ -48,6 +64,10 @@ export const SheetsThreadCommentPanel = () => {
|
||||
|
||||
const sort = (comments: IThreadComment[]) => {
|
||||
return comments.map((comment) => {
|
||||
const anchor = deserializeThreadCommentAnchor(comment.ref);
|
||||
if (anchor?.kind === ThreadCommentAnchorKind.SHEET_DRAWING) {
|
||||
return { ...comment, p: [sheetIndex[comment.subUnitId] ?? 0, Number.MAX_SAFE_INTEGER, 0] };
|
||||
}
|
||||
const ref = singleReferenceToGrid(comment.ref);
|
||||
const p = [sheetIndex[comment.subUnitId] ?? 0, ref.row, ref.column];
|
||||
return { ...comment, p };
|
||||
@@ -70,6 +90,9 @@ export const SheetsThreadCommentPanel = () => {
|
||||
}, [workbook]);
|
||||
|
||||
const showShape = useCallback((comment: IThreadComment) => {
|
||||
if (deserializeThreadCommentAnchor(comment.ref)) {
|
||||
return null;
|
||||
}
|
||||
if (comment.unitId === unitId && comment.subUnitId === subUnitId && !comment.resolved) {
|
||||
const { row, column } = singleReferenceToGrid(comment.ref);
|
||||
const worksheet = workbook.getSheetBySheetId(comment.subUnitId);
|
||||
@@ -157,6 +180,12 @@ export const SheetsThreadCommentPanel = () => {
|
||||
handleLeave();
|
||||
return true;
|
||||
}}
|
||||
tempComment={tempDrawingComment}
|
||||
onTempCommentClose={() => draftService.cancel()}
|
||||
formatRef={(comment) => {
|
||||
const anchor = deserializeThreadCommentAnchor(comment.ref);
|
||||
return anchor?.kind === ThreadCommentAnchorKind.SHEET_DRAWING ? `#${anchor.elementId}` : comment.ref;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user