From 313ab796d7d951838d593d3801a1771d39e13153 Mon Sep 17 00:00:00 2001 From: zhangw Date: Tue, 21 May 2024 11:07:29 +0800 Subject: [PATCH] feat(sheets-thread-comment): comment support for sheets (#2228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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: 白熱 --- examples/package.json | 3 + .../sheets/demo/default-workbook-data-demo.ts | 4 + examples/src/sheets/main.ts | 35 +- .../user-manager/user-manager.service.ts | 16 +- .../src/types/interfaces/i-document-data.ts | 1 + packages/design/package.json | 2 + .../design/src/components/button/Button.tsx | 2 + .../components/mentions/Mentions.stories.tsx | 68 +++ .../src/components/mentions/Mentions.tsx | 33 ++ .../src/components/mentions/index.module.less | 71 +++ .../design/src/components/mentions/index.ts | 19 + .../design/src/components/popup/RectPopup.tsx | 60 ++- .../src/components/popup/index.module.less | 2 +- packages/design/src/index.ts | 2 + .../src/engine/utils/reference.ts | 2 +- packages/engine-formula/src/index.ts | 1 + .../src/controllers/dv-render.controller.ts | 1 + packages/sheets-thread-comment/README.md | 16 + packages/sheets-thread-comment/package.json | 97 ++++ .../commands/operations/comment.operation.ts | 70 +++ .../src/controllers/menu.ts | 52 ++ .../render-controllers/render.controller.ts | 90 ++++ ...ts-thread-comment-copy-paste.controller.ts | 158 ++++++ .../sheets-thread-comment-hover.controller.ts | 62 +++ ...ets-thread-comment-ref-range.controller.ts | 182 +++++++ ...sheets-thread-comment-remove.controller.ts | 77 +++ .../sheets-thread-comment.controller.ts | 177 +++++++ packages/sheets-thread-comment/src/index.ts | 29 + .../sheets-thread-comment/src/locales/enUS.ts | 24 + .../src/locales/index.ts | 18 + .../sheets-thread-comment/src/locales/zhCN.ts | 24 + .../src/models/sheets-thread-comment.model.ts | 234 +++++++++ packages/sheets-thread-comment/src/plugin.ts | 79 +++ .../sheets-thread-comment-popup.service.ts | 134 +++++ .../sheets-thread-comment/src/types/const.ts | 19 + .../interfaces/i-sheet-thread-comment.ts | 22 + .../sheets-thread-comment-cell/index.tsx | 62 +++ .../sheets-thread-comment-panel/index.tsx | 85 +++ .../sheets-thread-comment/src/vite-env.d.ts | 1 + packages/sheets-thread-comment/tsconfig.json | 9 + .../sheets-thread-comment/tsconfig.node.json | 4 + packages/sheets-thread-comment/vite.config.ts | 12 + packages/sheets-ui/package.json | 2 +- .../services/canvas-pop-manager.service.ts | 16 +- packages/sheets/src/index.ts | 2 +- .../sheet-interceptor/utils/interceptor.ts | 9 +- packages/thread-comment-ui/README.md | 16 + packages/thread-comment-ui/package.json | 92 ++++ .../commands/operations/comment.operations.ts | 61 +++ .../thread-comment-ui.controller.ts | 37 ++ packages/thread-comment-ui/src/index.ts | 25 + .../thread-comment-ui/src/locales/enUS.ts | 49 ++ .../thread-comment-ui/src/locales/index.ts | 18 + .../thread-comment-ui/src/locales/zhCN.ts | 49 ++ packages/thread-comment-ui/src/plugin.ts | 59 +++ .../services/thread-comment-panel.service.ts | 76 +++ packages/thread-comment-ui/src/types/const.ts | 21 + .../interfaces/i-thread-comment-mention.ts | 28 + .../thread-comment-editor/index.module.less | 25 + .../src/views/thread-comment-editor/index.tsx | 137 +++++ .../src/views/thread-comment-editor/util.ts | 164 ++++++ .../thread-comment-panel/index.module.less | 40 ++ .../src/views/thread-comment-panel/index.tsx | 212 ++++++++ .../thread-comment-tree/index.module.less | 120 +++++ .../src/views/thread-comment-tree/index.tsx | 337 ++++++++++++ packages/thread-comment-ui/src/vite-env.d.ts | 1 + packages/thread-comment-ui/tsconfig.json | 9 + packages/thread-comment-ui/tsconfig.node.json | 4 + packages/thread-comment-ui/vite.config.ts | 12 + packages/thread-comment/README.md | 16 + packages/thread-comment/package.json | 84 +++ .../src/commands/commands/comment.command.ts | 337 ++++++++++++ .../commands/mutations/comment.mutation.ts | 129 +++++ .../src/controllers/tc-resource.controller.ts | 83 +++ packages/thread-comment/src/index.ts | 52 ++ .../src/models/thread-comment.model.ts | 339 ++++++++++++ packages/thread-comment/src/plugin.ts | 65 +++ .../src/services/tc-datasource.service.ts | 60 +++ .../thread-comment/src/types/const/index.ts | 17 + .../src/types/interfaces/i-thread-comment.ts | 39 ++ packages/thread-comment/src/vite-env.d.ts | 1 + packages/thread-comment/tsconfig.json | 9 + packages/thread-comment/tsconfig.node.json | 4 + packages/thread-comment/vite.config.ts | 12 + .../zen-zone/desktop-zen-zone.service.ts | 7 + .../src/services/zen-zone/zen-zone.service.ts | 2 + packages/ui/src/views/App.tsx | 4 +- packages/ui/src/views/app.module.less | 1 + .../views/components/popup/CanvasPopup.tsx | 1 - pnpm-lock.yaml | 494 ++++++++++++++++-- 90 files changed, 5343 insertions(+), 93 deletions(-) create mode 100644 packages/design/src/components/mentions/Mentions.stories.tsx create mode 100644 packages/design/src/components/mentions/Mentions.tsx create mode 100644 packages/design/src/components/mentions/index.module.less create mode 100644 packages/design/src/components/mentions/index.ts create mode 100644 packages/sheets-thread-comment/README.md create mode 100644 packages/sheets-thread-comment/package.json create mode 100644 packages/sheets-thread-comment/src/commands/operations/comment.operation.ts create mode 100644 packages/sheets-thread-comment/src/controllers/menu.ts create mode 100644 packages/sheets-thread-comment/src/controllers/render-controllers/render.controller.ts create mode 100644 packages/sheets-thread-comment/src/controllers/sheets-thread-comment-copy-paste.controller.ts create mode 100644 packages/sheets-thread-comment/src/controllers/sheets-thread-comment-hover.controller.ts create mode 100644 packages/sheets-thread-comment/src/controllers/sheets-thread-comment-ref-range.controller.ts create mode 100644 packages/sheets-thread-comment/src/controllers/sheets-thread-comment-remove.controller.ts create mode 100644 packages/sheets-thread-comment/src/controllers/sheets-thread-comment.controller.ts create mode 100644 packages/sheets-thread-comment/src/index.ts create mode 100644 packages/sheets-thread-comment/src/locales/enUS.ts create mode 100644 packages/sheets-thread-comment/src/locales/index.ts create mode 100644 packages/sheets-thread-comment/src/locales/zhCN.ts create mode 100644 packages/sheets-thread-comment/src/models/sheets-thread-comment.model.ts create mode 100644 packages/sheets-thread-comment/src/plugin.ts create mode 100644 packages/sheets-thread-comment/src/services/sheets-thread-comment-popup.service.ts create mode 100644 packages/sheets-thread-comment/src/types/const.ts create mode 100644 packages/sheets-thread-comment/src/types/interfaces/i-sheet-thread-comment.ts create mode 100644 packages/sheets-thread-comment/src/views/sheets-thread-comment-cell/index.tsx create mode 100644 packages/sheets-thread-comment/src/views/sheets-thread-comment-panel/index.tsx create mode 100644 packages/sheets-thread-comment/src/vite-env.d.ts create mode 100644 packages/sheets-thread-comment/tsconfig.json create mode 100644 packages/sheets-thread-comment/tsconfig.node.json create mode 100644 packages/sheets-thread-comment/vite.config.ts create mode 100644 packages/thread-comment-ui/README.md create mode 100644 packages/thread-comment-ui/package.json create mode 100644 packages/thread-comment-ui/src/commands/operations/comment.operations.ts create mode 100644 packages/thread-comment-ui/src/controllers/thread-comment-ui.controller.ts create mode 100644 packages/thread-comment-ui/src/index.ts create mode 100644 packages/thread-comment-ui/src/locales/enUS.ts create mode 100644 packages/thread-comment-ui/src/locales/index.ts create mode 100644 packages/thread-comment-ui/src/locales/zhCN.ts create mode 100644 packages/thread-comment-ui/src/plugin.ts create mode 100644 packages/thread-comment-ui/src/services/thread-comment-panel.service.ts create mode 100644 packages/thread-comment-ui/src/types/const.ts create mode 100644 packages/thread-comment-ui/src/types/interfaces/i-thread-comment-mention.ts create mode 100644 packages/thread-comment-ui/src/views/thread-comment-editor/index.module.less create mode 100644 packages/thread-comment-ui/src/views/thread-comment-editor/index.tsx create mode 100644 packages/thread-comment-ui/src/views/thread-comment-editor/util.ts create mode 100644 packages/thread-comment-ui/src/views/thread-comment-panel/index.module.less create mode 100644 packages/thread-comment-ui/src/views/thread-comment-panel/index.tsx create mode 100644 packages/thread-comment-ui/src/views/thread-comment-tree/index.module.less create mode 100644 packages/thread-comment-ui/src/views/thread-comment-tree/index.tsx create mode 100644 packages/thread-comment-ui/src/vite-env.d.ts create mode 100644 packages/thread-comment-ui/tsconfig.json create mode 100644 packages/thread-comment-ui/tsconfig.node.json create mode 100644 packages/thread-comment-ui/vite.config.ts create mode 100644 packages/thread-comment/README.md create mode 100644 packages/thread-comment/package.json create mode 100644 packages/thread-comment/src/commands/commands/comment.command.ts create mode 100644 packages/thread-comment/src/commands/mutations/comment.mutation.ts create mode 100644 packages/thread-comment/src/controllers/tc-resource.controller.ts create mode 100644 packages/thread-comment/src/index.ts create mode 100644 packages/thread-comment/src/models/thread-comment.model.ts create mode 100644 packages/thread-comment/src/plugin.ts create mode 100644 packages/thread-comment/src/services/tc-datasource.service.ts create mode 100644 packages/thread-comment/src/types/const/index.ts create mode 100644 packages/thread-comment/src/types/interfaces/i-thread-comment.ts create mode 100644 packages/thread-comment/src/vite-env.d.ts create mode 100644 packages/thread-comment/tsconfig.json create mode 100644 packages/thread-comment/tsconfig.node.json create mode 100644 packages/thread-comment/vite.config.ts diff --git a/examples/package.json b/examples/package.json index 76a48e7f706..74e5e9269c6 100644 --- a/examples/package.json +++ b/examples/package.json @@ -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", diff --git a/examples/src/data/sheets/demo/default-workbook-data-demo.ts b/examples/src/data/sheets/demo/default-workbook-data-demo.ts index 1e024ad3766..5e298fcf917 100644 --- a/examples/src/data/sheets/demo/default-workbook-data-demo.ts +++ b/examples/src/data/sheets/demo/default-workbook-data-demo.ts @@ -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: [ // { diff --git a/examples/src/sheets/main.ts b/examples/src/sheets/main.ts index 1c527e2f89c..06c3d845ab5 100644 --- a/examples/src/sheets/main.ts +++ b/examples/src/sheets/main.ts @@ -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; diff --git a/packages/core/src/services/user-manager/user-manager.service.ts b/packages/core/src/services/user-manager/user-manager.service.ts index fe8976e2bab..e0e4202421e 100644 --- a/packages/core/src/services/user-manager/user-manager.service.ts +++ b/packages/core/src/services/user-manager/user-manager.service.ts @@ -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(); private _userChange$ = new Subject<{ type: 'add' | 'delete'; user: IUser } | { type: 'clear' }>(); public userChange$ = this._userChange$.asObservable(); + private _currentUser: IUser | null; + private _currentUser$ = new BehaviorSubject(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 }); diff --git a/packages/core/src/types/interfaces/i-document-data.ts b/packages/core/src/types/interfaces/i-document-data.ts index 8621f2b850e..7819be05bd4 100644 --- a/packages/core/src/types/interfaces/i-document-data.ts +++ b/packages/core/src/types/interfaces/i-document-data.ts @@ -277,6 +277,7 @@ export enum CustomRangeType { BOOKMARK, COMMENT, CUSTOM, + MENTION, } /** diff --git a/packages/design/package.json b/packages/design/package.json index b13b59022ee..f589676e859 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -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": { diff --git a/packages/design/src/components/button/Button.tsx b/packages/design/src/components/button/Button.tsx index 20a15b6bc68..4f8a9743e56 100644 --- a/packages/design/src/components/button/Button.tsx +++ b/packages/design/src/components/button/Button.tsx @@ -60,6 +60,8 @@ export interface IButtonProps { /** Set the handler to handle `click` event */ onClick?: (e: React.MouseEvent) => void; + + id?: string; } export function Button(props: IButtonProps) { diff --git a/packages/design/src/components/mentions/Mentions.stories.tsx b/packages/design/src/components/mentions/Mentions.stories.tsx new file mode 100644 index 00000000000..cb18c89f34b --- /dev/null +++ b/packages/design/src/components/mentions/Mentions.stories.tsx @@ -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 = { + title: 'Components / Mentions', + component: Mentions, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; + +export const InputBasic = { + + render() { + const [value, onChange] = useState(''); + + return ( +
+ onChange(e.target.value)} + > + + +
+ + ); + }, +}; + diff --git a/packages/design/src/components/mentions/Mentions.tsx b/packages/design/src/components/mentions/Mentions.tsx new file mode 100644 index 00000000000..a3ae171f08a --- /dev/null +++ b/packages/design/src/components/mentions/Mentions.tsx @@ -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, IMentionsProps>((props, ref) => { + return ( + + ); +}); + diff --git a/packages/design/src/components/mentions/index.module.less b/packages/design/src/components/mentions/index.module.less new file mode 100644 index 00000000000..9a85dbadb3e --- /dev/null +++ b/packages/design/src/components/mentions/index.module.less @@ -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)); +} diff --git a/packages/design/src/components/mentions/index.ts b/packages/design/src/components/mentions/index.ts new file mode 100644 index 00000000000..fd9b5794dba --- /dev/null +++ b/packages/design/src/components/mentions/index.ts @@ -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'; diff --git a/packages/design/src/components/popup/RectPopup.tsx b/packages/design/src/components/popup/RectPopup.tsx index 163c76e101a..5311833af35 100644 --- a/packages/design/src/components/popup/RectPopup.tsx +++ b/packages/design/src/components/popup/RectPopup.tsx @@ -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(null); const clickOtherFn = useEvent(onClickOutside ?? (() => { /* empty */ })); - const [position, setPosition] = useState>({ 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 (
{ e.stopPropagation(); }} diff --git a/packages/design/src/components/popup/index.module.less b/packages/design/src/components/popup/index.module.less index 14c5edad5c4..14af4e449c2 100644 --- a/packages/design/src/components/popup/index.module.less +++ b/packages/design/src/components/popup/index.module.less @@ -1,6 +1,6 @@ .popup-absolute { position: absolute; - z-index: 1070; + z-index: 100; top: -9999px; left: -9999px; } diff --git a/packages/design/src/index.ts b/packages/design/src/index.ts index d0e86d34a1b..ad1f7dd05d5 100644 --- a/packages/design/src/index.ts +++ b/packages/design/src/index.ts @@ -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'; diff --git a/packages/engine-formula/src/engine/utils/reference.ts b/packages/engine-formula/src/engine/utils/reference.ts index 74c023d1290..7d6dae3c544 100644 --- a/packages/engine-formula/src/engine/utils/reference.ts +++ b/packages/engine-formula/src/engine/utils/reference.ts @@ -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, '')); diff --git a/packages/engine-formula/src/index.ts b/packages/engine-formula/src/index.ts index aae93011353..7b8a6801143 100644 --- a/packages/engine-formula/src/index.ts +++ b/packages/engine-formula/src/index.ts @@ -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'; diff --git a/packages/sheets-data-validation/src/controllers/dv-render.controller.ts b/packages/sheets-data-validation/src/controllers/dv-render.controller.ts index e08ed27d467..16b24747e65 100644 --- a/packages/sheets-data-validation/src/controllers/dv-render.controller.ts +++ b/packages/sheets-data-validation/src/controllers/dv-render.controller.ts @@ -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; diff --git a/packages/sheets-thread-comment/README.md b/packages/sheets-thread-comment/README.md new file mode 100644 index 00000000000..919bf1d1357 --- /dev/null +++ b/packages/sheets-thread-comment/README.md @@ -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 +``` diff --git a/packages/sheets-thread-comment/package.json b/packages/sheets-thread-comment/package.json new file mode 100644 index 00000000000..f84362b4bed --- /dev/null +++ b/packages/sheets-thread-comment/package.json @@ -0,0 +1,97 @@ +{ + "name": "@univerjs/sheets-thread-comment", + "version": "0.0.1", + "private": true, + "description": "", + "author": "DreamNum ", + "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" + } +} diff --git a/packages/sheets-thread-comment/src/commands/operations/comment.operation.ts b/packages/sheets-thread-comment/src/commands/operations/comment.operation.ts new file mode 100644 index 00000000000..7e0a1c39dae --- /dev/null +++ b/packages/sheets-thread-comment/src/commands/operations/comment.operation.ts @@ -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; + }, +}; + diff --git a/packages/sheets-thread-comment/src/controllers/menu.ts b/packages/sheets-thread-comment/src/controllers/menu.ts new file mode 100644 index 00000000000..8cb5f0df382 --- /dev/null +++ b/packages/sheets-thread-comment/src/controllers/menu.ts @@ -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, +}; diff --git a/packages/sheets-thread-comment/src/controllers/render-controllers/render.controller.ts b/packages/sheets-thread-comment/src/controllers/render-controllers/render.controller.ts new file mode 100644 index 00000000000..4bd8d5b41e8 --- /dev/null +++ b/packages/sheets-thread-comment/src/controllers/render-controllers/render.controller.ts @@ -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(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(); + })); + } +} diff --git a/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-copy-paste.controller.ts b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-copy-paste.controller.ts new file mode 100644 index 00000000000..0d484a14afb --- /dev/null +++ b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-copy-paste.controller.ts @@ -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: [], + }; + }, + }) + ); + } +} diff --git a/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-hover.controller.ts b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-hover.controller.ts new file mode 100644 index 00000000000..fda44e348d3 --- /dev/null +++ b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-hover.controller.ts @@ -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(); + } + } + } + }); + } +} diff --git a/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-ref-range.controller.ts b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-ref-range.controller.ts new file mode 100644 index 00000000000..60ad5a99f63 --- /dev/null +++ b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-ref-range.controller.ts @@ -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(); + + 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(); + })); + } +} diff --git a/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-remove.controller.ts b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-remove.controller.ts new file mode 100644 index 00000000000..f0c61363312 --- /dev/null +++ b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment-remove.controller.ts @@ -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(params.unitId) : this._univerInstanceService.getCurrentUnitForType(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: [] }; + }, + }) + ); + } +} diff --git a/packages/sheets-thread-comment/src/controllers/sheets-thread-comment.controller.ts b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment.controller.ts new file mode 100644 index 00000000000..b40e2819d61 --- /dev/null +++ b/packages/sheets-thread-comment/src/controllers/sheets-thread-comment.controller.ts @@ -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(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(); + } + })); + } +} diff --git a/packages/sheets-thread-comment/src/index.ts b/packages/sheets-thread-comment/src/index.ts new file mode 100644 index 00000000000..e0d37004172 --- /dev/null +++ b/packages/sheets-thread-comment/src/index.ts @@ -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'; diff --git a/packages/sheets-thread-comment/src/locales/enUS.ts b/packages/sheets-thread-comment/src/locales/enUS.ts new file mode 100644 index 00000000000..4ab7a55a914 --- /dev/null +++ b/packages/sheets-thread-comment/src/locales/enUS.ts @@ -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', + }, + }, +}; diff --git a/packages/sheets-thread-comment/src/locales/index.ts b/packages/sheets-thread-comment/src/locales/index.ts new file mode 100644 index 00000000000..8b2fb10568b --- /dev/null +++ b/packages/sheets-thread-comment/src/locales/index.ts @@ -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'; diff --git a/packages/sheets-thread-comment/src/locales/zhCN.ts b/packages/sheets-thread-comment/src/locales/zhCN.ts new file mode 100644 index 00000000000..f42e6bc193f --- /dev/null +++ b/packages/sheets-thread-comment/src/locales/zhCN.ts @@ -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: '评论管理', + }, + }, +}; diff --git a/packages/sheets-thread-comment/src/models/sheets-thread-comment.model.ts b/packages/sheets-thread-comment/src/models/sheets-thread-comment.model.ts new file mode 100644 index 00000000000..bb1a5846758 --- /dev/null +++ b/packages/sheets-thread-comment/src/models/sheets-thread-comment.model.ts @@ -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>> = new Map(); + private _locationMap: Map>> = new Map(); + private _commentUpdate$ = new Subject(); + + 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; + } +} diff --git a/packages/sheets-thread-comment/src/plugin.ts b/packages/sheets-thread-comment/src/plugin.ts new file mode 100644 index 00000000000..8b651fb2d84 --- /dev/null +++ b/packages/sheets-thread-comment/src/plugin.ts @@ -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); + }); + } +} diff --git a/packages/sheets-thread-comment/src/services/sheets-thread-comment-popup.service.ts b/packages/sheets-thread-comment/src/services/sheets-thread-comment-popup.service.ts new file mode 100644 index 00000000000..9b52a065fa3 --- /dev/null +++ b/packages/sheets-thread-comment/src/services/sheets-thread-comment-popup.service.ts @@ -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 = null; + private _activePopup: Nullable; + private _activePopup$ = new BehaviorSubject>(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); + } +} diff --git a/packages/sheets-thread-comment/src/types/const.ts b/packages/sheets-thread-comment/src/types/const.ts new file mode 100644 index 00000000000..1159ad97eb9 --- /dev/null +++ b/packages/sheets-thread-comment/src/types/const.ts @@ -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'; diff --git a/packages/sheets-thread-comment/src/types/interfaces/i-sheet-thread-comment.ts b/packages/sheets-thread-comment/src/types/interfaces/i-sheet-thread-comment.ts new file mode 100644 index 00000000000..4ae71f95fee --- /dev/null +++ b/packages/sheets-thread-comment/src/types/interfaces/i-sheet-thread-comment.ts @@ -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; +} diff --git a/packages/sheets-thread-comment/src/views/sheets-thread-comment-cell/index.tsx b/packages/sheets-thread-comment/src/views/sheets-thread-comment-cell/index.tsx new file mode 100644 index 00000000000..6e8ee9a5bd9 --- /dev/null +++ b/packages/sheets-thread-comment/src/views/sheets-thread-comment-cell/index.tsx @@ -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(UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId(id)?.getName() ?? ''; + }; + + return ( + { + sheetsThreadCommentPopupService.persistPopup(); + }} + prefix="cell" + id={rootId} + unitId={unitId} + subUnitId={subUnitId} + type={UniverInstanceType.UNIVER_SHEET} + refStr={ref} + onClose={onClose} + getSubUnitName={getSubUnitName} + autoFocus={trigger === 'context-menu'} + /> + ); +}; diff --git a/packages/sheets-thread-comment/src/views/sheets-thread-comment-panel/index.tsx b/packages/sheets-thread-comment/src/views/sheets-thread-comment-panel/index.tsx new file mode 100644 index 00000000000..c2f2278199e --- /dev/null +++ b/packages/sheets-thread-comment/src/views/sheets-thread-comment-panel/index.tsx @@ -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(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 = {}; + 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 ( + + ); +}; diff --git a/packages/sheets-thread-comment/src/vite-env.d.ts b/packages/sheets-thread-comment/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/packages/sheets-thread-comment/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/sheets-thread-comment/tsconfig.json b/packages/sheets-thread-comment/tsconfig.json new file mode 100644 index 00000000000..d676ad2a20d --- /dev/null +++ b/packages/sheets-thread-comment/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@univerjs/shared/tsconfigs/base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src"] +} diff --git a/packages/sheets-thread-comment/tsconfig.node.json b/packages/sheets-thread-comment/tsconfig.node.json new file mode 100644 index 00000000000..e53dac88688 --- /dev/null +++ b/packages/sheets-thread-comment/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@univerjs/shared/tsconfigs/node", + "include": ["vite.config.ts"] +} diff --git a/packages/sheets-thread-comment/vite.config.ts b/packages/sheets-thread-comment/vite.config.ts new file mode 100644 index 00000000000..925b530b4d5 --- /dev/null +++ b/packages/sheets-thread-comment/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, + }, +}); diff --git a/packages/sheets-ui/package.json b/packages/sheets-ui/package.json index f9951078166..c458d3595f0 100644 --- a/packages/sheets-ui/package.json +++ b/packages/sheets-ui/package.json @@ -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", diff --git a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts index ef5e67e1d33..0c322115ac0 100644 --- a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts +++ b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts @@ -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), }; } diff --git a/packages/sheets/src/index.ts b/packages/sheets/src/index.ts index edbb1c8d08f..ef9958b094a 100644 --- a/packages/sheets/src/index.ts +++ b/packages/sheets/src/index.ts @@ -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'; diff --git a/packages/sheets/src/services/sheet-interceptor/utils/interceptor.ts b/packages/sheets/src/services/sheet-interceptor/utils/interceptor.ts index 88f86dc6095..b4cec443c4e 100644 --- a/packages/sheets/src/services/sheet-interceptor/utils/interceptor.ts +++ b/packages/sheets/src/services/sheet-interceptor/utils/interceptor.ts @@ -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; diff --git a/packages/thread-comment-ui/README.md b/packages/thread-comment-ui/README.md new file mode 100644 index 00000000000..cd952e570f8 --- /dev/null +++ b/packages/thread-comment-ui/README.md @@ -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 +``` diff --git a/packages/thread-comment-ui/package.json b/packages/thread-comment-ui/package.json new file mode 100644 index 00000000000..6afba100209 --- /dev/null +++ b/packages/thread-comment-ui/package.json @@ -0,0 +1,92 @@ +{ + "name": "@univerjs/thread-comment-ui", + "version": "0.0.1", + "private": true, + "description": "", + "author": "DreamNum ", + "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" + } +} diff --git a/packages/thread-comment-ui/src/commands/operations/comment.operations.ts b/packages/thread-comment-ui/src/commands/operations/comment.operations.ts new file mode 100644 index 00000000000..4d1a1873b5e --- /dev/null +++ b/packages/thread-comment-ui/src/commands/operations/comment.operations.ts @@ -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 = { + id: 'thread-comment-ui.operation.set-active-comment', + type: CommandType.OPERATION, + handler(accessor, params) { + const panelService = accessor.get(ThreadCommentPanelService); + panelService.setActiveComment(params); + return true; + }, +}; diff --git a/packages/thread-comment-ui/src/controllers/thread-comment-ui.controller.ts b/packages/thread-comment-ui/src/controllers/thread-comment-ui.controller.ts new file mode 100644 index 00000000000..462dafe9aa9 --- /dev/null +++ b/packages/thread-comment-ui/src/controllers/thread-comment-ui.controller.ts @@ -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, + }); + } +} diff --git a/packages/thread-comment-ui/src/index.ts b/packages/thread-comment-ui/src/index.ts new file mode 100644 index 00000000000..9b53cc184e0 --- /dev/null +++ b/packages/thread-comment-ui/src/index.ts @@ -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'; diff --git a/packages/thread-comment-ui/src/locales/enUS.ts b/packages/thread-comment-ui/src/locales/enUS.ts new file mode 100644 index 00000000000..9016a2affc8 --- /dev/null +++ b/packages/thread-comment-ui/src/locales/enUS.ts @@ -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', + }, + }, + }, +}; diff --git a/packages/thread-comment-ui/src/locales/index.ts b/packages/thread-comment-ui/src/locales/index.ts new file mode 100644 index 00000000000..8b2fb10568b --- /dev/null +++ b/packages/thread-comment-ui/src/locales/index.ts @@ -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'; diff --git a/packages/thread-comment-ui/src/locales/zhCN.ts b/packages/thread-comment-ui/src/locales/zhCN.ts new file mode 100644 index 00000000000..1e1f1497ade --- /dev/null +++ b/packages/thread-comment-ui/src/locales/zhCN.ts @@ -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: '与我有关', + }, + }, + }, +}; diff --git a/packages/thread-comment-ui/src/plugin.ts b/packages/thread-comment-ui/src/plugin.ts new file mode 100644 index 00000000000..25942e132fa --- /dev/null +++ b/packages/thread-comment-ui/src/plugin.ts @@ -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); + }); + } +} diff --git a/packages/thread-comment-ui/src/services/thread-comment-panel.service.ts b/packages/thread-comment-ui/src/services/thread-comment-panel.service.ts new file mode 100644 index 00000000000..d397af4938d --- /dev/null +++ b/packages/thread-comment-ui/src/services/thread-comment-panel.service.ts @@ -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(false); + + private _activeCommentId: ActiveCommentInfo; + private _activeCommentId$ = new BehaviorSubject(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); + } +} diff --git a/packages/thread-comment-ui/src/types/const.ts b/packages/thread-comment-ui/src/types/const.ts new file mode 100644 index 00000000000..c87a58a7bd4 --- /dev/null +++ b/packages/thread-comment-ui/src/types/const.ts @@ -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'; diff --git a/packages/thread-comment-ui/src/types/interfaces/i-thread-comment-mention.ts b/packages/thread-comment-ui/src/types/interfaces/i-thread-comment-mention.ts new file mode 100644 index 00000000000..f44e37a6286 --- /dev/null +++ b/packages/thread-comment-ui/src/types/interfaces/i-thread-comment-mention.ts @@ -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; + trigger: string; + renderSuggestion?: MentionProps['renderSuggestion']; +} + +export interface IThreadCommentUIConfig { + mentions?: IThreadCommentMentionConfig[]; +} diff --git a/packages/thread-comment-ui/src/views/thread-comment-editor/index.module.less b/packages/thread-comment-ui/src/views/thread-comment-editor/index.module.less new file mode 100644 index 00000000000..78b94fc33c4 --- /dev/null +++ b/packages/thread-comment-ui/src/views/thread-comment-editor/index.module.less @@ -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; +} diff --git a/packages/thread-comment-ui/src/views/thread-comment-editor/index.tsx b/packages/thread-comment-ui/src/views/thread-comment-editor/index.tsx new file mode 100644 index 00000000000..cfd53f6d04f --- /dev/null +++ b/packages/thread-comment-ui/src/views/thread-comment-editor/index.tsx @@ -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; + onSave?: (comment: Pick) => 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 ( +
+ {icon ? : null} +
+ {mention.display ?? mention.id} +
+
+ ); +}; + +export const ThreadCommentEditor = forwardRef((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(PLUGIN_NAME)?.mentions ?? []; + const inputRef = useRef(null); + + useImperativeHandle(ref, () => ({ + reply(text) { + setLocalComment({ + ...comment, + text, + attachments: [], + }); + (inputRef.current as any)?.inputElement.focus(); + }, + })); + + return ( +
e.preventDefault()}> + { + 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.getMentions!(query) + .then((res) => res.map(transformMention)).then(callback) as any} + displayTransform={(id, label) => `@${label} `} + renderSuggestion={mention.renderSuggestion ?? defaultRenderSuggestion} + /> + ))} + + {editing + ? ( +
+ + +
+ ) + : null} +
+ ); +}); diff --git a/packages/thread-comment-ui/src/views/thread-comment-editor/util.ts b/packages/thread-comment-ui/src/views/thread-comment-editor/util.ts new file mode 100644 index 00000000000..16a3ebdf8a9 --- /dev/null +++ b/packages/thread-comment-ui/src/views/thread-comment-editor/util.ts @@ -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 = []; + + 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, +}); diff --git a/packages/thread-comment-ui/src/views/thread-comment-panel/index.module.less b/packages/thread-comment-ui/src/views/thread-comment-panel/index.module.less new file mode 100644 index 00000000000..acf10703ca5 --- /dev/null +++ b/packages/thread-comment-ui/src/views/thread-comment-panel/index.module.less @@ -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; + } + } +} diff --git a/packages/thread-comment-ui/src/views/thread-comment-panel/index.tsx b/packages/thread-comment-ui/src/views/thread-comment-panel/index.tsx new file mode 100644 index 00000000000..7692397ae5c --- /dev/null +++ b/packages/thread-comment-ui/src/views/thread-comment-panel/index.tsx @@ -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; + 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 ( +
+
+ 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'), + }, + ]} + /> +
+ {statuedComments?.map((comment) => ( + { + 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 + : ( +
+ {isFiltering ? + localeService.t('threadCommentUI.panel.filterEmpty') + : localeService.t('threadCommentUI.panel.empty')} + {isFiltering + ? ( + + ) + : ( + + )} +
+ )} +
+ ); +}; diff --git a/packages/thread-comment-ui/src/views/thread-comment-tree/index.module.less b/packages/thread-comment-ui/src/views/thread-comment-tree/index.module.less new file mode 100644 index 00000000000..12b73881a02 --- /dev/null +++ b/packages/thread-comment-ui/src/views/thread-comment-tree/index.module.less @@ -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)); +} diff --git a/packages/thread-comment-ui/src/views/thread-comment-tree/index.tsx b/packages/thread-comment-ui/src/views/thread-comment-tree/index.tsx new file mode 100644 index 00000000000..2ebfae67a9e --- /dev/null +++ b/packages/thread-comment-ui/src/views/thread-comment-tree/index.tsx @@ -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 ( +
setShowReply(false)} onMouseEnter={() => setShowReply(true)}> + +
+
+ {user?.name || ' '} +
+
+ {(isMock || resolved) + ? null + : ( + showReply + ? ( +
onReply(user)}> + +
+ ) + : null + )} + {isCommentBySelf && !isMock && !resolved + ? ( + + onEditingChange?.(true)}>{localeService.t('threadCommentUI.item.edit')} + {localeService.t('threadCommentUI.item.delete')} + + )} + > +
+ +
+
+ ) + : null} +
+
+
{item.dT}
+ {editing + ? ( + onEditingChange?.(false)} + autoFocus + onSave={({ text, attachments }) => { + onEditingChange?.(false); + commandService.executeCommand( + UpdateCommentCommand.id, + { + unitId, + subUnitId, + payload: { + commentId: item.id, + text, + attachments, + }, + } as IUpdateCommentCommandParams + ); + }} + /> + ) + : ( +
+ {transformDocument2TextNodes(item.text).map((item, i) => { + switch (item.type) { + case 'mention': + return ( + + @ + {item.content.label} + {' '} + + ); + default: + return item.content; + } + })} +
+ )} +
+ ); +}; + +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(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 ( +
+ {showHighlight ?
: null} +
+
+
+ {refStr || comments?.root.ref} + {' · '} + {getSubUnitName(comments?.root.subUnitId ?? subUnitId)} +
+ {comments + ? ( +
+
+ {resolved ? : } +
+ {currentUser?.userID === comments.root.personId + ? ( +
+ +
+ ) + : null} +
+ ) + : null} +
+
+ {renderComments.map( + (item) => ( + { + 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, + }, + }])); + }} + /> + ) + )} +
+ {showEdit && !editingId && !resolved + ? ( +
+ { + 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?.(); + } + }} + /> +
+ ) + : null} +
+ ); +}; diff --git a/packages/thread-comment-ui/src/vite-env.d.ts b/packages/thread-comment-ui/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/packages/thread-comment-ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/thread-comment-ui/tsconfig.json b/packages/thread-comment-ui/tsconfig.json new file mode 100644 index 00000000000..d676ad2a20d --- /dev/null +++ b/packages/thread-comment-ui/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@univerjs/shared/tsconfigs/base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src"] +} diff --git a/packages/thread-comment-ui/tsconfig.node.json b/packages/thread-comment-ui/tsconfig.node.json new file mode 100644 index 00000000000..e53dac88688 --- /dev/null +++ b/packages/thread-comment-ui/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@univerjs/shared/tsconfigs/node", + "include": ["vite.config.ts"] +} diff --git a/packages/thread-comment-ui/vite.config.ts b/packages/thread-comment-ui/vite.config.ts new file mode 100644 index 00000000000..925b530b4d5 --- /dev/null +++ b/packages/thread-comment-ui/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, + }, +}); diff --git a/packages/thread-comment/README.md b/packages/thread-comment/README.md new file mode 100644 index 00000000000..cd952e570f8 --- /dev/null +++ b/packages/thread-comment/README.md @@ -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 +``` diff --git a/packages/thread-comment/package.json b/packages/thread-comment/package.json new file mode 100644 index 00000000000..54c82c84383 --- /dev/null +++ b/packages/thread-comment/package.json @@ -0,0 +1,84 @@ +{ + "name": "@univerjs/thread-comment", + "version": "0.0.1", + "private": true, + "description": "", + "author": "DreamNum ", + "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" + } +} diff --git a/packages/thread-comment/src/commands/commands/comment.command.ts b/packages/thread-comment/src/commands/commands/comment.command.ts new file mode 100644 index 00000000000..707a0f0500e --- /dev/null +++ b/packages/thread-comment/src/commands/commands/comment.command.ts @@ -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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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; + }, +}; diff --git a/packages/thread-comment/src/commands/mutations/comment.mutation.ts b/packages/thread-comment/src/commands/mutations/comment.mutation.ts new file mode 100644 index 00000000000..1421ca8f477 --- /dev/null +++ b/packages/thread-comment/src/commands/mutations/comment.mutation.ts @@ -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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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); + }, +}; diff --git a/packages/thread-comment/src/controllers/tc-resource.controller.ts b/packages/thread-comment/src/controllers/tc-resource.controller.ts new file mode 100644 index 00000000000..04c489289a6 --- /dev/null +++ b/packages/thread-comment/src/controllers/tc-resource.controller.ts @@ -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; + +@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); + }); + }); + }, + }) + ); + } +} diff --git a/packages/thread-comment/src/index.ts b/packages/thread-comment/src/index.ts new file mode 100644 index 00000000000..2e984f22e86 --- /dev/null +++ b/packages/thread-comment/src/index.ts @@ -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'; diff --git a/packages/thread-comment/src/models/thread-comment.model.ts b/packages/thread-comment/src/models/thread-comment.model.ts new file mode 100644 index 00000000000..14cd2252c7e --- /dev/null +++ b/packages/thread-comment/src/models/thread-comment.model.ts @@ -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>> = {}; + private _commentsTreeMap: Map>> = new Map(); + private _commentUpdate$ = new Subject(); + private _commentsTreeMap$ = new BehaviorSubject>>>({}); + private _commentsMap$ = new BehaviorSubject>>>({}); + + 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>> = {}; + 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(); + 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; + } +} diff --git a/packages/thread-comment/src/plugin.ts b/packages/thread-comment/src/plugin.ts new file mode 100644 index 00000000000..8ad8bbae2ab --- /dev/null +++ b/packages/thread-comment/src/plugin.ts @@ -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); + }); + } +} diff --git a/packages/thread-comment/src/services/tc-datasource.service.ts b/packages/thread-comment/src/services/tc-datasource.service.ts new file mode 100644 index 00000000000..387f87c846c --- /dev/null +++ b/packages/thread-comment/src/services/tc-datasource.service.ts @@ -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>; + +export interface IThreadCommentDataSourceService { + addComment: (comment: IThreadComment) => Promise; + updateComment: (comment: IThreadComment) => Promise; + deleteComment: (commentId: string) => Promise; + deleteCommentBatch: (commentIds: string[]) => Promise; + loadFormSnapshot: (unitComments: Record) => Promise>; + saveToSnapshot: (unitComments: Record) => Record; +} + +/** + * 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) { + return unitComments as Record; + } + + saveToSnapshot(unitComments: Record) { + return unitComments; + } +} + +export const IThreadCommentDataSourceService = createIdentifier('univer.thread-comment.data-source-service'); diff --git a/packages/thread-comment/src/types/const/index.ts b/packages/thread-comment/src/types/const/index.ts new file mode 100644 index 00000000000..5658211ee14 --- /dev/null +++ b/packages/thread-comment/src/types/const/index.ts @@ -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'; diff --git a/packages/thread-comment/src/types/interfaces/i-thread-comment.ts b/packages/thread-comment/src/types/interfaces/i-thread-comment.ts new file mode 100644 index 00000000000..da79a1d5a01 --- /dev/null +++ b/packages/thread-comment/src/types/interfaces/i-thread-comment.ts @@ -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[]; +} diff --git a/packages/thread-comment/src/vite-env.d.ts b/packages/thread-comment/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/packages/thread-comment/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/thread-comment/tsconfig.json b/packages/thread-comment/tsconfig.json new file mode 100644 index 00000000000..d676ad2a20d --- /dev/null +++ b/packages/thread-comment/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@univerjs/shared/tsconfigs/base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src"] +} diff --git a/packages/thread-comment/tsconfig.node.json b/packages/thread-comment/tsconfig.node.json new file mode 100644 index 00000000000..e53dac88688 --- /dev/null +++ b/packages/thread-comment/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@univerjs/shared/tsconfigs/node", + "include": ["vite.config.ts"] +} diff --git a/packages/thread-comment/vite.config.ts b/packages/thread-comment/vite.config.ts new file mode 100644 index 00000000000..925b530b4d5 --- /dev/null +++ b/packages/thread-comment/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, + }, +}); diff --git a/packages/ui/src/services/zen-zone/desktop-zen-zone.service.ts b/packages/ui/src/services/zen-zone/desktop-zen-zone.service.ts index e1b08906af7..a12c9c1570e 100644 --- a/packages/ui/src/services/zen-zone/desktop-zen-zone.service.ts +++ b/packages/ui/src/services/zen-zone/desktop-zen-zone.service.ts @@ -25,6 +25,11 @@ import type { IZenZoneService } from './zen-zone.service'; export class DesktopZenZoneService implements IZenZoneService { readonly visible$ = new Subject(); readonly componentKey$ = new Subject(); + 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); } } diff --git a/packages/ui/src/services/zen-zone/zen-zone.service.ts b/packages/ui/src/services/zen-zone/zen-zone.service.ts index 0e314e0b0e8..f6a1c6e791b 100644 --- a/packages/ui/src/services/zen-zone/zen-zone.service.ts +++ b/packages/ui/src/services/zen-zone/zen-zone.service.ts @@ -24,6 +24,8 @@ export interface IZenZoneService { readonly visible$: Subject; readonly componentKey$: Subject; + readonly visible: boolean; + set(key: string, component: any): IDisposable; open(): void; diff --git a/packages/ui/src/views/App.tsx b/packages/ui/src/views/App.tsx index 3382c1cb64c..7ce059d75a5 100644 --- a/packages/ui/src/views/App.tsx +++ b/packages/ui/src/views/App.tsx @@ -150,6 +150,7 @@ export function App(props: IUniverAppProps) { data-range-selector onContextMenu={(e) => e.preventDefault()} > +
@@ -167,11 +168,8 @@ export function App(props: IUniverAppProps) { )} - - - {contextMenu && } diff --git a/packages/ui/src/views/app.module.less b/packages/ui/src/views/app.module.less index 9cc4e51faf1..b71c767e9f3 100644 --- a/packages/ui/src/views/app.module.less +++ b/packages/ui/src/views/app.module.less @@ -47,6 +47,7 @@ &-canvas { position: relative; + overflow: hidden; } &-left-sidebar { diff --git a/packages/ui/src/views/components/popup/CanvasPopup.tsx b/packages/ui/src/views/components/popup/CanvasPopup.tsx index dbbf89c8769..3eedd656d17 100644 --- a/packages/ui/src/views/components/popup/CanvasPopup.tsx +++ b/packages/ui/src/views/components/popup/CanvasPopup.tsx @@ -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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9904418b0c4..3390fd9693a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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,15 +13528,15 @@ 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)': + '@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/icons@0.1.45(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@univerjs/protocol@0.1.23(@grpc/grpc-js@1.10.4)(rxjs@7.8.1)': dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@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: @@ -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