mirror of
https://github.com/suitenumerique/docs.git
synced 2026-08-17 21:25:43 +02:00
✨(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
This commit is contained in:
@@ -9,6 +9,7 @@ and this project adheres to
|
||||
### Added
|
||||
|
||||
- ✨(backend) support creating subdoc from file #1987
|
||||
- ✨(frontend) comment side panel #2279
|
||||
- ✨(buildpack) add PaaS deployment support, tested with Scalingo #2293
|
||||
- 🔧(backend) allow configuring settings OIDC_OP_USER_ENDPOINT_FORMAT
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
getOtherBrowserName,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { getEditor, writeInEditor } from './utils-editor';
|
||||
import {
|
||||
getEditor,
|
||||
tryFocusEditorContent,
|
||||
writeInEditor,
|
||||
} from './utils-editor';
|
||||
import {
|
||||
addNewMember,
|
||||
connectOtherUserToDoc,
|
||||
@@ -430,3 +434,117 @@ test.describe('Doc Comments mobile', () => {
|
||||
await expect(thread.getByText('This is a comment').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Comments Side Panel', () => {
|
||||
test('it checks comments side bar interaction', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'comment-doc-panel', browserName, 1);
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Show the comments sidebar' }),
|
||||
).toBeHidden();
|
||||
|
||||
// Create comment thread
|
||||
const editor = await writeInEditor({ page, text: 'Hello World' });
|
||||
await editor.getByText('Hello').selectText();
|
||||
await page.getByRole('button', { name: 'Add comment' }).click();
|
||||
|
||||
const thread = page.locator('.bn-thread');
|
||||
await thread.getByRole('paragraph').first().fill('This is a comment');
|
||||
await thread.locator('[data-test="save"]').click();
|
||||
|
||||
// Open comment side panel and check comment is visible in side panel
|
||||
await page
|
||||
.getByRole('button', { name: 'Show the comments sidebar' })
|
||||
.click();
|
||||
|
||||
const elCommentsSidePanel = page.getByLabel('Comments side panel');
|
||||
await expect(
|
||||
elCommentsSidePanel.getByText('This is a comment'),
|
||||
).toBeVisible();
|
||||
|
||||
// Click on comment in side panel and check it scrolls to the comment in the doc
|
||||
await tryFocusEditorContent({ page });
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
await writeInEditor({
|
||||
page,
|
||||
text: 'New paragraph',
|
||||
});
|
||||
|
||||
await expect(editor.getByText('New paragraph')).toBeInViewport();
|
||||
await elCommentsSidePanel.getByText('This is a comment').click();
|
||||
|
||||
await expect(editor.getByText('Hello World')).toBeVisible();
|
||||
await expect(editor.getByText('New paragraph')).not.toBeInViewport();
|
||||
await expect(editor.getByText('Hello World')).toHaveClass(
|
||||
'bn-thread-mark-selected',
|
||||
);
|
||||
|
||||
// Add a comment in the side panel
|
||||
await elCommentsSidePanel
|
||||
.locator(
|
||||
'.bn-editor[contenteditable="true"] div[data-content-type="paragraph"]',
|
||||
)
|
||||
.first()
|
||||
.fill('This is another comment');
|
||||
await elCommentsSidePanel.locator('[data-test="save"]').click();
|
||||
await expect(
|
||||
elCommentsSidePanel
|
||||
.locator(
|
||||
'.bn-editor[contenteditable="false"] div[data-content-type="paragraph"]',
|
||||
)
|
||||
.getByText('This is another comment'),
|
||||
).toBeVisible();
|
||||
|
||||
// Close the side panel and check the comments in the doc
|
||||
await page
|
||||
.getByRole('button', { name: 'Close the comments sidebar' })
|
||||
.click();
|
||||
await expect(elCommentsSidePanel).toBeHidden();
|
||||
await editor.getByText('Hello World').click();
|
||||
await expect(thread.getByText('This is another comment')).toBeVisible();
|
||||
|
||||
// Resolve the comment and check it disappears from the side panel and the doc
|
||||
await thread.getByText('This is a comment').first().hover();
|
||||
await thread.locator('[data-test="resolve"]').click();
|
||||
await expect(thread).toBeHidden();
|
||||
await page
|
||||
.getByRole('button', { name: 'Show the comments sidebar' })
|
||||
.click();
|
||||
await expect(
|
||||
elCommentsSidePanel.getByText('This is a comment'),
|
||||
).toBeHidden();
|
||||
|
||||
// Goto resolved part, the comment should be visible in the side panel
|
||||
await elCommentsSidePanel
|
||||
.getByRole('button', { name: 'Filter comments' })
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Resolved' }).click();
|
||||
await elCommentsSidePanel.getByText('This is a comment').click();
|
||||
await expect(editor.getByText('Hello World')).toHaveClass(
|
||||
'bn-thread-mark-selected',
|
||||
);
|
||||
|
||||
// Unresolve the comment and check it does not appears in the side panel resolved part
|
||||
await thread.getByText('This is a comment').first().hover();
|
||||
await thread.locator('[data-test="re-open"]').click();
|
||||
await expect(
|
||||
elCommentsSidePanel.getByText('This is a comment'),
|
||||
).toBeHidden();
|
||||
|
||||
// It should be back in the side panel and in the doc
|
||||
await page.getByRole('button', { name: 'Filter comments' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Open' }).click();
|
||||
await expect(
|
||||
elCommentsSidePanel.getByText('This is a comment'),
|
||||
).toBeVisible();
|
||||
await elCommentsSidePanel.getByText('This is a comment').click();
|
||||
await expect(editor.getByText('Hello World')).toHaveClass(
|
||||
'bn-thread-mark-selected',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,24 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
export const getEditor = async ({ page }: { page: Page }) => {
|
||||
const editor = page.locator('.ProseMirror');
|
||||
const editor = page.locator('.--docs--editor-container .ProseMirror');
|
||||
await editor.click();
|
||||
return editor;
|
||||
};
|
||||
|
||||
export const tryFocusEditorContent = async ({ page }: { page: Page }) => {
|
||||
const editor = await getEditor({ page });
|
||||
if (
|
||||
(await editor.locator('.bn-trailing-block.ProseMirror-widget').count()) > 0
|
||||
) {
|
||||
await editor.locator('.bn-trailing-block.ProseMirror-widget').click();
|
||||
} else {
|
||||
await editor.click();
|
||||
}
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
export const openSuggestionMenu = async ({
|
||||
page,
|
||||
suggestion,
|
||||
@@ -15,14 +28,7 @@ export const openSuggestionMenu = async ({
|
||||
page: Page;
|
||||
suggestion?: string;
|
||||
}) => {
|
||||
const editor = await getEditor({ page });
|
||||
if (
|
||||
(await editor.locator('.bn-trailing-block.ProseMirror-widget').count()) > 0
|
||||
) {
|
||||
await editor.locator('.bn-trailing-block.ProseMirror-widget').click();
|
||||
} else {
|
||||
await editor.click();
|
||||
}
|
||||
const editor = await tryFocusEditorContent({ page });
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type('/');
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 9C18.1667 9 17.4583 8.70833 16.875 8.125C16.2917 7.54167 16 6.83333 16 6C16 5.16667 16.2917 4.45833 16.875 3.875C17.4583 3.29167 18.1667 3 19 3C19.8333 3 20.5417 3.29167 21.125 3.875C21.7083 4.45833 22 5.16667 22 6C22 6.83333 21.7083 7.54167 21.125 8.125C20.5417 8.70833 19.8333 9 19 9Z" fill="#222631"/>
|
||||
<path d="M14.4042 8C14.1363 7.38254 14 6.71027 14 6H3V8H14.4042Z" fill="#222631"/>
|
||||
<path d="M10 16V18H14V16H10Z" fill="#222631"/>
|
||||
<path d="M6 11V13H18V11H6Z" fill="#222631"/>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19 9C18.1667 9 17.4583 8.70833 16.875 8.125C16.2917 7.54167 16 6.83333 16 6C16 5.16667 16.2917 4.45833 16.875 3.875C17.4583 3.29167 18.1667 3 19 3C19.8333 3 20.5417 3.29167 21.125 3.875C21.7083 4.45833 22 5.16667 22 6C22 6.83333 21.7083 7.54167 21.125 8.125C20.5417 8.70833 19.8333 9 19 9Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M14.4042 8C14.1363 7.38254 14 6.71027 14 6H3V8H14.4042Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M10 16V18H14V16H10Z" fill="currentColor" />
|
||||
<path d="M6 11V13H18V11H6Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 596 B After Width: | Height: | Size: 625 B |
@@ -1,3 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 18V16H14V18H10ZM6 13V11H18V13H6ZM3 8V6H21V8H3Z" fill="#222631"/>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M10 18V16H14V18H10ZM6 13V11H18V13H6ZM3 8V6H21V8H3Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 176 B |
@@ -1,3 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.32391 19.677C4.17816 19.5379 4.08209 19.3723 4.03571 19.1801C3.98934 18.988 3.98934 18.7959 4.03571 18.6037C4.08872 18.4116 4.18478 18.2493 4.32391 18.1168L10.4258 11.995L4.32391 5.88323C4.18478 5.75072 4.09203 5.58841 4.04565 5.39627C3.99927 5.20414 3.99927 5.01201 4.04565 4.81988C4.09203 4.62774 4.18478 4.46211 4.32391 4.32298C4.46967 4.17723 4.63861 4.08116 4.83074 4.03478C5.02288 3.98841 5.21501 3.98841 5.40714 4.03478C5.59927 4.08116 5.76491 4.17391 5.90404 4.31304L12.0158 10.4248L18.1177 4.31304C18.2568 4.16729 18.4225 4.07453 18.6146 4.03478C18.8067 3.98841 18.9955 3.98841 19.1811 4.03478C19.3732 4.08116 19.5454 4.17723 19.6978 4.32298C19.837 4.46211 19.9297 4.62774 19.9761 4.81988C20.0291 5.01201 20.0291 5.20414 19.9761 5.39627C19.9297 5.58178 19.837 5.74741 19.6978 5.89317L13.596 11.995L19.6978 18.1068C19.837 18.2526 19.9297 18.4215 19.9761 18.6137C20.0225 18.7992 20.0225 18.988 19.9761 19.1801C19.9297 19.3723 19.837 19.5379 19.6978 19.677C19.5521 19.8228 19.3831 19.9188 19.191 19.9652C18.9989 20.0116 18.8067 20.0116 18.6146 19.9652C18.4225 19.9188 18.2568 19.8261 18.1177 19.687L12.0158 13.5752L5.90404 19.687C5.76491 19.8261 5.59927 19.9188 5.40714 19.9652C5.22163 20.0116 5.0295 20.0116 4.83074 19.9652C4.63861 19.9188 4.46967 19.8228 4.32391 19.677Z" fill="#222631"/>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.32391 19.677C4.17816 19.5379 4.08209 19.3723 4.03571 19.1801C3.98934 18.988 3.98934 18.7959 4.03571 18.6037C4.08872 18.4116 4.18478 18.2493 4.32391 18.1168L10.4258 11.995L4.32391 5.88323C4.18478 5.75072 4.09203 5.58841 4.04565 5.39627C3.99927 5.20414 3.99927 5.01201 4.04565 4.81988C4.09203 4.62774 4.18478 4.46211 4.32391 4.32298C4.46967 4.17723 4.63861 4.08116 4.83074 4.03478C5.02288 3.98841 5.21501 3.98841 5.40714 4.03478C5.59927 4.08116 5.76491 4.17391 5.90404 4.31304L12.0158 10.4248L18.1177 4.31304C18.2568 4.16729 18.4225 4.07453 18.6146 4.03478C18.8067 3.98841 18.9955 3.98841 19.1811 4.03478C19.3732 4.08116 19.5454 4.17723 19.6978 4.32298C19.837 4.46211 19.9297 4.62774 19.9761 4.81988C20.0291 5.01201 20.0291 5.20414 19.9761 5.39627C19.9297 5.58178 19.837 5.74741 19.6978 5.89317L13.596 11.995L19.6978 18.1068C19.837 18.2526 19.9297 18.4215 19.9761 18.6137C20.0225 18.7992 20.0225 18.988 19.9761 19.1801C19.9297 19.3723 19.837 19.5379 19.6978 19.677C19.5521 19.8228 19.3831 19.9188 19.191 19.9652C18.9989 20.0116 18.8067 20.0116 18.6146 19.9652C18.4225 19.9188 18.2568 19.8261 18.1177 19.687L12.0158 13.5752L5.90404 19.687C5.76491 19.8261 5.59927 19.9188 5.40714 19.9652C5.22163 20.0116 5.0295 20.0116 4.83074 19.9652C4.63861 19.9188 4.46967 19.8228 4.32391 19.677Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,7 +1,6 @@
|
||||
import { Button, type ButtonProps } from '@gouvfr-lasuite/cunningham-react';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@/components';
|
||||
import CloseIcon from '@/assets/icons/ui-kit/x-mark.svg';
|
||||
|
||||
export const ButtonCloseModal = (props: ButtonProps) => {
|
||||
return (
|
||||
@@ -10,14 +9,7 @@ export const ButtonCloseModal = (props: ButtonProps) => {
|
||||
size="small"
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
icon={
|
||||
<Icon
|
||||
iconName="close"
|
||||
className="material-icons-filled"
|
||||
$size="24px!important"
|
||||
$color="var(--c--contextuals--content--semantic--neutral--secondary)"
|
||||
/>
|
||||
}
|
||||
icon={<CloseIcon width="24" height="24" aria-hidden="true" />}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
+36
-7
@@ -11,9 +11,15 @@ import '@blocknote/core/fonts/inter.css';
|
||||
import * as localesBN from '@blocknote/core/locales';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import { useCreateBlockNote } from '@blocknote/react';
|
||||
import {
|
||||
FloatingComposerController,
|
||||
FloatingThreadController,
|
||||
ThreadsSidebar,
|
||||
useCreateBlockNote,
|
||||
} from '@blocknote/react';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Awareness } from 'y-protocols/awareness';
|
||||
import * as Y from 'yjs';
|
||||
@@ -41,7 +47,11 @@ import { randomColor, sanitizeColor } from '../utils';
|
||||
import BlockNoteAI from './AI';
|
||||
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
|
||||
import { DocsCommentsStyle, useComments } from './comments/';
|
||||
import {
|
||||
DocsCommentsStyle,
|
||||
useCommentSidebarStore,
|
||||
useComments,
|
||||
} from './comments/';
|
||||
import { CalloutBlock, PdfBlock, UploadLoaderBlock } from './custom-blocks';
|
||||
const AIMenu = BlockNoteAI?.AIMenu;
|
||||
const AIMenuController = BlockNoteAI?.AIMenuController;
|
||||
@@ -82,11 +92,8 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const { setEditor } = useEditorStore();
|
||||
const { themeTokens } = useCunninghamTheme();
|
||||
const refEditorContainer = useRef<HTMLDivElement>(null);
|
||||
const canSeeComment = doc.abilities.comment;
|
||||
// Determine if comments should be visible in the UI
|
||||
const showComments = canSeeComment;
|
||||
|
||||
useSaveDoc(doc.id, provider.document);
|
||||
|
||||
const { i18n, t } = useTranslation();
|
||||
const langLocalesBN =
|
||||
!i18n.resolvedLanguage || !(i18n.resolvedLanguage in localesBN)
|
||||
@@ -118,12 +125,22 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const cursorName = collabName || t('Anonymous');
|
||||
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
|
||||
|
||||
// Comments
|
||||
const canSeeComment = doc.abilities.comment;
|
||||
const showComments = canSeeComment; // Determine if comments should be visible in the UI
|
||||
const { resolveUsers, threadStore } = useComments(
|
||||
doc.id,
|
||||
canSeeComment,
|
||||
user,
|
||||
);
|
||||
|
||||
// Comment sidebar
|
||||
const {
|
||||
threadsSidebarTarget,
|
||||
filter: threadsSidebarFilter,
|
||||
isSideBarOpen,
|
||||
} = useCommentSidebarStore();
|
||||
|
||||
const currentUserAvatarUrl = useMemo(() => {
|
||||
if (canSeeComment) {
|
||||
return avatarUrlFromName(collabName, themeTokens?.font?.families?.base);
|
||||
@@ -275,14 +292,26 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
formattingToolbar={false}
|
||||
slashMenu={false}
|
||||
theme="light"
|
||||
comments={showComments}
|
||||
comments={false}
|
||||
aria-label={t('Document editor')}
|
||||
// To not clipped the floating part in the editor area
|
||||
portalElements={{ default: null }}
|
||||
>
|
||||
{aiBlockNoteAllowed && AIMenuController && AIMenu && (
|
||||
<AIMenuController aiMenu={AIMenu} />
|
||||
)}
|
||||
<BlockNoteSuggestionMenu aiAllowed={aiBlockNoteAllowed} />
|
||||
<BlockNoteToolbar aiAllowed={aiBlockNoteAllowed} />
|
||||
{showComments && <FloatingComposerController />}
|
||||
{showComments && !isSideBarOpen && <FloatingThreadController />}
|
||||
{threadsSidebarTarget &&
|
||||
createPortal(
|
||||
<ThreadsSidebar
|
||||
filter={threadsSidebarFilter}
|
||||
sort="recent-activity"
|
||||
/>,
|
||||
threadsSidebarTarget,
|
||||
)}
|
||||
</BlockNoteView>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -45,6 +45,7 @@ export const DocEditorContainer = ({
|
||||
$width="100%"
|
||||
$flex="1"
|
||||
className={DOCS_EDITOR_CLASS}
|
||||
$margin={{ horizontal: 'auto' }}
|
||||
>
|
||||
<Box
|
||||
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import { Button, Tooltip } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { DropdownMenu } from '@gouvfr-lasuite/ui-kit';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import CommentsIcon from '@/assets/icons/ui-kit/bubble-text.svg';
|
||||
import SortingResolvedSVG from '@/assets/icons/ui-kit/filter-notification.svg';
|
||||
import SortingOpenSVG from '@/assets/icons/ui-kit/filter_list.svg';
|
||||
import { Box, ButtonCloseModal, Text } from '@/components/';
|
||||
import { useRightPanelStore } from '@/features/right-panel/components/useRightPanelStore';
|
||||
|
||||
import { useCommentSidebarStore } from './useCommentSidebarStore';
|
||||
|
||||
interface CommentSideBarProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CommentSideBar = ({ onClose }: CommentSideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setThreadsSidebarTarget, filter, setFilter } =
|
||||
useCommentSidebarStore();
|
||||
const portalRef = useRef<HTMLDivElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (portalRef.current) {
|
||||
setThreadsSidebarTarget(portalRef.current);
|
||||
}
|
||||
return () => {
|
||||
setThreadsSidebarTarget(null);
|
||||
};
|
||||
}, [setThreadsSidebarTarget]);
|
||||
|
||||
return (
|
||||
<Box $height="inherit">
|
||||
<Box
|
||||
$padding={{ vertical: 'base', horizontal: 'sm' }}
|
||||
$css={css`
|
||||
border-bottom: 1px solid
|
||||
var(--c--contextuals--border--surface--primary);
|
||||
`}
|
||||
>
|
||||
<Box $direction="row" $align="center" $justify="space-between">
|
||||
<Box $direction="row" $align="center" $gap="2xs">
|
||||
<Text $weight="bold">{t('Comments')}</Text>
|
||||
|
||||
<DropdownMenu
|
||||
options={[
|
||||
{
|
||||
label: t('Open'),
|
||||
callback: () => setFilter('open'),
|
||||
isChecked: filter === 'open',
|
||||
},
|
||||
{
|
||||
label: t('Resolved'),
|
||||
callback: () => setFilter('resolved'),
|
||||
isChecked: filter === 'resolved',
|
||||
},
|
||||
]}
|
||||
isOpen={open}
|
||||
shouldCloseOnInteractOutside={() => true}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<Tooltip content={t('Filter comments')} placement="bottom">
|
||||
<Button
|
||||
aria-label={t('Filter comments')}
|
||||
size="nano"
|
||||
icon={
|
||||
filter === 'open' ? (
|
||||
<SortingOpenSVG
|
||||
width={18}
|
||||
height={18}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<SortingResolvedSVG
|
||||
width={18}
|
||||
height={18}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
}
|
||||
color={filter === 'open' ? 'neutral' : 'brand'}
|
||||
variant={filter === 'open' ? 'tertiary' : 'secondary'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setOpen((o) => !o);
|
||||
}}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Tooltip>
|
||||
</DropdownMenu>
|
||||
</Box>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the comments sidebar')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<div
|
||||
ref={portalRef}
|
||||
className="--docs--comments-sidebar bn-root bn-mantine"
|
||||
data-mantine-color-scheme="light"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommentSideBarButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isPanelOpen, togglePanel } = useRightPanelStore();
|
||||
const { setIsSideBarOpen } = useCommentSidebarStore();
|
||||
|
||||
useEffect(() => {
|
||||
setIsSideBarOpen(isPanelOpen);
|
||||
}, [isPanelOpen, setIsSideBarOpen]);
|
||||
|
||||
const ariaLabel = isPanelOpen
|
||||
? t('Hide the comments sidebar')
|
||||
: t('Show the comments sidebar');
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={togglePanel}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={isPanelOpen}
|
||||
color="neutral"
|
||||
variant={isPanelOpen ? 'secondary' : 'tertiary'}
|
||||
icon={<CommentsIcon width={24} height={24} aria-hidden="true" />}
|
||||
></Button>
|
||||
);
|
||||
};
|
||||
-11
@@ -47,16 +47,6 @@ export const CommentToolbarButton = () => {
|
||||
return !!selectedBlocks.find((block) => block.content !== undefined);
|
||||
}, [selectedBlocks]);
|
||||
|
||||
const focusOnInputThread = () => {
|
||||
// Use setTimeout to ensure the DOM has been updated with the new comment
|
||||
setTimeout(() => {
|
||||
const threadElement = document.querySelector<HTMLElement>(
|
||||
'.bn-thread .bn-editor',
|
||||
);
|
||||
threadElement?.focus();
|
||||
}, 400);
|
||||
};
|
||||
|
||||
if (
|
||||
!comments ||
|
||||
!show ||
|
||||
@@ -74,7 +64,6 @@ export const CommentToolbarButton = () => {
|
||||
onClick={() => {
|
||||
comments.startPendingComment();
|
||||
store.setState(false);
|
||||
focusOnInputThread();
|
||||
}}
|
||||
aria-haspopup="dialog"
|
||||
data-test="comment-toolbar-button"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './CommentToolbarButton';
|
||||
export * from './styles';
|
||||
export * from './useCommentSidebarStore';
|
||||
export * from './useComments';
|
||||
|
||||
+123
-13
@@ -5,7 +5,9 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
currentUserAvatarUrl?: string;
|
||||
}>`
|
||||
.--docs--main-editor.bn-root,
|
||||
.--docs--main-editor.bn-root .ProseMirror {
|
||||
.--docs--main-editor.bn-root .ProseMirror,
|
||||
.--docs--comments-sidebar.bn-root,
|
||||
.--docs--comments-sidebar.bn-root .ProseMirror {
|
||||
// Comments marks in the editor
|
||||
.bn-editor {
|
||||
// Resets blocknote comments styles
|
||||
@@ -38,6 +40,22 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.bn-thread-mark[data-orphan='true']:has(> .bn-thread-mark-selected) {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--c--contextuals--background--palette--blue-1--tertiary) 40%,
|
||||
transparent
|
||||
);
|
||||
border-bottom: 2px solid
|
||||
var(--c--contextuals--background--palette--blue-1--secondary);
|
||||
|
||||
mix-blend-mode: multiply;
|
||||
|
||||
transition:
|
||||
background-color var(--c--globals--transitions--duration),
|
||||
border-bottom-color var(--c--globals--transitions--duration);
|
||||
}
|
||||
`}
|
||||
|
||||
[data-show-selection] {
|
||||
@@ -56,7 +74,7 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
min-width: calc(min(400px, 90vw));
|
||||
max-width: calc(min(400px, 90vw));
|
||||
max-height: calc(min(500px, 60vh));
|
||||
padding: 8px;
|
||||
padding: var(--c--globals--spacings--xxs) var(--c--globals--spacings--xxxs);
|
||||
box-shadow: 0px 6px 18px 0px #00001229;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
@@ -81,10 +99,20 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .bn-thread-comments {
|
||||
gap: var(--c--globals--spacings--xxxs);
|
||||
}
|
||||
|
||||
.bn-thread-comment {
|
||||
padding: 8px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0px;
|
||||
flex-direction: column;
|
||||
align-items: initial;
|
||||
|
||||
& > div:first-child {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
& .bn-editor {
|
||||
padding-left: var(--c--globals--spacings--lg);
|
||||
@@ -100,7 +128,7 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
padding: var(--c--globals--spacings--0)
|
||||
var(--c--globals--spacings--st);
|
||||
background: none;
|
||||
border: 1px solid var(--c--globals--colors--gray-300);
|
||||
border: 1px solid var(--c--contextuals--border--semantic--neutral--tertiary);
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
height: var(--c--globals--spacings--md);
|
||||
}
|
||||
@@ -131,6 +159,8 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
|
||||
// Date
|
||||
span.mantine-focus-auto {
|
||||
font-weight: 400;
|
||||
margin-left: var(--c--globals--spacings--2xs) !important;
|
||||
}
|
||||
|
||||
.bn-comment-actions {
|
||||
@@ -139,6 +169,9 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
|
||||
.mantine-Button-root {
|
||||
background-color: transparent;
|
||||
height: var(--c--globals--spacings--md);
|
||||
width: var(--c--globals--spacings--md);
|
||||
padding: var(--c--globals--spacings--0);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--globals--colors--gray-100);
|
||||
@@ -167,19 +200,20 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
& > button {
|
||||
height: var(--c--globals--spacings--md);
|
||||
padding-inline: var(--c--globals--spacings--st);
|
||||
border: 1px solid
|
||||
var(--c--contextuals--background--semantic--brand--primary);
|
||||
background: var(
|
||||
--c--contextuals--background--semantic--brand--primary
|
||||
);
|
||||
color: var(
|
||||
--c--contextuals--content--semantic--brand--on-brand
|
||||
);
|
||||
|
||||
&[data-test='save'] {
|
||||
border: 1px solid
|
||||
var(--c--contextuals--background--semantic--brand--primary);
|
||||
background: var(
|
||||
--c--contextuals--background--semantic--brand--primary
|
||||
);
|
||||
color: var(
|
||||
--c--contextuals--content--semantic--brand--on-brand
|
||||
);
|
||||
&:hover {
|
||||
background-color: var(--c--contextuals--background--semantic--brand--primary);
|
||||
}
|
||||
|
||||
&[data-test='cancel'] {
|
||||
&:last-child {
|
||||
background: white;
|
||||
border: 1px solid
|
||||
var(--c--contextuals--border--surface--primary);
|
||||
@@ -214,6 +248,11 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
& .bn-block-content:has(.ProseMirror-trailingBreak:only-child):after {
|
||||
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions button send comment
|
||||
@@ -223,6 +262,7 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
|
||||
.bn-action-toolbar.bn-comment-actions {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
button {
|
||||
font-size: 0;
|
||||
@@ -261,4 +301,74 @@ export const DocsCommentsStyle = createGlobalStyle<{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Styles for the comments sidebar
|
||||
*/
|
||||
.--docs--comments-sidebar.bn-root{
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
|
||||
.bn-threads-sidebar {
|
||||
gap: 0;
|
||||
border-radius: 0;
|
||||
min-height: 100%;
|
||||
|
||||
.bn-thread-expand-prompt p {
|
||||
font-size: var(--c--globals--font--sizes--xs);
|
||||
}
|
||||
|
||||
.bn-thread {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
padding: var(--c--globals--spacings--xxs) var(--c--globals--spacings--xxxs);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
|
||||
&.selected {
|
||||
border: none;
|
||||
background: var(--c--contextuals--background--semantic--neutral--tertiary);
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--c--contextuals--background--semantic--neutral--tertiary);
|
||||
}
|
||||
|
||||
& .bn-header-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.bn-thread-orphaned {
|
||||
& .bn-header-text {
|
||||
display: block;
|
||||
padding-inline: var(--c--globals--spacings--xs);
|
||||
}
|
||||
}
|
||||
|
||||
.bn-thread-comment {
|
||||
padding: var(--c--globals--spacings--xs);
|
||||
|
||||
&:has(.bn-comment-actions) {
|
||||
& > .mantine-Group-root:first-child {
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
var(--c--contextuals--background--semantic--neutral--tertiary) 90%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.bn-menu-dropdown {
|
||||
box-shadow: 0px 0px 6px 0px #0000911a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface CommentSidebarStore {
|
||||
filter: 'open' | 'resolved';
|
||||
isSideBarOpen: boolean;
|
||||
setIsSideBarOpen: (isSideBarOpen: boolean) => void;
|
||||
setThreadsSidebarTarget: (el: HTMLElement | null) => void;
|
||||
setFilter: (filter: 'open' | 'resolved') => void;
|
||||
threadsSidebarTarget: HTMLElement | null;
|
||||
}
|
||||
|
||||
export const useCommentSidebarStore = create<CommentSidebarStore>((set) => ({
|
||||
filter: 'open',
|
||||
isSideBarOpen: false,
|
||||
setFilter: (filter) => set(() => ({ filter })),
|
||||
setIsSideBarOpen: (isSideBarOpen) => set(() => ({ isSideBarOpen })),
|
||||
setThreadsSidebarTarget: (threadsSidebarTarget) => {
|
||||
set(() => ({ threadsSidebarTarget }));
|
||||
},
|
||||
threadsSidebarTarget: null,
|
||||
}));
|
||||
@@ -4,6 +4,22 @@ export const DocsEditorStyle = createGlobalStyle`
|
||||
.bn-container {
|
||||
height: 100%;
|
||||
}
|
||||
/**
|
||||
* Token Blocknote
|
||||
*/
|
||||
.bn-root[data-color-scheme] {
|
||||
--bn-colors-editor-text: var(
|
||||
--c--contextuals--content--semantic--neutral--primary
|
||||
);
|
||||
--bn-colors-side-menu: var(
|
||||
--c--contextuals--content--semantic--neutral--tertiary
|
||||
);
|
||||
}
|
||||
.bn-root .mantine-Chip-label {
|
||||
--chip-color: var(--c--contextuals--content--semantic--brand--tertiary);
|
||||
--mantine-primary-color-filled-hover: var(--c--contextuals--content--semantic--brand--tertiary);
|
||||
}
|
||||
|
||||
.bn-root {
|
||||
.bn-editor {
|
||||
height: 100%;
|
||||
@@ -14,16 +30,6 @@ export const DocsEditorStyle = createGlobalStyle`
|
||||
font-family: var(--c--components--button--font-family);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Mantine
|
||||
*/
|
||||
--bn-colors-editor-text: var(
|
||||
--c--contextuals--content--semantic--neutral--primary
|
||||
);
|
||||
--bn-colors-side-menu: var(
|
||||
--c--contextuals--content--semantic--neutral--tertiary
|
||||
);
|
||||
|
||||
/**
|
||||
* Ensure long placeholder text is truncated with ellipsis
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { CommentSideBar } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
|
||||
import { useDocStore, useProviderStore } from '@/features/docs/doc-management';
|
||||
import { HEADER_HEIGHT } from '@/features/header';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
@@ -11,7 +12,7 @@ import { useRightPanelStore } from './useRightPanelStore';
|
||||
export const RightPanel = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentDoc: doc } = useDocStore();
|
||||
const { _, isPanelOpen } = useRightPanelStore();
|
||||
const { setIsPanelOpen, isPanelOpen } = useRightPanelStore();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const { provider, isReady } = useProviderStore();
|
||||
const isProviderReady =
|
||||
@@ -47,6 +48,8 @@ export const RightPanel = () => {
|
||||
width: 0;
|
||||
`}
|
||||
`}
|
||||
></Box>
|
||||
>
|
||||
<CommentSideBar onClose={() => setIsPanelOpen(false)} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
+21
-20
@@ -1,19 +1,29 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import CommentsIcon from '@/assets/icons/ui-kit/bubble-text.svg';
|
||||
import { Card } from '@/components';
|
||||
|
||||
import { useRightPanelStore } from './useRightPanelStore';
|
||||
import { CommentSideBarButton } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
|
||||
import { useEditorStore } from '@/features/docs/doc-editor/stores/useEditorStore';
|
||||
|
||||
export const RightPanelCollapseButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isPanelOpen, togglePanel } = useRightPanelStore();
|
||||
const { threadStore } = useEditorStore();
|
||||
const [hasThreads, setHasThreads] = useState(
|
||||
!!threadStore?.getThreads().size,
|
||||
);
|
||||
|
||||
const ariaLabel = isPanelOpen
|
||||
? t('Hide the right side panel')
|
||||
: t('Show the right side panel');
|
||||
useEffect(() => {
|
||||
if (!threadStore) {
|
||||
setHasThreads(false);
|
||||
return;
|
||||
}
|
||||
return threadStore.subscribe((threads) => {
|
||||
setHasThreads(threads.size > 0);
|
||||
});
|
||||
}, [threadStore]);
|
||||
|
||||
if (!hasThreads) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -27,16 +37,7 @@ export const RightPanelCollapseButton = () => {
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={togglePanel}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={isPanelOpen}
|
||||
color="neutral"
|
||||
variant={isPanelOpen ? 'secondary' : 'tertiary'}
|
||||
icon={<CommentsIcon width={24} height={24} aria-hidden="true" />}
|
||||
data-testid="floating-bar-toggle-right-panel"
|
||||
></Button>
|
||||
{hasThreads && <CommentSideBarButton />}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user