snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
# @univerjs/sheets-thread-comment-ui
[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-thread-comment-ui?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-thread-comment-ui)
[![license](https://img.shields.io/npm/l/@univerjs/sheets-thread-comment-ui?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-thread-comment-ui)
[![downloads](https://img.shields.io/npm/dm/@univerjs/sheets-thread-comment-ui?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-thread-comment-ui)
`@univerjs/sheets-thread-comment-ui` adds thread comment UI integration for Univer Sheets.
## Package Overview
| Package | UMD global | CSS | Locales | Facade entry |
| --- | --- | :---: | :---: | :---: |
| `@univerjs/sheets-thread-comment-ui` | `UniverSheetsThreadCommentUi` | Yes | Yes | No |
## Installation
```sh
pnpm add @univerjs/sheets-thread-comment-ui @univerjs/thread-comment-ui
# or
npm install @univerjs/sheets-thread-comment-ui @univerjs/thread-comment-ui
```
Keep all `@univerjs/*` packages on the same version.
## Usage
```ts
import { LocaleType, mergeLocales, Univer } from '@univerjs/core';
import { UniverSheetsThreadCommentUIPlugin } from '@univerjs/sheets-thread-comment-ui';
import SheetsThreadCommentUIEnUS from '@univerjs/sheets-thread-comment-ui/locale/en-US';
import { UniverThreadCommentUIPlugin } from '@univerjs/thread-comment-ui';
import ThreadCommentUIEnUS from '@univerjs/thread-comment-ui/locale/en-US';
import '@univerjs/sheets-thread-comment-ui/lib/index.css';
import '@univerjs/thread-comment-ui/lib/index.css';
const univer = new Univer({
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: mergeLocales(SheetsThreadCommentUIEnUS, ThreadCommentUIEnUS),
},
});
univer.registerPlugin(UniverThreadCommentUIPlugin);
univer.registerPlugin(UniverSheetsThreadCommentUIPlugin);
```
## Resources
- [Documentation](https://docs.univer.ai)
- [NPM package](https://npmjs.com/package/@univerjs/sheets-thread-comment-ui)
- [GitHub repository](https://github.com/dream-num/univer)

View File

@@ -0,0 +1,98 @@
{
"name": "@univerjs/sheets-thread-comment-ui",
"version": "1.0.0-rc.0",
"private": false,
"description": "Thread comment UI integration for Univer Sheets.",
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
"license": "Apache-2.0",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/univer"
},
"homepage": "https://univer.ai",
"repository": {
"type": "git",
"url": "https://github.com/dream-num/univer"
},
"bugs": {
"url": "https://github.com/dream-num/univer/issues"
},
"keywords": [
"univer",
"sheets",
"comment",
"thread-comment",
"ui"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*",
"./locale/*": "./src/locale/*.ts"
},
"main": "./src/index.ts",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/es/index.js",
"module": "./lib/es/index.js",
"exports": {
".": {
"import": "./lib/es/index.js",
"require": "./lib/cjs/index.js",
"types": "./lib/types/index.d.ts"
},
"./*": {
"import": "./lib/es/*",
"require": "./lib/cjs/*",
"types": "./lib/types/index.d.ts"
},
"./locale/*": {
"import": "./lib/es/locale/*.js",
"require": "./lib/cjs/locale/*.js",
"types": "./lib/types/locale/*.d.ts"
},
"./lib/*": "./lib/*"
}
},
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"build:bundle": "univer-cli build",
"build:types": "tsc -p tsconfig.node.json",
"build": "pnpm run build:bundle && pnpm run build:types"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/drawing": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "1.38.0",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-thread-comment": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/thread-comment": "workspace:*",
"@univerjs/thread-comment-ui": "workspace:*",
"@univerjs/ui": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"postcss": "^8.5.28",
"react": "18.3.1",
"rxjs": "^7.8.2",
"tailwindcss": "3.4.18",
"typescript": "^6.0.3",
"vitest": "^5.0.0"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@univerjs-infra/shared/postcss';

View File

@@ -0,0 +1,137 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
import type { ISelectionWithStyle } from '@univerjs/sheets';
import {
ICommandService,
ILogService,
Inject,
Injector,
IUniverInstanceService,
LifecycleService,
LifecycleStages,
LocaleType,
LogLevel,
Plugin,
Tools,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
import { CellPopupManagerService, ISheetClipboardService, SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
import { AddCommentMutation, DeleteCommentMutation, IThreadCommentDataSourceService, ThreadCommentDataSourceService, ThreadCommentModel } from '@univerjs/thread-comment';
import { vi } from 'vitest';
const TEST_WORKBOOK_DATA: IWorkbookData = {
id: 'test',
appVersion: '3.0.0-alpha',
locale: LocaleType.EN_US,
name: '',
sheetOrder: ['sheet1', 'sheet2'],
styles: {},
sheets: {
sheet1: {
id: 'sheet1',
name: 'Sheet1',
rowCount: 20,
columnCount: 20,
cellData: {},
},
sheet2: {
id: 'sheet2',
name: 'Sheet2',
rowCount: 20,
columnCount: 20,
cellData: {},
},
},
};
export function createThreadCommentUiTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]) {
const univer = new Univer();
const injector = univer.__getInjector();
const get = injector.get.bind(injector);
let clipboardHook: {
onBeforeCopy: (unitId: string, subUnitId: string, range: ISelectionWithStyle['range']) => void;
onPasteCells: (...args: any[]) => { redos: Array<{ id: string; params: unknown }>; undos: Array<{ id: string; params: unknown }> };
} | undefined;
const popupDisposable = {
dispose: vi.fn(),
};
const cellPopupManagerService = {
showPopup: vi.fn(() => popupDisposable),
};
const sheetClipboardService = {
addClipboardHook: vi.fn((hook) => {
clipboardHook = hook;
return {
dispose: vi.fn(),
};
}),
};
class TestPlugin extends Plugin {
static override pluginName = 'test-plugin';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: undefined,
@Inject(Injector) override readonly _injector: Injector
) {
super();
}
override onStarting(): void {
([
[IThreadCommentDataSourceService, { useClass: ThreadCommentDataSourceService }],
[ThreadCommentModel],
[SheetsThreadCommentModel],
[ISheetClipboardService, { useValue: sheetClipboardService as unknown as ISheetClipboardService }],
[CellPopupManagerService, { useValue: cellPopupManagerService as unknown as CellPopupManagerService }],
[SheetCanvasPopManagerService, { useValue: {} as SheetCanvasPopManagerService }],
] as Dependency[]).forEach((dependency) => this._injector.add(dependency));
dependencies?.forEach((dependency) => this._injector.add(dependency));
}
}
univer.registerPlugin(TestPlugin);
const workbook = univer.createUnit<IWorkbookData, Workbook>(
UniverInstanceType.UNIVER_SHEET,
Tools.deepClone(workbookData ?? TEST_WORKBOOK_DATA)
);
get(ILogService).setLogLevel(LogLevel.SILENT);
get(IUniverInstanceService).focusUnit(workbook.getUnitId());
get(LifecycleService).stage = LifecycleStages.Rendered;
get(ICommandService).registerCommand(AddCommentMutation);
get(ICommandService).registerCommand(DeleteCommentMutation);
return {
univer,
injector,
get,
workbook,
commandService: get(ICommandService),
cellPopupManagerService,
popupDisposable,
getClipboardHook: () => clipboardHook,
};
}

View File

@@ -0,0 +1,219 @@
/**
* 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 { IWorkbookData } from '@univerjs/core';
import type { ISelectionWithStyle } from '@univerjs/sheets';
import type { IThreadComment } from '@univerjs/thread-comment';
import {
DrawingTypeEnum,
ICommandService,
IUniverInstanceService,
LifecycleService,
LifecycleStages,
LocaleType,
toDisposable,
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';
import {
AddCommentMutation,
IThreadCommentDataSourceService,
ThreadCommentDataSourceService,
ThreadCommentModel,
} from '@univerjs/thread-comment';
import { ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
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 { AddSheetDrawingCommentOperation, ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../comment.operation';
const unitId = 'comment-workbook';
const subUnitId = 'sheet-1';
const workbookData: IWorkbookData = {
id: unitId,
appVersion: '3.0.0-alpha',
name: 'comment workbook',
locale: LocaleType.EN_US,
sheetOrder: [subUnitId],
styles: {},
sheets: {
[subUnitId]: {
id: subUnitId,
name: 'Sheet 1',
cellData: {},
},
},
};
class TestCellPopupManagerService {
showPopup() {
return toDisposable(() => {});
}
}
class TestSheetCanvasPopManagerService {}
function createSelection(row: number, col: number): ISelectionWithStyle {
return {
range: {
startRow: row,
endRow: row,
startColumn: col,
endColumn: col,
},
primary: {
startRow: row,
endRow: row,
startColumn: col,
endColumn: col,
actualRow: row,
actualColumn: col,
isMerged: false,
isMergedMainCell: false,
},
style: null,
};
}
function createComment(id: string, ref: string): IThreadComment {
return {
id,
threadId: id,
ref,
unitId,
subUnitId,
dT: '2026-06-17T00:00:00.000Z',
personId: 'user-1',
text: {
dataStream: 'Review this cell.\r\n',
},
};
}
describe('sheet thread comment operations', () => {
let univer: Univer;
let commandService: ICommandService;
let selectionsService: SheetsSelectionsService;
let popupService: SheetsThreadCommentPopupService;
let panelService: ThreadCommentPanelService;
let sidebarService: ISidebarService;
beforeEach(() => {
univer = new Univer();
const injector = univer.__getInjector();
injector.add([SheetsSelectionsService]);
injector.add([IThreadCommentDataSourceService, { useClass: ThreadCommentDataSourceService }]);
injector.add([ThreadCommentModel]);
injector.add([SheetsThreadCommentModel]);
injector.add([CellPopupManagerService, { useClass: TestCellPopupManagerService as never }]);
injector.add([SheetCanvasPopManagerService, { useClass: TestSheetCanvasPopManagerService as never }]);
injector.add([SheetsThreadCommentPopupService]);
injector.add([ISidebarService, { useClass: DesktopSidebarService }]);
injector.add([ThreadCommentPanelService]);
univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookData);
injector.get(IUniverInstanceService).focusUnit(unitId);
injector.get(LifecycleService).stage = LifecycleStages.Rendered;
commandService = injector.get(ICommandService);
commandService.registerCommand(AddCommentMutation);
commandService.registerCommand(ShowAddSheetCommentModalOperation);
commandService.registerCommand(ToggleSheetCommentPanelOperation);
selectionsService = injector.get(SheetsSelectionsService);
popupService = injector.get(SheetsThreadCommentPopupService);
panelService = injector.get(ThreadCommentPanelService);
sidebarService = injector.get(ISidebarService);
});
afterEach(() => {
univer.dispose();
});
it('opens the add-comment popup at the active cell and activates the existing thread', async () => {
await commandService.executeCommand(AddCommentMutation.id, {
unitId,
subUnitId,
comment: createComment('comment-1', 'D3'),
});
selectionsService.setSelections(unitId, subUnitId, [createSelection(2, 3)]);
const result = await commandService.executeCommand(ShowAddSheetCommentModalOperation.id);
expect(result).toBe(true);
expect(popupService.activePopup).toMatchObject({
unitId,
subUnitId,
row: 2,
col: 3,
});
expect(panelService.activeCommentId).toEqual({
unitId,
subUnitId,
commentId: 'comment-1',
trigger: 'context-menu',
});
});
it('does not open the add-comment popup without an active cell', async () => {
const result = await commandService.executeCommand(ShowAddSheetCommentModalOperation.id);
expect(result).toBe(false);
expect(popupService.activePopup).toBeUndefined();
expect(panelService.activeCommentId).toBeUndefined();
});
it('toggles the sheet comment panel through the sidebar', async () => {
const openResult = await commandService.executeCommand(ToggleSheetCommentPanelOperation.id);
expect(openResult).toBe(true);
expect(panelService.panelVisible).toBe(true);
expect(sidebarService.visible).toBe(true);
expect(sidebarService.options.children?.label).toBe(SHEETS_THREAD_COMMENT_PANEL);
const closeResult = await commandService.executeCommand(ToggleSheetCommentPanelOperation.id);
expect(closeResult).toBe(true);
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();
});
});

View File

@@ -0,0 +1,153 @@
/**
* 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 { IOperation, Workbook } from '@univerjs/core';
import type { ISheetLocation } from '@univerjs/sheets';
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 { 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, SHOW_ADD_SHEET_COMMENT_OPERATION_ID } from '../../types/const';
export const ShowAddSheetCommentModalOperation: IOperation = {
type: CommandType.OPERATION,
id: SHOW_ADD_SHEET_COMMENT_OPERATION_ID,
handler(accessor) {
const selectionManagerService = accessor.get(SheetsSelectionsService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const sheetsThreadCommentPopupService = accessor.get(SheetsThreadCommentPopupService);
const threadCommentPanelService = accessor.get(ThreadCommentPanelService);
const activeCell = selectionManagerService.getCurrentLastSelection()?.primary;
const model = accessor.get(SheetsThreadCommentModel);
if (!activeCell) {
return false;
}
const result = getSheetCommandTarget(univerInstanceService);
if (!result) {
return false;
}
const { workbook, worksheet, unitId, subUnitId } = result;
const location: ISheetLocation = {
workbook,
worksheet,
unitId,
subUnitId,
row: activeCell.startRow,
col: activeCell.startColumn,
};
sheetsThreadCommentPopupService.showPopup(location);
const rootId = model.getByLocation(unitId, subUnitId, activeCell.startRow, activeCell.startColumn);
if (rootId) {
threadCommentPanelService.setActiveComment({
unitId,
subUnitId,
commentId: rootId,
trigger: 'context-menu',
});
}
return true;
},
};
export const ToggleSheetCommentPanelOperation: IOperation = {
id: 'sheet.operation.toggle-comment-panel',
type: CommandType.OPERATION,
handler(accessor) {
const sidebarService = accessor.get(ISidebarService);
const panelService = accessor.get(ThreadCommentPanelService);
if (panelService.panelVisible) {
sidebarService.close();
panelService.setPanelVisible(false);
} else {
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;
},
};
export 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;
},
};

View File

@@ -0,0 +1,64 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IOperation } from '@univerjs/core';
import { CommandType, IUniverInstanceService } from '@univerjs/core';
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
import { ThreadCommentDraftService, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
import { ISidebarService } from '@univerjs/ui';
import { MobileSheetCommentDraftService } from '../../../services/mobile/mobile-sheet-comment-draft.service';
import { SHOW_ADD_SHEET_COMMENT_OPERATION_ID } from '../../../types/const';
import { openSheetCommentPanel } from '../comment.operation';
export const ShowAddSheetCommentMobileOperation: IOperation = {
type: CommandType.OPERATION,
id: SHOW_ADD_SHEET_COMMENT_OPERATION_ID,
handler(accessor) {
const activeCell = accessor.get(SheetsSelectionsService).getCurrentLastSelection()?.primary;
if (!activeCell) {
return false;
}
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
if (!target) {
return false;
}
const { unitId, subUnitId } = target;
const row = activeCell.startRow;
const column = activeCell.startColumn;
const panelService = accessor.get(ThreadCommentPanelService);
const mobileDraftService = accessor.get(MobileSheetCommentDraftService);
const rootId = accessor.get(SheetsThreadCommentModel).getByLocation(unitId, subUnitId, row, column);
accessor.get(ThreadCommentDraftService).cancel();
if (rootId) {
mobileDraftService.cancel();
panelService.setActiveComment({
unitId,
subUnitId,
commentId: rootId,
});
} else {
panelService.setActiveComment(undefined);
mobileDraftService.place({ unitId, subUnitId, row, column });
}
openSheetCommentPanel(accessor.get(ISidebarService), panelService);
return true;
},
};

View File

@@ -0,0 +1,29 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { DependencyOverride } from '@univerjs/core';
import type { MenuConfig } from '@univerjs/ui';
export const SHEETS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY = 'sheets-thread-comment.config';
export const configSymbol = Symbol(SHEETS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY);
export interface IUniverSheetsThreadCommentUIConfig {
menu?: MenuConfig;
overrides?: DependencyOverride;
}
export const defaultPluginConfig: IUniverSheetsThreadCommentUIConfig = {};

View File

@@ -0,0 +1,157 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { COPY_TYPE } from '@univerjs/sheets-ui';
import { AddCommentMutation, DeleteCommentMutation } from '@univerjs/thread-comment';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createThreadCommentUiTestBed } from '../../__tests__/create-thread-comment-ui-test-bed';
import { SheetsThreadCommentCopyPasteController } from '../sheets-thread-comment-copy-paste.controller';
function createRootComment() {
return {
id: 'comment-1',
threadId: 'thread-1',
ref: 'A1',
dT: '2025-01-01T00:00:00.000Z',
personId: 'user-1',
text: { dataStream: 'hello\r\n' },
attachments: [],
unitId: 'test',
subUnitId: 'sheet1',
};
}
describe('SheetsThreadCommentCopyPasteController', () => {
let testBed: ReturnType<typeof createThreadCommentUiTestBed>;
beforeEach(() => {
testBed = createThreadCommentUiTestBed();
});
afterEach(() => {
testBed.univer.dispose();
});
it('creates delete/add mutations when cutting comments to another sheet', async () => {
await testBed.commandService.executeCommand(AddCommentMutation.id, {
unitId: 'test',
subUnitId: 'sheet1',
comment: createRootComment(),
});
const controller = testBed.injector.createInstance(SheetsThreadCommentCopyPasteController);
const hook = testBed.getClipboardHook();
expect(hook).toBeDefined();
hook!.onBeforeCopy('test', 'sheet1', {
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
});
const result = hook!.onPasteCells(
null,
{
unitId: 'test',
subUnitId: 'sheet2',
range: {
rows: [2],
cols: [3],
},
},
null,
{
copyType: COPY_TYPE.CUT,
}
);
expect(result.redos).toEqual([
{
id: DeleteCommentMutation.id,
params: {
unitId: 'test',
subUnitId: 'sheet1',
commentId: 'comment-1',
},
},
{
id: AddCommentMutation.id,
params: expect.objectContaining({
unitId: 'test',
subUnitId: 'sheet2',
sync: true,
comment: expect.objectContaining({
id: 'comment-1',
ref: expect.stringContaining('D3'),
unitId: 'test',
subUnitId: 'sheet2',
}),
}),
},
]);
expect(result.undos).toEqual([
{
id: DeleteCommentMutation.id,
params: {
unitId: 'test',
subUnitId: 'sheet2',
commentId: 'comment-1',
},
},
{
id: AddCommentMutation.id,
params: expect.objectContaining({
unitId: 'test',
subUnitId: 'sheet1',
sync: true,
comment: expect.objectContaining({
id: 'comment-1',
ref: 'A1',
}),
}),
},
]);
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();
});
});

View File

@@ -0,0 +1,107 @@
/**
* 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 { Subject } from 'rxjs';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { SheetsThreadCommentHoverController } from '../sheets-thread-comment-hover.controller';
afterEach(() => {
vi.useRealTimers();
});
function createController(options?: { activePopup?: unknown; canView?: boolean; resolved?: boolean }) {
const currentCell$ = new Subject<any>();
const popupService = {
activePopup: options?.activePopup ?? null,
showPopup: vi.fn(),
hidePopup: vi.fn(),
};
const controller = new SheetsThreadCommentHoverController(
{ currentCell$ } as never,
popupService as never,
{
getByLocation: vi.fn(() => 'comment-1'),
getComment: vi.fn(() => ({ id: 'comment-1', resolved: options?.resolved ?? false })),
} as never,
{ permissionCheckWithRanges: vi.fn(() => options?.canView ?? true) } as never
);
return { controller, currentCell$, popupService };
}
describe('SheetsThreadCommentHoverController', () => {
it('shows a temporary popup when hovering an unresolved visible comment marker', async () => {
vi.useFakeTimers();
const { controller, currentCell$, popupService } = createController();
currentCell$.next({
location: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 2,
col: 3,
},
});
await vi.advanceTimersByTimeAsync(120);
expect(popupService.showPopup).toHaveBeenCalledWith({
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 2,
col: 3,
commentId: 'comment-1',
temp: true,
});
controller.dispose();
});
it('does not replace a persistent popup and hides an existing temp popup when no marker exists', async () => {
vi.useFakeTimers();
const { controller, currentCell$, popupService } = createController({ activePopup: { temp: false } });
currentCell$.next({
location: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 2,
col: 3,
},
});
await vi.advanceTimersByTimeAsync(120);
expect(popupService.showPopup).not.toHaveBeenCalled();
controller.dispose();
const noMarkerCell$ = new Subject<any>();
const tempPopupService = {
activePopup: { temp: true },
showPopup: vi.fn(),
hidePopup: vi.fn(),
};
const noMarkerController = new SheetsThreadCommentHoverController(
{ currentCell$: noMarkerCell$ } as never,
tempPopupService as never,
{ getByLocation: vi.fn(() => null), getComment: vi.fn() } as never,
{ permissionCheckWithRanges: vi.fn(() => true) } as never
);
noMarkerCell$.next({ location: { unitId: 'unit-1', subUnitId: 'sheet-1', row: 2, col: 3 } });
await vi.advanceTimersByTimeAsync(120);
expect(tempPopupService.hidePopup).toHaveBeenCalled();
noMarkerController.dispose();
});
});

View File

@@ -0,0 +1,152 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ICommandService, Injector, 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 { AddSheetDrawingCommentOperation, 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', () => {
const permissionCheck = {
permissionCheckWithoutRange: vi.fn(() => false),
permissionCheckWithRanges: vi.fn(() => false),
blockExecuteWithoutPermission: vi.fn(),
};
const { controller, getBeforeCommandHandler } = createTestBed(
permissionCheck,
{
getComment: vi.fn(() => ({ id: 'comment-1', ref: 'C4' })),
}
);
const beforeCommandHandler = getBeforeCommandHandler();
beforeCommandHandler?.({ id: ShowAddSheetCommentModalOperation.id });
beforeCommandHandler?.({ id: AddSheetDrawingCommentOperation.id });
beforeCommandHandler?.({
id: AddCommentCommand.id,
params: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
comment: { ref: 'B2' },
},
});
beforeCommandHandler?.({
id: UpdateCommentCommand.id,
params: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
payload: { commentId: 'comment-1' },
},
});
beforeCommandHandler?.({
id: DeleteCommentCommand.id,
params: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
commentId: 'comment-1',
},
});
expect(permissionCheck.permissionCheckWithoutRange).toHaveBeenCalledTimes(2);
expect(permissionCheck.permissionCheckWithRanges).toHaveBeenCalledWith(expect.any(Object), [{
startRow: 1,
startColumn: 1,
endRow: 1,
endColumn: 1,
}], 'unit-1', 'sheet-1');
expect(permissionCheck.permissionCheckWithRanges).toHaveBeenCalledWith(expect.any(Object), [{
startRow: 3,
startColumn: 2,
endRow: 3,
endColumn: 2,
}], 'unit-1', 'sheet-1');
expect(permissionCheck.blockExecuteWithoutPermission).toHaveBeenCalledTimes(5);
expect(permissionCheck.blockExecuteWithoutPermission).toHaveBeenLastCalledWith('sheets-thread-comment-ui.permission.commentErr');
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();
});
});

View File

@@ -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();
});
});

View File

@@ -0,0 +1,52 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable, Inject } from '@univerjs/core';
import { CommentIcon, InsertCommentDoubleIcon } from '@univerjs/icons';
import { ComponentManager, IconManager } from '@univerjs/ui';
import { SHEETS_THREAD_COMMENT_MODAL, SHEETS_THREAD_COMMENT_PANEL } from '../types/const';
import { SheetsThreadCommentCell } from '../views/SheetsThreadCommentCell';
import { SheetsThreadCommentPanel } from '../views/SheetsThreadCommentPanel';
export class ComponentsController extends Disposable {
constructor(
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
@Inject(IconManager) private readonly _iconManager: IconManager
) {
super();
this._registerComponents();
this._registerIcons();
}
private _registerComponents(): void {
([
[SHEETS_THREAD_COMMENT_MODAL, SheetsThreadCommentCell],
[SHEETS_THREAD_COMMENT_PANEL, SheetsThreadCommentPanel],
] as const).forEach(([key, comp]) => {
this.disposeWithMe(
this._componentManager.register(key, comp)
);
});
}
private _registerIcons(): void {
this.disposeWithMe(this._iconManager.register({
CommentIcon,
InsertCommentDoubleIcon,
}));
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable, Inject } from '@univerjs/core';
import { CommentIcon, InsertCommentDoubleIcon } from '@univerjs/icons';
import { ComponentManager, IconManager } from '@univerjs/ui';
import { SHEETS_THREAD_COMMENT_PANEL } from '../../types/const';
import { MobileSheetsThreadCommentPanel } from '../../views/mobile/MobileSheetsThreadCommentPanel';
export class MobileComponentsController extends Disposable {
constructor(
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
@Inject(IconManager) private readonly _iconManager: IconManager
) {
super();
this.disposeWithMe(this._componentManager.register(SHEETS_THREAD_COMMENT_PANEL, MobileSheetsThreadCommentPanel));
this.disposeWithMe(this._iconManager.register({ CommentIcon, InsertCommentDoubleIcon }));
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable } from '@univerjs/core';
import { IMenuManagerService } from '@univerjs/ui';
import { mobileMenuSchema } from '../../menu/mobile-schema';
export class SheetsThreadCommentMobileController extends Disposable {
constructor(
@IMenuManagerService menuManagerService: IMenuManagerService
) {
super();
menuManagerService.mergeMenu(mobileMenuSchema);
}
}

View File

@@ -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();
});
});

View File

@@ -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'),
};
}
}

View File

@@ -0,0 +1,81 @@
/**
* 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 { Workbook } from '@univerjs/core';
import { Disposable, Inject, InterceptorEffectEnum, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
import { debounceTime } from 'rxjs';
export class SheetsThreadCommentRenderController extends Disposable {
constructor(
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(SheetsThreadCommentModel) private readonly _sheetsThreadCommentModel: SheetsThreadCommentModel,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
) {
super();
this._initViewModelIntercept();
this._initSkeletonChange();
}
private _initViewModelIntercept() {
this.disposeWithMe(
this._sheetInterceptorService.intercept(
INTERCEPTOR_POINT.CELL_CONTENT,
{
effect: InterceptorEffectEnum.Style,
handler: (cell, pos, next) => {
const { row, col, unitId, subUnitId } = pos;
if (this._sheetsThreadCommentModel.showCommentMarker(unitId, subUnitId, row, col)) {
if (!cell || cell === pos.rawData) {
cell = { ...pos.rawData };
}
cell.markers = {
...cell?.markers,
tr: {
color: '#FFBD37',
size: 6,
},
};
return next(cell);
}
return next(cell);
},
priority: 100,
}
)
);
}
private _initSkeletonChange() {
const markSkeletonDirty = () => {
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) return;
const unitId = workbook.getUnitId();
const currentRender = this._renderManagerService.getRenderUnitById(unitId);
currentRender?.mainComponent?.makeForceDirty();
};
this.disposeWithMe(this._sheetsThreadCommentModel.commentUpdate$.pipe(debounceTime(16)).subscribe(() => {
markSkeletonDirty();
}));
}
}

View File

@@ -0,0 +1,165 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IMutationInfo, IRange, Nullable } from '@univerjs/core';
import type { IThreadComment } from '@univerjs/thread-comment';
import { Disposable, Inject, Range } from '@univerjs/core';
import { serializeRange, singleReferenceToGrid } from '@univerjs/engine-formula';
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
import { COPY_TYPE, ISheetClipboardService } from '@univerjs/sheets-ui';
import { AddCommentMutation, DeleteCommentMutation, IThreadCommentDataSourceService } from '@univerjs/thread-comment';
import { PLUGIN_NAME } from '../types/const';
const transformRef = (ref: string, source: { row: number; column: number }, target: { row: number; column: number }) => {
const refObj = singleReferenceToGrid(ref);
const offsetRow = target.row - source.row;
const offsetCol = target.column - source.column;
const targetRange = {
startColumn: refObj.column + offsetCol,
startRow: refObj.row + offsetRow,
endColumn: refObj.column + offsetCol,
endRow: refObj.row + offsetRow,
};
return serializeRange(targetRange);
};
export class SheetsThreadCommentCopyPasteController extends Disposable {
private _copyInfo: Nullable<{
unitId: string;
subUnitId: string;
range: IRange;
}>;
constructor(
@Inject(ISheetClipboardService) private _sheetClipboardService: ISheetClipboardService,
@Inject(SheetsThreadCommentModel) private _sheetsThreadCommentModel: SheetsThreadCommentModel,
@IThreadCommentDataSourceService private _threadCommentDataSourceService: IThreadCommentDataSourceService
) {
super();
this._initClipboardHook();
}
// eslint-disable-next-line max-lines-per-function
private _initClipboardHook() {
this.disposeWithMe(
this._sheetClipboardService.addClipboardHook({
id: PLUGIN_NAME,
onBeforeCopy: (unitId, subUnitId, range) => {
this._copyInfo = {
unitId,
subUnitId,
range,
};
},
// eslint-disable-next-line max-lines-per-function
onPasteCells: (_pasteFrom, pasteTo, _data, payload) => {
const { unitId: targetUnitId, subUnitId: targetSubUnitId, range } = pasteTo;
const targetPos = {
row: range.rows[0],
column: range.cols[0],
};
if (payload.copyType === COPY_TYPE.CUT && this._copyInfo) {
const { range, unitId: sourceUnitId, subUnitId: sourceSubUnitId } = this._copyInfo;
const sourcePos = {
row: range.startRow,
column: range.startColumn,
};
if (!(targetUnitId === sourceUnitId && targetSubUnitId === sourceSubUnitId)) {
const roots: IThreadComment[] = [];
Range.foreach(range, (row, col) => {
const comments = this._sheetsThreadCommentModel.getAllByLocation(sourceUnitId, sourceSubUnitId, row, col);
if (this._threadCommentDataSourceService.syncUpdateMutationToColla) {
comments.forEach((comment) => {
roots.push(comment);
});
} else {
comments.forEach(({ children, ...comment }) => {
if (!comment.parentId) {
roots.push(comment);
}
});
}
});
const sourceRedos: IMutationInfo[] = [];
const sourceUndos: IMutationInfo[] = [];
const targetRedos: IMutationInfo[] = [];
const targetUndos: IMutationInfo[] = [];
const handleCommentItem = (item: IThreadComment) => {
sourceRedos.unshift({
id: DeleteCommentMutation.id,
params: {
unitId: sourceUnitId,
subUnitId: sourceSubUnitId,
commentId: item.id,
},
});
targetRedos.push({
id: AddCommentMutation.id,
params: {
unitId: targetUnitId,
subUnitId: targetSubUnitId,
comment: {
...item,
ref: transformRef(item.ref, sourcePos, targetPos),
unitId: targetUnitId,
subUnitId: targetSubUnitId,
},
sync: true,
},
});
sourceUndos.push({
id: AddCommentMutation.id,
params: {
unitId: sourceUnitId,
subUnitId: sourceSubUnitId,
comment: item,
sync: true,
},
});
targetUndos.unshift({
id: DeleteCommentMutation.id,
params: {
unitId: targetUnitId,
subUnitId: targetSubUnitId,
commentId: item.id,
},
});
};
roots.forEach((root) => {
handleCommentItem(root);
});
return {
redos: [...sourceRedos, ...targetRedos],
undos: [...targetUndos, ...sourceUndos],
};
}
}
return {
redos: [],
undos: [],
};
},
})
);
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable, Inject } from '@univerjs/core';
import { RangeProtectionPermissionViewPoint, SheetPermissionCheckController, WorkbookCommentPermission, WorksheetViewPermission } from '@univerjs/sheets';
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
import { HoverManagerService } from '@univerjs/sheets-ui';
import { debounceTime } from 'rxjs';
import { SheetsThreadCommentPopupService } from '../services/sheets-thread-comment-popup.service';
export class SheetsThreadCommentHoverController extends Disposable {
constructor(
@Inject(HoverManagerService) private readonly _hoverManagerService: HoverManagerService,
@Inject(SheetsThreadCommentPopupService) private readonly _sheetsThreadCommentPopupService: SheetsThreadCommentPopupService,
@Inject(SheetsThreadCommentModel) private readonly _sheetsThreadCommentModel: SheetsThreadCommentModel,
@Inject(SheetPermissionCheckController) private readonly _sheetPermissionCheckController: SheetPermissionCheckController
) {
super();
this._initHoverEvent();
}
private _initHoverEvent() {
this.disposeWithMe(
this._hoverManagerService.currentCell$.pipe(debounceTime(100)).subscribe((cell) => {
const currentPopup = this._sheetsThreadCommentPopupService.activePopup;
if (cell && ((currentPopup && currentPopup.temp) || !currentPopup)) {
const { location } = cell;
const { unitId, subUnitId, row, col } = location;
const commentId = this._sheetsThreadCommentModel.getByLocation(unitId, subUnitId, row, col);
if (commentId) {
const commentPermission = this._sheetPermissionCheckController.permissionCheckWithRanges({
workbookTypes: [WorkbookCommentPermission],
worksheetTypes: [WorksheetViewPermission],
rangeTypes: [RangeProtectionPermissionViewPoint],
}, [{ startRow: row, startColumn: col, endRow: row, endColumn: col }], unitId, subUnitId);
if (!commentPermission) {
return;
}
const comment = this._sheetsThreadCommentModel.getComment(unitId, subUnitId, commentId);
if (comment && !comment.resolved) {
this._sheetsThreadCommentPopupService.showPopup({
unitId,
subUnitId,
row,
col,
commentId,
temp: true,
});
}
} else {
if (currentPopup) {
this._sheetsThreadCommentPopupService.hidePopup();
}
}
}
})
);
}
}

View File

@@ -0,0 +1,122 @@
/**
* 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 { ICommandInfo } from '@univerjs/core';
import type {
IAddCommentCommandParams,
IDeleteCommentCommandParams,
IDeleteCommentTreeCommandParams,
IResolveCommentCommandParams,
IUpdateCommentCommandParams,
} from '@univerjs/thread-comment';
import type { LocaleKey } from '../locale/types';
import { Disposable, ICommandService, Inject, LocaleService } from '@univerjs/core';
import { singleReferenceToGrid } from '@univerjs/engine-formula';
import {
RangeProtectionPermissionViewPoint,
SheetPermissionCheckController,
WorkbookCommentPermission,
WorksheetViewPermission,
} from '@univerjs/sheets';
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
import {
AddCommentCommand,
DeleteCommentCommand,
DeleteCommentTreeCommand,
deserializeThreadCommentAnchor,
ResolveCommentCommand,
ThreadCommentAnchorKind,
UpdateCommentCommand,
} from '@univerjs/thread-comment';
import { AddSheetDrawingCommentOperation, ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../commands/operations/comment.operation';
export class SheetsThreadCommentPermissionController extends Disposable {
constructor(
@Inject(LocaleService) private _localeService: LocaleService,
@ICommandService private readonly _commandService: ICommandService,
@Inject(SheetPermissionCheckController) private readonly _sheetPermissionCheckController: SheetPermissionCheckController,
@Inject(SheetsThreadCommentModel) private readonly _sheetsThreadCommentModel: SheetsThreadCommentModel
) {
super();
this._commandExecutedListener();
}
private _commandExecutedListener() {
this.disposeWithMe(
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
const { id } = command;
if (id === AddSheetDrawingCommentOperation.id || id === ShowAddSheetCommentModalOperation.id || id === ToggleSheetCommentPanelOperation.id) {
const permission = this._sheetPermissionCheckController.permissionCheckWithoutRange({
workbookTypes: [WorkbookCommentPermission],
worksheetTypes: [WorksheetViewPermission],
});
if (!permission) {
this._sheetPermissionCheckController.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-thread-comment-ui.permission.commentErr'));
}
} else if (id === AddCommentCommand.id) {
const params = command.params as IAddCommentCommandParams;
const { unitId, subUnitId, comment } = params;
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) {
const params = command.params as IUpdateCommentCommandParams;
const { unitId, subUnitId, payload } = params;
const { commentId } = payload;
const comment = this._sheetsThreadCommentModel.getComment(unitId, subUnitId, commentId);
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 && !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);
}
}

View File

@@ -0,0 +1,305 @@
/**
* 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 { 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, deserializeThreadCommentAnchor, ThreadCommentAnchorKind } from '@univerjs/thread-comment';
import { SetActiveCommentOperation, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
import { combineLatest, debounceTime } from 'rxjs';
import { SheetsThreadCommentPopupService } from '../services/sheets-thread-comment-popup.service';
interface ISelectionShapeInfo {
shapeId: string;
unitId: string;
subUnitId: string;
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;
constructor(
@ICommandService private readonly _commandService: ICommandService,
@Inject(SheetsThreadCommentPopupService) private readonly _sheetsThreadCommentPopupService: SheetsThreadCommentPopupService,
@Inject(SheetsThreadCommentModel) private readonly _sheetsThreadCommentModel: SheetsThreadCommentModel,
@Inject(ThreadCommentPanelService) private readonly _threadCommentPanelService: ThreadCommentPanelService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(SheetPermissionCheckController) private readonly _sheetPermissionCheckController: SheetPermissionCheckController,
@IMarkSelectionService private readonly _markSelectionService: IMarkSelectionService,
@Inject(SheetsSelectionsService) private readonly _sheetSelectionService: SheetsSelectionsService,
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
) {
super();
this._initCommandListener();
this._initPanelListener();
this._initMarkSelection();
this._initSelectionUpdateListener();
this._initEditorBridge();
}
private _handleSelectionChange(selections: ISelectionWithStyle[], unitId: string, subUnitId: string) {
const range = selections[0]?.range;
const render = this._renderManagerService.getRenderUnitById(unitId);
const skeleton = render?.with(SheetSkeletonManagerService).getSkeletonParam(subUnitId)?.skeleton;
if (!skeleton) {
return;
}
if (!range) {
return;
}
const actualCell = skeleton.getCellWithCoordByIndex(range.startRow, range.startColumn);
const rangeType = range.rangeType ?? RANGE_TYPE.NORMAL;
if ((rangeType !== RANGE_TYPE.NORMAL || range.endColumn - range.startColumn > 0 || range.endRow - range.startRow > 0) && !((actualCell.isMerged || actualCell.isMergedMainCell) && Rectangle.equals(actualCell.mergeInfo, range))) {
if (this._threadCommentPanelService.activeCommentId) {
this._commandService.executeCommand(SetActiveCommentOperation.id);
}
return;
}
const row = actualCell.actualRow;
const col = actualCell.actualColumn;
if (!this._sheetsThreadCommentModel.showCommentMarker(unitId, subUnitId, row, col)) {
if (this._threadCommentPanelService.activeCommentId) {
this._commandService.executeCommand(SetActiveCommentOperation.id);
}
return;
}
const commentId = this._sheetsThreadCommentModel.getByLocation(unitId, subUnitId, row, col);
if (commentId) {
this._commandService.executeCommand(SetActiveCommentOperation.id, {
unitId,
subUnitId,
commentId,
});
}
}
private _initSelectionUpdateListener() {
this.disposeWithMe(
this._sheetSelectionService.selectionMoveEnd$.subscribe((selections) => {
if (this._isSwitchToCommenting) {
return;
}
const current = this._sheetSelectionService.currentSelectionParam;
if (!current) {
return;
}
this._handleSelectionChange(selections, current.unitId, current.sheetId);
})
);
}
private _initEditorBridge() {
this.disposeWithMe(
this._editorBridgeService.visible$.subscribe((visible) => {
if (visible.visible) {
this._sheetsThreadCommentPopupService.hidePopup();
}
})
);
}
private _initCommandListener() {
this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === DeleteCommentMutation.id) {
const params = commandInfo.params as IDeleteCommentMutationParams;
const active = this._sheetsThreadCommentPopupService.activePopup;
if (!active) {
return;
}
const { unitId, subUnitId, commentId } = active;
if (params.unitId === unitId && params.subUnitId === subUnitId && params.commentId === commentId) {
this._sheetsThreadCommentPopupService.hidePopup();
}
}
});
}
private _initPanelListener() {
this.disposeWithMe(this._threadCommentPanelService.activeCommentId$.subscribe(async (commentInfo) => {
if (commentInfo) {
const { unitId, subUnitId, commentId, trigger } = commentInfo;
const comment = this._sheetsThreadCommentModel.getComment(unitId, subUnitId, commentId);
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) {
return;
}
const currentUnitId = currentUnit.getUnitId();
if (currentUnitId !== unitId) {
return;
}
this._isSwitchToCommenting = true;
const currentSheetId = currentUnit.getActiveSheet()?.getSheetId();
if (currentSheetId !== subUnitId) {
await this._commandService.executeCommand(SetWorksheetActiveOperation.id, {
unitId,
subUnitId,
});
}
this._isSwitchToCommenting = false;
const location = singleReferenceToGrid(comment.ref);
const { row, column: col } = location;
const commentPermission = this._sheetPermissionCheckController.permissionCheckWithRanges({
workbookTypes: [WorkbookCommentPermission],
worksheetTypes: [WorksheetViewPermission],
rangeTypes: [RangeProtectionPermissionViewPoint],
}, [{ startRow: row, startColumn: col, endRow: row, endColumn: col }], unitId, subUnitId);
if (!commentPermission) {
return;
}
const GAP = 1;
await this._commandService.executeCommand(ScrollToRangeOperation.id, {
range: {
startRow: Math.max(location.row - GAP, 0),
endRow: location.row + GAP,
startColumn: Math.max(location.column - GAP, 0),
endColumn: location.column + GAP,
},
});
if (this._editorBridgeService.isVisible().visible) {
return;
}
this._sheetsThreadCommentPopupService.showPopup({
unitId,
subUnitId,
row: location.row,
col: location.column,
commentId: comment.id,
trigger,
});
} else {
this._sheetsThreadCommentPopupService.hidePopup();
}
}));
}
private _initMarkSelection() {
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;
}
return;
}
const { unitId, subUnitId, commentId } = focusedComment;
if (this._selectionShapeInfo) {
this._markSelectionService.removeShape(this._selectionShapeInfo.shapeId);
this._selectionShapeInfo = null;
}
const comment = this._sheetsThreadCommentModel.getComment(unitId, subUnitId, commentId);
if (!comment) {
return;
}
const location = singleReferenceToGrid(comment.ref);
const { row, column } = location;
if (Number.isNaN(row) || Number.isNaN(column)) {
return null;
}
const worksheet = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId(subUnitId);
const mergeInfo = worksheet?.getMergedCell(row, column) ?? {
startColumn: column,
endColumn: column,
startRow: row,
endRow: row,
};
// TODO: use evented: false to solve this problem later
const shapeId = this._markSelectionService.addShape(
{
range: mergeInfo,
style: {
// hasAutoFill: false,
fill: 'rgba(255, 189, 55, 0.35)',
strokeWidth: 1,
stroke: '#FFBD37',
widgets: {},
},
primary: null,
},
[],
-1
);
if (!shapeId) {
return;
}
this._selectionShapeInfo = {
...focusedComment,
shapeId,
};
}));
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable } from '@univerjs/core';
import { IMenuManagerService, IShortcutService } from '@univerjs/ui';
import { AddCommentShortcut } from '../menu/menu';
import { menuSchema } from '../menu/schema';
export class SheetsThreadCommentController extends Disposable {
constructor(
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
@IShortcutService private readonly _shortcutService: IShortcutService
) {
super();
this._initMenu();
this._initShortcut();
}
private _initShortcut() {
this._shortcutService.registerShortcut(AddCommentShortcut);
}
private _initMenu() {
this._menuManagerService.mergeMenu(menuSchema);
}
}

View File

@@ -0,0 +1,2 @@
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './global.css';
export {
ShowAddSheetCommentModalOperation,
ToggleSheetCommentPanelOperation,
} from './commands/operations/comment.operation';
export type { IUniverSheetsThreadCommentUIConfig } from './config/config';
export { menuSchema as SheetsThreadCommentUIMenuSchema } from './menu/schema';
export { UniverSheetsThreadCommentMobileUIPlugin } from './mobile-plugin';
export { UniverSheetsThreadCommentUIPlugin } from './plugin';
export { SheetsThreadCommentPopupService } from './services/sheets-thread-comment-popup.service';

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'إدارة التعليقات',
},
permission: {
commentErr: 'ليس لديك إذن لاستخدام التعليقات.',
},
menu: {
addComment: 'إضافة تعليق',
openComments: 'فتح التعليقات',
commentManagement: 'إدارة التعليقات',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Gestió de comentaris',
},
permission: {
commentErr: 'No teniu permís per utilitzar comentaris.',
},
menu: {
addComment: 'Afegeix un comentari',
openComments: 'Obre els comentaris',
commentManagement: 'Gestió de comentaris',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Kommentarverwaltung',
},
permission: {
commentErr: 'Sie haben keine Berechtigung, Kommentare zu verwenden.',
},
menu: {
addComment: 'Kommentar hinzufügen',
openComments: 'Kommentare öffnen',
commentManagement: 'Kommentarverwaltung',
},
},
};
export default locale;

View File

@@ -0,0 +1,33 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const locale = {
'sheets-thread-comment-ui': {
panel: {
title: 'Comment Management',
},
permission: {
commentErr: 'You do not have permission to use comments.',
},
menu: {
addComment: 'Add Comment',
openComments: 'Open Comments',
commentManagement: 'Comment Management',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Gestión de comentarios',
},
permission: {
commentErr: 'No tiene permiso para usar comentarios.',
},
menu: {
addComment: 'Añadir comentario',
openComments: 'Abrir comentarios',
commentManagement: 'Gestión de comentarios',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'مدیریت نظر',
},
permission: {
commentErr: 'شما مجوز استفاده از نظرات را ندارید.',
},
menu: {
addComment: 'افزودن نظر',
openComments: 'باز کردن نظرات',
commentManagement: 'مدیریت نظر',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Gestion des commentaires',
},
permission: {
commentErr: 'Vous n’avez pas l’autorisation d’utiliser les commentaires.',
},
menu: {
addComment: 'Ajouter un commentaire',
openComments: 'Ouvrir les commentaires',
commentManagement: 'Gestion des commentaires',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Manajemen Komentar',
},
permission: {
commentErr: 'Anda tidak memiliki izin untuk menggunakan komentar.',
},
menu: {
addComment: 'Tambah Komentar',
openComments: 'Buka Komentar',
commentManagement: 'Manajemen Komentar',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Gestione commenti',
},
permission: {
commentErr: 'Non hai l’autorizzazione per usare i commenti.',
},
menu: {
addComment: 'Aggiungi commento',
openComments: 'Apri commenti',
commentManagement: 'Gestione commenti',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'コメント管理',
},
permission: {
commentErr: 'コメントを使用する権限がありません。',
},
menu: {
addComment: 'コメントを追加',
openComments: 'コメントを開く',
commentManagement: 'コメント管理',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: '댓글 관리',
},
permission: {
commentErr: '댓글을 사용할 권한이 없습니다.',
},
menu: {
addComment: '댓글 추가',
openComments: '댓글 열기',
commentManagement: '댓글 관리',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Zarządzanie komentarzami',
},
permission: {
commentErr: 'Nie masz uprawnień do używania komentarzy.',
},
menu: {
addComment: 'Dodaj komentarz',
openComments: 'Otwórz komentarze',
commentManagement: 'Zarządzanie komentarzami',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Gerenciamento de Comentários',
},
permission: {
commentErr: 'Você não tem permissão para usar comentários.',
},
menu: {
addComment: 'Adicionar Comentário',
openComments: 'Abrir comentários',
commentManagement: 'Gerenciamento de Comentários',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Управление комментариями',
},
permission: {
commentErr: 'У вас нет разрешения на использование комментариев.',
},
menu: {
addComment: 'Добавить комментарий',
openComments: 'Открыть комментарии',
commentManagement: 'Управление комментариями',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Správa komentárov',
},
permission: {
commentErr: 'Nemáte povolenie používať komentáre.',
},
menu: {
addComment: 'Pridať komentár',
openComments: 'Otvoriť komentáre',
commentManagement: 'Správa komentárov',
},
},
};
export default locale;

View File

@@ -0,0 +1,20 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { LocaleLeafKeys } from '@univerjs/core';
import type enUS from './en-US';
export type LocaleKey = LocaleLeafKeys<typeof enUS>;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: 'Quản lý bình luận',
},
permission: {
commentErr: 'Bạn không có quyền sử dụng bình luận.',
},
menu: {
addComment: 'Thêm bình luận',
openComments: 'Mở bình luận',
commentManagement: 'Quản lý bình luận',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: '评论管理',
},
permission: {
commentErr: '你没有权限使用评论。',
},
menu: {
addComment: '添加评论',
openComments: '打开评论',
commentManagement: '评论管理',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: '評論管理',
},
permission: {
commentErr: '你沒有權限使用評論。',
},
menu: {
addComment: '新增評論',
openComments: '開啟評論',
commentManagement: '評論管理',
},
},
};
export default locale;

View File

@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-thread-comment-ui': {
panel: {
title: '評論管理',
},
permission: {
commentErr: '你沒有權限使用評論。',
},
menu: {
addComment: '新增評論',
openComments: '開啟評論',
commentManagement: '評論管理',
},
},
};
export default locale;

View File

@@ -0,0 +1,101 @@
/**
* 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 {
FOCUSING_COMMON_DRAWINGS,
IContextService,
Injector,
IPermissionService,
IUniverInstanceService,
UserManagerService,
} from '@univerjs/core';
import { RangeProtectionRuleModel, SheetsSelectionsService, WorksheetProtectionRuleModel } from '@univerjs/sheets';
import { BehaviorSubject, of, Subject } from 'rxjs';
import { describe, expect, it } from 'vitest';
import { drawingCommentMenuFactory, threadCommentMenuFactory } from '../menu';
describe('drawingCommentMenuFactory', () => {
it('stays enabled for a focused drawing until comment permission is denied', () => {
const permissionValues$ = new BehaviorSubject([{ value: true }, { value: true }]);
const workbook = {
getUnitId: () => 'unit-1',
activeSheet$: of({ getSheetId: () => 'sheet-1' }),
};
const injector = new Injector([
[IContextService, {
useValue: {
subscribeContextValue$: (key: string) => of(key === FOCUSING_COMMON_DRAWINGS),
},
}],
[IPermissionService, {
useValue: {
permissionPointUpdate$: new Subject(),
composePermission$: () => permissionValues$,
},
}],
[IUniverInstanceService, {
useValue: {
focused$: new Subject(),
getCurrentTypeOfUnit$: () => of(workbook),
},
}],
[RangeProtectionRuleModel, { useValue: { getSubunitRuleList: () => [] } }],
[SheetsSelectionsService, {
useValue: {
selectionChanged$: new Subject(),
getWorkbookSelections: () => ({ getSelectionsOfWorksheet: () => [] }),
},
}],
[UserManagerService, { useValue: { currentUser$: of(null) } }],
[WorksheetProtectionRuleModel, { useValue: { getRule: () => null } }],
]);
const disabledValues: boolean[] = [];
const subscription = drawingCommentMenuFactory(injector).disabled$.subscribe((disabled) => {
disabledValues.push(disabled);
});
expect(disabledValues).toEqual([false]);
permissionValues$.next([{ value: false }, { value: true }]);
expect(disabledValues).toEqual([false, true]);
subscription.unsubscribe();
injector.dispose();
});
});
describe('thread comment menu', () => {
it('shows the add comment label in its tooltip', () => {
const service = {
currentUser$: of(null),
focused$: of(null),
getCurrentTypeOfUnit$: () => of(null),
subscribeContextValue$: () => of(false),
};
const accessor = new Injector([
[IContextService, { useValue: service }],
[IUniverInstanceService, { useValue: service }],
[UserManagerService, { useValue: service }],
]);
expect(threadCommentMenuFactory(accessor)).toMatchObject({
title: 'sheets-thread-comment-ui.menu.addComment',
tooltip: 'sheets-thread-comment-ui.menu.addComment',
});
accessor.dispose();
});
});

View File

@@ -0,0 +1,87 @@
/**
* 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 { IAccessor } from '@univerjs/core';
import type { IShortcutItem } from '@univerjs/ui';
import { IPermissionService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { RangeProtectionPermissionViewPoint, WorkbookCommentPermission, WorksheetViewPermission } from '@univerjs/sheets';
import { deriveStateFromActiveSheet$, getCurrentRangeDisable$, whenSheetEditorFocused } from '@univerjs/sheets-ui';
import { getMenuHiddenObservable, KeyCode, MenuItemType, MetaKeys } from '@univerjs/ui';
import { map } from 'rxjs';
import { AddSheetDrawingCommentOperation, ShowAddSheetCommentModalOperation, ToggleSheetCommentPanelOperation } from '../commands/operations/comment.operation';
function getDrawingCommentDisable$(accessor: IAccessor) {
const univerInstanceService = accessor.get(IUniverInstanceService);
const permissionService = accessor.get(IPermissionService);
return deriveStateFromActiveSheet$(univerInstanceService, true, ({ workbook, worksheet }) => {
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
return permissionService.composePermission$([
new WorkbookCommentPermission(unitId).id,
new WorksheetViewPermission(unitId, subUnitId).id,
]).pipe(map((permissions) => permissions.some((permission) => permission.value === false)));
});
}
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),
disabled$: getDrawingCommentDisable$(accessor),
});
export const threadCommentMenuFactory = (accessor: IAccessor) => {
return {
id: ShowAddSheetCommentModalOperation.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),
disabled$: getCurrentRangeDisable$(accessor, {
workbookTypes: [WorkbookCommentPermission],
worksheetTypes: [WorksheetViewPermission],
rangeTypes: [RangeProtectionPermissionViewPoint],
}),
};
};
export const threadPanelMenuFactory = (accessor: IAccessor) => {
return {
id: ToggleSheetCommentPanelOperation.id,
type: MenuItemType.BUTTON,
icon: 'CommentIcon',
title: 'sheets-thread-comment-ui.menu.openComments',
tooltip: 'sheets-thread-comment-ui.menu.openComments',
disabled$: getCurrentRangeDisable$(accessor, {
workbookTypes: [WorkbookCommentPermission],
worksheetTypes: [WorksheetViewPermission],
rangeTypes: [RangeProtectionPermissionViewPoint],
}),
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
};
};
export const AddCommentShortcut: IShortcutItem = {
id: ShowAddSheetCommentModalOperation.id,
binding: KeyCode.M | MetaKeys.CTRL_COMMAND | MetaKeys.ALT,
preconditions: whenSheetEditorFocused,
};

View File

@@ -0,0 +1,61 @@
/**
* 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 { IAccessor } from '@univerjs/core';
import type { LocaleKey } from '../locale/types';
import { UniverInstanceType } from '@univerjs/core';
import { RangeProtectionPermissionViewPoint, SheetsSelectionsService, WorkbookCommentPermission, WorksheetViewPermission } from '@univerjs/sheets';
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
import { getCurrentRangeDisable$ } from '@univerjs/sheets-ui';
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
import { ShowAddSheetCommentMobileOperation } from '../commands/operations/mobile/show-add-sheet-comment.operation';
export function getMobileThreadCommentMenuTitle(accessor: IAccessor): LocaleKey {
const selectionService = accessor.get(SheetsSelectionsService);
const selection = selectionService.getCurrentLastSelection()?.primary;
const selectionParam = selectionService.currentSelectionParam;
if (!selection || !selectionParam) {
return 'sheets-thread-comment-ui.menu.addComment';
}
const commentId = accessor.get(SheetsThreadCommentModel).getByLocation(
selectionParam.unitId,
selectionParam.sheetId,
selection.startRow,
selection.startColumn
);
return commentId
? 'sheets-thread-comment-ui.menu.openComments'
: 'sheets-thread-comment-ui.menu.addComment';
}
export const mobileThreadCommentMenuFactory = (accessor: IAccessor) => ({
id: ShowAddSheetCommentMobileOperation.id,
type: MenuItemType.BUTTON,
icon: 'InsertCommentDoubleIcon',
title: 'sheets-thread-comment-ui.menu.addComment',
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
disabled$: getCurrentRangeDisable$(accessor, {
workbookTypes: [WorkbookCommentPermission],
worksheetTypes: [WorksheetViewPermission],
rangeTypes: [RangeProtectionPermissionViewPoint],
}),
});
export const mobileContextThreadCommentMenuFactory = (accessor: IAccessor) => ({
...mobileThreadCommentMenuFactory(accessor),
title: getMobileThreadCommentMenuTitle(accessor),
});

View File

@@ -0,0 +1,59 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { MenuSchemaType } from '@univerjs/ui';
import { ContextMenuGroup, ContextMenuPosition, FloatingObjectToolbarPosition, RibbonInsertGroup } from '@univerjs/ui';
import { AddSheetDrawingCommentOperation, ToggleSheetCommentPanelOperation } from '../commands/operations/comment.operation';
import { ShowAddSheetCommentMobileOperation } from '../commands/operations/mobile/show-add-sheet-comment.operation';
import { drawingCommentMenuFactory, threadPanelMenuFactory } from './menu';
import { mobileContextThreadCommentMenuFactory, mobileThreadCommentMenuFactory } from './mobile-menu';
export const mobileMenuSchema: MenuSchemaType = {
[RibbonInsertGroup.MEDIA]: {
[ToggleSheetCommentPanelOperation.id]: {
order: 2,
gridLayout: { row: 1, column: 3, showLabel: true },
menuItemFactory: threadPanelMenuFactory,
},
[ShowAddSheetCommentMobileOperation.id]: {
order: 2.1,
gridLayout: { row: 2, column: 3, showLabel: true },
menuItemFactory: mobileThreadCommentMenuFactory,
},
},
[ContextMenuPosition.MAIN_AREA]: {
[ContextMenuGroup.OTHERS]: {
[ShowAddSheetCommentMobileOperation.id]: {
order: 0,
menuItemFactory: mobileContextThreadCommentMenuFactory,
},
},
},
[FloatingObjectToolbarPosition.SHEET]: {
[AddSheetDrawingCommentOperation.id]: {
order: 10,
menuItemFactory: drawingCommentMenuFactory,
},
},
[ContextMenuPosition.DRAWING]: {
[ContextMenuGroup.OTHERS]: {
[AddSheetDrawingCommentOperation.id]: {
order: 0,
menuItemFactory: drawingCommentMenuFactory,
},
},
},
};

View File

@@ -0,0 +1,57 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { MenuSchemaType } from '@univerjs/ui';
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, showLabel: true },
menuItemFactory: threadPanelMenuFactory,
},
[ShowAddSheetCommentModalOperation.id]: {
order: 2.1,
gridLayout: { row: 2, column: 3, showLabel: true },
menuItemFactory: threadCommentMenuFactory,
},
},
[ContextMenuPosition.MAIN_AREA]: {
[ContextMenuGroup.OTHERS]: {
[ShowAddSheetCommentModalOperation.id]: {
order: 0,
menuItemFactory: threadCommentMenuFactory,
},
},
},
[FloatingObjectToolbarPosition.SHEET]: {
[AddSheetDrawingCommentOperation.id]: {
order: 10,
menuItemFactory: drawingCommentMenuFactory,
},
},
[ContextMenuPosition.DRAWING]: {
[ContextMenuGroup.OTHERS]: {
[AddSheetDrawingCommentOperation.id]: {
order: 0,
menuItemFactory: drawingCommentMenuFactory,
},
},
},
};

View File

@@ -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 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 { UniverSheetsPlugin } from '@univerjs/sheets';
import { UniverSheetsThreadCommentPlugin } from '@univerjs/sheets-thread-comment';
import { UniverSheetsMobileUIPlugin } from '@univerjs/sheets-ui';
import { UniverThreadCommentPlugin } from '@univerjs/thread-comment';
import { UniverThreadCommentMobileUIPlugin } from '@univerjs/thread-comment-ui';
import { UniverMobileUIPlugin } from '@univerjs/ui';
import pkg from '../package.json';
import {
AddSheetDrawingCommentOperation,
OpenSheetCommentPanelOperation,
ToggleSheetCommentPanelOperation,
} from './commands/operations/comment.operation';
import { ShowAddSheetCommentMobileOperation } from './commands/operations/mobile/show-add-sheet-comment.operation';
import { defaultPluginConfig, SHEETS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY } from './config/config';
import { MobileComponentsController } from './controllers/mobile/components.controller';
import { SheetsThreadCommentMobileController } from './controllers/mobile/sheets-thread-comment.controller';
import { SheetsThreadCommentRenderController } from './controllers/render-controllers/render.controller';
import { SheetsThreadCommentCopyPasteController } from './controllers/sheets-thread-comment-copy-paste.controller';
import { SheetsThreadCommentPermissionController } from './controllers/sheets-thread-comment-permission.controller';
import { MobileSheetCommentDraftService } from './services/mobile/mobile-sheet-comment-draft.service';
import { SheetsThreadCommentPopupService } from './services/sheets-thread-comment-popup.service';
import { PLUGIN_NAME } from './types/const';
@DependentOn(
UniverRenderEnginePlugin,
UniverThreadCommentPlugin,
UniverSheetsPlugin,
UniverThreadCommentMobileUIPlugin,
UniverSheetsThreadCommentPlugin,
UniverMobileUIPlugin,
UniverSheetsMobileUIPlugin
)
export class UniverSheetsThreadCommentMobileUIPlugin extends Plugin {
static override pluginName = PLUGIN_NAME;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
private readonly _config: Partial<IUniverSheetsThreadCommentUIConfig> = defaultPluginConfig,
@Inject(Injector) protected override _injector: Injector,
@Inject(ICommandService) private readonly _commandService: ICommandService,
@IConfigService private readonly _configService: IConfigService
) {
super();
const { menu, ...rest } = merge({}, defaultPluginConfig, this._config);
if (menu) {
this._configService.setConfig('menu', menu, { merge: true });
}
this._configService.setConfig(SHEETS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
([
[MobileComponentsController],
[SheetsThreadCommentMobileController],
[SheetsThreadCommentRenderController],
[SheetsThreadCommentCopyPasteController],
[SheetsThreadCommentPopupService],
[SheetsThreadCommentPermissionController],
[MobileSheetCommentDraftService],
] as Dependency[]).forEach((dependency) => this._injector.add(dependency));
this._injector.get(MobileComponentsController);
[
AddSheetDrawingCommentOperation,
OpenSheetCommentPanelOperation,
ShowAddSheetCommentMobileOperation,
ToggleSheetCommentPanelOperation,
].forEach((command) => this._commandService.registerCommand(command));
this._injector.get(SheetsThreadCommentMobileController);
}
override onReady(): void {
this._injector.get(SheetsThreadCommentRenderController);
}
override onRendered(): void {
this._injector.get(SheetsThreadCommentCopyPasteController);
this._injector.get(SheetsThreadCommentPermissionController);
}
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency } from '@univerjs/core';
import type { IUniverSheetsThreadCommentUIConfig } from './config/config';
import { DependentOn, ICommandService, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
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 { 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';
import { SheetsThreadCommentPermissionController } from './controllers/sheets-thread-comment-permission.controller';
import { SheetsThreadCommentPopupController } from './controllers/sheets-thread-comment-popup.controller';
import { SheetsThreadCommentController } from './controllers/sheets-thread-comment.controller';
import { SheetsThreadCommentPopupService } from './services/sheets-thread-comment-popup.service';
import { PLUGIN_NAME } from './types/const';
@DependentOn(
UniverRenderEnginePlugin,
UniverThreadCommentPlugin,
UniverDrawingPlugin,
UniverSheetsPlugin,
UniverThreadCommentUIPlugin,
UniverSheetsThreadCommentPlugin,
UniverSheetsUIPlugin
)
export class UniverSheetsThreadCommentUIPlugin extends Plugin {
static override pluginName = PLUGIN_NAME;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
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();
// Manage the plugin configuration.
const { menu, ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
if (menu) {
this._configService.setConfig('menu', menu, { merge: true });
}
this._configService.setConfig(SHEETS_THREAD_COMMENT_UI_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
this._injector.add([ComponentsController]);
this._injector.get(ComponentsController);
([
[SheetsThreadCommentController],
[SheetsThreadCommentRenderController],
[SheetsThreadCommentCopyPasteController],
[SheetsThreadCommentHoverController],
[SheetsThreadCommentPopupController],
[SheetsThreadCommentPopupService],
[SheetsThreadCommentPermissionController],
] as Dependency[]).forEach((dep) => {
this._injector.add(dep);
});
[
AddSheetDrawingCommentOperation,
OpenSheetCommentPanelOperation,
ShowAddSheetCommentModalOperation,
ToggleSheetCommentPanelOperation,
].forEach((command) => {
this._commandService.registerCommand(command);
});
this._injector.get(SheetsThreadCommentController);
}
override onReady(): void {
this._injector.get(SheetsThreadCommentRenderController);
}
override onRendered(): void {
this._renderManagerService.registerRenderModule(
UniverInstanceType.UNIVER_SHEET,
[SheetsThreadCommentDrawingRenderController]
);
this._injector.get(SheetsThreadCommentCopyPasteController);
this._injector.get(SheetsThreadCommentHoverController);
this._injector.get(SheetsThreadCommentPopupController);
this._injector.get(SheetsThreadCommentPermissionController);
}
}

View File

@@ -0,0 +1,144 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IDisposable } from '@univerjs/core';
import { Inject, Injector } from '@univerjs/core';
import { CellPopupManagerService, SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SHEETS_THREAD_COMMENT_MODAL } from '../../types/const';
import { SheetsThreadCommentPopupService } from '../sheets-thread-comment-popup.service';
class TestSheetCanvasPopManagerService {}
class TestCellPopupManagerService extends CellPopupManagerService {
disposed = false;
lastLocation?: Parameters<CellPopupManagerService['showPopup']>[0];
lastPopup?: Parameters<CellPopupManagerService['showPopup']>[1];
constructor(
@Inject(SheetCanvasPopManagerService) sheetCanvasPopManagerService: SheetCanvasPopManagerService
) {
super(sheetCanvasPopManagerService);
}
override showPopup(
location: Parameters<CellPopupManagerService['showPopup']>[0],
popup: Parameters<CellPopupManagerService['showPopup']>[1]
): IDisposable {
this.lastLocation = location;
this.lastPopup = popup;
return {
dispose: () => {
this.disposed = true;
},
};
}
}
describe('SheetsThreadCommentPopupService', () => {
let injector: Injector;
let service: SheetsThreadCommentPopupService;
let cellPopupManagerService: TestCellPopupManagerService;
let hideCount: number;
beforeEach(() => {
hideCount = 0;
injector = new Injector();
injector.add([SheetCanvasPopManagerService, { useClass: TestSheetCanvasPopManagerService }]);
injector.add([CellPopupManagerService, { useClass: TestCellPopupManagerService }]);
injector.add([SheetsThreadCommentPopupService]);
service = injector.get(SheetsThreadCommentPopupService);
cellPopupManagerService = injector.get(CellPopupManagerService) as unknown as TestCellPopupManagerService;
});
afterEach(() => {
service.dispose();
});
it('shows, persists and hides the active popup through the popup manager boundary', () => {
service.showPopup({
unitId: 'test',
subUnitId: 'sheet1',
row: 1,
col: 2,
commentId: 'comment-1',
temp: true,
trigger: 'hover',
}, () => {
hideCount += 1;
});
expect(service.activePopup).toEqual({
unitId: 'test',
subUnitId: 'sheet1',
row: 1,
col: 2,
commentId: 'comment-1',
temp: true,
trigger: 'hover',
});
expect(cellPopupManagerService.lastLocation).toEqual({
unitId: 'test',
subUnitId: 'sheet1',
row: 1,
col: 2,
});
expect(cellPopupManagerService.lastPopup).toEqual(expect.objectContaining({
componentKey: SHEETS_THREAD_COMMENT_MODAL,
direction: 'horizontal',
}));
service.persistPopup();
expect(service.activePopup).toEqual(expect.objectContaining({
temp: false,
}));
service.hidePopup();
expect(service.activePopup).toBeNull();
expect(cellPopupManagerService.disposed).toBe(true);
expect(hideCount).toBe(1);
});
it('updates the active popup in place when the same cell is targeted again', () => {
service.showPopup({
unitId: 'test',
subUnitId: 'sheet1',
row: 1,
col: 2,
commentId: 'comment-1',
});
service.showPopup({
unitId: 'test',
subUnitId: 'sheet1',
row: 1,
col: 2,
commentId: 'comment-2',
trigger: 'cell',
});
expect(service.activePopup).toEqual({
unitId: 'test',
subUnitId: 'sheet1',
row: 1,
col: 2,
commentId: 'comment-2',
trigger: 'cell',
});
expect(cellPopupManagerService.disposed).toBe(false);
});
});

View File

@@ -0,0 +1,49 @@
/**
* 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 { Nullable } from '@univerjs/core';
import { Disposable } from '@univerjs/core';
import { BehaviorSubject } from 'rxjs';
export interface IMobileSheetCommentDraft {
unitId: string;
subUnitId: string;
row: number;
column: number;
}
export class MobileSheetCommentDraftService extends Disposable {
private readonly _draft$ = new BehaviorSubject<Nullable<IMobileSheetCommentDraft>>(null);
readonly draft$ = this._draft$.asObservable();
constructor() {
super();
this.disposeWithMe(() => this._draft$.complete());
}
get draft(): Nullable<IMobileSheetCommentDraft> {
return this._draft$.getValue();
}
place(draft: IMobileSheetCommentDraft): void {
this._draft$.next(draft);
}
cancel(): void {
this._draft$.next(null);
}
}

View File

@@ -0,0 +1,138 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IDisposable, Nullable } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import { Disposable, DisposableCollection, Inject } from '@univerjs/core';
import { CellPopupManagerService, SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
import { BehaviorSubject } from 'rxjs';
import { SHEETS_THREAD_COMMENT_MODAL } from '../types/const';
export interface IThreadCommentPopup extends ISheetLocationBase {
commentId?: string;
// when triggered by hover, temp is set to be `true`
temp?: boolean;
trigger?: string;
}
export class SheetsThreadCommentPopupService extends Disposable {
private _lastPopup: Nullable<IDisposable> = null;
private _activePopup: Nullable<IThreadCommentPopup>;
private _activePopup$ = new BehaviorSubject<Nullable<IThreadCommentPopup>>(null);
activePopup$ = this._activePopup$.asObservable();
get activePopup() {
return this._activePopup;
}
constructor(
@Inject(SheetCanvasPopManagerService) private readonly _canvasPopupManagerService: SheetCanvasPopManagerService,
@Inject(CellPopupManagerService) private readonly _cellPopupManagerService: CellPopupManagerService
) {
super();
this.disposeWithMe(() => {
this._activePopup$.complete();
});
}
override dispose(): void {
super.dispose();
this.hidePopup();
}
showPopup(location: IThreadCommentPopup, onHide?: () => void) {
const { row, col, unitId, subUnitId } = location;
if (
this._activePopup &&
row === this._activePopup.row &&
col === this._activePopup.col &&
unitId === this._activePopup.unitId &&
subUnitId === this.activePopup?.subUnitId
) {
this._activePopup = location;
this._activePopup$.next(location);
return;
}
if (this._lastPopup) {
this._lastPopup.dispose();
};
this._activePopup = location;
this._activePopup$.next(location);
const popupDisposable = this._cellPopupManagerService.showPopup(
{
row,
col,
unitId,
subUnitId,
},
{
componentKey: SHEETS_THREAD_COMMENT_MODAL,
onClickOutside: () => {
this.hidePopup();
},
direction: 'horizontal',
excludeOutside: [
...Array.from(document.querySelectorAll('.univer-thread-comment')),
document.getElementById('thread-comment-add'),
].filter(Boolean) as HTMLElement[],
priority: 2,
}
);
if (!popupDisposable) {
throw new Error('[SheetsThreadCommentPopupService]: cannot show popup!');
}
const disposableCollection = new DisposableCollection();
disposableCollection.add(popupDisposable);
disposableCollection.add({
dispose: () => {
onHide?.();
},
});
this._lastPopup = disposableCollection;
}
hidePopup() {
if (!this._activePopup) {
return;
}
if (this._lastPopup) {
this._lastPopup.dispose();
}
this._lastPopup = null;
this._activePopup = null;
this._activePopup$.next(null);
}
persistPopup() {
if (!this._activePopup || !this._activePopup.temp) {
return;
}
this._activePopup = {
...this._activePopup,
temp: false,
};
this._activePopup$.next(this._activePopup);
}
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const SHEETS_THREAD_COMMENT_MODAL = 'univer.sheet.thread-comment-modal';
export const SHEETS_THREAD_COMMENT_PANEL = 'univer.sheet.thread-comment-panel';
export const SHOW_ADD_SHEET_COMMENT_OPERATION_ID = 'sheet.operation.show-comment-modal';
export const PLUGIN_NAME = 'SHEET_THREAD_COMMENT_UI_PLUGIN';

View File

@@ -0,0 +1,22 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IThreadComment } from '@univerjs/thread-comment';
export interface ISheetThreadComment extends IThreadComment {
row: number;
column: number;
}

View File

@@ -0,0 +1,84 @@
/**
* 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 { Workbook } from '@univerjs/core';
import { IUniverInstanceService, Tools, UniverInstanceType } from '@univerjs/core';
import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
import { ThreadCommentTree, ThreadCommentTreeLocation } from '@univerjs/thread-comment-ui';
import { useDependency, useObservable } from '@univerjs/ui';
import { map, startWith } from 'rxjs';
import { SheetsThreadCommentPopupService } from '../services/sheets-thread-comment-popup.service';
export const SheetsThreadCommentCell = () => {
const univerInstanceService = useDependency(IUniverInstanceService);
const sheetsThreadCommentPopupService = useDependency(SheetsThreadCommentPopupService);
const activePopup = useObservable(sheetsThreadCommentPopupService.activePopup$);
const sheetThreadCommentModel = useDependency(SheetsThreadCommentModel);
const getRootId = () => {
if (!activePopup) {
return undefined;
}
const { unitId, subUnitId, row, col, commentId } = activePopup;
const activeComment = commentId
? sheetThreadCommentModel.getAllByLocation(unitId, subUnitId, row, col).find((comment) => comment.id === commentId)
: undefined;
return activeComment && !activeComment.resolved
? activeComment.id
: sheetThreadCommentModel.getByLocation(unitId, subUnitId, row, col);
};
const rootId = useObservable(
activePopup
? () => sheetThreadCommentModel.commentUpdate$.pipe(
map(() => getRootId()),
startWith(getRootId())
)
: null,
undefined,
false,
[activePopup, sheetThreadCommentModel]
);
if (!activePopup) {
return null;
}
const { row, col, unitId, subUnitId, trigger } = activePopup;
const ref = `${Tools.chatAtABC(col)}${row + 1}`;
const onClose = () => {
sheetsThreadCommentPopupService.hidePopup();
};
const getSubUnitName = (id: string) => {
return univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId(id)?.getName() ?? '';
};
return (
<ThreadCommentTree
onClick={() => {
sheetsThreadCommentPopupService.persistPopup();
}}
location={ThreadCommentTreeLocation.CELL}
id={rootId}
unitId={unitId}
subUnitId={subUnitId}
type={UniverInstanceType.UNIVER_SHEET}
refStr={ref}
onClose={onClose}
getSubUnitName={getSubUnitName}
autoFocus={trigger === 'context-menu'}
/>
);
};

View File

@@ -0,0 +1,197 @@
/**
* 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 { Workbook } from '@univerjs/core';
import type { IThreadComment } from '@univerjs/thread-comment';
import { ICommandService, IUniverInstanceService, UniverInstanceType, UserManagerService } from '@univerjs/core';
import { singleReferenceToGrid } from '@univerjs/engine-formula';
import { IMarkSelectionService } from '@univerjs/sheets-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';
import { ShowAddSheetCommentModalOperation } from '../commands/operations/comment.operation';
import { SheetsThreadCommentPopupService } from '../services/sheets-thread-comment-popup.service';
export interface ISheetsThreadCommentPanelProps {
ThreadCommentPanelComponent?: typeof ThreadCommentPanel;
}
export const SheetsThreadCommentPanel = ({
ThreadCommentPanelComponent = ThreadCommentPanel,
}: ISheetsThreadCommentPanelProps = {}) => {
const markSelectionService = useDependency(IMarkSelectionService);
const univerInstanceService = useDependency(IUniverInstanceService);
const sheetsThreadCommentPopupService = useDependency(SheetsThreadCommentPopupService);
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);
const panelService = useDependency(ThreadCommentPanelService);
const activeCommentId = useObservable(panelService.activeCommentId$);
const panelVisible = useObservable(panelService.panelVisible$, panelService.panelVisible);
const sortComments = useCallback((comments: IThreadComment[]) => {
const worksheets = workbook.getSheets();
const sheetIndex: Record<string, number> = {};
worksheets.forEach((sheet, i) => {
sheetIndex[sheet.getSheetId()] = i;
});
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 };
}).sort((pre, aft) => {
if (pre.p[0] === aft.p[0]) {
if (pre.p[1] === aft.p[1]) {
return pre.p[2] - aft.p[2];
}
return pre.p[1] - aft.p[1];
}
return pre.p[0] - aft.p[0];
});
};
return [
...sort(comments.filter((comment) => !comment.resolved)),
...sort(comments.filter((comment) => comment.resolved)),
];
}, [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);
const mergeInfo = worksheet?.getMergedCell(row, column) ?? {
startColumn: column,
endColumn: column,
startRow: row,
endRow: row,
};
if (!Number.isNaN(row) && !Number.isNaN(column)) {
return markSelectionService.addShape({
range: mergeInfo,
style: {
// hasAutoFill: false,
fill: 'rgb(255, 189, 55, 0.35)',
strokeWidth: 1,
stroke: '#FFBD37',
widgets: {},
},
primary: null,
});
}
}
return null;
}, [markSelectionService, subUnitId, unitId]);
const getSubUnitName = (id: string) => {
return workbook.getSheetBySheetId(id)?.getName() ?? '';
};
const handleAdd = () => {
commandService.executeCommand(ShowAddSheetCommentModalOperation.id);
};
const handleHover = (comment: IThreadComment) => {
if (hoverShapeId.current) {
markSelectionService.removeShape(hoverShapeId.current);
hoverShapeId.current = null;
}
if (
activeCommentId &&
activeCommentId.unitId === comment.unitId &&
activeCommentId.subUnitId === comment.subUnitId &&
activeCommentId.commentId === comment.id
) {
return;
}
hoverShapeId.current = showShape(comment);
};
const handleLeave = () => {
if (hoverShapeId.current) {
markSelectionService.removeShape(hoverShapeId.current);
hoverShapeId.current = null;
}
};
const handleResolve = (id: string, resolved: boolean) => {
if (resolved) {
sheetsThreadCommentPopupService.hidePopup();
}
};
useEffect(() => {
if (!panelVisible && hoverShapeId.current) {
markSelectionService.removeShape(hoverShapeId.current);
hoverShapeId.current = null;
}
}, [markSelectionService, panelVisible]);
return (
<ThreadCommentPanelComponent
unitId={unitId}
subUnitId$={subUnitId$}
type={UniverInstanceType.UNIVER_SHEET}
onAdd={handleAdd}
getSubUnitName={getSubUnitName}
onResolve={handleResolve}
sortComments={sortComments}
onItemEnter={handleHover}
onItemLeave={handleLeave}
onDeleteComment={() => {
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;
}}
/>
);
};

View File

@@ -0,0 +1,59 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IThreadComment } from '@univerjs/thread-comment';
import type { IThreadCommentPanelProps } from '@univerjs/thread-comment-ui';
import { Tools, UserManagerService } from '@univerjs/core';
import { MobileThreadCommentPanel } from '@univerjs/thread-comment-ui';
import { useDependency, useObservable } from '@univerjs/ui';
import { MobileSheetCommentDraftService } from '../../services/mobile/mobile-sheet-comment-draft.service';
import { SheetsThreadCommentPanel } from '../SheetsThreadCommentPanel';
function MobileSheetsThreadCommentPanelContent(props: IThreadCommentPanelProps) {
const draftService = useDependency(MobileSheetCommentDraftService);
const userManagerService = useDependency(UserManagerService);
const draft = useObservable(draftService.draft$, draftService.draft);
const tempCellComment: IThreadComment | null = draft?.unitId === props.unitId
? {
id: '',
threadId: '',
unitId: draft.unitId,
subUnitId: draft.subUnitId,
ref: `${Tools.chatAtABC(draft.column)}${draft.row + 1}`,
dT: '',
personId: userManagerService.getCurrentUser().userID,
text: { dataStream: '\r\n' },
}
: null;
const handleTempCommentClose = () => {
draftService.cancel();
props.onTempCommentClose?.();
};
return (
<MobileThreadCommentPanel
{...props}
tempComment={props.tempComment ?? tempCellComment}
onTempCommentClose={handleTempCommentClose}
autoFocusActiveComment
/>
);
}
export function MobileSheetsThreadCommentPanel() {
return <SheetsThreadCommentPanel ThreadCommentPanelComponent={MobileSheetsThreadCommentPanelContent} />;
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Config } from 'tailwindcss';
import preset from '@univerjs-infra/shared/tailwind';
const config: Config = {
presets: [preset],
content: [
'./src/**/*.{js,ts,jsx,tsx}',
],
};
export default config;

View File

@@ -0,0 +1,8 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/node",
"compilerOptions": {
"rootDir": "src",
"declaration": true,
"emitDeclarationOnly": true,
"noEmit": false,
"outDir": "lib/types"
},
"include": [
"src"
],
"exclude": [
"src/**/__tests__/**",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

View File

@@ -0,0 +1,3 @@
import createConfig from '@univerjs-infra/shared/vitest';
export default createConfig();