♻️(frontend) update some elements

- add panel information when document is
authenticated
- add a copy link button in the toolbox
on the document
- fix when long title document
- modals fit design
- mobile responsive changes
This commit is contained in:
Anthony LC
2025-02-24 10:24:06 +01:00
committed by Anthony LC
parent 50d098c777
commit 54a75bc338
17 changed files with 130 additions and 98 deletions

View File

@@ -17,6 +17,7 @@ and this project adheres to
- 🛂(frontend) Restore version visibility #629 - 🛂(frontend) Restore version visibility #629
- 📝(doc) minor README.md formatting and wording enhancements - 📝(doc) minor README.md formatting and wording enhancements
-Stop setting a default title on doc creation #634 -Stop setting a default title on doc creation #634
- ♻️(frontend) misc ui improvements #644
## Fixed ## Fixed

View File

@@ -7,17 +7,9 @@ type SmallDoc = {
title: string; title: string;
}; };
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Documents Grid mobile', () => { test.describe('Documents Grid mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } }); test.use({ viewport: { width: 500, height: 1200 } });
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('it checks the grid when mobile', async ({ page }) => { test('it checks the grid when mobile', async ({ page }) => {
await page.route('**/documents/**', async (route) => { await page.route('**/documents/**', async (route) => {
const request = route.request(); const request = route.request();
@@ -94,6 +86,10 @@ test.describe('Documents Grid mobile', () => {
}); });
test.describe('Document grid item options', () => { test.describe('Document grid item options', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('it pins a document', async ({ page, browserName }) => { test('it pins a document', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `Favorite doc`, browserName); const [docTitle] = await createDoc(page, `Favorite doc`, browserName);
@@ -212,6 +208,8 @@ test.describe('Document grid item options', () => {
test.describe('Documents filters', () => { test.describe('Documents filters', () => {
test('it checks the prebuild left panel filters', async ({ page }) => { test('it checks the prebuild left panel filters', async ({ page }) => {
await page.goto('/');
// All Docs // All Docs
const response = await page.waitForResponse( const response = await page.waitForResponse(
(response) => (response) =>
@@ -282,11 +280,9 @@ test.describe('Documents filters', () => {
}); });
test.describe('Documents Grid', () => { test.describe('Documents Grid', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('checks all the elements are visible', async ({ page }) => { test('checks all the elements are visible', async ({ page }) => {
await page.goto('/');
let docs: SmallDoc[] = []; let docs: SmallDoc[] = [];
const response = await page.waitForResponse( const response = await page.waitForResponse(
(response) => (response) =>
@@ -314,11 +310,12 @@ test.describe('Documents Grid', () => {
test('checks the infinite scroll', async ({ page }) => { test('checks the infinite scroll', async ({ page }) => {
let docs: SmallDoc[] = []; let docs: SmallDoc[] = [];
const responsePromisePage1 = page.waitForResponse( const responsePromisePage1 = page.waitForResponse((response) => {
(response) => return (
response.url().endsWith(`/documents/?page=1`) && response.url().endsWith(`/documents/?page=1`) &&
response.status() === 200, response.status() === 200
); );
});
const responsePromisePage2 = page.waitForResponse( const responsePromisePage2 = page.waitForResponse(
(response) => (response) =>
@@ -326,6 +323,8 @@ test.describe('Documents Grid', () => {
response.status() === 200, response.status() === 200,
); );
await page.goto('/');
const responsePage1 = await responsePromisePage1; const responsePage1 = await responsePromisePage1;
expect(responsePage1.ok()).toBeTruthy(); expect(responsePage1.ok()).toBeTruthy();
let result = await responsePage1.json(); let result = await responsePage1.json();

View File

@@ -416,6 +416,14 @@ test.describe('Doc Visibility: Authenticated', () => {
page.getByText('The document visibility has been updated.'), page.getByText('The document visibility has been updated.'),
).toBeVisible(); ).toBeVisible();
await expect(
page
.getByLabel('It is the card information about the document.')
.getByText('Document accessible to any connected person', {
exact: true,
}),
).toBeVisible();
await page.getByRole('button', { name: 'close' }).click(); await page.getByRole('button', { name: 'close' }).click();
const urlDoc = page.url(); const urlDoc = page.url();

View File

@@ -76,13 +76,13 @@ export const cssEditor = (readonly: boolean) => css`
.bn-block-outer:not(:first-child) { .bn-block-outer:not(:first-child) {
&:has(h1) { &:has(h1) {
padding-top: 32px; margin-top: 32px;
} }
&:has(h2) { &:has(h2) {
padding-top: 24px; margin-top: 24px;
} }
&:has(h3) { &:has(h3) {
padding-top: 16px; margin-top: 16px;
} }
} }
@@ -92,9 +92,16 @@ export const cssEditor = (readonly: boolean) => css`
border-radius: 4px; border-radius: 4px;
} }
@media screen and (width <= 768px) {
& .bn-editor {
padding-right: 36px;
}
}
@media screen and (width <= 560px) { @media screen and (width <= 560px) {
& .bn-editor { & .bn-editor {
${readonly && `padding-left: 10px;`} ${readonly && `padding-left: 10px;`}
padding-right: 10px;
} }
.bn-side-menu[data-block-type='heading'][data-level='1'] { .bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px; height: 46px;

View File

@@ -27,6 +27,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const docIsPublic = doc.link_reach === LinkReach.PUBLIC; const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED;
const { transRole } = useTrans(); const { transRole } = useTrans();
@@ -38,7 +39,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
$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.')}
> >
{docIsPublic && ( {(docIsPublic || docIsAuth) && (
<Box <Box
aria-label={t('Public document')} aria-label={t('Public document')}
$color={colors['primary-800']} $color={colors['primary-800']}
@@ -57,10 +58,12 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
$theme="primary" $theme="primary"
$variation="800" $variation="800"
data-testid="public-icon" data-testid="public-icon"
iconName="public" iconName={docIsPublic ? 'public' : 'vpn_lock'}
/> />
<Text $theme="primary" $variation="800"> <Text $theme="primary" $variation="800">
{t('Public document')} {docIsPublic
? t('Public document')
: t('Document accessible to any connected person')}
</Text> </Text>
</Box> </Box>
)} )}
@@ -76,8 +79,9 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
$css="flex:1;" $css="flex:1;"
$gap="0.5rem 1rem" $gap="0.5rem 1rem"
$align="center" $align="center"
$maxWidth="100%"
> >
<Box $gap={spacings['3xs']}> <Box $gap={spacings['3xs']} $overflow="auto">
<DocTitle doc={doc} /> <DocTitle doc={doc} />
<Box $direction="row"> <Box $direction="row">

View File

@@ -101,39 +101,35 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
}, [doc]); }, [doc]);
return ( return (
<> <Tooltip content={t('Rename')} placement="top">
<Tooltip content={t('Rename')} placement="top"> <Box
<Box as="span"
as="span" role="textbox"
role="textbox" contentEditable
contentEditable defaultValue={titleDisplay || undefined}
defaultValue={titleDisplay || undefined} onKeyDownCapture={handleKeyDown}
onKeyDownCapture={handleKeyDown} suppressContentEditableWarning={true}
suppressContentEditableWarning={true} aria-label="doc title input"
aria-label="doc title input" onBlurCapture={(event) =>
onBlurCapture={(event) => handleTitleSubmit(event.target.textContent || '')
handleTitleSubmit(event.target.textContent || '') }
$color={colorsTokens()['greyscale-1000']}
$css={css`
&[contenteditable='true']:empty:not(:focus):before {
content: '${untitledDocument}';
color: grey;
pointer-events: none;
font-style: italic;
} }
$color={colorsTokens()['greyscale-1000']} font-size: ${isDesktop
$margin={{ left: '-2px', right: '10px' }} ? css`var(--c--theme--font--sizes--h2)`
$css={css` : css`var(--c--theme--font--sizes--sm)`};
&[contenteditable='true']:empty:not(:focus):before { font-weight: 700;
content: '${untitledDocument}'; outline: none;
color: grey; `}
pointer-events: none; >
font-style: italic; {titleDisplay}
} </Box>
font-size: ${isDesktop </Tooltip>
? css`var(--c--theme--font--sizes--h2)`
: css`var(--c--theme--font--sizes--sm)`};
font-weight: 700;
outline: none;
`}
>
{titleDisplay}
</Box>
</Tooltip>
</>
); );
}; };

View File

@@ -18,7 +18,11 @@ import {
} from '@/components'; } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { useEditorStore } from '@/features/docs/doc-editor/'; import { useEditorStore } from '@/features/docs/doc-editor/';
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management'; import {
Doc,
ModalRemoveDoc,
useCopyDocLink,
} from '@/features/docs/doc-management';
import { DocShareModal } from '@/features/docs/doc-share'; import { DocShareModal } from '@/features/docs/doc-share';
import { import {
KEY_LIST_DOC_VERSIONS, KEY_LIST_DOC_VERSIONS,
@@ -34,7 +38,7 @@ 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 hasAccesses = doc.nb_accesses > 1 && doc.abilities.accesses_view;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const { spacingsTokens, colorsTokens } = useCunninghamTheme();
@@ -50,6 +54,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { isSmallMobile, isDesktop } = useResponsiveStore(); const { isSmallMobile, isDesktop } = useResponsiveStore();
const { editor } = useEditorStore(); const { editor } = useEditorStore();
const { toast } = useToastProvider(); const { toast } = useToastProvider();
const copyDocLink = useCopyDocLink(doc.id);
const options: DropdownMenuOption[] = [ const options: DropdownMenuOption[] = [
...(isSmallMobile ...(isSmallMobile
@@ -66,6 +71,11 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
setIsModalExportOpen(true); setIsModalExportOpen(true);
}, },
}, },
{
label: t('Copy link'),
icon: 'add_link',
callback: copyDocLink,
},
] ]
: []), : []),

View File

@@ -71,9 +71,15 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
</Button> </Button>
</> </>
} }
size={ModalSize.MEDIUM} size={ModalSize.SMALL}
title={ title={
<Text $size="h6" as="h6" $margin={{ all: '0' }} $align="flex-start"> <Text
$size="h6"
as="h6"
$margin={{ all: '0' }}
$align="flex-start"
$variation="1000"
>
{t('Delete a doc')} {t('Delete a doc')}
</Text> </Text>
} }

View File

@@ -33,9 +33,7 @@ export const DocShareModalFooter = ({ doc, onClose }: Props) => {
> >
<Button <Button
fullWidth={false} fullWidth={false}
onClick={() => { onClick={copyDocLink}
copyDocLink();
}}
color="tertiary" color="tertiary"
icon={<span className="material-icons">add_link</span>} icon={<span className="material-icons">add_link</span>}
> >

View File

@@ -100,17 +100,21 @@ export const ModalConfirmationVersion = ({
</Button> </Button>
</> </>
} }
size={ModalSize.MEDIUM} size={ModalSize.SMALL}
title={ title={
<Text $size="h6" $align="flex-start"> <Text $size="h6" $align="flex-start" $variation="1000">
{t('Warning')} {t('Warning')}
</Text> </Text>
} }
> >
<Box aria-label={t('Modal confirmation to restore the version')}> <Box aria-label={t('Modal confirmation to restore the version')}>
<Box> <Box>
<Text>{t('Your current document will revert to this version.')}</Text> <Text $variation="600">
<Text>{t('If a member is editing, his works can be lost.')}</Text> {t('Your current document will revert to this version.')}
</Text>
<Text $variation="600">
{t('If a member is editing, his works can be lost.')}
</Text>
</Box> </Box>
</Box> </Box>
</Modal> </Modal>

View File

@@ -61,12 +61,8 @@ export const DocsGrid = ({
$position="relative" $position="relative"
$width="100%" $width="100%"
$maxWidth="960px" $maxWidth="960px"
$maxHeight="calc(100vh - 52px - 1rem)" $maxHeight="calc(100vh - 52px - 2rem)"
$align="center" $align="center"
$css={css`
overflow-x: hidden;
overflow-y: auto;
`}
> >
<DocsGridLoader isLoading={isRefetching || loading} /> <DocsGridLoader isLoading={isRefetching || loading} />
<Card <Card
@@ -75,8 +71,7 @@ export const DocsGrid = ({
$height="100%" $height="100%"
$width="100%" $width="100%"
$css={css` $css={css`
overflow-x: hidden; ${!isDesktop ? 'border: none;' : ''}
overflow-y: auto;
`} `}
$padding={{ $padding={{
top: 'base', top: 'base',
@@ -101,7 +96,7 @@ export const DocsGrid = ({
</Box> </Box>
)} )}
{hasDocs && ( {hasDocs && (
<Box $gap="6px"> <Box $gap="6px" $overflow="auto">
<Box <Box
$direction="row" $direction="row"
$padding={{ horizontal: 'xs' }} $padding={{ horizontal: 'xs' }}
@@ -122,27 +117,29 @@ export const DocsGrid = ({
)} )}
</Box> </Box>
{/* Body */}
{data?.pages.map((currentPage) => { {data?.pages.map((currentPage) => {
return currentPage.results.map((doc) => ( return currentPage.results.map((doc) => (
<DocsGridItem doc={doc} key={doc.id} /> <DocsGridItem doc={doc} key={doc.id} />
)); ));
})} })}
</Box>
)}
{hasNextPage && !loading && ( {hasNextPage && !loading && (
<InView <InView
data-testid="infinite-scroll-trigger" data-testid="infinite-scroll-trigger"
as="div" as="div"
onChange={loadMore} onChange={loadMore}
> >
{!isFetching && hasNextPage && ( {!isFetching && hasNextPage && (
<Button onClick={() => void fetchNextPage()} color="primary-text"> <Button
{t('More docs')} onClick={() => void fetchNextPage()}
</Button> color="primary-text"
>
{t('More docs')}
</Button>
)}
</InView>
)} )}
</InView> </Box>
)} )}
</Card> </Card>
</Box> </Box>

View File

@@ -54,6 +54,7 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
$css={css` $css={css`
flex: ${flexLeft}; flex: ${flexLeft};
align-items: center; align-items: center;
min-width: 0;
`} `}
href={`/docs/${doc.id}`} href={`/docs/${doc.id}`}
> >
@@ -64,6 +65,7 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
$gap={spacings.xs} $gap={spacings.xs}
$flex={flexLeft} $flex={flexLeft}
$padding={{ right: isDesktop ? 'md' : '3xs' }} $padding={{ right: isDesktop ? 'md' : '3xs' }}
$maxWidth="100%"
> >
<SimpleDocItem isPinned={doc.is_favorite} doc={doc} /> <SimpleDocItem isPinned={doc.is_favorite} doc={doc} />
{showAccesses && ( {showAccesses && (

View File

@@ -38,7 +38,7 @@ export const SimpleDocItem = ({
const { untitledDocument } = useTrans(); const { untitledDocument } = useTrans();
return ( return (
<Box $direction="row" $gap={spacings.sm}> <Box $direction="row" $gap={spacings.sm} $overflow="auto">
<Box <Box
$direction="row" $direction="row"
$align="center" $align="center"
@@ -53,7 +53,7 @@ export const SimpleDocItem = ({
<SimpleFileIcon aria-label={t('Simple document icon')} /> <SimpleFileIcon aria-label={t('Simple document icon')} />
)} )}
</Box> </Box>
<Box $justify="center"> <Box $justify="center" $overflow="auto">
<Text <Text
aria-describedby="doc-title" aria-describedby="doc-title"
aria-label={doc.title} aria-label={doc.title}

View File

@@ -55,7 +55,7 @@ export default function HomeBanner() {
$textAlign="center" $textAlign="center"
$margin="none" $margin="none"
$css={css` $css={css`
line-height: 56px; line-height: ${!isMobile ? '56px' : '45px'};
`} `}
> >
{t('Collaborative writing, Simplified.')} {t('Collaborative writing, Simplified.')}

View File

@@ -116,8 +116,8 @@ export const HomeSection = ({
`} `}
$variation="1000" $variation="1000"
$weight="bold" $weight="bold"
$size={!isSmallDevice ? 'xs-alt' : 'h4'} $size={!isSmallDevice ? 'xs-alt' : isSmallMobile ? 'h6' : 'h4'}
$textAlign={isSmallMobile ? 'center' : 'left'} $textAlign="left"
$margin="none" $margin="none"
> >
{title} {title}

View File

@@ -39,7 +39,7 @@ export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => {
`} `}
key={doc.id} key={doc.id}
> >
<StyledLink href={`/docs/${doc.id}`}> <StyledLink href={`/docs/${doc.id}`} $css="overflow: auto;">
<SimpleDocItem showAccesses doc={doc} /> <SimpleDocItem showAccesses doc={doc} />
</StyledLink> </StyledLink>
<div className="pinned-actions"> <div className="pinned-actions">

View File

@@ -19,8 +19,8 @@ export function MainLayout({
}: PropsWithChildren<MainLayoutProps>) { }: PropsWithChildren<MainLayoutProps>) {
const { isDesktop } = useResponsiveStore(); const { isDesktop } = useResponsiveStore();
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const colors = colorsTokens(); const colors = colorsTokens();
const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor;
return ( return (
<div> <div>
@@ -39,10 +39,10 @@ export function MainLayout({
$width="100%" $width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`} $height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
$padding={{ $padding={{
all: isDesktop ? 'base' : '2xs', all: isDesktop ? 'base' : '0',
}} }}
$background={ $background={
backgroundColor === 'white' currentBackgroundColor === 'white'
? colors['greyscale-000'] ? colors['greyscale-000']
: colors['greyscale-050'] : colors['greyscale-050']
} }