feat(sheets-thread-comment): comment support for sheets (#2228)

* feat: add thread-comment pkg

* feat: update

* feat: update

* feat: update

* feat: update

* feat: update

* feat: update

* feat: temp

* feat: update

* feat: update

* feat: u

* feat: update

* feat: update

* feat: update

* feat: update

* fix: #2121

* fix: close #2120

* fix: #2114

* fix: #2112

* feat: temp

* feat: update

* feat: support at-input

* feat: ref-range

* feat: update mentions

* feat: user

* feat: update

* feat: update comment

* feat: style update

* feat: update

* feat: update

* feat: update

* feat: update

* feat: rename

* feat: scroller

* feat: update

* feat: update

* feat: update

* feat: update

* fix: #684 univer-pro

* feat: auto-height

* feat: update

* feat: update

* fix: eslint

* feat: remove useless code

* build: fix build error

* chore: fix lockfile

* build: fix deps

* feat: update

* feat: update

* feat: update

* feat: i18n

* feat: add sheet name

* feat: disable undo on resolve comment operation

* feat: update

* fix: #2174

* feat: #2162 checkbox ticked and unticked value

* feat: update

* fix: hover-manager-service

* feat: support copy-paste

* feat: sidebar visible status

* feat: style

* feat: update

* feat: update init

* feat: temp

* feat: update

* feat: update

* fix: checkbox validator

* feat: lint

* fix: error when delete worksheet

* feat: refactor canvas popup service

* feat: update

* feat: lock

* feat: custom-render event handler

* feat: update ref

* fix: copy-paste

* feat: update

* feat: update

* feat: update max-height

* feat: update

* feat: fix delete comment

* fix: #715

* feat: update

* feat: update scroll

* feat: style update

* feat: bugfix

* feat: update

* feat: close sidebar

* fix: cv undo

* feat: update

* feat: render z-index

* feat: code review issue

* feat: transform mentions data

* feat: u

* feat: bugfix

* feat: update

* feat: update

* feat: style & bugfix

* feat: style update

* feat: update

* feat: update

* feat: update

* feat: add thread-comment-data-source-service to support async comment crud

* feat: rename

* feat: build issue

* fix: sort order of comments

* feat: lint

* feat: rename

* feat: rebase dev

---------

Co-authored-by: 白熱 <sonne@asaki.me>
This commit is contained in:
zhangw
2024-05-21 11:07:29 +08:00
committed by GitHub
co-authored by 白熱
parent 57d4e8a980
commit 313ab796d7
90 changed files with 5344 additions and 94 deletions
+3
View File
@@ -34,10 +34,13 @@
"@univerjs/sheets-find-replace": "workspace:*",
"@univerjs/sheets-formula": "workspace:*",
"@univerjs/sheets-numfmt": "workspace:*",
"@univerjs/sheets-thread-comment": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/sheets-zen-editor": "workspace:*",
"@univerjs/slides": "workspace:*",
"@univerjs/slides-ui": "workspace:*",
"@univerjs/thread-comment": "workspace:*",
"@univerjs/thread-comment-ui": "workspace:*",
"@univerjs/ui": "workspace:*",
"@univerjs/uniscript": "workspace:*",
"@wendellhu/redi": "^0.15.1",
@@ -23560,6 +23560,10 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = {
},
}),
},
{
name: 'SHEET_THREAD_COMMENT_PLUGIN',
data: '{"sheet-0011":[{"text":{"textRuns":[],"paragraphs":[{"startIndex":3,"paragraphStyle":{}}],"sectionBreaks":[{"startIndex":4}],"dataStream":"123\\n\\r","customRanges":[]},"dT":"2024/05/17 21:16","id":"jwV0QtHwUbhG3o--iy1qa","ref":"H9","personId":"mockId","unitId":"workbook-01","subUnitId":"sheet-0011"}]}',
},
],
// namedRanges: [
// {
+34 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { LocaleType, LogLevel, Univer, UniverInstanceType } from '@univerjs/core';
import { LocaleType, LogLevel, Univer, UniverInstanceType, UserManagerService } from '@univerjs/core';
import { defaultTheme } from '@univerjs/design';
import { UniverDocsPlugin } from '@univerjs/docs';
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
@@ -34,6 +34,7 @@ import { UniverUIPlugin } from '@univerjs/ui';
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
import { UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation';
import { UniverSheetsConditionalFormattingUIPlugin } from '@univerjs/sheets-conditional-formatting-ui';
import { UniverSheetsThreadCommentPlugin } from '@univerjs/sheets-thread-comment';
import { UniverDebuggerPlugin } from '@univerjs/debugger';
import { FUniver } from '@univerjs/facade';
@@ -98,9 +99,41 @@ if (!IS_E2E) {
univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO);
}
const mockUser = {
userID: 'mockId',
name: 'MockUser',
avatar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAInSURBVHgBtZU9TxtBEIbfWRzFSIdkikhBSqRQkJqkCKTCFkqVInSUSaT0wC8w/gXxD4gU2nRJkXQWhAZowDUUWKIwEgWWbEEB3mVmx3dn4DA2nB/ppNuPeWd29mMIPXDr+RxwtgRHeW6+guNPRxogqnL7Dwz9psJ27S4NShaeZTH3kwXy6I81dlRKcmRui88swdq9AcSFL7Buz1Vmlns64MiLsCjzwnIYHLH57tbfFbs7KRaXyEU8FVZofqccOfA5l7Q8LPIkGrwnb2RPNEXWFVMUF3L+kDCk0btDDAMzOm5YfAHDwp4tG74wnzAsiOYMnJ3GoDybA7IT98/jm5+JNnfiIzAS6LlqHQBN/i6b2t/cV1Hh6BfwYlHnHP4AXi5q/8kmMMpOs8+BixZw/Fd6xUEHEbnkgclvQP2fGp7uShRKnQ3G32rkjV1th8JhIGG7tR/JyjGteSOZELwGMmNqIIigRCLRh2OZIE6BjItdd7pCW6Uhm1zzkUtungSxwEUzNpQ+GQumtH1ej1MqgmNT6vwmhCq5yuwq56EYTbgeQUz3yvrpV1b4ok3nYJ+eYhgYmjRUqErx2EDq0Fr8FhG++iqVGqxlUJI/70Ar0UgJaWHj6hYVHJrfKssAHot1JfqwE9WVWzXZVd5z2Ws/4PnmtEjkXeKJDvxUecLbWOXH/DP6QQ4J72NS0adedp1aseBfXP8odlZFfPvBF7SN/8hky1TYuPOAXAEipMx15u5ToAAAAABJRU5ErkJggg==',
};
univer.registerPlugin(UniverSheetsThreadCommentPlugin, {
mentions: [{
trigger: '@',
getMentions: async () => {
return [
{
id: mockUser.userID,
label: mockUser.name,
type: 'user',
icon: mockUser.avatar,
},
{
id: '2',
label: 'User2',
type: 'user',
icon: mockUser.avatar,
},
];
},
}],
});
// debugger plugin
univer.registerPlugin(UniverDebuggerPlugin);
const injector = univer.__getInjector();
const userManagerService = injector.get(UserManagerService);
userManagerService.setCurrentUser(mockUser);
declare global {
interface Window {
univer?: Univer;
@@ -15,13 +15,27 @@
*/
import type { IUser } from '@univerjs/protocol';
import { Subject } from 'rxjs';
import { BehaviorSubject, Subject } from 'rxjs';
export class UserManagerService {
private _model = new Map<string, IUser>();
private _userChange$ = new Subject<{ type: 'add' | 'delete'; user: IUser } | { type: 'clear' }>();
public userChange$ = this._userChange$.asObservable();
private _currentUser: IUser | null;
private _currentUser$ = new BehaviorSubject<IUser | null>(null);
public currentUser$ = this._currentUser$.asObservable();
getCurrentUser() {
return this._currentUser;
}
setCurrentUser(user: IUser) {
this._currentUser = user;
this.addUser(user);
this._currentUser$.next(user);
}
addUser(user: IUser) {
this._model.set(user.userID, user);
this._userChange$.next({ type: 'add', user });
@@ -277,6 +277,7 @@ export enum CustomRangeType {
BOOKMARK,
COMMENT,
CUSTOM,
MENTION,
}
/**
+2
View File
@@ -74,6 +74,7 @@
"dependencies": {
"@rc-component/color-picker": "^1.5.3",
"@rc-component/trigger": "^1.18.3",
"@types/react-mentions": "^4.1.13",
"@univerjs/icons": "^0.1.45",
"rc-dialog": "^9.4.0",
"rc-dropdown": "^4.2.0",
@@ -88,6 +89,7 @@
"rc-util": "^5.39.3",
"react-draggable": "^4.4.6",
"react-grid-layout": "^1.4.4",
"react-mentions": "^4.4.10",
"react-transition-group": "^4.4.5"
},
"devDependencies": {
@@ -60,6 +60,8 @@ export interface IButtonProps {
/** Set the handler to handle `click` event */
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
id?: string;
}
export function Button(props: IButtonProps) {
@@ -0,0 +1,68 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { Meta } from '@storybook/react';
import React, { useState } from 'react';
import { Mention } from 'react-mentions';
import { Mentions } from './Mentions';
const meta: Meta<typeof Mentions> = {
title: 'Components / Mentions',
component: Mentions,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
};
export default meta;
export const InputBasic = {
render() {
const [value, onChange] = useState('');
return (
<div style={{ width: 400 }}>
<Mentions
value={value}
onChange={(e) => onChange(e.target.value)}
>
<Mention
trigger="@"
data={[
{
id: '1',
display: 'zhangwei ',
},
{
id: '2',
display: 'zhangwei2 ',
},
{
id: '3',
display: 'zhangwei3 ',
},
]}
/>
</Mentions>
</div>
);
},
};
@@ -0,0 +1,33 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 React, { forwardRef } from 'react';
import type { MentionsInputProps } from 'react-mentions';
import { MentionsInput } from 'react-mentions';
import styles from './index.module.less';
export interface IMentionsProps extends MentionsInputProps {}
export const Mentions = forwardRef<React.Component<MentionsInputProps, any, any>, IMentionsProps>((props, ref) => {
return (
<MentionsInput
ref={ref}
{...props}
className={styles.mentions}
/>
);
});
@@ -0,0 +1,71 @@
.mentions {
width: 100%;
}
.mentions__control {
min-height: 32px;
}
.mentions__highlighter {
border-radius: 6px;
background: rgba(var(--color-white));
padding: 6px 10px;
font-size: 13px !important;
line-height: 20px !important;
max-height: 114px;
strong {
color: rgb(var(--blue-500));
}
}
.mentions__highlighter__substring {
visibility: inherit !important;
color: rgb(var(--color-black));
}
.mentions__input {
width: 100%;
caret-color: red;
background-color: transparent;
color: transparent;
padding: 6px 10px;
border: 1px solid rgb(var(--border-color));
border-radius: 6px;
font-size: 13px !important;
line-height: 20px !important;
max-height: 114px;
}
.mentions__input:focus {
border: 1px solid rgb(var(--blue-500));
outline: none !important;
}
.mentions__suggestions {
border-radius: 8px;
overflow: hidden;
background: rgb(var(--color-white)) !important;
border: 1px solid rgb(var(--grey-200)) !important;
box-shadow: var(--box-shadow-base) !important;
width: 100%;
box-sizing: border-box;
margin-top: 20px !important;
}
.mentions__suggestions__list {
display: flex;
flex-direction: column;
padding: 8px !important;
width: 100%;
box-sizing: border-box;
}
.mentions__suggestions__item {
padding: 4px 8px;
border-radius: 6px;
}
.mentions__suggestions__item--focused {
background-color: rgb(var(--grey-50));
}
@@ -0,0 +1,19 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { Mentions } from './Mentions';
export type { IMentionsProps } from './Mentions';
export { Mention, type MentionProps } from 'react-mentions';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useEvent } from 'rc-util';
import styles from './index.module.less';
@@ -93,27 +93,39 @@ function RectPopup(props: IRectPopupProps) {
const { children, anchorRect, direction = 'vertical', onClickOutside, excludeOutside } = props;
const nodeRef = useRef<HTMLElement>(null);
const clickOtherFn = useEvent(onClickOutside ?? (() => { /* empty */ }));
const [position, setPosition] = useState<Partial<IAbsolutePosition>>({
top: -9999,
left: -9999,
});
// TODO@weird94: push the content into the visible area.
const style = useMemo(() => ({ ...position }), [position]);
useEffect(() => {
const { clientWidth, clientHeight } = nodeRef.current!;
const { innerWidth, innerHeight } = window;
setPosition(calcPopupPosition({
position: anchorRect,
width: clientWidth,
height: clientHeight,
containerWidth: innerWidth,
containerHeight: innerHeight,
direction,
}));
requestAnimationFrame(() => {
if (!nodeRef.current) {
return;
}
const { clientWidth, clientHeight } = nodeRef.current;
const parent = nodeRef.current.parentElement;
if (!parent) {
return;
}
const { clientWidth: innerWidth, clientHeight: innerHeight } = parent;
setPosition(
calcPopupPosition(
{
position: anchorRect,
width: clientWidth,
height: clientHeight,
containerWidth: innerWidth,
containerHeight: innerHeight,
direction,
}
)
);
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[
anchorRect.left,
anchorRect.top,
@@ -124,12 +136,18 @@ function RectPopup(props: IRectPopupProps) {
useEffect(() => {
const handleClickOther = (e: MouseEvent) => {
if (excludeOutside && (excludeOutside.indexOf(e.target as any) > -1)) {
if (
excludeOutside &&
(
(excludeOutside.indexOf(e.target as any) > -1) ||
excludeOutside.some((item) => item.contains(e.target as any)
)
)
) {
return;
}
const x = e.clientX;
const y = e.clientY;
const x = e.offsetX;
const y = e.offsetY;
if (x <= anchorRect.right && x >= anchorRect.left && y <= anchorRect.bottom && y >= anchorRect.top) {
return;
}
@@ -146,8 +164,8 @@ function RectPopup(props: IRectPopupProps) {
return (
<section
ref={nodeRef}
className={styles.popup}
style={position}
style={style}
className={styles.popupAbsolute}
onClick={(e) => {
e.stopPropagation();
}}
@@ -1,6 +1,6 @@
.popup-absolute {
position: absolute;
z-index: 1070;
z-index: 100;
top: -9999px;
left: -9999px;
}
+2
View File
@@ -61,3 +61,5 @@ export { enUS, zhCN, ruRU } from './locale';
export { type ILocale } from './locale/interface';
export { defaultTheme, greenTheme, themeInstance } from './themes';
export { DraggableList, type IDraggableListProps } from './components/draggable-list';
export { Textarea, type ITextareaProps } from './components/textarea';
export { Mentions, type IMentionsProps, Mention, type MentionProps } from './components/mentions';
@@ -186,7 +186,7 @@ export function serializeRangeToRefString(gridRangeName: IUnitRangeName) {
return serializeRange(range);
}
function singleReferenceToGrid(refBody: string) {
export function singleReferenceToGrid(refBody: string) {
const row = Number.parseInt(refBody.replace($ROW_REGEX, '')) - 1;
const column = Tools.ABCatNum(refBody.replace($COLUMN_REGEX, ''));
+1
View File
@@ -84,6 +84,7 @@ export {
isReferenceStringWithEffectiveColumn,
getRangeWithRefsString,
isReferenceStrings,
singleReferenceToGrid,
} from './engine/utils/reference';
export { generateStringWithSequence, type ISequenceNode, sequenceNodeType } from './engine/utils/sequence';
export { ArrayValueObject, ValueObjectFactory } from './engine/value-object/array-value-object';
@@ -206,6 +206,7 @@ export class DataValidationRenderController extends RxDisposable {
this._sheetInterceptorService.intercept(
INTERCEPTOR_POINT.CELL_CONTENT,
{
priority: 200,
// eslint-disable-next-line max-lines-per-function, complexity
handler: (cell, pos, next) => {
const { row, col, unitId, subUnitId } = pos;
+16
View File
@@ -0,0 +1,16 @@
# @univerjs/sheets-thread-comment
[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-thread-comment)](https://npmjs.org/packages/@univerjs/sheets-thread-comment)
[![license](https://img.shields.io/npm/l/@univerjs/sheets-thread-comment)](https://img.shields.io/npm/l/@univerjs/sheets-thread-comment)
## Introduction
> TODO: Introduction
## Usage
### Installation
```shell
npm i @univerjs/sheets-thread-comment
```
@@ -0,0 +1,97 @@
{
"name": "@univerjs/sheets-thread-comment",
"version": "0.0.1",
"private": true,
"description": "",
"author": "DreamNum <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": [],
"sideEffects": [
"**/*.css"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*"
},
"main": "./lib/cjs/index.js",
"module": "./lib/es/index.js",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/cjs/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"
},
"./lib/*": "./lib/*"
}
},
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"lint:types": "tsc --noEmit",
"build": "tsc && vite build"
},
"peerDependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/thread-comment": "workspace:*",
"@univerjs/thread-comment-ui": "workspace:*",
"@univerjs/ui": "workspace:*",
"@wendellhu/redi": "^0.15.1",
"react": "^16.9.0 || ^17.0.0 || ^18.0.0",
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/icons": "^0.1.45",
"@univerjs/thread-comment": "workspace:*",
"@univerjs/thread-comment-ui": "workspace:*"
},
"devDependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/shared": "workspace:*",
"@univerjs/thread-comment": "workspace:*",
"@univerjs/thread-comment-ui": "workspace:*",
"@univerjs/ui": "workspace:*",
"@wendellhu/redi": "^0.15.1",
"clsx": "^2.1.0",
"less": "^4.2.0",
"react": "^18.2.0",
"rxjs": "^7.8.1",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vitest": "^1.3.1"
}
}
@@ -0,0 +1,70 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommand } from '@univerjs/core';
import { CommandType, IUniverInstanceService } from '@univerjs/core';
import type { ISheetLocation } from '@univerjs/sheets';
import { getSheetCommandTarget, SelectionManagerService } from '@univerjs/sheets';
import { ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
import { SheetsThreadCommentPopupService } from '../../services/sheets-thread-comment-popup.service';
import { SheetsThreadCommentModel } from '../../models/sheets-thread-comment.model';
export const ShowAddSheetCommentModalOperation: ICommand = {
type: CommandType.OPERATION,
id: 'sheets.operation.show-comment-modal',
handler(accessor) {
const selectionManagerService = accessor.get(SelectionManagerService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const sheetsThreadCommentPopupService = accessor.get(SheetsThreadCommentPopupService);
const threadCommentPanelService = accessor.get(ThreadCommentPanelService);
const activeCell = selectionManagerService.getFirst()?.primary;
const current = selectionManagerService.getCurrent();
const model = accessor.get(SheetsThreadCommentModel);
if (!current || !activeCell) {
return false;
}
const { unitId, sheetId } = current;
const result = getSheetCommandTarget(univerInstanceService, { unitId, subUnitId: sheetId });
if (!result) {
return false;
}
const { workbook, worksheet } = result;
const location: ISheetLocation = {
workbook,
worksheet,
unitId,
subUnitId: sheetId,
row: activeCell.actualRow,
col: activeCell.startColumn,
};
sheetsThreadCommentPopupService.showPopup(location);
const rootId = model.getByLocation(unitId, sheetId, activeCell.actualRow, activeCell.startColumn);
if (rootId) {
threadCommentPanelService.setActiveComment({
unitId,
subUnitId: sheetId,
commentId: rootId,
trigger: 'context-menu',
});
}
return true;
},
};
@@ -0,0 +1,52 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { IMenuItem, IShortcutItem } from '@univerjs/ui';
import { getMenuHiddenObservable, KeyCode, MenuItemType, MenuPosition, MetaKeys } from '@univerjs/ui';
import type { IAccessor } from '@wendellhu/redi';
import { ToggleSheetCommentPanelOperation } from '@univerjs/thread-comment-ui';
import { UniverInstanceType } from '@univerjs/core';
import { whenSheetEditorFocused } from '@univerjs/sheets-ui';
import { ShowAddSheetCommentModalOperation } from '../commands/operations/comment.operation';
import { COMMENT_SINGLE_ICON } from '../types/const';
export const threadCommentMenuFactory = (accessor: IAccessor) => {
return {
id: ShowAddSheetCommentModalOperation.id,
type: MenuItemType.BUTTON,
positions: [MenuPosition.CONTEXT_MENU],
icon: COMMENT_SINGLE_ICON,
title: 'sheetThreadComment.menu.addComment',
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
} as IMenuItem;
};
export const threadPanelMenuFactory = (accessor: IAccessor) => {
return {
id: ToggleSheetCommentPanelOperation.id,
type: MenuItemType.BUTTON,
icon: COMMENT_SINGLE_ICON,
tooltip: 'sheetThreadComment.menu.commentManagement',
positions: MenuPosition.TOOLBAR_START,
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
};
};
export const AddCommentShortcut: IShortcutItem = {
id: ShowAddSheetCommentModalOperation.id,
binding: KeyCode.M | MetaKeys.CTRL_COMMAND | MetaKeys.ALT,
preconditions: whenSheetEditorFocused,
};
@@ -0,0 +1,90 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType } from '@univerjs/core';
import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
import { Inject } from '@wendellhu/redi';
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import type { Spreadsheet } from '@univerjs/engine-render';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SheetsThreadCommentModel } from '../../models/sheets-thread-comment.model';
@OnLifecycle(LifecycleStages.Ready, SheetsThreadCommentRenderController)
export class SheetsThreadCommentRenderController extends Disposable {
constructor(
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(SheetsThreadCommentModel) private readonly _sheetsThreadCommentModel: SheetsThreadCommentModel,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
) {
super();
this._initViewModelIntercept();
this._initSkeletonChange();
}
private _initViewModelIntercept() {
this.disposeWithMe(
this._sheetInterceptorService.intercept(
INTERCEPTOR_POINT.CELL_CONTENT,
{
handler: (cell, pos, next) => {
const { row, col, unitId, subUnitId } = pos;
if (this._sheetsThreadCommentModel.showCommentMarker(unitId, subUnitId, row, col)) {
return next({
...cell,
markers: {
...cell?.markers,
tr: {
color: '#FFBD37',
size: 6,
},
},
});
}
return next(cell);
},
priority: 100,
}
)
);
}
private _initSkeletonChange() {
const markSkeletonDirty = () => {
const workbook = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) return;
const unitId = workbook.getUnitId();
const subUnitId = workbook.getActiveSheet().getSheetId();
const skeleton = this._sheetSkeletonManagerService.getOrCreateSkeleton({ unitId, sheetId: subUnitId });
const currentRender = this._renderManagerService.getRenderById(unitId);
skeleton?.makeDirty(true);
skeleton?.calculate();
if (currentRender) {
(currentRender.mainComponent as Spreadsheet).makeForceDirty();
}
};
this.disposeWithMe(this._sheetsThreadCommentModel.commentUpdate$.subscribe(() => {
markSkeletonDirty();
}));
}
}
@@ -0,0 +1,158 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { Disposable, LifecycleStages, OnLifecycle, Range } from '@univerjs/core';
import { COPY_TYPE, ISheetClipboardService } from '@univerjs/sheets-ui';
import { Inject } from '@wendellhu/redi';
import { AddCommentMutation, DeleteCommentMutation, type IThreadComment } from '@univerjs/thread-comment';
import { serializeRange, singleReferenceToGrid } from '@univerjs/engine-formula';
import { SHEETS_THREAD_COMMENT } from '../types/const';
import { SheetsThreadCommentModel } from '../models/sheets-thread-comment.model';
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);
};
@OnLifecycle(LifecycleStages.Rendered, SheetsThreadCommentCopyPasteController)
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
) {
super();
this._initClipboardHook();
}
// eslint-disable-next-line max-lines-per-function
private _initClipboardHook() {
this.disposeWithMe(
this._sheetClipboardService.addClipboardHook({
id: SHEETS_THREAD_COMMENT,
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: { root: IThreadComment; children: IThreadComment[] }[] = [];
Range.foreach(range, (row, col) => {
const root = this._sheetsThreadCommentModel.getCommentWithChildren(sourceUnitId, sourceSubUnitId, row, col);
if (root) {
roots.push(root);
}
});
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,
},
},
});
sourceUndos.push({
id: AddCommentMutation.id,
params: {
unitId: sourceUnitId,
subUnitId: sourceSubUnitId,
comment: item,
},
});
targetUndos.unshift({
id: DeleteCommentMutation.id,
params: {
unitId: targetUnitId,
subUnitId: targetSubUnitId,
commentId: item.id,
},
});
};
roots.forEach((root) => {
handleCommentItem(root.root);
root.children.forEach((child) => {
handleCommentItem(child);
});
});
return {
redos: [...sourceRedos, ...targetRedos],
undos: [...targetUndos, ...sourceUndos],
};
}
}
return {
redos: [],
undos: [],
};
},
})
);
}
}
@@ -0,0 +1,62 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, LifecycleStages, OnLifecycle } from '@univerjs/core';
import { HoverManagerService } from '@univerjs/sheets-ui';
import { Inject } from '@wendellhu/redi';
import { SheetsThreadCommentPopupService } from '../services/sheets-thread-comment-popup.service';
import { SheetsThreadCommentModel } from '../models/sheets-thread-comment.model';
@OnLifecycle(LifecycleStages.Rendered, SheetsThreadCommentHoverController)
export class SheetsThreadCommentHoverController extends Disposable {
constructor(
@Inject(HoverManagerService) private readonly _hoverManagerService: HoverManagerService,
@Inject(SheetsThreadCommentPopupService) private readonly _sheetsThreadCommentPopupService: SheetsThreadCommentPopupService,
@Inject(SheetsThreadCommentModel) private readonly _sheetsThreadCommentModel: SheetsThreadCommentModel
) {
super();
this._initHoverEvent();
}
private _initHoverEvent() {
this._hoverManagerService.currentCell$.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 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();
}
}
}
});
}
}
@@ -0,0 +1,182 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { IRange } from '@univerjs/core';
import { Disposable, LifecycleStages, OnLifecycle, toDisposable } from '@univerjs/core';
import type { IDisposable } from '@wendellhu/redi';
import { Inject } from '@wendellhu/redi';
import type { EffectRefRangeParams } from '@univerjs/sheets';
import { handleDefaultRangeChangeWithEffectRefCommands, RefRangeService } from '@univerjs/sheets';
import type { IAddCommentMutationParams, IUpdateCommentRefMutationParams } from '@univerjs/thread-comment';
import { AddCommentMutation, DeleteCommentMutation, ThreadCommentModel, UpdateCommentRefMutation } from '@univerjs/thread-comment';
import { serializeRange, singleReferenceToGrid } from '@univerjs/engine-formula';
import type { ISheetThreadComment } from '../types/interfaces/i-sheet-thread-comment';
import { SheetsThreadCommentModel } from '../models/sheets-thread-comment.model';
@OnLifecycle(LifecycleStages.Starting, SheetsThreadCommentRefRangeController)
export class SheetsThreadCommentRefRangeController extends Disposable {
private _disposableMap = new Map<string, IDisposable>();
constructor(
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService,
@Inject(SheetsThreadCommentModel) private readonly _sheetsThreadCommentModel: SheetsThreadCommentModel,
@Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel
) {
super();
this._initData();
this._initRefRange();
}
private _getIdWithUnitId(unitId: string, subUnitId: string, id: string) {
return `${unitId}-${subUnitId}-${id}`;
}
private _register(unitId: string, subUnitId: string, comment: ISheetThreadComment) {
const commentId = comment.id;
const oldRange: IRange = {
startColumn: comment.column,
endColumn: comment.column,
startRow: comment.row,
endRow: comment.row,
};
const handleRangeChange = (commandInfo: EffectRefRangeParams) => {
const resultRange = handleDefaultRangeChangeWithEffectRefCommands(oldRange, commandInfo);
if (resultRange && resultRange.startColumn === oldRange.startColumn && resultRange.startRow === oldRange.startRow) {
return {
undos: [],
redos: [],
};
}
if (!resultRange) {
return {
redos: [{
id: DeleteCommentMutation.id,
params: {
unitId,
subUnitId,
commentId,
},
}],
undos: [{
id: AddCommentMutation.id,
params: {
unitId,
subUnitId,
comment,
} as IAddCommentMutationParams,
}],
};
}
return {
redos: [{
id: UpdateCommentRefMutation.id,
params: {
unitId,
subUnitId,
payload: {
ref: serializeRange(resultRange),
commentId,
},
} as IUpdateCommentRefMutationParams,
}],
undos: [{
id: UpdateCommentRefMutation.id,
params: {
unitId,
subUnitId,
payload: {
ref: serializeRange(oldRange),
commentId,
},
} as IUpdateCommentRefMutationParams,
}],
};
};
this._disposableMap.set(
this._getIdWithUnitId(unitId, subUnitId, commentId),
this._refRangeService.registerRefRange(oldRange, handleRangeChange, unitId, subUnitId)
);
}
private _initData() {
const data = this._threadCommentModel.getAll();
for (const unitId in data) {
const unitMap = data[unitId];
for (const subUnitId in unitMap) {
const subUnitMap = unitMap[subUnitId];
for (const id in subUnitMap) {
const comment = subUnitMap[id];
const ref = comment.ref;
const pos = singleReferenceToGrid(ref);
this._register(unitId, subUnitId, {
...comment,
...pos,
});
}
}
}
}
private _initRefRange() {
this.disposeWithMe(
this._sheetsThreadCommentModel.commentUpdate$.subscribe((option) => {
const { unitId, subUnitId } = option;
switch (option.type) {
case 'add': {
const comment = option.payload;
this._register(option.unitId, option.subUnitId, {
...comment,
row: option.row,
column: option.column,
});
break;
}
case 'delete': {
const disposable = this._disposableMap.get(this._getIdWithUnitId(unitId, subUnitId, option.payload.commentId));
disposable?.dispose();
break;
}
case 'updateRef': {
const comment = this._sheetsThreadCommentModel.getComment(unitId, subUnitId, option.payload.commentId);
if (!comment) {
return;
}
const disposable = this._disposableMap.get(this._getIdWithUnitId(unitId, subUnitId, comment.id));
disposable?.dispose();
this._register(option.unitId, option.subUnitId, {
...comment,
row: option.row,
column: option.column,
});
break;
}
}
})
);
this.disposeWithMe(toDisposable(() => {
this._disposableMap.forEach((item) => {
item.dispose();
});
this._disposableMap.clear();
}));
}
}
@@ -0,0 +1,77 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType } from '@univerjs/core';
import { Inject } from '@wendellhu/redi';
import type { IRemoveSheetCommandParams } from '@univerjs/sheets';
import { RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
import type { IDeleteCommentMutationParams } from '@univerjs/thread-comment';
import { AddCommentMutation, DeleteCommentMutation, ThreadCommentModel } from '@univerjs/thread-comment';
@OnLifecycle(LifecycleStages.Ready, ThreadCommentRemoveSheetsController)
export class ThreadCommentRemoveSheetsController extends Disposable {
constructor(
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
@IUniverInstanceService private _univerInstanceService: IUniverInstanceService,
@Inject(ThreadCommentModel) private _threadCommentModel: ThreadCommentModel
) {
super();
this._initSheetChange();
}
private _initSheetChange() {
this.disposeWithMe(
this._sheetInterceptorService.interceptCommand({
getMutations: (commandInfo) => {
if (commandInfo.id === RemoveSheetCommand.id) {
const params = commandInfo.params as IRemoveSheetCommandParams;
const workbook = params.unitId ? this._univerInstanceService.getUnit<Workbook>(params.unitId) : this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return { redos: [], undos: [] };
}
const unitId = workbook.getUnitId();
const subUnitId = params.subUnitId || workbook.getActiveSheet().getSheetId();
const { commentMap } = this._threadCommentModel.ensureMap(unitId, subUnitId);
const ids = Array.from(Object.keys(commentMap));
const comments = Array.from(Object.values(commentMap));
const redos = ids.map((id) => ({
id: DeleteCommentMutation.id,
params: {
unitId,
subUnitId,
commentId: id,
} as IDeleteCommentMutationParams,
}));
const undos = comments.map((comment) => ({
id: AddCommentMutation.id,
params: {
unitId,
subUnitId,
comment,
},
}));
return { redos, undos };
}
return { redos: [], undos: [] };
},
})
);
}
}
@@ -0,0 +1,177 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, ICommandService, IUniverInstanceService, LifecycleStages, LocaleService, OnLifecycle, UniverInstanceType } from '@univerjs/core';
import { ComponentManager, IMenuService, IShortcutService } from '@univerjs/ui';
import { Inject, Injector } from '@wendellhu/redi';
import { CommentSingle } from '@univerjs/icons';
import { SetActiveCommentOperation, THREAD_COMMENT_PANEL, ThreadCommentPanelService } from '@univerjs/thread-comment-ui';
import type { ISetSelectionsOperationParams } from '@univerjs/sheets';
import { SelectionMoveType, SetSelectionsOperation, SetWorksheetActiveOperation } from '@univerjs/sheets';
import { singleReferenceToGrid } from '@univerjs/engine-formula';
import { ScrollToCellCommand } from '@univerjs/sheets-ui';
import type { IDeleteCommentMutationParams } from '@univerjs/thread-comment';
import { DeleteCommentMutation } from '@univerjs/thread-comment';
import { SheetsThreadCommentCell } from '../views/sheets-thread-comment-cell';
import { COMMENT_SINGLE_ICON, SHEETS_THREAD_COMMENT_MODAL } from '../types/const';
import { SheetsThreadCommentPanel } from '../views/sheets-thread-comment-panel';
import { enUS, zhCN } from '../locales';
import { SheetsThreadCommentPopupService } from '../services/sheets-thread-comment-popup.service';
import { SheetsThreadCommentModel } from '../models/sheets-thread-comment.model';
import { AddCommentShortcut, threadCommentMenuFactory, threadPanelMenuFactory } from './menu';
@OnLifecycle(LifecycleStages.Starting, SheetsThreadCommentController)
export class SheetsThreadCommentController extends Disposable {
constructor(
@IMenuService private readonly _menuService: IMenuService,
@Inject(Injector) private readonly _injector: Injector,
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
@Inject(LocaleService) private readonly _localeService: LocaleService,
@ICommandService private readonly _commandService: ICommandService,
@Inject(SheetsThreadCommentPopupService) private readonly _sheetsThreadCommentPopupService: SheetsThreadCommentPopupService,
@Inject(SheetsThreadCommentModel) private readonly _sheetsThreadCommentModel: SheetsThreadCommentModel,
@Inject(ThreadCommentPanelService) private readonly _threadCommentPanelService: ThreadCommentPanelService,
@IShortcutService private readonly _shortcutService: IShortcutService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
) {
super();
this._initMenu();
this._initShortcut();
this._initComponent();
this._initLocale();
this._initCommandListener();
this._initPanelListener();
}
private _initShortcut() {
this._shortcutService.registerShortcut(AddCommentShortcut);
}
private _initCommandListener() {
this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === SetSelectionsOperation.id) {
const params = commandInfo.params as ISetSelectionsOperationParams;
const { unitId, subUnitId, selections, type } = params;
if ((type === SelectionMoveType.MOVE_END || type === undefined) && selections[0].primary) {
const row = selections[0].primary.actualRow;
const col = selections[0].primary.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,
});
}
}
}
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 _initMenu() {
[
threadCommentMenuFactory,
threadPanelMenuFactory,
].forEach((menuFactory) => {
this._menuService.addMenuItem(menuFactory(this._injector));
});
}
private _initComponent() {
([
[SHEETS_THREAD_COMMENT_MODAL, SheetsThreadCommentCell],
[THREAD_COMMENT_PANEL, SheetsThreadCommentPanel],
[COMMENT_SINGLE_ICON, CommentSingle],
] as const).forEach(([key, comp]) => {
this._componentManager.register(key, comp);
});
}
private _initLocale() {
this._localeService.load({
zhCN,
enUS,
});
}
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 currentUnit = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!currentUnit) {
return;
}
const currentUnitId = currentUnit.getUnitId();
if (currentUnitId !== unitId) {
return;
}
const currentSheetId = currentUnit.getActiveSheet().getSheetId();
if (currentSheetId !== subUnitId) {
await this._commandService.executeCommand(SetWorksheetActiveOperation.id, {
unitId,
subUnitId,
});
}
const location = singleReferenceToGrid(comment.ref);
await this._commandService.executeCommand(ScrollToCellCommand.id, {
range: {
startColumn: location.column,
endColumn: location.column,
startRow: location.row,
endRow: location.row,
},
});
this._sheetsThreadCommentPopupService.showPopup({
unitId,
subUnitId,
row: location.row,
col: location.column,
commentId: comment.id,
trigger,
});
} else {
this._sheetsThreadCommentPopupService.hidePopup();
}
}));
}
}
@@ -0,0 +1,29 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { Dependency } from '@wendellhu/redi';
export { Inject, Injector } from '@wendellhu/redi';
export { ICommandService, Plugin, UniverInstanceType } from '@univerjs/core';
export { UniverThreadCommentUIPlugin } from '@univerjs/thread-comment-ui';
export { SheetsThreadCommentController } from './controllers/sheets-thread-comment.controller';
export { SheetsThreadCommentRefRangeController } from './controllers/sheets-thread-comment-ref-range.controller';
export { SheetsThreadCommentModel } from './models/sheets-thread-comment.model';
export { SheetsThreadCommentPopupService } from './services/sheets-thread-comment-popup.service';
export { UniverSheetsThreadCommentPlugin } from './plugin';
export { SHEETS_THREAD_COMMENT } from './types/const';
export { SheetsThreadCommentCopyPasteController } from './controllers/sheets-thread-comment-copy-paste.controller';
export { SheetsThreadCommentHoverController } from './controllers/sheets-thread-comment-hover.controller';
export { ThreadCommentRemoveSheetsController } from './controllers/sheets-thread-comment-remove.controller';
@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 default {
sheetThreadComment: {
menu: {
addComment: 'Add Comment',
commentManagement: 'Comment Management',
},
},
};
@@ -0,0 +1,18 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { default as zhCN } from './zhCN';
export { default as enUS } from './enUS';
@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 default {
sheetThreadComment: {
menu: {
addComment: '添加评论',
commentManagement: '评论管理',
},
},
};
@@ -0,0 +1,234 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, IUniverInstanceService, ObjectMatrix, UniverInstanceType } from '@univerjs/core';
import type { CommentUpdate, IThreadComment } from '@univerjs/thread-comment';
import { ThreadCommentModel } from '@univerjs/thread-comment';
import { Inject } from '@wendellhu/redi';
import { singleReferenceToGrid } from '@univerjs/engine-formula';
import { Subject } from 'rxjs';
export type SheetCommentUpdate = CommentUpdate & {
row: number;
column: number;
};
export class SheetsThreadCommentModel extends Disposable {
private _matrixMap: Map<string, Map<string, ObjectMatrix<string>>> = new Map();
private _locationMap: Map<string, Map<string, Map<string, { row: number; column: number }>>> = new Map();
private _commentUpdate$ = new Subject<SheetCommentUpdate>();
commentUpdate$ = this._commentUpdate$.asObservable();
constructor(
@Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
) {
super();
this._init();
}
private _init() {
this._initData();
this._initUpdateTransform();
}
private _ensureCommentMatrix(unitId: string, subUnitId: string) {
let unitMap = this._matrixMap.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._matrixMap.set(unitId, unitMap);
}
let subUnitMap = unitMap.get(subUnitId);
if (!subUnitMap) {
subUnitMap = new ObjectMatrix();
unitMap.set(subUnitId, subUnitMap);
}
return subUnitMap;
}
private _ensureCommentLocationMap(unitId: string, subUnitId: string) {
let unitMap = this._locationMap.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._locationMap.set(unitId, unitMap);
}
let subUnitMap = unitMap.get(subUnitId);
if (!subUnitMap) {
subUnitMap = new Map();
unitMap.set(subUnitId, subUnitMap);
}
return subUnitMap;
}
private _ensure(unitId: string, subUnitId: string) {
const matrix = this._ensureCommentMatrix(unitId, subUnitId);
const locationMap = this._ensureCommentLocationMap(unitId, subUnitId);
return { matrix, locationMap };
}
private _initData() {
const data = this._threadCommentModel.getAll();
for (const unitId in data) {
const unitMap = data[unitId];
for (const subUnitId in unitMap) {
const subUnitMap = unitMap[subUnitId];
for (const id in subUnitMap) {
const comment = subUnitMap[id];
this._addComment(unitId, subUnitId, comment);
}
}
}
}
private _addComment(unitId: string, subUnitId: string, comment: IThreadComment) {
const location = singleReferenceToGrid(comment.ref);
const parentId = comment.parentId;
const { row, column } = location;
const commentId = comment.id;
const { matrix, locationMap } = this._ensure(unitId, subUnitId);
if (!parentId && row >= 0 && column >= 0) {
matrix.setValue(row, column, commentId);
locationMap.set(commentId, { row, column });
}
this._commentUpdate$.next({
unitId,
subUnitId,
payload: comment,
type: 'add',
...location,
});
}
private _initUpdateTransform() {
this.disposeWithMe(this._threadCommentModel.commentUpdate$.subscribe((update) => {
const { unitId, subUnitId } = update;
try {
const type = this._univerInstanceService.getUnitType(unitId);
if (type !== UniverInstanceType.UNIVER_SHEET) {
return;
}
} catch (error) {
// do nothing
}
const { matrix, locationMap } = this._ensure(unitId, subUnitId);
switch (update.type) {
case 'add': {
this._addComment(update.unitId, update.subUnitId, update.payload);
break;
}
case 'delete': {
const { isRoot, comment } = update.payload;
const location = singleReferenceToGrid(comment.ref);
if (isRoot) {
const { row, column } = location;
if (row >= 0 && column >= 0) {
matrix.realDeleteValue(row, column);
}
}
this._commentUpdate$.next({
...update,
...location,
});
break;
}
case 'update': {
const { commentId } = update.payload;
const comment = this._threadCommentModel.getComment(unitId, subUnitId, commentId);
if (!comment) {
return;
}
const location = singleReferenceToGrid(comment.ref);
this._commentUpdate$.next({
...update,
...location,
});
break;
}
case 'updateRef': {
const location = singleReferenceToGrid(update.payload.ref);
const { commentId } = update.payload;
const currentLoc = locationMap.get(commentId);
if (!currentLoc) {
return;
}
const { row, column } = currentLoc;
const currentCommentId = matrix.getValue(row, column);
if (currentCommentId === commentId) {
matrix.realDeleteValue(row, column);
locationMap.delete(commentId);
}
if (location.row >= 0 && location.column >= 0) {
matrix.setValue(location.row, location.column, commentId);
locationMap.set(commentId, { row: location.row, column: location.column });
}
this._commentUpdate$.next({
...update,
...location,
});
break;
}
default:
break;
}
}));
}
getByLocation(unitId: string, subUnitId: string, row: number, column: number): string | undefined {
const matrix = this._ensureCommentMatrix(unitId, subUnitId);
return matrix.getValue(row, column);
}
getComment(unitId: string, subUnitId: string, commentId: string) {
return this._threadCommentModel.getComment(unitId, subUnitId, commentId);
}
getCommentWithChildren(unitId: string, subUnitId: string, row: number, column: number) {
const matrix = this._ensureCommentMatrix(unitId, subUnitId);
const commentId = matrix.getValue(row, column);
if (!commentId) {
return undefined;
}
return this._threadCommentModel.getCommentWithChildren(unitId, subUnitId, commentId);
}
showCommentMarker(unitId: string, subUnitId: string, row: number, column: number) {
const commentId = this.getByLocation(unitId, subUnitId, row, column);
if (!commentId) {
return false;
}
const comment = this.getComment(unitId, subUnitId, commentId);
if (comment && !comment.resolved) {
return true;
}
return false;
}
}
@@ -0,0 +1,79 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 '@wendellhu/redi';
import { Inject, Injector } from '@wendellhu/redi';
import { ICommandService, IConfigService, UniverInstanceType } from '@univerjs/core';
import type { IThreadCommentUIConfig } from '@univerjs/thread-comment-ui';
import { UniverThreadCommentUIPlugin } from '@univerjs/thread-comment-ui';
import { SheetsThreadCommentController } from './controllers/sheets-thread-comment.controller';
import { SheetsThreadCommentRefRangeController } from './controllers/sheets-thread-comment-ref-range.controller';
import { SheetsThreadCommentModel } from './models/sheets-thread-comment.model';
import { SheetsThreadCommentPopupService } from './services/sheets-thread-comment-popup.service';
import { ShowAddSheetCommentModalOperation } from './commands/operations/comment.operation';
import { SheetsThreadCommentRenderController } from './controllers/render-controllers/render.controller';
import { SHEETS_THREAD_COMMENT } from './types/const';
import { SheetsThreadCommentCopyPasteController } from './controllers/sheets-thread-comment-copy-paste.controller';
import { SheetsThreadCommentHoverController } from './controllers/sheets-thread-comment-hover.controller';
import { ThreadCommentRemoveSheetsController } from './controllers/sheets-thread-comment-remove.controller';
const defaultConfig: IThreadCommentUIConfig = {
mentions: [{
trigger: '@',
async getMentions() {
return [{
id: 'mock',
label: 'MockUser',
type: 'user',
}];
},
}],
};
export class UniverSheetsThreadCommentPlugin extends UniverThreadCommentUIPlugin {
static override pluginName = SHEETS_THREAD_COMMENT;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: IThreadCommentUIConfig = defaultConfig,
@Inject(Injector) protected override _injector: Injector,
@Inject(ICommandService) protected override _commandService: ICommandService,
@Inject(IConfigService) protected override _configService: IConfigService
) {
super(_config, _injector, _commandService, _configService);
}
override onStarting(injector: Injector): void {
super.onStarting(injector);
([
[SheetsThreadCommentModel],
[SheetsThreadCommentController],
[SheetsThreadCommentRefRangeController],
[SheetsThreadCommentRenderController],
[SheetsThreadCommentCopyPasteController],
[SheetsThreadCommentHoverController],
[ThreadCommentRemoveSheetsController],
[SheetsThreadCommentPopupService],
] as Dependency[]).forEach((dep) => {
this._injector.add(dep);
});
[ShowAddSheetCommentModalOperation].forEach((command) => {
this._commandService.registerCommand(command);
});
}
}
@@ -0,0 +1,134 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, DisposableCollection } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import { SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
import { IZenZoneService } from '@univerjs/ui';
import { type IDisposable, Inject } from '@wendellhu/redi';
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,
@IZenZoneService private readonly _zenZoneService: IZenZoneService
) {
super();
this._initZenVisible();
}
private _initZenVisible() {
this.disposeWithMe(this._zenZoneService.visible$.subscribe((visible) => {
if (visible) {
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;
}
this._lastPopup && this._lastPopup.dispose();
if (this._zenZoneService.visible) {
return;
}
this._activePopup = location;
this._activePopup$.next(location);
const popupDisposable = this._canvasPopupManagerService.attachPopupToCell(
row,
col,
{
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[],
}
);
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;
}
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);
}
}
@@ -0,0 +1,19 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 COMMENT_SINGLE_ICON = 'comment-single';
export const SHEETS_THREAD_COMMENT = 'univer.sheet.thread-comment';
@@ -0,0 +1,22 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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;
}
@@ -0,0 +1,62 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { useDependency } from '@wendellhu/redi/react-bindings';
import React from 'react';
import { ThreadCommentTree } from '@univerjs/thread-comment-ui';
import type { Workbook } from '@univerjs/core';
import { IUniverInstanceService, Tools, UniverInstanceType } from '@univerjs/core';
import { useObservable } from '@univerjs/ui';
import { SheetsThreadCommentModel } from '../../models/sheets-thread-comment.model';
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);
useObservable(sheetThreadCommentModel.commentUpdate$);
if (!activePopup) {
return null;
}
const { row, col, unitId, subUnitId, trigger } = activePopup;
const rootId = sheetThreadCommentModel.getByLocation(unitId, subUnitId, row, col);
const ref = `${Tools.chatAtABC(col)}${row + 1}`;
const onClose = () => {
sheetsThreadCommentPopupService.hidePopup();
};
const getSubUnitName = (id: string) => {
return univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId(id)?.getName() ?? '';
};
return (
<ThreadCommentTree
onClick={() => {
sheetsThreadCommentPopupService.persistPopup();
}}
prefix="cell"
id={rootId}
unitId={unitId}
subUnitId={subUnitId}
type={UniverInstanceType.UNIVER_SHEET}
refStr={ref}
onClose={onClose}
getSubUnitName={getSubUnitName}
autoFocus={trigger === 'context-menu'}
/>
);
};
@@ -0,0 +1,85 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { ThreadCommentPanel } from '@univerjs/thread-comment-ui';
import { useDependency } from '@wendellhu/redi/react-bindings';
import React, { useCallback, useMemo } from 'react';
import { map } from 'rxjs';
import type { IThreadComment } from '@univerjs/thread-comment';
import { singleReferenceToGrid } from '@univerjs/engine-formula';
import { ShowAddSheetCommentModalOperation } from '../../commands/operations/comment.operation';
import { SheetsThreadCommentPopupService } from '../../services/sheets-thread-comment-popup.service';
export const SheetsThreadCommentPanel = () => {
const univerInstanceService = useDependency(IUniverInstanceService);
const sheetsThreadCommentPopupService = useDependency(SheetsThreadCommentPopupService);
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return null;
}
const unitId = workbook.getUnitId();
const commandService = useDependency(ICommandService);
const subUnitId$ = useMemo(() => workbook.activeSheet$.pipe(map((i) => i?.getSheetId())), [workbook.activeSheet$]);
const sortComments = useCallback((comments: IThreadComment[]) => {
const worksheets = workbook.getSheets();
const sheetIndex: Record<string, number> = {};
worksheets.forEach((sheet, i) => {
sheetIndex[sheet.getSheetId()] = i;
});
return comments.map((comment) => {
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];
});
}, [workbook]);
const getSubUnitName = (id: string) => {
return workbook.getSheetBySheetId(id)?.getName() ?? '';
};
const handleAdd = () => {
commandService.executeCommand(ShowAddSheetCommentModalOperation.id);
};
const handleResolve = () => {
sheetsThreadCommentPopupService.hidePopup();
};
return (
<ThreadCommentPanel
unitId={unitId}
subUnitId$={subUnitId$}
type={UniverInstanceType.UNIVER_SHEET}
onAdd={handleAdd}
getSubUnitName={getSubUnitName}
onResolve={handleResolve}
sortComments={sortComments}
/>
);
};
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,9 @@
{
"extends": "@univerjs/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src"]
}
@@ -0,0 +1,4 @@
{
"extends": "@univerjs/shared/tsconfigs/node",
"include": ["vite.config.ts"]
}
@@ -0,0 +1,12 @@
import createViteConfig from '@univerjs/shared/vite';
import pkg from './package.json';
export default ({ mode }) => createViteConfig({}, {
mode,
pkg,
features: {
react: false,
css: true,
dom: true,
},
});
+1 -1
View File
@@ -81,7 +81,7 @@
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/icons": "^0.1.44"
"@univerjs/icons": "^0.1.45"
},
"devDependencies": {
"@types/react": "^18.2.79",
@@ -69,13 +69,12 @@ export class SheetCanvasPopManagerService extends Disposable {
};
const offsetBound = transformBound2OffsetBound(bound, scene, skeleton, worksheet);
const bounding = currentRender.engine.getCanvasElement().getBoundingClientRect();
const position = {
left: offsetBound.left,
right: offsetBound.right,
top: offsetBound.top + bounding.top,
bottom: offsetBound.bottom + bounding.top,
top: offsetBound.top,
bottom: offsetBound.bottom,
};
return position;
};
@@ -256,7 +255,7 @@ export class SheetCanvasPopManagerService extends Disposable {
skeleton: SpreadsheetSkeleton,
activeViewport: Viewport
): IBoundRectNoAngle {
const { scene, engine } = currentRender;
const { scene } = currentRender;
const primaryWithCoord = skeleton.getCellByIndex(row, col);
const cellInfo = primaryWithCoord.isMergedMainCell ? primaryWithCoord.mergeInfo : primaryWithCoord;
@@ -267,12 +266,11 @@ export class SheetCanvasPopManagerService extends Disposable {
y: activeViewport.actualScrollY,
};
const bounding = engine.getCanvasElement().getBoundingClientRect();
return {
left: ((cellInfo.startX - scrollXY.x) * scaleX) + bounding.left,
right: (cellInfo.endX - scrollXY.x) * scaleX + bounding.left,
top: ((cellInfo.startY - scrollXY.y) * scaleY) + bounding.top,
bottom: ((cellInfo.endY - scrollXY.y) * scaleY) + bounding.top,
left: ((cellInfo.startX - scrollXY.x) * scaleX),
right: (cellInfo.endX - scrollXY.x) * scaleX,
top: ((cellInfo.startY - scrollXY.y) * scaleY),
bottom: ((cellInfo.endY - scrollXY.y) * scaleY),
};
}
+1 -1
View File
@@ -287,7 +287,7 @@ export {
} from './services/ref-range/util';
export { INTERCEPTOR_POINT } from './services/sheet-interceptor/interceptor-const';
export { SheetInterceptorService } from './services/sheet-interceptor/sheet-interceptor.service';
export type { ISheetLocation } from './services/sheet-interceptor/utils/interceptor';
export type { ISheetLocation, ISheetLocationBase, ISheetRowLocation } from './services/sheet-interceptor/utils/interceptor';
export { MergeCellController } from './controllers/merge-cell.controller';
export { AddMergeRedoSelectionsOperationFactory, AddMergeUndoSelectionsOperationFactory } from './commands/utils/handle-merge-operation';
@@ -16,15 +16,18 @@
import type { Workbook, Worksheet } from '@univerjs/core';
export interface ISheetLocation {
workbook: Workbook;
worksheet: Worksheet;
export interface ISheetLocationBase {
unitId: string;
subUnitId: string;
row: number;
col: number;
}
export interface ISheetLocation extends ISheetLocationBase {
workbook: Workbook;
worksheet: Worksheet;
}
export interface ISheetRowLocation {
workbook: Workbook;
worksheet: Worksheet;
+16
View File
@@ -0,0 +1,16 @@
# @univerjs/thread-comment
[![npm version](https://img.shields.io/npm/v/@univerjs/thread-comment)](https://npmjs.org/packages/@univerjs/thread-comment)
[![license](https://img.shields.io/npm/l/@univerjs/thread-comment)](https://img.shields.io/npm/l/@univerjs/thread-comment)
## Introduction
> TODO: Introduction
## Usage
### Installation
```shell
npm i @univerjs/thread-comment
```
+92
View File
@@ -0,0 +1,92 @@
{
"name": "@univerjs/thread-comment-ui",
"version": "0.0.1",
"private": true,
"description": "",
"author": "DreamNum <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": [],
"sideEffects": [
"**/*.css"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*"
},
"main": "./lib/cjs/index.js",
"module": "./lib/es/index.js",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/cjs/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"
},
"./lib/*": "./lib/*"
}
},
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"lint:types": "tsc --noEmit",
"build": "tsc && vite build"
},
"peerDependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/thread-comment": "workspace:*",
"@univerjs/ui": "workspace:*",
"@wendellhu/redi": "^0.15.1",
"dayjs": ">=1.11.0",
"react": "^16.9.0 || ^17.0.0 || ^18.0.0",
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/icons": "^0.1.45",
"@univerjs/protocol": "^0.1.20",
"@univerjs/thread-comment": "workspace:*"
},
"devDependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/shared": "workspace:*",
"@univerjs/ui": "workspace:*",
"@wendellhu/redi": "^0.15.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.10",
"less": "^4.2.0",
"react": "^18.2.0",
"rxjs": "^7.8.1",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vitest": "^1.5.0"
}
}
@@ -0,0 +1,61 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommand } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
import { ISidebarService } from '@univerjs/ui';
import type { IAccessor } from '@wendellhu/redi';
import { ThreadCommentPanelService } from '../../services/thread-comment-panel.service';
import { THREAD_COMMENT_PANEL } from '../../types/const';
export const ToggleSheetCommentPanelOperation: ICommand = {
id: 'thread-comment-ui.operation.toggle-panel',
type: CommandType.OPERATION,
handler(accessor: IAccessor) {
const sidebarService = accessor.get(ISidebarService);
const panelService = accessor.get(ThreadCommentPanelService);
if (panelService.panelVisible) {
sidebarService.close();
panelService.setPanelVisible(false);
} else {
sidebarService.open({
header: { title: 'threadCommentUI.panel.title' },
children: { label: THREAD_COMMENT_PANEL },
width: 320,
});
panelService.setPanelVisible(true);
}
return true;
},
};
export interface ISetActiveCommentOperationParams {
unitId: string;
subUnitId: string;
commentId: string;
}
export const SetActiveCommentOperation: ICommand<ISetActiveCommentOperationParams> = {
id: 'thread-comment-ui.operation.set-active-comment',
type: CommandType.OPERATION,
handler(accessor, params) {
const panelService = accessor.get(ThreadCommentPanelService);
panelService.setActiveComment(params);
return true;
},
};
@@ -0,0 +1,37 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, LifecycleStages, LocaleService, OnLifecycle } from '@univerjs/core';
import { Inject } from '@wendellhu/redi';
import { enUS, zhCN } from '../locales';
@OnLifecycle(LifecycleStages.Starting, ThreadCommentUIController)
export class ThreadCommentUIController extends Disposable {
constructor(
@Inject(LocaleService) private readonly _localeService: LocaleService
) {
super();
this._initLocales();
}
private _initLocales() {
this._localeService.load({
zhCN,
enUS,
});
}
}
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { UniverThreadCommentUIPlugin } from './plugin';
export { ToggleSheetCommentPanelOperation, SetActiveCommentOperation, type ISetActiveCommentOperationParams } from './commands/operations/comment.operations';
export { ThreadCommentPanelService } from './services/thread-comment-panel.service';
export { ThreadCommentPanel } from './views/thread-comment-panel';
export type { IThreadCommentPanelProps } from './views/thread-comment-panel';
export { ThreadCommentTree } from './views/thread-comment-tree';
export type { IThreadCommentTreeProps } from './views/thread-comment-tree';
export { THREAD_COMMENT_PANEL } from './types/const';
export type { IThreadCommentUIConfig } from './types/interfaces/i-thread-comment-mention';
@@ -0,0 +1,49 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 default {
threadCommentUI: {
panel: {
title: 'Comment Management',
empty: 'No comments yet',
filterEmpty: 'No match result',
reset: 'Reset Filter',
addComment: 'Add Comment',
},
editor: {
placeholder: 'Reply or add others with @',
reply: 'Comment',
cancel: 'Cancel',
save: 'Save',
},
item: {
edit: 'Edit',
delete: 'Delete This Comment',
},
filter: {
sheet: {
all: 'All sheet',
current: 'Current sheet',
},
status: {
all: 'All comments',
resolved: 'Resolved',
unsolved: 'Not resolved',
concernMe: 'Concern me',
},
},
},
};
@@ -0,0 +1,18 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { default as zhCN } from './zhCN';
export { default as enUS } from './enUS';
@@ -0,0 +1,49 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 default {
threadCommentUI: {
panel: {
title: '评论管理',
empty: '暂无评论',
filterEmpty: '没有匹配的结果',
reset: '重置',
addComment: '新建评论',
},
editor: {
placeholder: '回复',
reply: '回复',
cancel: '取消',
save: '保存',
},
item: {
edit: '编辑',
delete: '删除',
},
filter: {
sheet: {
all: '所有表格',
current: '当前表格',
},
status: {
all: '所有评论',
resolved: '已解决',
unsolved: '未解决',
concernMe: '与我有关',
},
},
},
};
+59
View File
@@ -0,0 +1,59 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { UniverThreadCommentPlugin } from '@univerjs/thread-comment';
import { ICommandService, IConfigService, UniverInstanceType } from '@univerjs/core';
import type { Dependency } from '@wendellhu/redi';
import { Inject, Injector } from '@wendellhu/redi';
import { PLUGIN_NAME } from './types/const';
import { ThreadCommentPanelService } from './services/thread-comment-panel.service';
import { SetActiveCommentOperation, ToggleSheetCommentPanelOperation } from './commands/operations/comment.operations';
import { ThreadCommentUIController } from './controllers/thread-comment-ui.controller';
import type { IThreadCommentUIConfig } from './types/interfaces/i-thread-comment-mention';
export class UniverThreadCommentUIPlugin extends UniverThreadCommentPlugin {
static override pluginName = PLUGIN_NAME;
static override type = UniverInstanceType.UNIVER_UNKNOWN;
constructor(
_config: IThreadCommentUIConfig,
@Inject(Injector) protected override _injector: Injector,
@ICommandService protected override _commandService: ICommandService,
@IConfigService protected _configService: IConfigService
) {
super(
_config,
_injector,
_commandService
);
this._configService.setConfig(PLUGIN_NAME, _config);
}
override onStarting(injector: Injector): void {
super.onStarting(injector);
([
[ThreadCommentUIController],
[ThreadCommentPanelService],
] as Dependency[]).forEach((dep) => {
injector.add(dep);
});
[ToggleSheetCommentPanelOperation, SetActiveCommentOperation].forEach((command) => {
this._commandService.registerCommand(command);
});
}
}
@@ -0,0 +1,76 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { ISidebarService } from '@univerjs/ui';
import { Inject } from '@wendellhu/redi';
import { BehaviorSubject, filter } from 'rxjs';
type ActiveCommentInfo = { unitId: string; subUnitId: string; commentId: string; trigger?: string } | undefined;
export class ThreadCommentPanelService extends Disposable {
private _panelVisible = false;
private _panelVisible$ = new BehaviorSubject<boolean>(false);
private _activeCommentId: ActiveCommentInfo;
private _activeCommentId$ = new BehaviorSubject<ActiveCommentInfo>(undefined);
panelVisible$ = this._panelVisible$.asObservable();
activeCommentId$ = this._activeCommentId$.asObservable();
constructor(
@Inject(ISidebarService) private readonly _sidebarService: ISidebarService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
) {
super();
this._init();
}
private _init() {
this.disposeWithMe(
this._sidebarService.sidebarOptions$.subscribe((opt) => {
if (!opt.visible) {
this.setPanelVisible(false);
}
})
);
this.disposeWithMe(
this._univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET)
.pipe(filter((sheet) => !sheet)).subscribe(() => {
this._sidebarService.close();
})
);
}
get panelVisible() {
return this._panelVisible;
}
get activeCommentId() {
return this._activeCommentId;
}
setPanelVisible(visible: boolean) {
this._panelVisible = visible;
this._panelVisible$.next(visible);
}
setActiveComment(commentInfo: ActiveCommentInfo) {
this._activeCommentId = commentInfo;
this._activeCommentId$.next(commentInfo);
}
}
@@ -0,0 +1,21 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 THREAD_COMMENT_POPUP = 'thread-comment-popup';
export const THREAD_COMMENT_PANEL = 'thread-comment-panel';
export const PLUGIN_NAME = 'thread-comment-ui-plugin';
@@ -0,0 +1,28 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { IThreadCommentMention } from '@univerjs/thread-comment';
import type { MentionProps } from '@univerjs/design';
export interface IThreadCommentMentionConfig {
getMentions: (search: string) => Promise<IThreadCommentMention[]>;
trigger: string;
renderSuggestion?: MentionProps['renderSuggestion'];
}
export interface IThreadCommentUIConfig {
mentions?: IThreadCommentMentionConfig[];
}
@@ -0,0 +1,25 @@
.thread-comment-editor-buttons {
margin-top: 12px;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.thread-comment-editor-suggestion {
display: flex;
display: flex;
align-items: center;
font-size: 13px;
color: rgb(var(--color-black));
}
.thread-comment-editor-suggestionActive {
background-color: rgba(var(--grey-50));
}
.thread-comment-editor-suggestion-icon {
width: 24px;
height: 24px;
border-radius: 12px;
margin-right: 6px;
}
@@ -0,0 +1,137 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { MentionProps } from '@univerjs/design';
import { Button, Mention, Mentions } from '@univerjs/design';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { useDependency } from '@wendellhu/redi/react-bindings';
import type { IDocumentBody } from '@univerjs/core';
import { IConfigService, LocaleService } from '@univerjs/core';
import { PLUGIN_NAME } from '../../types/const';
import type { IThreadCommentUIConfig } from '../../types/interfaces/i-thread-comment-mention';
import styles from './index.module.less';
import { parseMentions, transformDocument2TextNodes, transformMention, transformTextNode2Text, transformTextNodes2Document } from './util';
export interface IThreadCommentEditorProps {
id?: string;
comment?: Pick<IThreadComment, 'attachments' | 'text' | 'mentions'>;
onSave?: (comment: Pick<IThreadComment, 'attachments' | 'text'>) => void;
onCancel?: () => void;
autoFocus?: boolean;
}
export interface IThreadCommentEditorInstance {
reply: (text: IDocumentBody) => void;
}
const defaultRenderSuggestion: MentionProps['renderSuggestion'] = (mention, search, highlightedDisplay, index, focused) => {
const icon = (mention as any).raw?.icon;
return (
<div className={styles.threadCommentEditorSuggestion}>
{icon ? <img className={styles.threadCommentEditorSuggestionIcon} src={icon} /> : null}
<div>
{mention.display ?? mention.id}
</div>
</div>
);
};
export const ThreadCommentEditor = forwardRef<IThreadCommentEditorInstance, IThreadCommentEditorProps>((props, ref) => {
const { comment, onSave, id, onCancel, autoFocus } = props;
const configService = useDependency(IConfigService);
const localeService = useDependency(LocaleService);
const [localComment, setLocalComment] = useState({ ...comment });
const [editing, setEditing] = useState(false);
const mentions = configService.getConfig<IThreadCommentUIConfig>(PLUGIN_NAME)?.mentions ?? [];
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
reply(text) {
setLocalComment({
...comment,
text,
attachments: [],
});
(inputRef.current as any)?.inputElement.focus();
},
}));
return (
<div className={styles.threadCommentEditor} onClick={(e) => e.preventDefault()}>
<Mentions
ref={inputRef}
autoFocus={autoFocus}
style={{ width: '100%' }}
placeholder={localeService.t('threadCommentUI.editor.placeholder')}
value={localComment?.text ? transformTextNode2Text(transformDocument2TextNodes(localComment.text)) : ''}
onChange={(e) => {
const text = e.target.value;
if (!text) {
setLocalComment({ ...comment, text: undefined });
}
setLocalComment?.({ ...comment, text: transformTextNodes2Document(parseMentions(e.target.value)) });
}}
onFocus={() => {
setEditing(true);
}}
>
{mentions.map((mention) => (
<Mention
key={mention.trigger}
trigger={mention.trigger}
data={(query, callback) => mention.getMentions!(query)
.then((res) => res.map(transformMention)).then(callback) as any}
displayTransform={(id, label) => `@${label} `}
renderSuggestion={mention.renderSuggestion ?? defaultRenderSuggestion}
/>
))}
</Mentions>
{editing
? (
<div className={styles.threadCommentEditorButtons}>
<Button
style={{ marginRight: 12 }}
onClick={() => {
onCancel?.();
setEditing(false);
setLocalComment({ text: undefined });
}}
>
{localeService.t('threadCommentUI.editor.cancel')}
</Button>
<Button
type="primary"
disabled={!localComment.text}
onClick={() => {
if (localComment.text) {
onSave?.({
...localComment,
text: localComment.text,
});
setEditing(false);
setLocalComment({ text: undefined });
}
}}
>
{localeService.t(id ? 'threadCommentUI.editor.save' : 'threadCommentUI.editor.reply')}
</Button>
</div>
)
: null}
</div>
);
});
@@ -0,0 +1,164 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { CustomRangeType, type IDocumentBody } from '@univerjs/core';
import type { IThreadCommentMention } from '@univerjs/thread-comment';
export type TextNode = {
type: 'text';
content: string;
} | {
type: 'mention';
content: IThreadCommentMention;
};
export const parseMentions = (text: string): TextNode[] => {
const regex = /@\[(.*?)\]\((.*?)\)|(\w+)/g;
let match;
let lastIndex = 0;
const result: TextNode[] = [];
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
// Add the text between two user mentions or before the first user mention
result.push({
type: 'text',
content: text.substring(lastIndex, match.index),
});
}
if (match[1] && match[2]) {
// User mention found
result.push({
type: 'mention',
content: {
label: match[1],
id: match[2],
},
});
} else if (match[3]) {
// Text (numbers) found
result.push({
type: 'text',
content: match[3],
});
}
lastIndex = regex.lastIndex;
}
// Add any remaining text after the last mention (if any)
if (lastIndex < text.length) {
result.push({
type: 'text',
content: text.substring(lastIndex),
});
}
return result;
};
export const transformTextNode2Text = (nodes: TextNode[]) => {
return nodes.map((item) => {
switch (item.type) {
case 'mention':
return `@[${item.content.label}](${item.content.id})`;
default:
return item.content;
}
}).join('');
};
export const transformDocument2TextNodes = (doc: IDocumentBody) => {
const { dataStream, customRanges } = doc;
const end = dataStream.length - 2;
const textNodes: TextNode[] = [];
let lastIndex = 0;
customRanges?.forEach((range) => {
if (lastIndex < range.startIndex) {
textNodes.push({
type: 'text',
content: dataStream.slice(lastIndex, range.startIndex),
});
}
textNodes.push({
type: 'mention',
content: {
label: dataStream.slice(range.startIndex, range.endIndex).slice(1, -1),
id: range.rangeId,
},
});
lastIndex = range.endIndex;
});
textNodes.push({
type: 'text',
content: dataStream.slice(lastIndex, end),
});
return textNodes;
};
export const transformTextNodes2Document = (nodes: TextNode[]): IDocumentBody => {
let str = '';
const customRanges: Required<IDocumentBody['customRanges']> = [];
nodes.forEach((node) => {
switch (node.type) {
case 'text':
str += node.content;
break;
case 'mention': {
const start = str.length;
str += `\x1F${node.content.label}\x1E`;
const end = str.length;
customRanges.push({
rangeId: node.content.id,
rangeType: CustomRangeType.MENTION,
startIndex: start,
endIndex: end,
});
break;
}
default:
break;
}
});
str += '\n\r';
return {
textRuns: [],
paragraphs: [
{
startIndex: str.length - 2,
paragraphStyle: {},
},
],
sectionBreaks: [
{
startIndex: str.length - 1,
},
],
dataStream: str,
customRanges,
};
};
export const transformMention = (mention: IThreadCommentMention) => ({
display: mention.label,
id: `${mention.id}`,
raw: mention,
});
@@ -0,0 +1,40 @@
.thread-comment-panel {
min-height: 100%;
display: flex;
flex-direction: column;
.thread-comment {
margin-top: 12px;
}
&-forms {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 12px;
.select {
width: 120px;
}
}
&-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: rgb(var(--grey-600));
font-size: 13px;
flex: 1;
}
&-add {
margin-top: 8px;
display: flex;
flex-direction: row;
svg {
margin-right: 6px;
}
}
}
@@ -0,0 +1,212 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 React, { useEffect, useMemo, useRef, useState } from 'react';
import { useDependency } from '@wendellhu/redi/react-bindings';
import type { IThreadComment } from '@univerjs/thread-comment';
import { ThreadCommentModel } from '@univerjs/thread-comment';
import { ICommandService, LocaleService, type UniverInstanceType, UserManagerService } from '@univerjs/core';
import { useObservable } from '@univerjs/ui';
import { Button, Select } from '@univerjs/design';
import { IncreaseSingle } from '@univerjs/icons';
import type { Observable } from 'rxjs';
import { ThreadCommentTree } from '../thread-comment-tree';
import { ThreadCommentPanelService } from '../../services/thread-comment-panel.service';
import { SetActiveCommentOperation } from '../../commands/operations/comment.operations';
import styles from './index.module.less';
export interface IThreadCommentPanelProps {
unitId: string;
subUnitId$: Observable<string | undefined>;
type: UniverInstanceType;
onAdd: () => void;
getSubUnitName: (subUnitId: string) => string;
onResolve?: (id: string) => void;
sortComments?: (comments: IThreadComment[]) => IThreadComment[];
}
export const ThreadCommentPanel = (props: IThreadCommentPanelProps) => {
const { unitId, subUnitId$, type, onAdd, getSubUnitName, onResolve, sortComments } = props;
const [unit, setUnit] = useState('all');
const [status, setStatus] = useState('all');
const localeService = useDependency(LocaleService);
const userService = useDependency(UserManagerService);
const threadCommentModel = useDependency(ThreadCommentModel);
const [unitComments, setUnitComments] = useState(() => threadCommentModel.getUnit(unitId));
const panelService = useDependency(ThreadCommentPanelService);
const activeCommentId = useObservable(panelService.activeCommentId$);
const update = useObservable(threadCommentModel.commentUpdate$);
const commandService = useDependency(ICommandService);
const subUnitId = useObservable(subUnitId$);
const currentUser = userService.getCurrentUser();
const shouldScroll = useRef(true);
const prefix = 'panel';
const comments = useMemo(() => {
if (unit === 'all') {
const filteredComments = unitComments.map((i) => i[1]).flat().filter((i) => !i.parentId);
return sortComments ? sortComments(filteredComments) : filteredComments;
} else {
return unitComments.find((i) => i[0] === subUnitId)?.[1] ?? [];
}
}, [unit, unitComments, subUnitId, sortComments]);
const statuedComments = useMemo(() => {
if (status === 'resolved') {
return comments.filter((comment) => comment.resolved);
}
if (status === 'unsolved') {
return comments.filter((comment) => !comment.resolved);
}
if (status === 'concern_me') {
if (!currentUser?.userID) {
return comments;
}
return comments.map((comment) => threadCommentModel.getCommentWithChildren(comment.unitId, comment.subUnitId, comment.id)).map((comment) => {
if (comment?.relativeUsers.has(currentUser.userID)) {
return comment.root;
}
return null;
}).filter(Boolean) as IThreadComment[];
}
return comments;
}, [comments, currentUser?.userID, status, threadCommentModel]);
const isFiltering = status !== 'all' || unit !== 'all';
const onReset = () => {
setStatus('all');
setUnit('all');
};
useEffect(() => {
if (unitId) {
setUnitComments(
threadCommentModel.getUnit(unitId)
);
}
}, [unitId, threadCommentModel, update]);
useEffect(() => {
if (!activeCommentId) {
return;
}
if (!shouldScroll.current) {
shouldScroll.current = true;
return;
}
const { unitId, subUnitId, commentId } = activeCommentId;
const id = `${prefix}-${unitId}-${subUnitId}-${commentId}`;
document.getElementById(id)?.scrollIntoView({ block: 'center' });
}, [activeCommentId]);
return (
<div className={styles.threadCommentPanel}>
<div className={styles.threadCommentPanelForms}>
<Select
borderless
value={unit}
onChange={(e) => setUnit(e)}
options={[
{
value: 'current',
label: localeService.t('threadCommentUI.filter.sheet.current'),
}, {
value: 'all',
label: localeService.t('threadCommentUI.filter.sheet.all'),
},
]}
/>
<Select
borderless
value={status}
onChange={(e) => setStatus(e)}
options={[
{
value: 'all',
label: localeService.t('threadCommentUI.filter.status.all'),
}, {
value: 'resolved',
label: localeService.t('threadCommentUI.filter.status.resolved'),
},
{
value: 'unsolved',
label: localeService.t('threadCommentUI.filter.status.unsolved'),
},
{
value: 'concern_me',
label: localeService.t('threadCommentUI.filter.status.concernMe'),
},
]}
/>
</div>
{statuedComments?.map((comment) => (
<ThreadCommentTree
prefix={prefix}
getSubUnitName={getSubUnitName}
key={comment.id}
id={comment.id}
unitId={comment.unitId}
subUnitId={comment.subUnitId}
type={type}
showEdit={activeCommentId?.commentId === comment.id}
showHighlight={activeCommentId?.commentId === comment.id}
onClick={() => {
shouldScroll.current = false;
commandService.executeCommand(SetActiveCommentOperation.id, {
unitId: comment.unitId,
subUnitId: comment.subUnitId,
commentId: comment.id,
temp: true,
});
}}
onClose={() => onResolve?.(comment.id)}
/>
))}
{statuedComments.length
? null
: (
<div className={styles.threadCommentPanelEmpty}>
{isFiltering ?
localeService.t('threadCommentUI.panel.filterEmpty')
: localeService.t('threadCommentUI.panel.empty')}
{isFiltering
? (
<Button
onClick={onReset}
type="link"
>
{localeService.t('threadCommentUI.panel.reset')}
</Button>
)
: (
<Button
id="thread-comment-add"
className={styles.threadCommentPanelAdd}
type="primary"
onClick={onAdd}
>
<IncreaseSingle />
{localeService.t('threadCommentUI.panel.addComment')}
</Button>
)}
</div>
)}
</div>
);
};
@@ -0,0 +1,120 @@
.thread-comment {
padding: 16px;
background: rgba(var(--color-white));
border: 1px solid rgba(var(--grey-200));
width: 279px;
border-radius: 8px;
box-sizing: border-box;
position: relative;
box-shadow: var(--box-shadow-base);
}
.thread-comment-content {
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-color: rgba(var(--scrollbar-color), 0.7) transparent;
scrollbar-gutter: auto;
scrollbar-width: thin;
}
.thread-comment-highlight {
background-color: rgb(var(--gold-400));
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.thread-comment-icon-container {
display: flex;
flex-direction: row;
}
.thread-comment-icon {
width: 24px;
height: 24px;
border-radius: 3px;
margin-left: 4px;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.thread-comment-icon:hover {
background-color: rgba(var(--grey-50));
}
.thread-comment-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.thread-comment-username {
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
.thread-comment-item {
margin-bottom: 12px;
padding-left: 30px;
position: relative;
}
.thread-comment-item-head {
width: 24px;
height: 24px;
border-radius: 12px;
position: absolute;
left: 0;
top: 0;
}
.thread-comment-item-title {
display: flex;
justify-content: space-between;
height: 24px;
align-items: center;
margin-bottom: 4px;
&-position {
color: rgba(var(--color-black));
font-size: 14px;
line-height: 20px;
}
&-highlight {
width: 3px;
height: 14px;
border-radius: 2px;
margin-right: 8px;
margin-top: 3px;
background-color: rgba(var(--gold-400));
}
}
.thread-comment-item-time {
font-size: 12px;
line-height: 1.5;
margin-bottom: 4px;
color: rgba(var(--grey-600));
}
.thread-comment-item-content {
font-size: 13px;
line-height: 20px;
word-break: break-all;
color: rgba(var(--color-black));
}
.thread-comment-item-at {
color: rgba(var(--blue-400));
}
@@ -0,0 +1,337 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { useDependency } from '@wendellhu/redi/react-bindings';
import type { IAddCommentCommandParams, IThreadComment, IUpdateCommentCommandParams } from '@univerjs/thread-comment';
import { AddCommentCommand, DeleteCommentCommand, DeleteCommentTreeCommand, ResolveCommentCommand, ThreadCommentModel, UpdateCommentCommand } from '@univerjs/thread-comment';
import React, { useRef, useState } from 'react';
import { DeleteSingle, MoreHorizontalSingle, ReplyToCommentSingle, ResolvedSingle, SolveSingle } from '@univerjs/icons';
import { ICommandService, LocaleService, Tools, type UniverInstanceType, UserManagerService } from '@univerjs/core';
import { useObservable } from '@univerjs/ui';
import dayjs from 'dayjs';
import { Dropdown, Menu, MenuItem } from '@univerjs/design';
import type { IUser } from '@univerjs/protocol';
import type { IThreadCommentEditorInstance } from '../thread-comment-editor';
import { ThreadCommentEditor } from '../thread-comment-editor';
import { transformDocument2TextNodes, transformTextNodes2Document } from '../thread-comment-editor/util';
import styles from './index.module.less';
export interface IThreadCommentTreeProps {
id?: string;
unitId: string;
subUnitId: string;
type: UniverInstanceType;
refStr?: string;
showEdit?: boolean;
onClick?: () => void;
showHighlight?: boolean;
onClose?: () => void;
getSubUnitName: (subUnitId: string) => string;
prefix?: string;
autoFocus?: boolean;
}
export interface IThreadCommentItemProps {
item: IThreadComment;
unitId: string;
subUnitId: string;
onEditingChange?: (editing: boolean) => void;
editing?: boolean;
onClick?: () => void;
resolved?: boolean;
onReply: (user: IUser | undefined) => void;
isRoot?: boolean;
onClose?: () => void;
}
const MOCK_ID = '__mock__';
const ThreadCommentItem = (props: IThreadCommentItemProps) => {
const { item, unitId, subUnitId, editing, onEditingChange, onReply, resolved, isRoot, onClose } = props;
const commandService = useDependency(ICommandService);
const localeService = useDependency(LocaleService);
const userManagerService = useDependency(UserManagerService);
const user = userManagerService.getUser(item.personId);
const currentUser = useObservable(userManagerService.currentUser$);
const isCommentBySelf = currentUser?.userID === item.personId;
const isMock = item.id === MOCK_ID;
const [showReply, setShowReply] = useState(false);
const handleDeleteItem = () => {
commandService.executeCommand(
isRoot ? DeleteCommentTreeCommand.id : DeleteCommentCommand.id,
{
unitId,
subUnitId,
commentId: item.id,
}
);
if (isRoot) {
onClose?.();
}
};
return (
<div className={styles.threadCommentItem} onMouseLeave={() => setShowReply(false)} onMouseEnter={() => setShowReply(true)}>
<img className={styles.threadCommentItemHead} src={user?.avatar} />
<div className={styles.threadCommentItemTitle}>
<div className={styles.threadCommentUsername}>
{user?.name || ' '}
</div>
<div>
{(isMock || resolved)
? null
: (
showReply
? (
<div className={styles.threadCommentIcon} onClick={() => onReply(user)}>
<ReplyToCommentSingle />
</div>
)
: null
)}
{isCommentBySelf && !isMock && !resolved
? (
<Dropdown
overlay={(
<Menu>
<MenuItem onClick={() => onEditingChange?.(true)}>{localeService.t('threadCommentUI.item.edit')}</MenuItem>
<MenuItem onClick={handleDeleteItem}>{localeService.t('threadCommentUI.item.delete')}</MenuItem>
</Menu>
)}
>
<div className={styles.threadCommentIcon}>
<MoreHorizontalSingle />
</div>
</Dropdown>
)
: null}
</div>
</div>
<div className={styles.threadCommentItemTime}>{item.dT}</div>
{editing
? (
<ThreadCommentEditor
id={item.id}
comment={item}
onCancel={() => onEditingChange?.(false)}
autoFocus
onSave={({ text, attachments }) => {
onEditingChange?.(false);
commandService.executeCommand(
UpdateCommentCommand.id,
{
unitId,
subUnitId,
payload: {
commentId: item.id,
text,
attachments,
},
} as IUpdateCommentCommandParams
);
}}
/>
)
: (
<div className={styles.threadCommentItemContent}>
{transformDocument2TextNodes(item.text).map((item, i) => {
switch (item.type) {
case 'mention':
return (
<a className={styles.threadCommentItemAt} key={i}>
@
{item.content.label}
{' '}
</a>
);
default:
return item.content;
}
})}
</div>
)}
</div>
);
};
export const ThreadCommentTree = (props: IThreadCommentTreeProps) => {
const {
id,
unitId,
subUnitId,
refStr,
showEdit = true,
onClick,
showHighlight,
onClose,
getSubUnitName,
prefix,
autoFocus,
} = props;
const threadCommentModel = useDependency(ThreadCommentModel);
const [editingId, setEditingId] = useState('');
useObservable(threadCommentModel.commentMap$);
const comments = id ? threadCommentModel.getCommentWithChildren(unitId, subUnitId, id) : null;
const commandService = useDependency(ICommandService);
const userManagerService = useDependency(UserManagerService);
const resolved = comments?.root.resolved;
const currentUser = useObservable(userManagerService.currentUser$);
const editorRef = useRef<IThreadCommentEditorInstance>(null);
const renderComments = [
...comments ?
[comments.root] :
// mock empty comment
[{
id: MOCK_ID,
text: {
dataStream: '\n\r',
},
personId: currentUser?.userID ?? '',
ref: refStr ?? '',
dT: '',
unitId,
subUnitId,
}],
...comments?.children ?? [],
];
const handleResolve = () => {
commandService.executeCommand(ResolveCommentCommand.id, {
unitId,
subUnitId,
commentId: id,
resolved: !resolved,
});
onClose?.();
};
const handleDeleteRoot = () => {
commandService.executeCommand(
DeleteCommentTreeCommand.id,
{
unitId,
subUnitId,
commentId: id,
}
);
onClose?.();
};
return (
<div className={styles.threadComment} onClick={onClick} id={`${prefix}-${unitId}-${subUnitId}-${id}`}>
{showHighlight ? <div className={styles.threadCommentHighlight} /> : null}
<div className={styles.threadCommentTitle}>
<div className={styles.threadCommentTitlePosition}>
<div className={styles.threadCommentTitleHighlight} />
{refStr || comments?.root.ref}
{' · '}
{getSubUnitName(comments?.root.subUnitId ?? subUnitId)}
</div>
{comments
? (
<div className={styles.threadCommentIconContainer}>
<div
onClick={handleResolve}
className={styles.threadCommentIcon}
style={{ color: resolved ? 'rgb(var(--green-500))' : '' }}
>
{resolved ? <ResolvedSingle /> : <SolveSingle />}
</div>
{currentUser?.userID === comments.root.personId
? (
<div className={styles.threadCommentIcon} onClick={handleDeleteRoot}>
<DeleteSingle />
</div>
)
: null}
</div>
)
: null}
</div>
<div className={styles.threadCommentContent}>
{renderComments.map(
(item) => (
<ThreadCommentItem
onClose={onClose}
unitId={unitId}
subUnitId={subUnitId}
item={item}
key={item.id}
isRoot={item.id === comments?.root.id}
editing={editingId === item.id}
resolved={comments?.root.resolved}
onEditingChange={(editing) => {
if (editing) {
setEditingId(item.id);
} else {
setEditingId('');
}
}}
onReply={(user) => {
if (!user) {
return;
}
editorRef.current?.reply(transformTextNodes2Document([{
type: 'mention',
content: {
id: user.userID,
label: user.name,
},
}]));
}}
/>
)
)}
</div>
{showEdit && !editingId && !resolved
? (
<div>
<ThreadCommentEditor
key={`${autoFocus}`}
ref={editorRef}
onSave={({ text, attachments }) => {
commandService.executeCommand(
AddCommentCommand.id,
{
unitId,
subUnitId,
comment: {
text,
attachments,
dT: dayjs().format('YYYY/MM/DD HH:mm'),
id: Tools.generateRandomId(),
ref: refStr,
personId: currentUser?.userID,
parentId: comments?.root.id,
unitId,
subUnitId,
},
} as IAddCommentCommandParams
);
}}
autoFocus={autoFocus || (!comments)}
onCancel={() => {
if (!comments) {
onClose?.();
}
}}
/>
</div>
)
: null}
</div>
);
};
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@univerjs/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src"]
}
@@ -0,0 +1,4 @@
{
"extends": "@univerjs/shared/tsconfigs/node",
"include": ["vite.config.ts"]
}
+12
View File
@@ -0,0 +1,12 @@
import createViteConfig from '@univerjs/shared/vite';
import pkg from './package.json';
export default ({ mode }) => createViteConfig({}, {
mode,
pkg,
features: {
react: false,
css: true,
dom: true,
},
});
+16
View File
@@ -0,0 +1,16 @@
# @univerjs/thread-comment
[![npm version](https://img.shields.io/npm/v/@univerjs/thread-comment)](https://npmjs.org/packages/@univerjs/thread-comment)
[![license](https://img.shields.io/npm/l/@univerjs/thread-comment)](https://img.shields.io/npm/l/@univerjs/thread-comment)
## Introduction
> TODO: Introduction
## Usage
### Installation
```shell
npm i @univerjs/thread-comment
```
+84
View File
@@ -0,0 +1,84 @@
{
"name": "@univerjs/thread-comment",
"version": "0.0.1",
"private": true,
"description": "",
"author": "DreamNum <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": [],
"sideEffects": [
"**/*.css"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*"
},
"main": "./lib/cjs/index.js",
"module": "./lib/es/index.js",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/cjs/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"
},
"./lib/*": "./lib/*"
}
},
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"lint:types": "tsc --noEmit",
"build": "tsc && vite build"
},
"peerDependencies": {
"@univerjs/core": "workspace:*",
"@wendellhu/redi": "^0.15.1",
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/protocol": "^0.1.20"
},
"devDependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/shared": "workspace:*",
"@univerjs/ui": "workspace:*",
"@wendellhu/redi": "^0.15.1",
"clsx": "^2.1.0",
"less": "^4.2.0",
"react": "^18.2.0",
"rxjs": "^7.8.1",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vitest": "^1.3.1"
}
}
@@ -0,0 +1,337 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { CommandType, type ICommand, ICommandService, IUndoRedoService, sequenceExecute } from '@univerjs/core';
import type { IThreadComment } from '../../types/interfaces/i-thread-comment';
import { ThreadCommentModel } from '../../models/thread-comment.model';
import { AddCommentMutation, DeleteCommentMutation, type IUpdateCommentPayload, ResolveCommentMutation, UpdateCommentMutation } from '../mutations/comment.mutation';
import { IThreadCommentDataSourceService } from '../../services/tc-datasource.service';
export interface IAddCommentCommandParams {
unitId: string;
subUnitId: string;
comment: IThreadComment;
}
export const AddCommentCommand: ICommand<IAddCommentCommandParams> = {
id: 'thread-comment.command.add-comment',
type: CommandType.COMMAND,
async handler(accessor, params) {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const dataSourceService = accessor.get(IThreadCommentDataSourceService);
const { unitId, subUnitId, comment: originComment } = params;
const comment = await dataSourceService.addComment(originComment);
const redo = {
id: AddCommentMutation.id,
params,
};
const undo = {
id: DeleteCommentMutation.id,
params: {
unitId,
subUnitId,
commentId: comment.id,
},
};
undoRedoService.pushUndoRedo({
undoMutations: [undo],
redoMutations: [redo],
unitID: unitId,
});
commandService.executeCommand(redo.id, redo.params);
return true;
},
};
export interface IUpdateCommentCommandParams {
unitId: string;
subUnitId: string;
payload: IUpdateCommentPayload;
}
export const UpdateCommentCommand: ICommand<IUpdateCommentCommandParams> = {
id: 'thread-comment.command.update-comment',
type: CommandType.COMMAND,
async handler(accessor, params) {
if (!params) {
return false;
}
const { unitId, subUnitId, payload } = params;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const threadCommentModel = accessor.get(ThreadCommentModel);
const dataSourceService = accessor.get(IThreadCommentDataSourceService);
const currentComment = threadCommentModel.getComment(
unitId,
subUnitId,
payload.commentId
);
if (!currentComment) {
return false;
}
const success = await dataSourceService.updateComment({
...currentComment,
...payload,
});
if (!success) {
return false;
}
const redo = {
id: UpdateCommentMutation.id,
params,
};
const undo = {
id: UpdateCommentMutation.id,
params: {
unitId,
subUnitId,
payload: {
commentId: payload.commentId,
text: currentComment.text,
attachments: currentComment.attachments,
updateT: currentComment.updateT,
updated: currentComment.updated,
},
},
};
undoRedoService.pushUndoRedo({
undoMutations: [undo],
redoMutations: [redo],
unitID: unitId,
});
commandService.executeCommand(redo.id, redo.params);
return true;
},
};
export interface IUpdateCommentRefPayload {
commentId: string;
ref: string;
}
export interface IUpdateCommentRefCommandParams {
unitId: string;
subUnitId: string;
payload: IUpdateCommentRefPayload;
}
export const UpdateCommentRefCommand: ICommand<IUpdateCommentRefCommandParams> = {
id: 'thread-comment.command.update-comment-ref',
type: CommandType.COMMAND,
async handler(accessor, params) {
if (!params) {
return false;
}
const dataSourceService = accessor.get(IThreadCommentDataSourceService);
const threadCommentModel = accessor.get(ThreadCommentModel);
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const { unitId, subUnitId, payload } = params;
const currentComment = threadCommentModel.getComment(unitId, subUnitId, payload.commentId);
if (!currentComment) {
return false;
}
const success = await dataSourceService.updateComment({
...currentComment,
ref: payload.ref,
});
if (!success) {
return false;
}
const redo = {
id: UpdateCommentMutation.id,
params,
};
const undo = {
id: UpdateCommentMutation.id,
params: {
unitId,
subUnitId,
payload: {
commentId: payload.commentId,
ref: currentComment.ref,
},
},
};
undoRedoService.pushUndoRedo({
undoMutations: [undo],
redoMutations: [redo],
unitID: unitId,
});
commandService.executeCommand(redo.id, redo.params);
return true;
},
};
export interface IResolveCommentCommandParams {
unitId: string;
subUnitId: string;
commentId: string;
resolved: boolean;
}
export const ResolveCommentCommand: ICommand<IResolveCommentCommandParams> = {
id: 'thread-comment.command.resolve-comment',
type: CommandType.COMMAND,
async handler(accessor, params) {
if (!params) {
return false;
}
const { unitId, subUnitId, resolved, commentId } = params;
const dataSourceService = accessor.get(IThreadCommentDataSourceService);
const threadCommentModel = accessor.get(ThreadCommentModel);
const currentComment = threadCommentModel.getComment(unitId, subUnitId, commentId);
if (!currentComment) {
return false;
}
const success = await dataSourceService.updateComment({
...currentComment,
resolved,
});
if (!success) {
return false;
}
const commandService = accessor.get(ICommandService);
commandService.executeCommand(
ResolveCommentMutation.id,
params
);
return true;
},
};
export interface IDeleteCommentCommandParams {
unitId: string;
subUnitId: string;
commentId: string;
}
export const DeleteCommentCommand: ICommand<IDeleteCommentCommandParams> = {
id: 'thread-comment.command.delete-comment',
type: CommandType.COMMAND,
async handler(accessor, params) {
if (!params) {
return false;
}
const threadCommentModel = accessor.get(ThreadCommentModel);
const dataSourceService = accessor.get(IThreadCommentDataSourceService);
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const { unitId, subUnitId, commentId } = params;
const comment = threadCommentModel.getComment(unitId, subUnitId, commentId);
if (!comment) {
return false;
}
if (!(await dataSourceService.deleteComment(commentId))) {
return false;
}
const redo = {
id: DeleteCommentMutation.id,
params,
};
const undo = {
id: AddCommentMutation.id,
params: {
unitId,
subUnitId,
comment,
},
};
undoRedoService.pushUndoRedo({
undoMutations: [undo],
redoMutations: [redo],
unitID: unitId,
});
return commandService.executeCommand(redo.id, redo.params);
},
};
export interface IDeleteCommentTreeCommandParams {
unitId: string;
subUnitId: string;
commentId: string;
}
export const DeleteCommentTreeCommand: ICommand<IDeleteCommentCommandParams> = {
id: 'thread-comment.command.delete-comment-tree',
type: CommandType.COMMAND,
async handler(accessor, params) {
if (!params) {
return false;
}
const threadCommentModel = accessor.get(ThreadCommentModel);
const commandService = accessor.get(ICommandService);
const dataSourceService = accessor.get(IThreadCommentDataSourceService);
const undoRedoService = accessor.get(IUndoRedoService);
const { unitId, subUnitId, commentId } = params;
const commentWithChildren = threadCommentModel.getCommentWithChildren(unitId, subUnitId, commentId);
if (!commentWithChildren) {
return false;
}
const comments = [commentWithChildren.root, ...commentWithChildren.children];
if (!(await dataSourceService.deleteCommentBatch(comments.map((comment) => comment.id)))) {
return false;
}
const redos = comments.map((item) => ({
id: DeleteCommentMutation.id,
params: {
unitId,
subUnitId,
commentId: item.id,
},
}));
const undos = comments.map((item) => ({
id: AddCommentMutation.id,
params: {
unitId,
subUnitId,
comment: item,
},
}));
const result = sequenceExecute(redos, commandService);
if (result.result) {
undoRedoService.pushUndoRedo({
undoMutations: undos,
redoMutations: redos,
unitID: unitId,
});
}
return result.result;
},
};
@@ -0,0 +1,129 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommand, IDocumentBody } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
import type { IThreadComment } from '../../types/interfaces/i-thread-comment';
import { ThreadCommentModel } from '../../models/thread-comment.model';
export interface IAddCommentMutationParams {
unitId: string;
subUnitId: string;
comment: IThreadComment;
}
export const AddCommentMutation: ICommand<IAddCommentMutationParams> = {
id: 'thread-comment.mutation.add-comment',
type: CommandType.MUTATION,
handler(accessor, params) {
if (!params) {
return false;
}
const threadCommentModel = accessor.get(ThreadCommentModel);
const { unitId, subUnitId, comment } = params;
return threadCommentModel.addComment(unitId, subUnitId, comment);
},
};
export interface IUpdateCommentPayload {
commentId: string;
text: IDocumentBody;
attachments?: string[];
updated?: boolean;
updateT?: string;
}
export interface IUpdateCommentMutationParams {
unitId: string;
subUnitId: string;
payload: IUpdateCommentPayload;
}
export const UpdateCommentMutation: ICommand<IUpdateCommentMutationParams> = {
id: 'thread-comment.mutation.update-comment',
type: CommandType.MUTATION,
handler(accessor, params) {
if (!params) {
return false;
}
const threadCommentModel = accessor.get(ThreadCommentModel);
const { unitId, subUnitId, payload } = params;
return threadCommentModel.updateComment(unitId, subUnitId, payload);
},
};
export interface IUpdateCommentRefPayload {
commentId: string;
ref: string;
}
export interface IUpdateCommentRefMutationParams {
unitId: string;
subUnitId: string;
payload: IUpdateCommentRefPayload;
}
export const UpdateCommentRefMutation: ICommand<IUpdateCommentRefMutationParams> = {
id: 'thread-comment.mutation.update-comment-ref',
type: CommandType.MUTATION,
handler(accessor, params) {
if (!params) {
return false;
}
const threadCommentModel = accessor.get(ThreadCommentModel);
const { unitId, subUnitId, payload } = params;
return threadCommentModel.updateCommentRef(unitId, subUnitId, payload);
},
};
export interface IResolveCommentMutationParams {
unitId: string;
subUnitId: string;
commentId: string;
resolved: boolean;
}
export const ResolveCommentMutation: ICommand<IResolveCommentMutationParams> = {
id: 'thread-comment.mutation.resolve-comment',
type: CommandType.MUTATION,
handler(accessor, params) {
if (!params) {
return false;
}
const threadCommentModel = accessor.get(ThreadCommentModel);
const { unitId, subUnitId, resolved, commentId } = params;
return threadCommentModel.resolveComment(unitId, subUnitId, commentId, resolved);
},
};
export interface IDeleteCommentMutationParams {
unitId: string;
subUnitId: string;
commentId: string;
}
export const DeleteCommentMutation: ICommand<IDeleteCommentMutationParams> = {
id: 'thread-comment.mutation.delete-comment',
type: CommandType.MUTATION,
handler(accessor, params) {
if (!params) {
return false;
}
const threadCommentModel = accessor.get(ThreadCommentModel);
const { unitId, subUnitId, commentId } = params;
return threadCommentModel.deleteComment(unitId, subUnitId, commentId);
},
};
@@ -0,0 +1,83 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, IResourceManagerService, LifecycleStages, OnLifecycle } from '@univerjs/core';
import { Inject } from '@wendellhu/redi';
import { UniverType } from '@univerjs/protocol';
import { ThreadCommentModel } from '../models/thread-comment.model';
import { TC_PLUGIN_NAME } from '../types/const';
import type { IThreadComment } from '../types/interfaces/i-thread-comment';
import { IThreadCommentDataSourceService } from '../services/tc-datasource.service';
export type UnitThreadCommentJSON = Record<string, IThreadComment[]>;
@OnLifecycle(LifecycleStages.Starting, ThreadCommentResourceController)
export class ThreadCommentResourceController extends Disposable {
constructor(
@IResourceManagerService private readonly _resourceManagerService: IResourceManagerService,
@Inject(ThreadCommentModel) private readonly _threadCommentModel: ThreadCommentModel,
@IThreadCommentDataSourceService private readonly _threadCommentDataSourceService: IThreadCommentDataSourceService
) {
super();
this._initSnapshot();
}
private _initSnapshot() {
const toJson = (unitID: string) => {
const map = this._threadCommentModel.getUnit(unitID);
const resultMap: UnitThreadCommentJSON = {};
if (map) {
map.forEach(([key, v]) => {
resultMap[key] = v;
});
return JSON.stringify(this._threadCommentDataSourceService.saveToSnapshot(resultMap));
}
return '';
};
const parseJson = (json: string): UnitThreadCommentJSON => {
if (!json) {
return {};
}
try {
return JSON.parse(json);
} catch (err) {
return {};
}
};
this.disposeWithMe(
this._resourceManagerService.registerPluginResource({
pluginName: `SHEET_${TC_PLUGIN_NAME}`,
businesses: [UniverType.UNIVER_SHEET],
toJson: (unitID) => toJson(unitID),
parseJson: (json) => parseJson(json),
onUnLoad: (unitID) => {
this._threadCommentModel.deleteUnit(unitID);
},
onLoad: async (unitID, value) => {
const unitComments = await this._threadCommentDataSourceService.loadFormSnapshot(value);
Object.keys(unitComments).forEach((subunitId) => {
const commentList = value[subunitId];
commentList.forEach((comment) => {
this._threadCommentModel.addComment(unitID, subunitId, comment);
});
});
},
})
);
}
}
+52
View File
@@ -0,0 +1,52 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { ThreadCommentModel, type CommentUpdate } from './models/thread-comment.model';
export { ThreadCommentResourceController } from './controllers/tc-resource.controller';
export { TC_PLUGIN_NAME } from './types/const';
export {
AddCommentMutation,
DeleteCommentMutation,
ResolveCommentMutation,
UpdateCommentMutation,
UpdateCommentRefMutation,
} from './commands/mutations/comment.mutation';
export type {
IAddCommentMutationParams,
IDeleteCommentMutationParams,
IResolveCommentMutationParams,
IUpdateCommentMutationParams,
IUpdateCommentPayload,
IUpdateCommentRefMutationParams,
} from './commands/mutations/comment.mutation';
export type { IThreadComment, IThreadCommentMention } from './types/interfaces/i-thread-comment';
export {
AddCommentCommand,
DeleteCommentCommand,
ResolveCommentCommand,
UpdateCommentCommand,
DeleteCommentTreeCommand,
} from './commands/commands/comment.command';
export type {
IAddCommentCommandParams,
IDeleteCommentCommandParams,
IResolveCommentCommandParams,
IUpdateCommentCommandParams,
IDeleteCommentTreeCommandParams,
IUpdateCommentRefCommandParams,
IUpdateCommentRefPayload,
} from './commands/commands/comment.command';
export { UniverThreadCommentPlugin } from './plugin';
@@ -0,0 +1,339 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { BehaviorSubject, map, Subject } from 'rxjs';
import { CustomRangeType } from '@univerjs/core';
import type { IThreadComment } from '../types/interfaces/i-thread-comment';
import type { IUpdateCommentPayload, IUpdateCommentRefPayload } from '../commands/mutations/comment.mutation';
export type CommentUpdate = {
unitId: string;
subUnitId: string;
type: 'add';
payload: IThreadComment;
} | {
unitId: string;
subUnitId: string;
type: 'update';
payload: IUpdateCommentPayload;
} | {
unitId: string;
subUnitId: string;
type: 'delete';
payload: {
commentId: string;
isRoot: boolean;
comment: IThreadComment;
};
} | {
unitId: string;
subUnitId: string;
type: 'updateRef';
payload: IUpdateCommentRefPayload;
} | {
unitId: string;
subUnitId: string;
type: 'resolve';
payload: {
commentId: string;
resolved: boolean;
};
};
export class ThreadCommentModel {
private _commentsMap: Record<string, Record<string, Record<string, IThreadComment>>> = {};
private _commentsTreeMap: Map<string, Map<string, Map<string, string[]>>> = new Map();
private _commentUpdate$ = new Subject<CommentUpdate>();
private _commentsTreeMap$ = new BehaviorSubject<Record<string, Record<string, Record<string, string[]>>>>({});
private _commentsMap$ = new BehaviorSubject<Record<string, Record<string, Record<string, IThreadComment>>>>({});
commentUpdate$ = this._commentUpdate$.asObservable();
commentTreeMap$ = this._commentsTreeMap$.asObservable();
commentMap$ = this._commentsMap$.asObservable();
private _ensureCommentMap(unitId: string, subUnitId: string) {
let unitMap = this._commentsMap[unitId];
if (!unitMap) {
unitMap = {};
this._commentsMap[unitId] = unitMap;
}
let subUnitMap = unitMap[subUnitId];
if (!subUnitMap) {
subUnitMap = {};
unitMap[subUnitId] = subUnitMap;
}
return subUnitMap;
}
private _ensureCommentChildrenMap(unitId: string, subUnitId: string) {
let unitMap = this._commentsTreeMap.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._commentsTreeMap.set(unitId, unitMap);
}
let subUnitMap = unitMap.get(subUnitId);
if (!subUnitMap) {
subUnitMap = new Map();
unitMap.set(subUnitId, subUnitMap);
}
return subUnitMap;
}
private _refreshCommentsMap$() {
this._commentsMap$.next({
...this._commentsMap,
});
}
private _refreshCommentsTreeMap$() {
const map: Record<string, Record<string, Record<string, string[]>>> = {};
this._commentsTreeMap.forEach((unit, unitId) => {
map[unitId] = {};
const unitRecord = map[unitId];
unit.forEach((subUnit, subUnitId) => {
unitRecord[subUnitId] = {};
const subUnitRecord = unitRecord[subUnitId];
subUnit.forEach((children, key) => {
subUnitRecord[key] = children;
});
});
});
this._commentsTreeMap$.next(map);
}
ensureMap(unitId: string, subUnitId: string) {
const commentMap = this._ensureCommentMap(unitId, subUnitId);
const commentChildrenMap = this._ensureCommentChildrenMap(unitId, subUnitId);
return {
commentMap,
commentChildrenMap,
};
}
addComment(unitId: string, subUnitId: string, comment: IThreadComment) {
const { commentMap, commentChildrenMap } = this.ensureMap(unitId, subUnitId);
let parentId = comment.parentId;
if (parentId) {
let parent = commentMap[parentId];
// find the top root
while (parent?.parentId) {
parent = commentMap[parent.parentId];
parentId = parent.parentId!;
}
if (parent) {
let children = commentChildrenMap.get(parentId);
if (!children) {
children = [];
}
children.push(comment.id);
commentChildrenMap.set(parentId, children);
}
} else {
commentChildrenMap.set(comment.id, []);
}
commentMap[comment.id] = comment;
this._commentUpdate$.next({
unitId,
subUnitId,
type: 'add',
payload: comment,
});
this._refreshCommentsMap$();
this._refreshCommentsTreeMap$();
return true;
}
updateComment(unitId: string, subUnitId: string, payload: IUpdateCommentPayload) {
const { commentMap } = this.ensureMap(unitId, subUnitId);
const oldComment = commentMap[payload.commentId];
if (!oldComment) {
return false;
}
oldComment.updated = true;
oldComment.text = payload.text;
oldComment.attachments = payload.attachments;
oldComment.updateT = payload.updateT;
this._commentUpdate$.next({
unitId,
subUnitId,
type: 'update',
payload,
});
this._refreshCommentsMap$();
this._refreshCommentsTreeMap$();
return true;
}
updateCommentRef(unitId: string, subUnitId: string, payload: IUpdateCommentRefPayload) {
const { commentMap } = this.ensureMap(unitId, subUnitId);
const oldComment = commentMap[payload.commentId];
if (!oldComment) {
return false;
}
oldComment.ref = payload.ref;
this._commentUpdate$.next({
unitId,
subUnitId,
type: 'updateRef',
payload,
});
this._refreshCommentsMap$();
this._refreshCommentsTreeMap$();
return true;
}
resolveComment(unitId: string, subUnitId: string, commentId: string, resolved: boolean) {
const { commentMap } = this.ensureMap(unitId, subUnitId);
const oldComment = commentMap[commentId];
if (!oldComment) {
return false;
}
oldComment.resolved = resolved;
this._commentUpdate$.next({
unitId,
subUnitId,
type: 'resolve',
payload: {
commentId,
resolved,
},
});
this._refreshCommentsMap$();
this._refreshCommentsTreeMap$();
return true;
}
getComment(unitId: string, subUnitId: string, commentId: string) {
const { commentMap } = this.ensureMap(unitId, subUnitId);
return commentMap[commentId] as IThreadComment | undefined;
}
getComment$(unitId: string, subUnitId: string, commentId: string) {
return this._commentsMap$.pipe(map((records) => records[unitId][subUnitId][commentId]));
}
getCommentWithChildren(unitId: string, subUnitId: string, commentId: string) {
const { commentMap, commentChildrenMap } = this.ensureMap(unitId, subUnitId);
const current = commentMap[commentId];
if (!current) {
return undefined;
}
const relativeUsers = new Set<string>();
const children = commentChildrenMap.get(commentId) ?? [];
const childrenComments = children?.map((childId) => commentMap[childId]!);
[current, ...childrenComments].forEach((comment) => {
relativeUsers.add(comment.personId);
comment.text.customRanges?.forEach((range) => {
if (range.rangeType === CustomRangeType.MENTION) {
relativeUsers.add(range.rangeId);
}
});
});
return {
root: current,
children: childrenComments,
relativeUsers,
};
}
deleteComment(unitId: string, subUnitId: string, commentId: string) {
const { commentMap, commentChildrenMap } = this.ensureMap(unitId, subUnitId);
const current = commentMap[commentId];
if (!current) {
return false;
}
if (current.parentId) {
const children = commentChildrenMap.get(current.parentId);
if (children) {
const index = children.indexOf(commentId);
children.splice(index, 1);
}
delete commentMap[commentId];
} else {
delete commentMap[commentId];
commentChildrenMap.delete(commentId);
}
this._commentUpdate$.next({
unitId,
subUnitId,
type: 'delete',
payload: {
commentId,
isRoot: !current.parentId,
comment: current,
},
});
this._refreshCommentsMap$();
this._refreshCommentsTreeMap$();
return true;
}
getUnit(unitId: string) {
const unitMap = this._commentsMap[unitId];
if (!unitMap) {
return [];
}
return Array.from(Object.entries(unitMap)).map(([subUnitId, subUnitMap]) => [subUnitId, Array.from(Object.values(subUnitMap))] as const);
}
deleteUnit(unitId: string) {
const unitMap = this._commentsMap[unitId];
if (!unitMap) {
return;
}
Object.entries(unitMap).forEach(([subUnitId, subUnitMap]) => {
Object.values(subUnitMap).forEach((comment) => {
this.deleteComment(unitId, subUnitId, comment.id);
});
});
}
getRootCommentIds(unitId: string, subUnitId: string) {
const commentChildrenMap = this._ensureCommentChildrenMap(unitId, subUnitId);
return Array.from(commentChildrenMap.keys());
}
getRootCommentIds$(unitId: string, subUnitId: string) {
return this._commentsTreeMap$.pipe(map(
(tree) => Object.keys(tree[unitId]?.[subUnitId])
));
}
getAll() {
return this._commentsMap;
}
}
+65
View File
@@ -0,0 +1,65 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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, Plugin, UniverInstanceType } from '@univerjs/core';
import { type Dependency, Inject, Injector } from '@wendellhu/redi';
import { ThreadCommentModel } from './models/thread-comment.model';
import { ThreadCommentResourceController } from './controllers/tc-resource.controller';
import { TC_PLUGIN_NAME } from './types/const';
import { AddCommentMutation, DeleteCommentMutation, ResolveCommentMutation, UpdateCommentMutation, UpdateCommentRefMutation } from './commands/mutations/comment.mutation';
import { AddCommentCommand, DeleteCommentCommand, DeleteCommentTreeCommand, ResolveCommentCommand, UpdateCommentCommand } from './commands/commands/comment.command';
import { IThreadCommentDataSourceService, ThreadCommentDataSourceService } from './services/tc-datasource.service';
export class UniverThreadCommentPlugin extends Plugin {
static override pluginName = TC_PLUGIN_NAME;
static override type = UniverInstanceType.UNIVER_UNKNOWN;
constructor(
_config: unknown,
@Inject(Injector) protected _injector: Injector,
@ICommandService protected _commandService: ICommandService
) {
super();
}
override onStarting(injector: Injector): void {
([
[ThreadCommentModel],
[ThreadCommentResourceController],
[IThreadCommentDataSourceService, { useClass: ThreadCommentDataSourceService }],
] as Dependency[]).forEach(
(d) => {
injector.add(d);
}
);
[
AddCommentCommand,
UpdateCommentCommand,
DeleteCommentCommand,
ResolveCommentCommand,
DeleteCommentTreeCommand,
AddCommentMutation,
UpdateCommentMutation,
UpdateCommentRefMutation,
DeleteCommentMutation,
ResolveCommentMutation,
].forEach((command) => {
this._commandService.registerCommand(command);
});
}
}
@@ -0,0 +1,60 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 { createIdentifier } from '@wendellhu/redi';
import type { IThreadComment } from '../types/interfaces/i-thread-comment';
export type ThreadCommentJSON = { id: string } & Partial<Omit<IThreadComment, 'id'>>;
export interface IThreadCommentDataSourceService {
addComment: (comment: IThreadComment) => Promise<IThreadComment>;
updateComment: (comment: IThreadComment) => Promise<boolean>;
deleteComment: (commentId: string) => Promise<boolean>;
deleteCommentBatch: (commentIds: string[]) => Promise<boolean>;
loadFormSnapshot: (unitComments: Record<string, ThreadCommentJSON[]>) => Promise<Record<string, IThreadComment[]>>;
saveToSnapshot: (unitComments: Record<string, IThreadComment[]>) => Record<string, ThreadCommentJSON[]>;
}
/**
* Preserve for import async comment system
*/
export class ThreadCommentDataSourceService implements IThreadCommentDataSourceService {
async addComment(comment: IThreadComment) {
return comment;
}
async updateComment(_comment: IThreadComment) {
return true;
}
async deleteComment(_commentId: string) {
return true;
}
async deleteCommentBatch(_commentIds: string[]) {
return true;
}
async loadFormSnapshot(unitComments: Record<string, ThreadCommentJSON[]>) {
return unitComments as Record<string, IThreadComment[]>;
}
saveToSnapshot(unitComments: Record<string, IThreadComment[]>) {
return unitComments;
}
}
export const IThreadCommentDataSourceService = createIdentifier<IThreadCommentDataSourceService>('univer.thread-comment.data-source-service');
@@ -0,0 +1,17 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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 TC_PLUGIN_NAME = 'THREAD_COMMENT_PLUGIN';
@@ -0,0 +1,39 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IDocumentBody } from '@univerjs/core';
export interface IThreadCommentMention {
label: string;
id: string;
icon?: string;
}
export interface IThreadComment {
id: string;
ref: string;
dT: string;
updateT?: string;
personId: string;
parentId?: string;
text: IDocumentBody;
attachments?: string[];
resolved?: boolean;
updated?: boolean;
unitId: string;
subUnitId: string;
mentions?: string[];
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@univerjs/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src"]
}
@@ -0,0 +1,4 @@
{
"extends": "@univerjs/shared/tsconfigs/node",
"include": ["vite.config.ts"]
}
+12
View File
@@ -0,0 +1,12 @@
import createViteConfig from '@univerjs/shared/vite';
import pkg from './package.json';
export default ({ mode }) => createViteConfig({}, {
mode,
pkg,
features: {
react: false,
css: true,
dom: true,
},
});
@@ -25,6 +25,11 @@ import type { IZenZoneService } from './zen-zone.service';
export class DesktopZenZoneService implements IZenZoneService {
readonly visible$ = new Subject<boolean>();
readonly componentKey$ = new Subject<string>();
private _visible = false;
get visible() {
return this._visible;
}
constructor(@Inject(ComponentManager) private readonly _componentManager: ComponentManager) {
// super
@@ -42,10 +47,12 @@ export class DesktopZenZoneService implements IZenZoneService {
}
open(): void {
this._visible = true;
this.visible$.next(true);
}
close() {
this._visible = false;
this.visible$.next(false);
}
}
@@ -24,6 +24,8 @@ export interface IZenZoneService {
readonly visible$: Subject<boolean>;
readonly componentKey$: Subject<string>;
readonly visible: boolean;
set(key: string, component: any): IDisposable;
open(): void;
+1 -3
View File
@@ -150,6 +150,7 @@ export function App(props: IUniverAppProps) {
data-range-selector
onContextMenu={(e) => e.preventDefault()}
>
<CanvasPopup />
<ComponentContainer components={contentComponents} />
</section>
</section>
@@ -167,11 +168,8 @@ export function App(props: IUniverAppProps) {
)}
<ZenZone />
</section>
<CanvasPopup />
</div>
<ComponentContainer components={globalComponents} />
<ComponentContainer components={builtInGlobalComponents} />
{contextMenu && <ContextMenu />}
+1
View File
@@ -47,6 +47,7 @@
&-canvas {
position: relative;
overflow: hidden;
}
&-left-sidebar {
@@ -56,7 +56,6 @@ export function CanvasPopup() {
const popupService = useDependency(ICanvasPopupService);
const popups = useObservable(popupService.popups$, undefined, true);
const componentManager = useDependency(ComponentManager);
return popups.map((item) => {
const [key, popup] = item;
const Component = componentManager.get(popup.componentKey);
+445 -51
View File
@@ -264,6 +264,9 @@ importers:
'@univerjs/sheets-numfmt':
specifier: workspace:*
version: link:../packages/sheets-numfmt
'@univerjs/sheets-thread-comment':
specifier: workspace:*
version: link:../packages/sheets-thread-comment
'@univerjs/sheets-ui':
specifier: workspace:*
version: link:../packages/sheets-ui
@@ -276,6 +279,12 @@ importers:
'@univerjs/slides-ui':
specifier: workspace:*
version: link:../packages/slides-ui
'@univerjs/thread-comment':
specifier: workspace:*
version: link:../packages/thread-comment
'@univerjs/thread-comment-ui':
specifier: workspace:*
version: link:../packages/thread-comment-ui
'@univerjs/ui':
specifier: workspace:*
version: link:../packages/ui
@@ -457,6 +466,9 @@ importers:
'@rc-component/trigger':
specifier: ^1.18.3
version: 1.18.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@types/react-mentions':
specifier: ^4.1.13
version: 4.1.13
'@univerjs/icons':
specifier: ^0.1.45
version: 0.1.45(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -499,6 +511,9 @@ importers:
react-grid-layout:
specifier: ^1.4.4
version: 1.4.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-mentions:
specifier: ^4.4.10
version: 4.4.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-transition-group:
specifier: ^4.4.5
version: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -1384,14 +1399,75 @@ importers:
specifier: ^1.6.0
version: 1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
packages/sheets-thread-comment:
dependencies:
'@univerjs/engine-render':
specifier: workspace:*
version: link:../engine-render
'@univerjs/icons':
specifier: ^0.1.45
version: 0.1.45(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@univerjs/sheets':
specifier: workspace:*
version: link:../sheets
'@univerjs/sheets-ui':
specifier: workspace:*
version: link:../sheets-ui
'@univerjs/thread-comment':
specifier: workspace:*
version: link:../thread-comment
'@univerjs/thread-comment-ui':
specifier: workspace:*
version: link:../thread-comment-ui
devDependencies:
'@univerjs/core':
specifier: workspace:*
version: link:../core
'@univerjs/design':
specifier: workspace:*
version: link:../design
'@univerjs/engine-formula':
specifier: workspace:*
version: link:../engine-formula
'@univerjs/shared':
specifier: workspace:*
version: link:../../common/shared
'@univerjs/ui':
specifier: workspace:*
version: link:../ui
'@wendellhu/redi':
specifier: ^0.15.1
version: 0.15.1
clsx:
specifier: ^2.1.0
version: 2.1.1
less:
specifier: ^4.2.0
version: 4.2.0
react:
specifier: ^18.2.0
version: 18.2.0
rxjs:
specifier: ^7.8.1
version: 7.8.1
typescript:
specifier: ^5.3.3
version: 5.4.5
vite:
specifier: ^5.1.4
version: 5.2.10(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
vitest:
specifier: ^1.3.1
version: 1.5.2(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
packages/sheets-ui:
dependencies:
'@univerjs/docs-ui':
specifier: workspace:*
version: link:../docs-ui
'@univerjs/icons':
specifier: ^0.1.44
version: 0.1.44(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
specifier: ^0.1.45
version: 0.1.45(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
devDependencies:
'@types/react':
specifier: ^18.2.79
@@ -1575,6 +1651,101 @@ importers:
specifier: ^1.6.0
version: 1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
packages/thread-comment:
dependencies:
'@univerjs/protocol':
specifier: ^0.1.20
version: 0.1.23(@grpc/grpc-js@1.10.4)(rxjs@7.8.1)
devDependencies:
'@univerjs/core':
specifier: workspace:*
version: link:../core
'@univerjs/design':
specifier: workspace:*
version: link:../design
'@univerjs/shared':
specifier: workspace:*
version: link:../../common/shared
'@univerjs/ui':
specifier: workspace:*
version: link:../ui
'@wendellhu/redi':
specifier: ^0.15.1
version: 0.15.1
clsx:
specifier: ^2.1.0
version: 2.1.1
less:
specifier: ^4.2.0
version: 4.2.0
react:
specifier: ^18.2.0
version: 18.2.0
rxjs:
specifier: ^7.8.1
version: 7.8.1
typescript:
specifier: ^5.3.3
version: 5.4.5
vite:
specifier: ^5.1.4
version: 5.2.10(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
vitest:
specifier: ^1.3.1
version: 1.5.2(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
packages/thread-comment-ui:
dependencies:
'@univerjs/icons':
specifier: ^0.1.45
version: 0.1.45(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@univerjs/protocol':
specifier: ^0.1.20
version: 0.1.23(@grpc/grpc-js@1.10.4)(rxjs@7.8.1)
'@univerjs/thread-comment':
specifier: workspace:*
version: link:../thread-comment
devDependencies:
'@univerjs/core':
specifier: workspace:*
version: link:../core
'@univerjs/design':
specifier: workspace:*
version: link:../design
'@univerjs/shared':
specifier: workspace:*
version: link:../../common/shared
'@univerjs/ui':
specifier: workspace:*
version: link:../ui
'@wendellhu/redi':
specifier: ^0.15.1
version: 0.15.1
clsx:
specifier: ^2.1.1
version: 2.1.1
dayjs:
specifier: ^1.11.10
version: 1.11.10
less:
specifier: ^4.2.0
version: 4.2.0
react:
specifier: ^18.2.0
version: 18.2.0
rxjs:
specifier: ^7.8.1
version: 7.8.1
typescript:
specifier: ^5.4.5
version: 5.4.5
vite:
specifier: ^5.2.10
version: 5.2.10(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
vitest:
specifier: ^1.5.0
version: 1.5.2(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
packages/ui:
dependencies:
'@univerjs/icons':
@@ -2561,6 +2732,9 @@ packages:
resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.4.5':
resolution: {integrity: sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==}
'@babel/template@7.24.0':
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
engines: {node: '>=6.9.0'}
@@ -4259,6 +4433,9 @@ packages:
'@types/react-grid-layout@1.3.5':
resolution: {integrity: sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==}
'@types/react-mentions@4.1.13':
resolution: {integrity: sha512-kRulAAjlmhCtsJ9bapO0foocknaE/rEuFKpmFEU81fBfnXZmZNBaJ9J/DBjwigT3WDHjQVUmYoi5sxEXrcdzAw==}
'@types/react-transition-group@4.4.10':
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
@@ -4393,18 +4570,19 @@ packages:
'@univerjs/icons-svg@0.1.45':
resolution: {integrity: sha512-v1oETviQ6RI9whV0HyUGlnQLEHyatZssaMJBY/ixIfBEZn4lZeMjPYszWM+Ymm2YilhHWA6+oCj4f5isT0ovIg==}
'@univerjs/icons@0.1.44':
resolution: {integrity: sha512-emPPhk9aNGIl/wF/z+9KLAq5EG/zmUoSwj2UaHHssnM/mv96Ieob4MpkhTfrVtvtiAUI2zZpUXrbsRX3PShrXg==}
peerDependencies:
react: '*'
react-dom: '*'
'@univerjs/icons@0.1.45':
resolution: {integrity: sha512-Yw7YDDs2lvcn9i5uyaaZmmQh0Z6N523RGhymduvHauckX5ool3RLc+tFMgkKxHfEX90qEYSbfRXNHAG+Ftd29w==}
peerDependencies:
react: '*'
react-dom: '*'
'@univerjs/protocol@0.1.23':
resolution: {integrity: sha512-xOilOZjr+qA3RtKsgcVhRXp5Bo45JryPutrcVNeM06PoGmljWBfbQZhHZ0HjHxN1iYZNl9AO4LPuouLEeB4IZQ==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
peerDependencies:
'@grpc/grpc-js': ^1.9.14
rxjs: '>=7.0.0'
'@univerjs/protocol@0.1.29':
resolution: {integrity: sha512-B3/OmxK54sME2qFvsih6uH7NMwTpjOcIFsalZcDA9y7ZisKzZDQpqNoc58HQO2+cARVO/WXKoCxpWiG7ziAg1w==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
@@ -4433,24 +4611,39 @@ packages:
'@vitest/expect@1.3.1':
resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==}
'@vitest/expect@1.5.2':
resolution: {integrity: sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==}
'@vitest/expect@1.6.0':
resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==}
'@vitest/runner@1.5.2':
resolution: {integrity: sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==}
'@vitest/runner@1.6.0':
resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==}
'@vitest/snapshot@1.5.2':
resolution: {integrity: sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==}
'@vitest/snapshot@1.6.0':
resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==}
'@vitest/spy@1.3.1':
resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==}
'@vitest/spy@1.5.2':
resolution: {integrity: sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==}
'@vitest/spy@1.6.0':
resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==}
'@vitest/utils@1.3.1':
resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==}
'@vitest/utils@1.5.2':
resolution: {integrity: sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==}
'@vitest/utils@1.6.0':
resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==}
@@ -5049,8 +5242,8 @@ packages:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
chromatic@11.3.2:
resolution: {integrity: sha512-0PuHl49VvBMoDHEfmNjC/bim9YYNhWF3axTZlFuatC0avwr2Xw4GDqJDG9fArEWN8oM8VtYHkE9D7qc87dmz2w==}
chromatic@11.3.3:
resolution: {integrity: sha512-XZMa/9HJpTZLnBh5yGolbiRVagtSr3gK815rFicSuQpXr82ppOsqstrOoeu0IBpgtt/F6YvN3/JqHe6k00nXGA==}
hasBin: true
peerDependencies:
'@chromatic-com/cypress': ^0.*.* || ^1.0.0
@@ -5447,6 +5640,9 @@ packages:
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
engines: {node: '>= 0.4'}
dayjs@1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dayjs@1.11.11:
resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==}
@@ -8564,6 +8760,12 @@ packages:
react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
react-mentions@4.4.10:
resolution: {integrity: sha512-JHiQlgF1oSZR7VYPjq32wy97z1w1oE4x10EuhKjPr4WUKhVzG1uFQhQjKqjQkbVqJrmahf+ldgBTv36NrkpKpA==}
peerDependencies:
react: '>=16.8.3'
react-dom: '>=16.8.3'
react-mosaic-component@6.1.0:
resolution: {integrity: sha512-iWrNUSdW6HK9SB6kaj7/auvIGZWlyEFR8ulQKC9lskY047uluo5ur4fiuZTNroUTZvGqL02AiLzBBj1+et8RZA==}
peerDependencies:
@@ -8678,6 +8880,9 @@ packages:
regenerate@1.4.2:
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
@@ -9194,6 +9399,11 @@ packages:
peerDependencies:
webpack: ^5.0.0
substyle@9.4.1:
resolution: {integrity: sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==}
peerDependencies:
react: '>=16.8.3'
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@@ -9676,6 +9886,11 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
vite-node@1.5.2:
resolution: {integrity: sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
vite-node@1.6.0:
resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -9691,6 +9906,34 @@ packages:
vite:
optional: true
vite@5.2.10:
resolution: {integrity: sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
vite@5.2.11:
resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -9724,6 +9967,31 @@ packages:
peerDependencies:
vitest: '*'
vitest@1.5.2:
resolution: {integrity: sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/node': ^18.0.0 || >=20.0.0
'@vitest/browser': 1.5.2
'@vitest/ui': 1.5.2
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
vitest@1.6.0:
resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -10144,11 +10412,11 @@ snapshots:
'@babel/helper-annotate-as-pure@7.22.5':
dependencies:
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@babel/helper-builder-binary-assignment-operator-visitor@7.22.15':
dependencies:
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@babel/helper-compilation-targets@7.23.6':
dependencies:
@@ -10168,7 +10436,7 @@ snapshots:
'@babel/helper-optimise-call-expression': 7.22.5
'@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5)
'@babel/helper-skip-transparent-expression-wrappers': 7.22.5
'@babel/helper-split-export-declaration': 7.24.5
'@babel/helper-split-export-declaration': 7.22.6
semver: 6.3.1
'@babel/helper-create-class-features-plugin@7.24.5(@babel/core@7.24.5)':
@@ -10215,7 +10483,7 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.23.0':
dependencies:
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@babel/helper-member-expression-to-functions@7.24.5':
dependencies:
@@ -10234,6 +10502,15 @@ snapshots:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/helper-validator-identifier': 7.22.20
'@babel/helper-module-transforms@7.23.3(@babel/core@7.24.5)':
dependencies:
'@babel/core': 7.24.5
'@babel/helper-environment-visitor': 7.22.20
'@babel/helper-module-imports': 7.24.3
'@babel/helper-simple-access': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
'@babel/helper-validator-identifier': 7.22.20
'@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5)':
dependencies:
'@babel/core': 7.24.5
@@ -10245,7 +10522,7 @@ snapshots:
'@babel/helper-optimise-call-expression@7.22.5':
dependencies:
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@babel/helper-plugin-utils@7.24.0': {}
@@ -10275,7 +10552,7 @@ snapshots:
'@babel/helper-skip-transparent-expression-wrappers@7.22.5':
dependencies:
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@babel/helper-split-export-declaration@7.22.6':
dependencies:
@@ -10297,7 +10574,7 @@ snapshots:
dependencies:
'@babel/helper-function-name': 7.23.0
'@babel/template': 7.24.0
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@babel/helpers@7.24.1':
dependencies:
@@ -10603,28 +10880,28 @@ snapshots:
'@babel/plugin-transform-modules-amd@7.24.1(@babel/core@7.24.5)':
dependencies:
'@babel/core': 7.24.5
'@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5)
'@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.5)
'@babel/helper-plugin-utils': 7.24.5
'@babel/plugin-transform-modules-commonjs@7.24.1(@babel/core@7.24.5)':
dependencies:
'@babel/core': 7.24.5
'@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5)
'@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.5)
'@babel/helper-plugin-utils': 7.24.0
'@babel/helper-simple-access': 7.24.5
'@babel/helper-simple-access': 7.22.5
'@babel/plugin-transform-modules-systemjs@7.24.1(@babel/core@7.24.5)':
dependencies:
'@babel/core': 7.24.5
'@babel/helper-hoist-variables': 7.22.5
'@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5)
'@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.5)
'@babel/helper-plugin-utils': 7.24.5
'@babel/helper-validator-identifier': 7.24.5
'@babel/helper-validator-identifier': 7.22.20
'@babel/plugin-transform-modules-umd@7.24.1(@babel/core@7.24.5)':
dependencies:
'@babel/core': 7.24.5
'@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5)
'@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.5)
'@babel/helper-plugin-utils': 7.24.5
'@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.5)':
@@ -10884,7 +11161,7 @@ snapshots:
dependencies:
'@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.24.5
'@babel/types': 7.24.5
'@babel/types': 7.24.0
esutils: 2.0.3
'@babel/preset-typescript@7.24.1(@babel/core@7.24.5)':
@@ -10911,6 +11188,10 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.4.5':
dependencies:
regenerator-runtime: 0.13.11
'@babel/template@7.24.0':
dependencies:
'@babel/code-frame': 7.24.2
@@ -10963,7 +11244,7 @@ snapshots:
'@chromatic-com/storybook@1.4.0(react@18.2.0)':
dependencies:
chromatic: 11.3.2
chromatic: 11.3.3
filesize: 10.1.1
jsonfile: 6.1.0
react-confetti: 6.1.0(react@18.2.0)
@@ -11121,7 +11402,7 @@ snapshots:
dependencies:
'@types/eslint': 8.56.10
'@types/estree': 1.0.5
'@typescript-eslint/types': 7.9.0
'@typescript-eslint/types': 7.7.1
comment-parser: 1.4.1
esquery: 1.5.0
jsdoc-type-pratt-parser: 4.0.0
@@ -12267,7 +12548,7 @@ snapshots:
fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.4.5)(webpack@5.91.0(@swc/core@1.4.11)(esbuild@0.20.2))
fs-extra: 11.2.0
html-webpack-plugin: 5.6.0(webpack@5.91.0(@swc/core@1.4.11)(esbuild@0.20.2))
magic-string: 0.30.10
magic-string: 0.30.8
path-browserify: 1.0.1
process: 0.11.10
semver: 7.6.0
@@ -12304,7 +12585,7 @@ snapshots:
'@storybook/cli@8.1.1(@babel/preset-env@7.24.5(@babel/core@7.24.5))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/core': 7.24.5
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@ndelangen/get-tarball': 3.0.9
'@storybook/codemod': 8.1.1
'@storybook/core-common': 8.1.1(prettier@3.2.5)
@@ -12356,7 +12637,7 @@ snapshots:
dependencies:
'@babel/core': 7.24.5
'@babel/preset-env': 7.24.5(@babel/core@7.24.5)
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@storybook/csf': 0.1.7
'@storybook/csf-tools': 8.1.1
'@storybook/node-logger': 8.1.1
@@ -12513,7 +12794,7 @@ snapshots:
'@babel/generator': 7.24.5
'@babel/parser': 7.24.5
'@babel/traverse': 7.24.1
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@storybook/csf': 0.1.7
'@storybook/types': 8.1.1
fs-extra: 11.2.0
@@ -12557,7 +12838,7 @@ snapshots:
'@storybook/core-events': 8.1.1
'@storybook/global': 5.0.0
'@storybook/preview-api': 8.1.1
'@vitest/utils': 1.6.0
'@vitest/utils': 1.5.2
util: 0.12.5
'@storybook/manager-api@8.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
@@ -12598,7 +12879,7 @@ snapshots:
'@types/semver': 7.5.8
find-up: 5.0.0
fs-extra: 11.2.0
magic-string: 0.30.10
magic-string: 0.30.8
react: 18.2.0
react-docgen: 7.0.3
react-dom: 18.2.0(react@18.2.0)
@@ -12739,7 +13020,7 @@ snapshots:
'@testing-library/jest-dom': 6.4.2(vitest@1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0))
'@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4)
'@vitest/expect': 1.3.1
'@vitest/spy': 1.6.0
'@vitest/spy': 1.5.2
util: 0.12.5
transitivePeerDependencies:
- '@jest/globals'
@@ -13070,6 +13351,10 @@ snapshots:
dependencies:
'@types/react': 18.3.1
'@types/react-mentions@4.1.13':
dependencies:
'@types/react': 18.3.1
'@types/react-transition-group@4.4.10':
dependencies:
'@types/react': 18.3.1
@@ -13243,16 +13528,16 @@ snapshots:
'@univerjs/icons-svg@0.1.45': {}
'@univerjs/icons@0.1.44(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@univerjs/icons@0.1.45(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@univerjs/protocol@0.1.23(@grpc/grpc-js@1.10.4)(rxjs@7.8.1)':
dependencies:
'@grpc/grpc-js': 1.10.4
rxjs: 7.8.1
'@univerjs/protocol@0.1.29(@grpc/grpc-js@1.10.4)(rxjs@7.8.1)':
dependencies:
'@grpc/grpc-js': 1.10.4
@@ -13295,18 +13580,36 @@ snapshots:
'@vitest/utils': 1.3.1
chai: 4.4.1
'@vitest/expect@1.5.2':
dependencies:
'@vitest/spy': 1.5.2
'@vitest/utils': 1.5.2
chai: 4.4.1
'@vitest/expect@1.6.0':
dependencies:
'@vitest/spy': 1.6.0
'@vitest/utils': 1.6.0
chai: 4.4.1
'@vitest/runner@1.5.2':
dependencies:
'@vitest/utils': 1.5.2
p-limit: 5.0.0
pathe: 1.1.2
'@vitest/runner@1.6.0':
dependencies:
'@vitest/utils': 1.6.0
p-limit: 5.0.0
pathe: 1.1.2
'@vitest/snapshot@1.5.2':
dependencies:
magic-string: 0.30.8
pathe: 1.1.2
pretty-format: 29.7.0
'@vitest/snapshot@1.6.0':
dependencies:
magic-string: 0.30.8
@@ -13317,6 +13620,10 @@ snapshots:
dependencies:
tinyspy: 2.2.1
'@vitest/spy@1.5.2':
dependencies:
tinyspy: 2.2.1
'@vitest/spy@1.6.0':
dependencies:
tinyspy: 2.2.1
@@ -13328,6 +13635,13 @@ snapshots:
loupe: 2.3.7
pretty-format: 29.7.0
'@vitest/utils@1.5.2':
dependencies:
diff-sequences: 29.6.3
estree-walker: 3.0.3
loupe: 2.3.7
pretty-format: 29.7.0
'@vitest/utils@1.6.0':
dependencies:
diff-sequences: 29.6.3
@@ -13355,7 +13669,6 @@ snapshots:
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.0
optional: true
'@vue/compiler-core@3.4.27':
dependencies:
@@ -13369,7 +13682,6 @@ snapshots:
dependencies:
'@vue/compiler-core': 3.4.21
'@vue/shared': 3.4.21
optional: true
'@vue/compiler-dom@3.4.27':
dependencies:
@@ -13416,8 +13728,8 @@ snapshots:
dependencies:
'@volar/language-core': 1.11.1
'@volar/source-map': 1.11.1
'@vue/compiler-dom': 3.4.27
'@vue/shared': 3.4.27
'@vue/compiler-dom': 3.4.21
'@vue/shared': 3.4.21
computeds: 0.0.1
minimatch: 9.0.4
muggle-string: 0.3.1
@@ -13472,8 +13784,7 @@ snapshots:
'@vue/shared': 3.4.27
vue: 3.4.27(typescript@5.4.5)
'@vue/shared@3.4.21':
optional: true
'@vue/shared@3.4.21': {}
'@vue/shared@3.4.27': {}
@@ -14073,7 +14384,7 @@ snapshots:
chownr@2.0.0: {}
chromatic@11.3.2: {}
chromatic@11.3.3: {}
chrome-trace-event@1.0.3: {}
@@ -14461,6 +14772,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.1
dayjs@1.11.10: {}
dayjs@1.11.11: {}
de-indent@1.0.2: {}
@@ -16421,7 +16734,7 @@ snapshots:
istanbul-lib-instrument@6.0.2:
dependencies:
'@babel/core': 7.24.3
'@babel/parser': 7.24.5
'@babel/parser': 7.24.1
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
semver: 7.6.0
@@ -16531,7 +16844,7 @@ snapshots:
jscodeshift@0.15.2(@babel/preset-env@7.24.5(@babel/core@7.24.5)):
dependencies:
'@babel/core': 7.24.5
'@babel/parser': 7.24.5
'@babel/parser': 7.24.1
'@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.5)
'@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5)
'@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.5)
@@ -16855,8 +17168,8 @@ snapshots:
magicast@0.3.3:
dependencies:
'@babel/parser': 7.24.5
'@babel/types': 7.24.5
'@babel/parser': 7.24.1
'@babel/types': 7.24.0
source-map-js: 1.2.0
make-dir@2.1.0:
@@ -17895,7 +18208,7 @@ snapshots:
dependencies:
'@babel/core': 7.24.3
'@babel/traverse': 7.24.1
'@babel/types': 7.24.5
'@babel/types': 7.24.0
'@types/babel__core': 7.20.5
'@types/babel__traverse': 7.20.5
'@types/doctrine': 0.0.9
@@ -17951,6 +18264,15 @@ snapshots:
react-is@18.2.0: {}
react-mentions@4.4.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.4.5
invariant: 2.2.4
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
substyle: 9.4.1(react@18.2.0)
react-mosaic-component@6.1.0(@types/node@20.12.12)(@types/react@18.3.1)(dnd-core@16.0.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
classnames: 2.5.1
@@ -18111,6 +18433,8 @@ snapshots:
regenerate@1.4.2: {}
regenerator-runtime@0.13.11: {}
regenerator-runtime@0.14.1: {}
regenerator-transform@0.15.2:
@@ -18755,6 +19079,12 @@ snapshots:
dependencies:
webpack: 5.91.0(@swc/core@1.4.11)(esbuild@0.20.2)
substyle@9.4.1(react@18.2.0):
dependencies:
'@babel/runtime': 7.24.1
invariant: 2.2.4
react: 18.2.0
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
@@ -19215,6 +19545,23 @@ snapshots:
vary@1.1.2: {}
vite-node@1.5.2(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0):
dependencies:
cac: 6.7.14
debug: 4.3.4
pathe: 1.1.2
picocolors: 1.0.0
vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
vite-node@1.6.0(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0):
dependencies:
cac: 6.7.14
@@ -19239,7 +19586,7 @@ snapshots:
'@vue/language-core': 1.8.27(typescript@5.4.5)
debug: 4.3.4
kolorist: 1.8.0
magic-string: 0.30.10
magic-string: 0.30.8
typescript: 5.4.5
vue-tsc: 1.8.27(typescript@5.4.5)
optionalDependencies:
@@ -19249,6 +19596,18 @@ snapshots:
- rollup
- supports-color
vite@5.2.10(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0):
dependencies:
esbuild: 0.20.2
postcss: 8.4.38
rollup: 4.13.2
optionalDependencies:
'@types/node': 20.12.12
fsevents: 2.3.3
less: 4.2.0
sass: 1.72.0
terser: 5.30.0
vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0):
dependencies:
esbuild: 0.20.2
@@ -19266,6 +19625,41 @@ snapshots:
jest-canvas-mock: 2.5.2
vitest: 1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
vitest@1.5.2(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0):
dependencies:
'@vitest/expect': 1.5.2
'@vitest/runner': 1.5.2
'@vitest/snapshot': 1.5.2
'@vitest/spy': 1.5.2
'@vitest/utils': 1.5.2
acorn-walk: 8.3.2
chai: 4.4.1
debug: 4.3.4
execa: 8.0.1
local-pkg: 0.5.0
magic-string: 0.30.8
pathe: 1.1.2
picocolors: 1.0.0
std-env: 3.7.0
strip-literal: 2.1.0
tinybench: 2.6.0
tinypool: 0.8.3
vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
vite-node: 1.5.2(@types/node@20.12.12)(less@4.2.0)(sass@1.72.0)(terser@5.30.0)
why-is-node-running: 2.2.2
optionalDependencies:
'@types/node': 20.12.12
happy-dom: 13.3.8
jsdom: 24.0.0
transitivePeerDependencies:
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
vitest@1.6.0(@types/node@20.12.12)(happy-dom@13.3.8)(jsdom@24.0.0)(less@4.2.0)(sass@1.72.0)(terser@5.30.0):
dependencies:
'@vitest/expect': 1.6.0