Skip to content

Commit f5a7fde

Browse files
committed
✨(frontend) add comment side panel
Add comment side panel to the right panel. We will be able to manage the threads of the document and see the content of the comments in a side panel. The advantage of this approach is that we will be able to: - see the comments that have been removed because of deleted text - see the resolved comments - see the unresolved comments
1 parent bad256b commit f5a7fde

15 files changed

Lines changed: 359 additions & 72 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to
99
### Added
1010

1111
- ✨(backend) support creating subdoc from file #1987
12+
- ✨(frontend) comment side panel #2279
1213
- ✨(buildpack) add PaaS deployment support, tested with Scalingo #2293
1314

1415
### Fixed
Lines changed: 11 additions & 5 deletions
Loading
Lines changed: 5 additions & 2 deletions
Loading
Lines changed: 5 additions & 2 deletions
Loading

src/frontend/apps/impress/src/components/modal/ButtonCloseModal.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Button, type ButtonProps } from '@gouvfr-lasuite/cunningham-react';
2-
import React from 'react';
32

4-
import { Icon } from '@/components';
3+
import CloseIcon from '@/assets/icons/ui-kit/x-mark.svg';
54

65
export const ButtonCloseModal = (props: ButtonProps) => {
76
return (
@@ -10,14 +9,7 @@ export const ButtonCloseModal = (props: ButtonProps) => {
109
size="small"
1110
color="neutral"
1211
variant="tertiary"
13-
icon={
14-
<Icon
15-
iconName="close"
16-
className="material-icons-filled"
17-
$size="24px!important"
18-
$color="var(--c--contextuals--content--semantic--neutral--secondary)"
19-
/>
20-
}
12+
icon={<CloseIcon width="24" height="24" aria-hidden="true" />}
2113
{...props}
2214
/>
2315
);

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ import '@blocknote/core/fonts/inter.css';
1111
import * as localesBN from '@blocknote/core/locales';
1212
import { BlockNoteView } from '@blocknote/mantine';
1313
import '@blocknote/mantine/style.css';
14-
import { useCreateBlockNote } from '@blocknote/react';
14+
import {
15+
FloatingComposerController,
16+
FloatingThreadController,
17+
ThreadsSidebar,
18+
useCreateBlockNote,
19+
} from '@blocknote/react';
1520
import { HocuspocusProvider } from '@hocuspocus/provider';
1621
import { useEffect, useMemo, useRef } from 'react';
22+
import { createPortal } from 'react-dom';
1723
import { useTranslation } from 'react-i18next';
1824
import type { Awareness } from 'y-protocols/awareness';
1925
import * as Y from 'yjs';
@@ -41,7 +47,11 @@ import { randomColor, sanitizeColor } from '../utils';
4147
import BlockNoteAI from './AI';
4248
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
4349
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
44-
import { DocsCommentsStyle, useComments } from './comments/';
50+
import {
51+
DocsCommentsStyle,
52+
useCommentSidebarStore,
53+
useComments,
54+
} from './comments/';
4555
import { CalloutBlock, PdfBlock, UploadLoaderBlock } from './custom-blocks';
4656
const AIMenu = BlockNoteAI?.AIMenu;
4757
const AIMenuController = BlockNoteAI?.AIMenuController;
@@ -124,6 +134,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
124134
user,
125135
);
126136

137+
const {
138+
threadsSidebarTarget,
139+
filter: threadsSidebarFilter,
140+
isSideBarOpen,
141+
} = useCommentSidebarStore();
142+
127143
const currentUserAvatarUrl = useMemo(() => {
128144
if (canSeeComment) {
129145
return avatarUrlFromName(collabName, themeTokens?.font?.families?.base);
@@ -275,14 +291,37 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
275291
formattingToolbar={false}
276292
slashMenu={false}
277293
theme="light"
278-
comments={showComments}
294+
comments={false}
279295
aria-label={t('Document editor')}
280296
>
281297
{aiBlockNoteAllowed && AIMenuController && AIMenu && (
282298
<AIMenuController aiMenu={AIMenu} />
283299
)}
284300
<BlockNoteSuggestionMenu aiAllowed={aiBlockNoteAllowed} />
285301
<BlockNoteToolbar aiAllowed={aiBlockNoteAllowed} />
302+
{showComments && <FloatingComposerController />}
303+
{showComments && !isSideBarOpen && (
304+
<FloatingThreadController
305+
floatingUIOptions={{
306+
useDismissProps: {
307+
/**
308+
* When the sidebar is open, prevent the floating thread popup
309+
* from being dismissed when clicking inside the sidebar.
310+
* Without this, useDismiss fires selectThread(undefined) on any
311+
* click outside the popup — including clicks on the reply input
312+
* in the sidebar — which causes the thread to be deselected.
313+
*/
314+
outsidePress: (event) =>
315+
!threadsSidebarTarget?.contains(event.target as Node),
316+
},
317+
}}
318+
/>
319+
)}
320+
{threadsSidebarTarget &&
321+
createPortal(
322+
<ThreadsSidebar filter={threadsSidebarFilter} sort="position" />,
323+
threadsSidebarTarget,
324+
)}
286325
</BlockNoteView>
287326
</Box>
288327
);

src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const DocEditorContainer = ({
4545
$width="100%"
4646
$flex="1"
4747
className={DOCS_EDITOR_CLASS}
48+
$margin={{ horizontal: 'auto' }}
4849
>
4950
<Box
5051
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { Button, Tooltip } from '@gouvfr-lasuite/cunningham-react';
2+
import { DropdownMenu } from '@gouvfr-lasuite/ui-kit';
3+
import { useEffect, useRef, useState } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
import { css } from 'styled-components';
6+
7+
import CommentsIcon from '@/assets/icons/ui-kit/bubble-text.svg';
8+
import SortingResolvedSVG from '@/assets/icons/ui-kit/filter-notification.svg';
9+
import SortingOpenSVG from '@/assets/icons/ui-kit/filter_list.svg';
10+
import { Box, ButtonCloseModal, Text } from '@/components/';
11+
import { useRightPanelStore } from '@/features/right-panel/components/useRightPanelStore';
12+
13+
import { useCommentSidebarStore } from './useCommentSidebarStore';
14+
15+
interface CommentSideBarProps {
16+
onClose: () => void;
17+
}
18+
19+
export const CommentSideBar = ({ onClose }: CommentSideBarProps) => {
20+
const { t } = useTranslation();
21+
const { setThreadsSidebarTarget, filter, setFilter } =
22+
useCommentSidebarStore();
23+
const portalRef = useRef<HTMLDivElement>(null);
24+
const [open, setOpen] = useState(false);
25+
26+
useEffect(() => {
27+
if (portalRef.current) {
28+
setThreadsSidebarTarget(portalRef.current);
29+
}
30+
return () => {
31+
setThreadsSidebarTarget(null);
32+
};
33+
}, [setThreadsSidebarTarget]);
34+
35+
return (
36+
<Box $height="inherit">
37+
<Box
38+
$padding={{ vertical: 'base', horizontal: 'sm' }}
39+
$css={css`
40+
border-bottom: 1px solid
41+
var(--c--contextuals--border--surface--primary);
42+
`}
43+
>
44+
<Box $direction="row" $align="center" $justify="space-between">
45+
<Box $direction="row" $align="center" $gap="2xs">
46+
<Text $weight="bold">{t('Comments')}</Text>
47+
48+
<DropdownMenu
49+
options={[
50+
{
51+
label: t('Open'),
52+
callback: () => setFilter('open'),
53+
isChecked: filter === 'open',
54+
},
55+
{
56+
label: t('Resolved'),
57+
callback: () => setFilter('resolved'),
58+
isChecked: filter === 'resolved',
59+
},
60+
]}
61+
isOpen={open}
62+
shouldCloseOnInteractOutside={() => true}
63+
onOpenChange={setOpen}
64+
>
65+
<Tooltip content={t('Filter comments')} placement="top">
66+
<Button
67+
aria-label={t('Filter comments')}
68+
size="nano"
69+
icon={
70+
filter === 'open' ? (
71+
<SortingOpenSVG
72+
width={18}
73+
height={18}
74+
aria-hidden="true"
75+
/>
76+
) : (
77+
<SortingResolvedSVG
78+
width={18}
79+
height={18}
80+
aria-hidden="true"
81+
/>
82+
)
83+
}
84+
color={filter === 'open' ? 'neutral' : 'brand'}
85+
variant={filter === 'open' ? 'tertiary' : 'secondary'}
86+
onClick={(e) => {
87+
e.stopPropagation();
88+
e.preventDefault();
89+
setOpen((o) => !o);
90+
}}
91+
tabIndex={-1}
92+
/>
93+
</Tooltip>
94+
</DropdownMenu>
95+
</Box>
96+
<ButtonCloseModal
97+
aria-label={t('Close the comments sidebar')}
98+
onClick={onClose}
99+
/>
100+
</Box>
101+
</Box>
102+
<div
103+
ref={portalRef}
104+
className="--docs--comments-sidebar bn-root bn-mantine"
105+
data-mantine-color-scheme="light"
106+
/>
107+
</Box>
108+
);
109+
};
110+
111+
export const CommentSideBarButton = () => {
112+
const { t } = useTranslation();
113+
const { isPanelOpen, togglePanel } = useRightPanelStore();
114+
const { setIsSideBarOpen } = useCommentSidebarStore();
115+
116+
useEffect(() => {
117+
setIsSideBarOpen(isPanelOpen);
118+
}, [isPanelOpen, setIsSideBarOpen]);
119+
120+
const ariaLabel = isPanelOpen
121+
? t('Hide the comments sidebar')
122+
: t('Show the comments sidebar');
123+
124+
return (
125+
<Button
126+
size="small"
127+
onClick={togglePanel}
128+
aria-label={ariaLabel}
129+
aria-expanded={isPanelOpen}
130+
color="neutral"
131+
variant={isPanelOpen ? 'secondary' : 'tertiary'}
132+
icon={<CommentsIcon width={24} height={24} aria-hidden="true" />}
133+
></Button>
134+
);
135+
};

src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentToolbarButton.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,6 @@ export const CommentToolbarButton = () => {
4747
return !!selectedBlocks.find((block) => block.content !== undefined);
4848
}, [selectedBlocks]);
4949

50-
const focusOnInputThread = () => {
51-
// Use setTimeout to ensure the DOM has been updated with the new comment
52-
setTimeout(() => {
53-
const threadElement = document.querySelector<HTMLElement>(
54-
'.bn-thread .bn-editor',
55-
);
56-
threadElement?.focus();
57-
}, 400);
58-
};
59-
6050
if (
6151
!comments ||
6252
!show ||
@@ -74,7 +64,6 @@ export const CommentToolbarButton = () => {
7464
onClick={() => {
7565
comments.startPendingComment();
7666
store.setState(false);
77-
focusOnInputThread();
7867
}}
7968
aria-haspopup="dialog"
8069
data-test="comment-toolbar-button"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './CommentToolbarButton';
22
export * from './styles';
3+
export * from './useCommentSidebarStore';
34
export * from './useComments';

0 commit comments

Comments
 (0)