(frontend) enhance document editor and header components

- Improved styling for headings in BlockNoteEditor for better visual
hierarchy.
- Adjusted padding in DocEditor and DocHeader based on device type for
responsive design.
- Updated DocTitle and ModalExport components to enhance typography and
spacing.
- Refactored DocToolBox to improve share button functionality and access
display.
- Enhanced versioning modal with better layout and accessibility
features.
- Cleaned up unused imports and optimized component structures for
maintainability.
This commit is contained in:
Nathan Panchout
2024-12-23 10:31:57 +01:00
committed by Anthony LC
parent 6ad1e27acf
commit fc27043e9e
11 changed files with 239 additions and 139 deletions

View File

@@ -24,6 +24,49 @@ const cssEditor = (readonly: boolean) => `
&, & > .bn-container, & .ProseMirror { &, & > .bn-container, & .ProseMirror {
height:100%; height:100%;
.bn-side-menu[data-block-type=heading][data-level="1"] {
height: 50px;
}
.bn-side-menu[data-block-type=heading][data-level="2"] {
height: 43px;
}
.bn-side-menu[data-block-type=heading][data-level="3"] {
height: 35px;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
a {
color: var(--c--theme--colors--greyscale-500);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
}
.bn-editor {
color: var(--c--theme--colors--greyscale-700);
}
.bn-block-outer:not(:first-child) {
&:has(h1) {
padding-top: 32px;
}
&:has(h2) {
padding-top: 24px;
}
&:has(h3) {
padding-top: 16px;
}
}; };
& .bn-inline-content code { & .bn-inline-content code {

View File

@@ -50,7 +50,7 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
</Box> </Box>
)} )}
<Box $maxWidth="868px" $width="100%" $height="100%"> <Box $maxWidth="868px" $width="100%" $height="100%">
<Box $padding={{ horizontal: '54px' }}> <Box $padding={{ horizontal: isDesktop ? '54px' : 'base' }}>
{isVersion ? ( {isVersion ? (
<DocVersionHeader title={doc.title} /> <DocVersionHeader title={doc.title} />
) : ( ) : (

View File

@@ -35,7 +35,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<> <>
<Box <Box
$width="100%" $width="100%"
$padding={{ top: 'base' }} $padding={{ top: isDesktop ? '4xl' : 'md' }}
$gap={spacings['base']} $gap={spacings['base']}
aria-label={t('It is the card information about the document.')} aria-label={t('It is the card information about the document.')}
> >
@@ -72,10 +72,10 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<Box $direction="row"> <Box $direction="row">
{isDesktop && ( {isDesktop && (
<> <>
<Text $variation="400" $size="s" $weight="bold"> <Text $variation="600" $size="s" $weight="bold">
{transRole(currentDocRole(doc.abilities))}&nbsp;·&nbsp; {transRole(currentDocRole(doc.abilities))}&nbsp;·&nbsp;
</Text> </Text>
<Text $variation="400" $size="s"> <Text $variation="600" $size="s">
{t('Last update: {{update}}', { {t('Last update: {{update}}', {
update: DateTime.fromISO(doc.updated_at).toRelative(), update: DateTime.fromISO(doc.updated_at).toRelative(),
})} })}
@@ -92,7 +92,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<DocToolBox doc={doc} /> <DocToolBox doc={doc} />
</Box> </Box>
</Box> </Box>
<HorizontalSeparator /> <HorizontalSeparator $withPadding={true} />
</Box> </Box>
</> </>
); );

View File

@@ -43,6 +43,7 @@ export const DocTitleText = ({ title }: DocTitleTextProps) => {
as="h2" as="h2"
$margin={{ all: 'none', left: 'none' }} $margin={{ all: 'none', left: 'none' }}
$size={isMobile ? 'h4' : 'h2'} $size={isMobile ? 'h4' : 'h2'}
$variation="1000"
> >
{title} {title}
</Text> </Text>
@@ -113,7 +114,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
onBlurCapture={(event) => onBlurCapture={(event) =>
handleTitleSubmit(event.target.textContent || '') handleTitleSubmit(event.target.textContent || '')
} }
$color={colorsTokens()['greyscale-text']} $color={colorsTokens()['greyscale-1000']}
$margin={{ left: '-2px', right: '10px' }} $margin={{ left: '-2px', right: '10px' }}
$css={css` $css={css`
&[contenteditable='true']:empty:not(:focus):before { &[contenteditable='true']:empty:not(:focus):before {

View File

@@ -4,7 +4,8 @@ import {
useModal, useModal,
useToastProvider, useToastProvider,
} from '@openfun/cunningham-react'; } from '@openfun/cunningham-react';
import { useState } from 'react'; import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { css } from 'styled-components'; import { css } from 'styled-components';
@@ -17,12 +18,12 @@ import {
} from '@/components'; } from '@/components';
import { useAuthStore } from '@/core'; import { useAuthStore } from '@/core';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { import { useEditorStore } from '@/features/docs/doc-editor/';
useEditorStore,
usePanelEditorStore,
} from '@/features/docs/doc-editor/';
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management'; import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
import { ModalSelectVersion } from '@/features/docs/doc-versioning'; import {
KEY_LIST_DOC_VERSIONS,
ModalSelectVersion,
} from '@/features/docs/doc-versioning';
import { useResponsiveStore } from '@/stores'; import { useResponsiveStore } from '@/stores';
import { DocShareModal } from '../../doc-share/component/DocShareModal'; import { DocShareModal } from '../../doc-share/component/DocShareModal';
@@ -35,6 +36,8 @@ interface DocToolBoxProps {
export const DocToolBox = ({ doc }: DocToolBoxProps) => { export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const hasAccesses = doc.nb_accesses > 1;
const queryClient = useQueryClient();
const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const spacings = spacingsTokens(); const spacings = spacingsTokens();
@@ -44,7 +47,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false); const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
const selectHistoryModal = useModal(); const selectHistoryModal = useModal();
const modalShare = useModal(); const modalShare = useModal();
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
const { isSmallMobile, isDesktop } = useResponsiveStore(); const { isSmallMobile, isDesktop } = useResponsiveStore();
const { authenticated } = useAuthStore(); const { authenticated } = useAuthStore();
@@ -80,14 +82,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}, },
show: isDesktop, show: isDesktop,
}, },
{
label: t('Table of contents'),
icon: 'summarize',
callback: () => {
setIsPanelOpen(true);
setIsPanelTableContentOpen(true);
},
},
{ {
label: t('Copy as {{format}}', { format: 'Markdown' }), label: t('Copy as {{format}}', { format: 'Markdown' }),
icon: 'content_copy', icon: 'content_copy',
@@ -135,6 +130,16 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
} }
}; };
useEffect(() => {
if (selectHistoryModal.isOpen) {
return;
}
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC_VERSIONS],
});
}, [selectHistoryModal.isOpen, queryClient]);
return ( return (
<Box <Box
$margin={{ left: 'auto' }} $margin={{ left: 'auto' }}
@@ -143,21 +148,55 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
$gap="0.5rem 1.5rem" $gap="0.5rem 1.5rem"
$wrap={isSmallMobile ? 'wrap' : 'nowrap'} $wrap={isSmallMobile ? 'wrap' : 'nowrap'}
> >
<Box $direction="row" $margin={{ left: 'auto' }} $gap={spacings['2xs']}> <Box
$direction="row"
$align="center"
$margin={{ left: 'auto' }}
$gap={spacings['2xs']}
>
{authenticated && !isSmallMobile && ( {authenticated && !isSmallMobile && (
<Button <>
color="primary-text" {!hasAccesses && (
onClick={() => { <Button
modalShare.open(); color="tertiary-text"
}} onClick={() => {
size={isSmallMobile ? 'small' : 'medium'} modalShare.open();
> }}
{t('Share')} size={isSmallMobile ? 'small' : 'medium'}
</Button> >
{t('Share')}
</Button>
)}
{hasAccesses && (
<Box
$css={css`
.c__button--medium {
height: 32px;
padding: 10px var(--c--theme--spacings--xs);
gap: 7px;
}
`}
>
<Button
color="tertiary"
aria-label="Share button"
icon={
<Icon iconName="group" $theme="primary" $variation="800" />
}
onClick={() => {
modalShare.open();
}}
size={isSmallMobile ? 'small' : 'medium'}
>
{doc.nb_accesses}
</Button>
</Box>
)}
</>
)} )}
{!isSmallMobile && ( {!isSmallMobile && (
<Button <Button
color="primary-text" color="tertiary-text"
icon={ icon={
<Icon iconName="download" $theme="primary" $variation="800" /> <Icon iconName="download" $theme="primary" $variation="800" />
} }
@@ -171,15 +210,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<IconOptions <IconOptions
isHorizontal isHorizontal
$theme="primary" $theme="primary"
$radius={spacings['3xs']} $padding={{ all: 'xs' }}
$css={ $css={css`
isSmallMobile &:hover {
background-color: ${colors['greyscale-100']};
}
${isSmallMobile
? css` ? css`
padding: 10px; padding: 10px;
border: 1px solid ${colors['greyscale-300']}; border: 1px solid ${colors['greyscale-300']};
` `
: '' : ''}
} `}
aria-label={t('Open the document options')} aria-label={t('Open the document options')}
/> />
</DropdownMenu> </DropdownMenu>

View File

@@ -130,7 +130,6 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
data-testid="modal-export" data-testid="modal-export"
isOpen isOpen
closeOnClickOutside closeOnClickOutside
hideCloseButton
onClose={() => onClose()} onClose={() => onClose()}
rightActions={ rightActions={
<> <>
@@ -155,7 +154,7 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
} }
size={ModalSize.MEDIUM} size={ModalSize.MEDIUM}
title={ title={
<Text $size="h6" $align="flex-start"> <Text $size="h6" $variation="1000" $align="flex-start">
{t('Download')} {t('Download')}
</Text> </Text>
} }
@@ -163,9 +162,9 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
<Box <Box
$margin={{ bottom: 'xl' }} $margin={{ bottom: 'xl' }}
aria-label={t('Content modal to export the document')} aria-label={t('Content modal to export the document')}
$gap="1.5rem" $gap="1rem"
> >
<Text $variation="600"> <Text $variation="600" $size="sm">
{t( {t(
'Upload your docs to a Microsoft Word, Open Office or PDF document.', 'Upload your docs to a Microsoft Word, Open Office or PDF document.',
)} )}

View File

@@ -38,7 +38,7 @@ export function useUpdateDoc({
mutationFn: updateDoc, mutationFn: updateDoc,
onSuccess: (data) => { onSuccess: (data) => {
listInvalideQueries?.forEach((queryKey) => { listInvalideQueries?.forEach((queryKey) => {
void queryClient.resetQueries({ void queryClient.invalidateQueries({
queryKey: [queryKey], queryKey: [queryKey],
}); });
}); });

View File

@@ -1,5 +1,4 @@
import { import {
Alert,
Button, Button,
Modal, Modal,
ModalSize, ModalSize,
@@ -48,41 +47,36 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
isOpen isOpen
closeOnClickOutside closeOnClickOutside
hideCloseButton hideCloseButton
leftActions={
<Button
aria-label={t('Close the modal')}
color="secondary"
fullWidth
onClick={() => onClose()}
>
{t('Cancel')}
</Button>
}
onClose={() => onClose()} onClose={() => onClose()}
rightActions={ rightActions={
<Button <>
aria-label={t('Confirm deletion')} <Button
color="danger" aria-label={t('Close the modal')}
fullWidth color="secondary"
onClick={() => fullWidth
removeDoc({ onClick={() => onClose()}
docId: doc.id, >
}) {t('Cancel')}
} </Button>
> <Button
{t('Confirm deletion')} aria-label={t('Confirm deletion')}
</Button> color="danger"
fullWidth
onClick={() =>
removeDoc({
docId: doc.id,
})
}
>
{t('Delete')}
</Button>
</>
} }
size={ModalSize.MEDIUM} size={ModalSize.SMALL}
title={ title={
<Box $align="center" $gap="1rem"> <Text $size="h6" as="h6" $margin={{ all: '0' }} $align="flex-start">
<Text $isMaterialIcon $size="48px" $theme="primary" $variation="600"> {t('Delete a doc')}
delete_forever </Text>
</Text>
<Text as="h2" $size="h3" $margin="none">
{t('Deleting the document "{{title}}"', { title: doc.title })}
</Text>
</Box>
} }
> >
<Box <Box
@@ -90,13 +84,11 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
aria-label={t('Content modal to delete document')} aria-label={t('Content modal to delete document')}
> >
{!isError && ( {!isError && (
<Alert canClose={false} type={VariantType.WARNING}> <Text $size="sm" $variation="600">
<Text> {t('Are you sure you want to delete the document "{{title}}"?', {
{t('Are you sure you want to delete the document "{{title}}"?', { title: doc.title,
title: doc.title, })}
})} </Text>
</Text>
</Alert>
)} )}
{isError && <TextErrors causes={error.cause} />} {isError && <TextErrors causes={error.cause} />}

View File

@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { css } from 'styled-components'; import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components'; import { Box, BoxButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useEditorStore, useHeadingStore } from '@/features/docs/doc-editor'; import { useEditorStore, useHeadingStore } from '@/features/docs/doc-editor';
import { MAIN_LAYOUT_ID } from '@/layouts/conf'; import { MAIN_LAYOUT_ID } from '@/layouts/conf';
@@ -11,6 +12,8 @@ import { Heading } from './Heading';
export const TableContent = () => { export const TableContent = () => {
const { headings } = useHeadingStore(); const { headings } = useHeadingStore();
const { editor } = useEditorStore(); const { editor } = useEditorStore();
const { spacingsTokens } = useCunninghamTheme();
const spacing = spacingsTokens();
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>(); const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
@@ -58,33 +61,33 @@ export const TableContent = () => {
}; };
}, [headings, setHeadingIdHighlight]); }, [headings, setHeadingIdHighlight]);
const onOpen = () => {
setIsHover(true);
setTimeout(() => {
const element = document.getElementById(`heading-${headingIdHighlight}`);
element?.scrollIntoView({
behavior: 'instant',
inline: 'center',
block: 'center',
});
}, 0); // 300ms is the transition time of the box
};
const onClose = () => {
setIsHover(false);
};
if (!editor) { if (!editor) {
return null; return null;
} }
return ( return (
<Box <Box
onMouseEnter={() => {
setIsHover(true);
setTimeout(() => {
const element = document.getElementById(
`heading-${headingIdHighlight}`,
);
element?.scrollIntoView({
behavior: 'smooth',
inline: 'center',
block: 'center',
});
}, 250); // 300ms is the transition time of the box
}}
onMouseLeave={() => {
setIsHover(false);
}}
id="summaryContainer" id="summaryContainer"
$effect="show" $width={!isHover ? '40px' : '200px'}
$width="40px" $height={!isHover ? '40px' : 'auto'}
$height="40px" $maxHeight="calc(50vh - 60px)"
$zIndex={1000} $zIndex={1000}
$align="center" $align="center"
$padding="xs" $padding="xs"
@@ -94,51 +97,69 @@ export const TableContent = () => {
overflow: hidden; overflow: hidden;
border-radius: var(--c--theme--spacings--3xs); border-radius: var(--c--theme--spacings--3xs);
background: var(--c--theme--colors--greyscale-000); background: var(--c--theme--colors--greyscale-000);
${isHover &&
&:hover { css`
overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
gap: var(--c--theme--spacings--2xs); gap: var(--c--theme--spacings--2xs);
width: 200px; `}
height: auto;
max-height: calc(100vh - 60px - 15vh);
}
`} `}
> >
{!isHover && ( {!isHover && (
<Box $justify="center" $align="center"> <BoxButton onClick={onOpen} $justify="center" $align="center">
<Icon iconName="list" $theme="primary" $variation="800" /> <Icon iconName="list" $theme="primary" $variation="800" />
</Box> </BoxButton>
)} )}
{isHover && ( {isHover && (
<Box $width="100%"> <Box
$width="100%"
$overflow="hidden"
$css={css`
user-select: none;
`}
>
<Box <Box
$margin={{ bottom: '20px' }} $margin={{ bottom: '10px' }}
$direction="row" $direction="row"
$justify="space-between" $justify="space-between"
$align="center" $align="center"
> >
<Text $weight="bold" $variation="800" $theme="primary"> <Text $weight="500" $size="sm" $variation="800" $theme="primary">
{t('Summary')} {t('Summary')}
</Text> </Text>
<Icon iconName="list" $theme="primary" $variation="800" /> <BoxButton
onClick={onClose}
$justify="center"
$align="center"
$css={css`
transform: rotate(180deg);
`}
>
<Icon iconName="menu_open" $theme="primary" $variation="800" />
</BoxButton>
</Box>
<Box
$gap={spacing['3xs']}
$css={css`
overflow-y: auto;
`}
>
{headings?.map(
(heading) =>
heading.contentText && (
<Heading
editor={editor}
headingId={heading.id}
level={heading.props.level}
text={heading.contentText}
key={heading.id}
isHighlight={headingIdHighlight === heading.id}
/>
),
)}
</Box> </Box>
{headings?.map(
(heading) =>
heading.contentText && (
<Heading
editor={editor}
headingId={heading.id}
level={heading.props.level}
text={heading.contentText}
key={heading.id}
isHighlight={headingIdHighlight === heading.id}
/>
),
)}
</Box> </Box>
)} )}
</Box> </Box>

View File

@@ -52,7 +52,8 @@ export const ModalSelectVersion = ({
aria-label="version history modal" aria-label="version history modal"
className="noPadding" className="noPadding"
$direction="row" $direction="row"
$height="calc(100vh - 50px);" $height="100%"
$maxHeight="calc(100vh - 2em - 12px)"
$overflow="hidden" $overflow="hidden"
> >
<Box <Box
@@ -64,7 +65,11 @@ export const ModalSelectVersion = ({
flex: 1; flex: 1;
`} `}
> >
<Box $width="100%" $padding="base" $align="center"> <Box
$width="100%"
$padding={{ horizontal: 'base', vertical: 'xl' }}
$align="center"
>
{selectedVersionId && ( {selectedVersionId && (
<DocEditor doc={doc} versionId={selectedVersionId} /> <DocEditor doc={doc} versionId={selectedVersionId} />
)} )}
@@ -81,7 +86,7 @@ export const ModalSelectVersion = ({
$direction="column" $direction="column"
$justify="space-between" $justify="space-between"
$width="250px" $width="250px"
$height="calc(100vh - 2em - 30px);" $height="calc(100vh - 2em - 12px)"
$css={css` $css={css`
overflow-y: hidden; overflow-y: hidden;
border-left: 1px solid var(--c--theme--colors--greyscale-200); border-left: 1px solid var(--c--theme--colors--greyscale-200);
@@ -105,7 +110,7 @@ export const ModalSelectVersion = ({
`} `}
$padding="sm" $padding="sm"
> >
<Text $size="h6" $weight="bold"> <Text $size="h6" $variation="1000" $weight="bold">
{t('History')} {t('History')}
</Text> </Text>
<Button <Button
@@ -123,7 +128,7 @@ export const ModalSelectVersion = ({
/> />
</Box> </Box>
<Box <Box
$padding="base" $padding="xs"
$css={css` $css={css`
border-top: 1px solid var(--c--theme--colors--greyscale-200); border-top: 1px solid var(--c--theme--colors--greyscale-200);
`} `}

View File

@@ -1,6 +1,5 @@
import { Loader } from '@openfun/cunningham-react'; import { Loader } from '@openfun/cunningham-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { APIError } from '@/api'; import { APIError } from '@/api';
@@ -105,11 +104,9 @@ export const VersionList = ({
docId: doc.id, docId: doc.id,
}); });
const versions = useMemo(() => { const versions = data?.pages.reduce((acc, page) => {
return data?.pages.reduce((acc, page) => { return acc.concat(page.versions);
return acc.concat(page.versions); }, [] as Versions[]);
}, [] as Versions[]);
}, [data?.pages]);
return ( return (
<Box $css="overflow-y: auto; overflow-x: hidden;"> <Box $css="overflow-y: auto; overflow-x: hidden;">