📱(frontend) docs mobile friendly

We adapt the docs component to be
mobile friendly.
This commit is contained in:
Anthony LC
2024-10-08 17:03:42 +02:00
committed by Anthony LC
parent 8dd7671d1f
commit c682bce6f6
26 changed files with 669 additions and 323 deletions

View File

@@ -16,6 +16,7 @@ and this project adheres to
- ✨(frontend) Activate versions feature #240 - ✨(frontend) Activate versions feature #240
- ✨(frontend) one-click document creation #275 - ✨(frontend) one-click document creation #275
- ✨(frontend) edit title inline #275 - ✨(frontend) edit title inline #275
- 📱(frontend) mobile responsive #304
- 🌐(frontend) Update translation #308 - 🌐(frontend) Update translation #308
## Changed ## Changed

View File

@@ -113,13 +113,14 @@ export const goToGridDoc = async (
const header = page.locator('header').first(); const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click(); await header.locator('h2').getByText('Docs').click();
const datagrid = page const datagrid = page.getByLabel('Datagrid of the documents page 1');
.getByLabel('Datagrid of the documents page 1') const datagridTable = datagrid.getByRole('table');
.getByRole('table');
await expect(datagrid.getByLabel('Loading data')).toBeHidden(); await expect(datagrid.getByLabel('Loading data')).toBeHidden({
timeout: 10000,
});
const rows = datagrid.getByRole('row'); const rows = datagridTable.getByRole('row');
const row = title const row = title
? rows.filter({ ? rows.filter({
hasText: title, hasText: title,
@@ -132,7 +133,7 @@ export const goToGridDoc = async (
expect(docTitle).toBeDefined(); expect(docTitle).toBeDefined();
await docTitleCell.click(); await row.getByRole('link').first().click();
return docTitle as string; return docTitle as string;
}; };

View File

@@ -18,12 +18,13 @@ test.describe('Doc Create', () => {
const header = page.locator('header').first(); const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click(); await header.locator('h2').getByText('Docs').click();
const datagrid = page const datagrid = page.getByLabel('Datagrid of the documents page 1');
.getByLabel('Datagrid of the documents page 1') const datagridTable = datagrid.getByRole('table');
.getByRole('table');
await expect(datagrid.getByLabel('Loading data')).toBeHidden(); await expect(datagrid.getByLabel('Loading data')).toBeHidden({
await expect(datagrid.getByText(docTitle)).toBeVisible({ timeout: 10000,
});
await expect(datagridTable.getByText(docTitle)).toBeVisible({
timeout: 5000, timeout: 5000,
}); });
}); });

View File

@@ -117,7 +117,9 @@ test.describe('Documents Grid', () => {
.getByRole('cell') .getByRole('cell')
.nth(cellNumber); .nth(cellNumber);
await expect(datagrid.getByLabel('Loading data')).toBeHidden(); await expect(datagrid.getByLabel('Loading data')).toBeHidden({
timeout: 10000,
});
// Initial state // Initial state
await expect(docNameRow1).toHaveText(/.*/); await expect(docNameRow1).toHaveText(/.*/);
@@ -134,7 +136,9 @@ test.describe('Documents Grid', () => {
const responseOrderingAsc = await responsePromiseOrderingAsc; const responseOrderingAsc = await responsePromiseOrderingAsc;
expect(responseOrderingAsc.ok()).toBeTruthy(); expect(responseOrderingAsc.ok()).toBeTruthy();
await expect(datagrid.getByLabel('Loading data')).toBeHidden(); await expect(datagrid.getByLabel('Loading data')).toBeHidden({
timeout: 10000,
});
await expect(docNameRow1).toHaveText(/.*/); await expect(docNameRow1).toHaveText(/.*/);
await expect(docNameRow2).toHaveText(/.*/); await expect(docNameRow2).toHaveText(/.*/);
@@ -155,7 +159,9 @@ test.describe('Documents Grid', () => {
const responseOrderingDesc = await responsePromiseOrderingDesc; const responseOrderingDesc = await responsePromiseOrderingDesc;
expect(responseOrderingDesc.ok()).toBeTruthy(); expect(responseOrderingDesc.ok()).toBeTruthy();
await expect(datagrid.getByLabel('Loading data')).toBeHidden(); await expect(datagrid.getByLabel('Loading data')).toBeHidden({
timeout: 10000,
});
await expect(docNameRow1).toHaveText(/.*/); await expect(docNameRow1).toHaveText(/.*/);
await expect(docNameRow2).toHaveText(/.*/); await expect(docNameRow2).toHaveText(/.*/);
@@ -244,3 +250,87 @@ test.describe('Documents Grid', () => {
await expect(datagrid.getByText(docName!)).toBeHidden(); await expect(datagrid.getByText(docName!)).toBeHidden();
}); });
}); });
test.describe('Documents Grid mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('it checks the grid when mobile', async ({ page }) => {
await page.route('**/documents/**', async (route) => {
const request = route.request();
if (request.method().includes('GET') && request.url().includes('page=')) {
await route.fulfill({
json: {
count: 1,
next: null,
previous: null,
results: [
{
id: 'b7fd9d9b-0642-4b4f-8617-ce50f69519ed',
title: 'My mocked document',
accesses: [
{
id: '8c1e047a-24e7-4a80-942b-8e9c7ab43e1f',
user: {
id: '7380f42f-02eb-4ad5-b8f0-037a0e66066d',
email: 'test@test.test',
full_name: 'John Doe',
short_name: 'John',
},
team: '',
role: 'owner',
abilities: {
destroy: false,
update: false,
partial_update: false,
retrieve: true,
set_role_to: [],
},
},
],
abilities: {
attachment_upload: true,
destroy: true,
link_configuration: true,
manage_accesses: true,
partial_update: true,
retrieve: true,
update: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
},
link_role: 'reader',
link_reach: 'public',
created_at: '2024-10-07T13:02:41.085298Z',
updated_at: '2024-10-07T13:30:21.829690Z',
},
],
},
});
} else {
await route.continue();
}
});
await page.goto('/');
const datagrid = page.getByLabel('Datagrid of the documents page 1');
const tableDatagrid = datagrid.getByRole('table');
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
timeout: 10000,
});
const rows = tableDatagrid.getByRole('row');
const row = rows.filter({
hasText: 'My mocked document',
});
await expect(row.getByRole('cell').nth(0)).toHaveText('My mocked document');
await expect(row.getByRole('cell').nth(1)).toHaveText('Public');
});
});

View File

@@ -385,3 +385,35 @@ test.describe('Doc Header', () => {
).toBeHidden(); ).toBeHidden();
}); });
}); });
test.describe('Documents Header mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('it checks the close button on Share modal', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: true, // Means owner
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
manage_accesses: true,
update: true,
partial_update: true,
retrieve: true,
},
});
await goToGridDoc(page);
await page.getByRole('button', { name: 'Share' }).click();
await expect(page.getByLabel('Share modal')).toBeVisible();
await page.getByRole('button', { name: 'close' }).click();
await expect(page.getByLabel('Share modal')).toBeHidden();
});
});

View File

@@ -8,6 +8,8 @@ test.beforeEach(async ({ page }) => {
test.describe('Doc Table Content', () => { test.describe('Doc Table Content', () => {
test('it checks the doc table content', async ({ page, browserName }) => { test('it checks the doc table content', async ({ page, browserName }) => {
test.setTimeout(60000);
const [randomDoc] = await createDoc( const [randomDoc] = await createDoc(
page, page,
'doc-table-content', 'doc-table-content',
@@ -37,7 +39,7 @@ test.describe('Doc Table Content', () => {
await page.locator('.bn-block-outer').last().click(); await page.locator('.bn-block-outer').last().click();
// Create space to fill the viewport // Create space to fill the viewport
for (let i = 0; i < 5; i++) { for (let i = 0; i < 10; i++) {
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
} }
@@ -48,7 +50,7 @@ test.describe('Doc Table Content', () => {
await page.locator('.bn-block-outer').last().click(); await page.locator('.bn-block-outer').last().click();
// Create space to fill the viewport // Create space to fill the viewport
for (let i = 0; i < 5; i++) { for (let i = 0; i < 10; i++) {
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
} }
@@ -61,11 +63,11 @@ test.describe('Doc Table Content', () => {
const another = panel.getByText('Another World'); const another = panel.getByText('Another World');
await expect(hello).toBeVisible(); await expect(hello).toBeVisible();
await expect(hello).toHaveCSS('font-size', /19/); await expect(hello).toHaveCSS('font-size', /17/);
await expect(hello).toHaveAttribute('aria-selected', 'true'); await expect(hello).toHaveAttribute('aria-selected', 'true');
await expect(superW).toBeVisible(); await expect(superW).toBeVisible();
await expect(superW).toHaveCSS('font-size', /16/); await expect(superW).toHaveCSS('font-size', /14/);
await expect(superW).toHaveAttribute('aria-selected', 'false'); await expect(superW).toHaveAttribute('aria-selected', 'false');
await expect(another).toBeVisible(); await expect(another).toBeVisible();

View File

@@ -23,7 +23,9 @@ test.describe('Doc Visibility', () => {
.getByLabel('Datagrid of the documents page 1') .getByLabel('Datagrid of the documents page 1')
.getByRole('table'); .getByRole('table');
await expect(datagrid.getByLabel('Loading data')).toBeHidden(); await expect(datagrid.getByLabel('Loading data')).toBeHidden({
timeout: 10000,
});
await expect(datagrid.getByText(docTitle)).toBeVisible(); await expect(datagrid.getByText(docTitle)).toBeVisible();

View File

@@ -507,6 +507,23 @@ input:-webkit-autofill:focus {
overflow-y: auto; overflow-y: auto;
} }
@media screen and (width <= 420px) {
.c__modal__scroller {
padding: 0.7rem;
}
.c__modal__title h2 {
font-size: 1rem;
}
}
@media (width <= 576px) {
.c__modal__footer--sided {
gap: 0.5rem;
flex-direction: column-reverse;
}
}
/** /**
* Toast * Toast
*/ */

View File

@@ -18,10 +18,14 @@ import { randomColor } from '../utils';
import { BlockNoteToolbar } from './BlockNoteToolbar'; import { BlockNoteToolbar } from './BlockNoteToolbar';
const cssEditor = ` const cssEditor = (readonly: boolean) => `
&, & > .bn-container, & .ProseMirror { &, & > .bn-container, & .ProseMirror {
height:100% height:100%
}; };
& .bn-editor {
padding-right: 30px;
${readonly && `padding-left: 30px;`}
};
& .collaboration-cursor__caret.ProseMirror-widget{ & .collaboration-cursor__caret.ProseMirror-widget{
word-wrap: initial; word-wrap: initial;
} }
@@ -30,6 +34,35 @@ const cssEditor = `
padding: 2px; padding: 2px;
border-radius: 4px; border-radius: 4px;
} }
@media screen and (width <= 560px) {
& .bn-editor {
padding-left: 40px;
padding-right: 10px;
${readonly && `padding-left: 10px;`}
};
.bn-side-menu[data-block-type=heading][data-level="1"] {
height: 46px;
}
.bn-side-menu[data-block-type=heading][data-level="2"] {
height: 40px;
}
.bn-side-menu[data-block-type=heading][data-level="3"] {
height: 40px;
}
& .bn-editor h1 {
font-size: 1.6rem;
}
& .bn-editor h2 {
font-size: 1.35rem;
}
& .bn-editor h3 {
font-size: 1.2rem;
}
.bn-block-content[data-is-empty-and-focused][data-content-type="paragraph"]
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
}
}
`; `;
interface BlockNoteEditorProps { interface BlockNoteEditorProps {
@@ -70,8 +103,9 @@ export const BlockNoteContent = ({
const isVersion = doc.id !== storeId; const isVersion = doc.id !== storeId;
const { userData } = useAuthStore(); const { userData } = useAuthStore();
const { setStore, docsStore } = useDocStore(); const { setStore, docsStore } = useDocStore();
const canSave = doc.abilities.partial_update && !isVersion;
useSaveDoc(doc.id, provider.document, canSave); const readOnly = !doc.abilities.partial_update || isVersion;
useSaveDoc(doc.id, provider.document, !readOnly);
const storedEditor = docsStore?.[storeId]?.editor; const storedEditor = docsStore?.[storeId]?.editor;
const { const {
mutateAsync: createDocAttachment, mutateAsync: createDocAttachment,
@@ -130,7 +164,7 @@ export const BlockNoteContent = ({
}, [editor, resetHeadings, setHeadings]); }, [editor, resetHeadings, setHeadings]);
return ( return (
<Box $css={cssEditor}> <Box $css={cssEditor(readOnly)}>
{isErrorAttachment && ( {isErrorAttachment && (
<Box $margin={{ bottom: 'big' }}> <Box $margin={{ bottom: 'big' }}>
<TextErrors <TextErrors
@@ -144,7 +178,7 @@ export const BlockNoteContent = ({
<BlockNoteView <BlockNoteView
editor={editor} editor={editor}
formattingToolbar={false} formattingToolbar={false}
editable={doc.abilities.partial_update && !isVersion} editable={!readOnly}
theme="light" theme="light"
> >
<BlockNoteToolbar /> <BlockNoteToolbar />

View File

@@ -9,6 +9,7 @@ import { useCunninghamTheme } from '@/cunningham';
import { DocHeader } from '@/features/docs/doc-header'; import { DocHeader } from '@/features/docs/doc-header';
import { Doc } from '@/features/docs/doc-management'; import { Doc } from '@/features/docs/doc-management';
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/'; import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
import { useResponsiveStore } from '@/stores';
import { useHeadingStore } from '../stores'; import { useHeadingStore } from '../stores';
@@ -25,6 +26,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
} = useRouter(); } = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { headings } = useHeadingStore(); const { headings } = useHeadingStore();
const { isMobile } = useResponsiveStore();
const isVersion = versionId && typeof versionId === 'string'; const isVersion = versionId && typeof versionId === 'string';
@@ -51,11 +53,12 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
$background={colorsTokens()['primary-bg']} $background={colorsTokens()['primary-bg']}
$height="100%" $height="100%"
$direction="row" $direction="row"
$margin={{ all: 'small', top: 'none' }} $margin={{ all: isMobile ? 'tiny' : 'small', top: 'none' }}
$css="overflow-x: clip;" $css="overflow-x: clip;"
$position="relative"
> >
<Card <Card
$padding="big" $padding={isMobile ? 'small' : 'big'}
$css="flex:1;" $css="flex:1;"
$overflow="auto" $overflow="auto"
$position="relative" $position="relative"
@@ -65,7 +68,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
) : ( ) : (
<BlockNoteEditor doc={doc} /> <BlockNoteEditor doc={doc} />
)} )}
<IconOpenPanelEditor headings={headings} /> {!isMobile && <IconOpenPanelEditor headings={headings} />}
</Card> </Card>
<PanelEditor doc={doc} headings={headings} /> <PanelEditor doc={doc} headings={headings} />
</Box> </Box>

View File

@@ -6,6 +6,7 @@ import { useCunninghamTheme } from '@/cunningham';
import { Doc } from '@/features/docs/doc-management'; import { Doc } from '@/features/docs/doc-management';
import { TableContent } from '@/features/docs/doc-table-content'; import { TableContent } from '@/features/docs/doc-table-content';
import { VersionList } from '@/features/docs/doc-versioning'; import { VersionList } from '@/features/docs/doc-versioning';
import { useResponsiveStore } from '@/stores';
import { usePanelEditorStore } from '../stores'; import { usePanelEditorStore } from '../stores';
import { HeadingBlock } from '../types'; import { HeadingBlock } from '../types';
@@ -21,7 +22,7 @@ export const PanelEditor = ({
}: PropsWithChildren<PanelProps>) => { }: PropsWithChildren<PanelProps>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const { isMobile } = useResponsiveStore();
const { isPanelTableContentOpen, setIsPanelTableContentOpen, isPanelOpen } = const { isPanelTableContentOpen, setIsPanelTableContentOpen, isPanelOpen } =
usePanelEditorStore(); usePanelEditorStore();
@@ -29,12 +30,12 @@ export const PanelEditor = ({
<Card <Card
$width="100%" $width="100%"
$maxWidth="20rem" $maxWidth="20rem"
$position="sticky" $position={isMobile ? 'absolute' : 'sticky'}
$maxHeight="99vh"
$height="100%" $height="100%"
$hasTransition="slow" $hasTransition="slow"
$css={` $css={`
top: 0vh; top: 0vh;
right: 0;
transform: translateX(0%); transform: translateX(0%);
flex: 1; flex: 1;
margin-left: 1rem; margin-left: 1rem;
@@ -60,8 +61,9 @@ export const PanelEditor = ({
top: 0; top: 0;
opacity: ${isPanelOpen ? '1' : '0'}; opacity: ${isPanelOpen ? '1' : '0'};
`} `}
$maxHeight="100%" $maxHeight="99vh"
> >
{isMobile && <IconOpenPanelEditor headings={headings} />}
<Box <Box
$direction="row" $direction="row"
$justify="space-between" $justify="space-between"
@@ -69,6 +71,7 @@ export const PanelEditor = ({
$position="relative" $position="relative"
$background={colorsTokens()['primary-400']} $background={colorsTokens()['primary-400']}
$margin={{ bottom: 'tiny' }} $margin={{ bottom: 'tiny' }}
$radius="4px 4px 0 0"
> >
<Box <Box
$background="white" $background="white"
@@ -78,7 +81,15 @@ export const PanelEditor = ({
$hasTransition="slow" $hasTransition="slow"
$css={` $css={`
border-top: 2px solid ${colorsTokens()['primary-600']}; border-top: 2px solid ${colorsTokens()['primary-600']};
${isPanelTableContentOpen ? 'transform: translateX(0);' : 'transform: translateX(100%);'} border-radius: 0 4px 0 0;
${
isPanelTableContentOpen
? `
transform: translateX(0);
border-radius: 4px 0 0 0;
`
: `transform: translateX(100%);`
}
`} `}
/> />
<BoxButton <BoxButton
@@ -134,6 +145,7 @@ export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => {
const { setIsPanelOpen, isPanelOpen, setIsPanelTableContentOpen } = const { setIsPanelOpen, isPanelOpen, setIsPanelTableContentOpen } =
usePanelEditorStore(); usePanelEditorStore();
const [hasBeenOpen, setHasBeenOpen] = useState(isPanelOpen); const [hasBeenOpen, setHasBeenOpen] = useState(isPanelOpen);
const { isMobile } = useResponsiveStore();
const setClosePanel = () => { const setClosePanel = () => {
setHasBeenOpen(true); setHasBeenOpen(true);
@@ -142,12 +154,18 @@ export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => {
// Open the panel if there are more than 1 heading // Open the panel if there are more than 1 heading
useEffect(() => { useEffect(() => {
if (headings?.length && headings.length > 1 && !hasBeenOpen) { if (headings?.length && headings.length > 1 && !hasBeenOpen && !isMobile) {
setIsPanelTableContentOpen(true); setIsPanelTableContentOpen(true);
setIsPanelOpen(true); setIsPanelOpen(true);
setHasBeenOpen(true); setHasBeenOpen(true);
} }
}, [headings, setIsPanelTableContentOpen, setIsPanelOpen, hasBeenOpen]); }, [
headings,
setIsPanelTableContentOpen,
setIsPanelOpen,
hasBeenOpen,
isMobile,
]);
// If open from the doc header we set the state as well // If open from the doc header we set the state as well
useEffect(() => { useEffect(() => {
@@ -169,7 +187,7 @@ export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => {
aria-label={isPanelOpen ? t('Close the panel') : t('Open the panel')} aria-label={isPanelOpen ? t('Close the panel') : t('Open the panel')}
$background="transparent" $background="transparent"
$size="h2" $size="h2"
$zIndex={1} $zIndex={10}
$hasTransition="slow" $hasTransition="slow"
$css={` $css={`
cursor: pointer; cursor: pointer;

View File

@@ -1,5 +1,4 @@
import { Button } from '@openfun/cunningham-react'; import React, { Fragment } from 'react';
import React, { Fragment, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, Card, StyledLink, Text } from '@/components'; import { Box, Card, StyledLink, Text } from '@/components';
@@ -10,8 +9,9 @@ import {
currentDocRole, currentDocRole,
useTrans, useTrans,
} from '@/features/docs/doc-management'; } from '@/features/docs/doc-management';
import { ModalVersion, Versions } from '@/features/docs/doc-versioning'; import { Versions } from '@/features/docs/doc-versioning';
import { useDate } from '@/hook'; import { useDate } from '@/hook';
import { useResponsiveStore } from '@/stores';
import { DocTagPublic } from './DocTagPublic'; import { DocTagPublic } from './DocTagPublic';
import { DocTitle } from './DocTitle'; import { DocTitle } from './DocTitle';
@@ -27,15 +27,19 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { formatDate } = useDate(); const { formatDate } = useDate();
const { transRole } = useTrans(); const { transRole } = useTrans();
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false); const { isMobile, isSmallMobile } = useResponsiveStore();
return ( return (
<> <>
<Card <Card
$margin="small" $margin={isMobile ? 'tiny' : 'small'}
aria-label={t('It is the card information about the document.')} aria-label={t('It is the card information about the document.')}
> >
<Box $padding="small" $direction="row" $align="center"> <Box
$padding={isMobile ? 'tiny' : 'small'}
$direction="row"
$align="center"
>
<StyledLink href="/"> <StyledLink href="/">
<Text <Text
$isMaterialIcon $isMaterialIcon
@@ -53,33 +57,39 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
$width="1px" $width="1px"
$height="70%" $height="70%"
$background={colorsTokens()['greyscale-100']} $background={colorsTokens()['greyscale-100']}
$margin={{ horizontal: 'small' }} $margin={{ horizontal: 'tiny' }}
/> />
<Box $gap="1rem" $direction="row" $align="center"> <Box
$direction="row"
$justify="space-between"
$css="flex:1;"
$gap="0.5rem 1rem"
$wrap="wrap"
$align="center"
>
<DocTitle doc={doc} /> <DocTitle doc={doc} />
{versionId && ( <DocToolBox doc={doc} versionId={versionId} />
<Button
onClick={() => {
setIsModalVersionOpen(true);
}}
size="small"
>
{t('Restore this version')}
</Button>
)}
</Box> </Box>
<DocToolBox doc={doc} />
</Box> </Box>
<Box <Box
$direction="row" $direction={isSmallMobile ? 'column' : 'row'}
$align="center" $align={isSmallMobile ? 'start' : 'center'}
$css="border-top:1px solid #eee" $css="border-top:1px solid #eee"
$padding={{ horizontal: 'big', vertical: 'tiny' }} $padding={{
horizontal: isMobile ? 'tiny' : 'big',
vertical: 'tiny',
}}
$gap="0.5rem 2rem" $gap="0.5rem 2rem"
$justify="space-between" $justify="space-between"
$wrap="wrap" $wrap="wrap"
$position="relative"
> >
<Box $direction="row" $align="center" $gap="0.5rem 2rem" $wrap="wrap"> <Box
$direction={isSmallMobile ? 'column' : 'row'}
$align={isSmallMobile ? 'start' : 'center'}
$gap="0.5rem 2rem"
$wrap="wrap"
>
<DocTagPublic doc={doc} /> <DocTagPublic doc={doc} />
<Text $size="s" $display="inline"> <Text $size="s" $display="inline">
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong> {t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
@@ -106,13 +116,6 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
</Text> </Text>
</Box> </Box>
</Card> </Card>
{isModalVersionOpen && versionId && (
<ModalVersion
onClose={() => setIsModalVersionOpen(false)}
docId={doc.id}
versionId={versionId}
/>
)}
</> </>
); );
}; };

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Text } from '@/components'; import { Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { Doc, LinkReach } from '@/features/docs/doc-management'; import { Doc, LinkReach } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
interface DocTagPublicProps { interface DocTagPublicProps {
doc: Doc; doc: Doc;
@@ -11,6 +12,7 @@ interface DocTagPublicProps {
export const DocTagPublic = ({ doc }: DocTagPublicProps) => { export const DocTagPublic = ({ doc }: DocTagPublicProps) => {
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const { isSmallMobile } = useResponsiveStore();
if (doc?.link_reach !== LinkReach.PUBLIC) { if (doc?.link_reach !== LinkReach.PUBLIC) {
return null; return null;
@@ -24,6 +26,8 @@ export const DocTagPublic = ({ doc }: DocTagPublicProps) => {
$padding="xtiny" $padding="xtiny"
$radius="3px" $radius="3px"
$size="s" $size="s"
$position={isSmallMobile ? 'absolute' : 'initial'}
$css={isSmallMobile ? 'right: 10px;' : ''}
> >
{t('Public')} {t('Public')}
</Text> </Text>

View File

@@ -19,6 +19,7 @@ import {
useTrans, useTrans,
useUpdateDoc, useUpdateDoc,
} from '@/features/docs/doc-management'; } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { isFirefox } from '@/utils/userAgent'; import { isFirefox } from '@/utils/userAgent';
const DocTitleStyle = createGlobalStyle` const DocTitleStyle = createGlobalStyle`
@@ -32,9 +33,15 @@ interface DocTitleProps {
} }
export const DocTitle = ({ doc }: DocTitleProps) => { export const DocTitle = ({ doc }: DocTitleProps) => {
const { isMobile } = useResponsiveStore();
if (!doc.abilities.partial_update) { if (!doc.abilities.partial_update) {
return ( return (
<Text as="h2" $align="center" $margin={{ all: 'none', left: 'tiny' }}> <Text
as="h2"
$margin={{ all: 'none', left: 'tiny' }}
$size={isMobile ? 'h4' : 'h2'}
>
{doc.title} {doc.title}
</Text> </Text>
); );
@@ -53,6 +60,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
const { headings } = useHeadingStore(); const { headings } = useHeadingStore();
const headingText = headings?.[0]?.contentText; const headingText = headings?.[0]?.contentText;
const debounceRef = useRef<NodeJS.Timeout>(); const debounceRef = useRef<NodeJS.Timeout>();
const { isMobile } = useResponsiveStore();
const { mutate: updateDoc } = useUpdateDoc({ const { mutate: updateDoc } = useUpdateDoc({
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC], listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
@@ -124,7 +132,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
as="h2" as="h2"
$radius="4px" $radius="4px"
$padding={{ horizontal: 'tiny', vertical: '4px' }} $padding={{ horizontal: 'tiny', vertical: '4px' }}
$align="center"
$margin="none" $margin="none"
contentEditable={isFirefox() ? 'true' : 'plaintext-only'} contentEditable={isFirefox() ? 'true' : 'plaintext-only'}
onClick={handleOnClick} onClick={handleOnClick}
@@ -141,7 +148,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
$css={` $css={`
${isUntitled && 'font-style: italic;'} ${isUntitled && 'font-style: italic;'}
cursor: text; cursor: text;
font-size: 1.5rem; font-size: ${isMobile ? '1.2rem' : '1.5rem'};
transition: box-shadow 0.5s, border-color 0.5s; transition: box-shadow 0.5s, border-color 0.5s;
border: 1px dashed transparent; border: 1px dashed transparent;

View File

@@ -9,98 +9,121 @@ import {
ModalRemoveDoc, ModalRemoveDoc,
ModalShare, ModalShare,
} from '@/features/docs/doc-management'; } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { ModalVersion, Versions } from '../../doc-versioning';
import { ModalPDF } from './ModalExport'; import { ModalPDF } from './ModalExport';
interface DocToolBoxProps { interface DocToolBoxProps {
doc: Doc; doc: Doc;
versionId?: Versions['version_id'];
} }
export const DocToolBox = ({ doc }: DocToolBoxProps) => { export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isModalShareOpen, setIsModalShareOpen] = useState(false); const [isModalShareOpen, setIsModalShareOpen] = useState(false);
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false); const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false); const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
const [isDropOpen, setIsDropOpen] = useState(false); const [isDropOpen, setIsDropOpen] = useState(false);
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore(); const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
const { isSmallMobile } = useResponsiveStore();
return ( return (
<Box <Box
$margin={{ left: 'auto' }} $margin={{ left: 'auto' }}
$direction="row" $direction="row"
$align="center" $align="center"
$gap="1rem" $gap="0.5rem 1.5rem"
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
> >
<Button {versionId && (
onClick={() => { <Box $margin={{ left: 'auto' }}>
setIsModalShareOpen(true); <Button
}} onClick={() => {
> setIsModalVersionOpen(true);
{t('Share')} }}
</Button> color="secondary"
<DropButton size={isSmallMobile ? 'small' : 'medium'}
button={ >
<IconOptions {t('Restore this version')}
isOpen={isDropOpen} </Button>
aria-label={t('Open the document options')} </Box>
/> )}
} <Box $direction="row" $margin={{ left: 'auto' }} $gap="1rem">
onOpenChange={(isOpen) => setIsDropOpen(isOpen)} <Button
isOpen={isDropOpen} onClick={() => {
> setIsModalShareOpen(true);
<Box> }}
{doc.abilities.versions_list && ( size={isSmallMobile ? 'small' : 'medium'}
>
{t('Share')}
</Button>
<DropButton
button={
<IconOptions
isOpen={isDropOpen}
aria-label={t('Open the document options')}
/>
}
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
isOpen={isDropOpen}
>
<Box>
{doc.abilities.versions_list && (
<Button
onClick={() => {
setIsPanelOpen(true);
setIsPanelTableContentOpen(false);
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">history</span>}
size="small"
>
<Text $theme="primary">{t('Version history')}</Text>
</Button>
)}
<Button <Button
onClick={() => { onClick={() => {
setIsPanelOpen(true); setIsPanelOpen(true);
setIsPanelTableContentOpen(false); setIsPanelTableContentOpen(true);
setIsDropOpen(false); setIsDropOpen(false);
}} }}
color="primary-text" color="primary-text"
icon={<span className="material-icons">history</span>} icon={<span className="material-icons">summarize</span>}
size="small" size="small"
> >
<Text $theme="primary">{t('Version history')}</Text> <Text $theme="primary">{t('Table of contents')}</Text>
</Button> </Button>
)}
<Button
onClick={() => {
setIsPanelOpen(true);
setIsPanelTableContentOpen(true);
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">summarize</span>}
size="small"
>
<Text $theme="primary">{t('Table of contents')}</Text>
</Button>
<Button
onClick={() => {
setIsModalPDFOpen(true);
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">file_download</span>}
size="small"
>
<Text $theme="primary">{t('Export')}</Text>
</Button>
{doc.abilities.destroy && (
<Button <Button
onClick={() => { onClick={() => {
setIsModalRemoveOpen(true); setIsModalPDFOpen(true);
setIsDropOpen(false); setIsDropOpen(false);
}} }}
color="primary-text" color="primary-text"
icon={<span className="material-icons">delete</span>} icon={<span className="material-icons">file_download</span>}
size="small" size="small"
> >
<Text $theme="primary">{t('Delete document')}</Text> <Text $theme="primary">{t('Export')}</Text>
</Button> </Button>
)} {doc.abilities.destroy && (
</Box> <Button
</DropButton> onClick={() => {
setIsModalRemoveOpen(true);
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">delete</span>}
size="small"
>
<Text $theme="primary">{t('Delete document')}</Text>
</Button>
)}
</Box>
</DropButton>
</Box>
{isModalShareOpen && ( {isModalShareOpen && (
<ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} /> <ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} />
)} )}
@@ -110,6 +133,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
{isModalRemoveOpen && ( {isModalRemoveOpen && (
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} /> <ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
)} )}
{isModalVersionOpen && versionId && (
<ModalVersion
onClose={() => setIsModalVersionOpen(false)}
docId={doc.id}
versionId={versionId}
/>
)}
</Box> </Box>
); );
}; };

View File

@@ -43,48 +43,57 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
$direction="row" $direction="row"
$align="center" $align="center"
$justify="space-between" $justify="space-between"
$gap="1rem"
> >
<Box $direction="row" $gap="1rem" $align="center"> <IconBG iconName="public" $margin="none" />
<IconBG iconName="public" $margin="none" /> <Box
<Switch $width="100%"
label={t(docPublic ? 'Doc public' : 'Doc private')} $wrap="wrap"
defaultChecked={docPublic} $gap="1rem"
onChange={() => { $justify="space-between"
api.mutate({ $direction="row"
id: doc.id,
link_reach: docPublic ? LinkReach.RESTRICTED : LinkReach.PUBLIC,
link_role: 'reader',
});
setDocPublic(!docPublic);
}}
disabled={!doc.abilities.link_configuration}
text={
docPublic
? t('Anyone on the internet with the link can view')
: t('Only for people with access')
}
/>
</Box>
<Button
onClick={() => {
navigator.clipboard
.writeText(window.location.href)
.then(() => {
toast(t('Link Copied !'), VariantType.SUCCESS, {
duration: 3000,
});
})
.catch(() => {
toast(t('Failed to copy link'), VariantType.ERROR, {
duration: 3000,
});
});
}}
color="primary"
icon={<span className="material-icons">copy</span>}
> >
{t('Copy link')} <Box $direction="row" $gap="1rem" $align="center">
</Button> <Switch
label={t(docPublic ? 'Doc public' : 'Doc private')}
defaultChecked={docPublic}
onChange={() => {
api.mutate({
id: doc.id,
link_reach: docPublic ? LinkReach.RESTRICTED : LinkReach.PUBLIC,
link_role: 'reader',
});
setDocPublic(!docPublic);
}}
disabled={!doc.abilities.link_configuration}
text={
docPublic
? t('Anyone on the internet with the link can view')
: t('Only for people with access')
}
/>
</Box>
<Button
onClick={() => {
navigator.clipboard
.writeText(window.location.href)
.then(() => {
toast(t('Link Copied !'), VariantType.SUCCESS, {
duration: 3000,
});
})
.catch(() => {
toast(t('Failed to copy link'), VariantType.ERROR, {
duration: 3000,
});
});
}}
color="primary"
icon={<span className="material-icons">copy</span>}
>
{t('Copy link')}
</Button>
</Box>
</Card> </Card>
); );
}; };

View File

@@ -5,6 +5,7 @@ import { Box, Card, SideModal, Text } from '@/components';
import { InvitationList } from '@/features/docs/members/invitation-list'; import { InvitationList } from '@/features/docs/members/invitation-list';
import { AddMembers } from '@/features/docs/members/members-add'; import { AddMembers } from '@/features/docs/members/members-add';
import { MemberList } from '@/features/docs/members/members-list'; import { MemberList } from '@/features/docs/members/members-list';
import { useResponsiveStore } from '@/stores';
import { Doc } from '../types'; import { Doc } from '../types';
import { currentDocRole } from '../utils'; import { currentDocRole } from '../utils';
@@ -20,6 +21,15 @@ const ModalShareStyle = createGlobalStyle`
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
.c__modal__close{
margin-right: 1rem;
button{
border-bottom: 1px solid #E0E0E0;
border-left: 1px solid #E0E0E0;
}
}
} }
`; `;
@@ -29,18 +39,21 @@ interface ModalShareProps {
} }
export const ModalShare = ({ onClose, doc }: ModalShareProps) => { export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
const { isMobile, isSmallMobile } = useResponsiveStore();
const width = isSmallMobile ? '100vw' : isMobile ? '90vw' : '70vw';
return ( return (
<> <>
<ModalShareStyle /> <ModalShareStyle />
<SideModal <SideModal
isOpen isOpen
closeOnClickOutside closeOnClickOutside
hideCloseButton hideCloseButton={!isSmallMobile}
onClose={onClose} onClose={onClose}
width="70vw" width={width}
$css="min-width: 320px;max-width: 777px;" $css="min-width: 320px;max-width: 777px;"
> >
<Box aria-label={t('Share modal')}> <Box aria-label={t('Share modal')} $margin={{ bottom: 'small' }}>
<Box $shrink="0"> <Box $shrink="0">
<Card <Card
$direction="row" $direction="row"

View File

@@ -3,10 +3,11 @@ import { useState } from 'react';
import { BoxButton, Text } from '@/components'; import { BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { useResponsiveStore } from '@/stores';
const sizeMap: { [key: number]: string } = { const sizeMap: { [key: number]: string } = {
1: '1.2rem', 1: '1.1rem',
2: '1rem', 2: '0.9rem',
3: '0.8rem', 3: '0.8rem',
}; };
@@ -32,6 +33,7 @@ export const Heading = ({
}: HeadingProps) => { }: HeadingProps) => {
const [isHover, setIsHover] = useState(isHighlight); const [isHover, setIsHover] = useState(isHighlight);
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const { isMobile } = useResponsiveStore();
return ( return (
<BoxButton <BoxButton
@@ -39,7 +41,11 @@ export const Heading = ({
onMouseOver={() => setIsHover(true)} onMouseOver={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)} onMouseLeave={() => setIsHover(false)}
onClick={() => { onClick={() => {
editor.focus(); // With mobile the focus open the keyboard and the scroll is not working
if (!isMobile) {
editor.focus();
}
editor.setTextCursorPosition(headingId, 'end'); editor.setTextCursorPosition(headingId, 'end');
document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({ document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Box, BoxButton, Text } from '@/components'; import { Box, BoxButton, Text } from '@/components';
import { HeadingBlock, useDocStore } from '@/features/docs/doc-editor'; import { HeadingBlock, useDocStore } from '@/features/docs/doc-editor';
import { Doc } from '@/features/docs/doc-management'; import { Doc } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { Heading } from './Heading'; import { Heading } from './Heading';
@@ -14,6 +15,7 @@ interface TableContentProps {
export const TableContent = ({ doc, headings }: TableContentProps) => { export const TableContent = ({ doc, headings }: TableContentProps) => {
const { docsStore } = useDocStore(); const { docsStore } = useDocStore();
const { isMobile } = useResponsiveStore();
const { t } = useTranslation(); const { t } = useTranslation();
const editor = docsStore?.[doc.id]?.editor; const editor = docsStore?.[doc.id]?.editor;
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>(); const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
@@ -66,17 +68,20 @@ export const TableContent = ({ doc, headings }: TableContentProps) => {
return ( return (
<Box $padding={{ all: 'small', right: 'none' }} $maxHeight="95%"> <Box $padding={{ all: 'small', right: 'none' }} $maxHeight="95%">
<Box $overflow="auto"> <Box $overflow="auto" $padding={{ left: '2px' }}>
{headings?.map((heading) => ( {headings?.map(
<Heading (heading) =>
editor={editor} heading.contentText && (
headingId={heading.id} <Heading
level={heading.props.level} editor={editor}
text={heading.contentText} headingId={heading.id}
key={heading.id} level={heading.props.level}
isHighlight={headingIdHighlight === heading.id} text={heading.contentText}
/> key={heading.id}
))} isHighlight={headingIdHighlight === heading.id}
/>
),
)}
</Box> </Box>
<Box <Box
$height="1px" $height="1px"
@@ -87,7 +92,11 @@ export const TableContent = ({ doc, headings }: TableContentProps) => {
/> />
<BoxButton <BoxButton
onClick={() => { onClick={() => {
editor.focus(); // With mobile the focus open the keyboard and the scroll is not working
if (!isMobile) {
editor.focus();
}
document.querySelector(`.bn-editor`)?.scrollIntoView({ document.querySelector(`.bn-editor`)?.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'start', block: 'start',
@@ -101,7 +110,11 @@ export const TableContent = ({ doc, headings }: TableContentProps) => {
</BoxButton> </BoxButton>
<BoxButton <BoxButton
onClick={() => { onClick={() => {
editor.focus(); // With mobile the focus open the keyboard and the scroll is not working
if (!isMobile) {
editor.focus();
}
document document
.querySelector( .querySelector(
`.bn-editor > .bn-block-group > .bn-block-outer:last-child`, `.bn-editor > .bn-block-group > .bn-block-outer:last-child`,

View File

@@ -1,4 +1,9 @@
import { DataGrid, SortModel, usePagination } from '@openfun/cunningham-react'; import {
Column,
DataGrid,
SortModel,
usePagination,
} from '@openfun/cunningham-react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components'; import { createGlobalStyle } from 'styled-components';
@@ -15,6 +20,7 @@ import {
useTrans, useTrans,
} from '@/features/docs/doc-management'; } from '@/features/docs/doc-management';
import { useDate } from '@/hook/'; import { useDate } from '@/hook/';
import { useResponsiveStore } from '@/stores';
import { PAGE_SIZE } from '../conf'; import { PAGE_SIZE } from '../conf';
@@ -62,6 +68,7 @@ export const DocsGrid = () => {
]); ]);
const { page, pageSize, setPagesCount } = pagination; const { page, pageSize, setPagesCount } = pagination;
const [docs, setDocs] = useState<Doc[]>([]); const [docs, setDocs] = useState<Doc[]>([]);
const { isMobile } = useResponsiveStore();
const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined; const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined;
@@ -82,19 +89,117 @@ export const DocsGrid = () => {
setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0); setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0);
}, [data?.count, pageSize, setPagesCount]); }, [data?.count, pageSize, setPagesCount]);
const columns: Column<Doc>[] = [
{
headerName: '',
id: 'visibility',
size: 95,
renderCell: ({ row }) => {
return (
row.link_reach === LinkReach.PUBLIC && (
<StyledLink href={`/docs/${row.id}`}>
<Text
$weight="bold"
$background={colorsTokens()['primary-600']}
$color="white"
$padding="xtiny"
$radius="3px"
>
{t('Public')}
</Text>
</StyledLink>
)
);
},
},
{
headerName: t('Document name'),
field: 'title',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold" $theme="primary">
{row.title}
</Text>
</StyledLink>
);
},
},
{
headerName: t('Created at'),
field: 'created_at',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold">{formatDate(row.created_at)}</Text>
</StyledLink>
);
},
},
{
headerName: t('Updated at'),
field: 'updated_at',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold">{formatDate(row.updated_at)}</Text>
</StyledLink>
);
},
},
{
headerName: t('Your role'),
id: 'your_role',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold">
{transRole(currentDocRole(row.abilities))}
</Text>
</StyledLink>
);
},
},
{
headerName: t('Members'),
id: 'users_number',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold">{row.accesses.length}</Text>
</StyledLink>
);
},
},
{
id: 'column-actions',
renderCell: ({ row }) => {
return <DocsGridActions doc={row} />;
},
},
];
// Inverse columns for mobile to have the most important information first
if (isMobile) {
const tmpCol = columns[0];
columns[0] = columns[1];
columns[1] = tmpCol;
}
return ( return (
<Card <Card
$padding={{ bottom: 'small', horizontal: 'big' }} $padding={{ bottom: 'small', horizontal: 'big' }}
$margin={{ all: 'big', top: 'none' }} $margin={{ all: isMobile ? 'small' : 'big', top: 'none' }}
$overflow="auto" $overflow="auto"
aria-label={t(`Datagrid of the documents page {{page}}`, { page })} aria-label={t(`Datagrid of the documents page {{page}}`, { page })}
$height="100%"
> >
<DocsGridStyle /> <DocsGridStyle />
<Text <Text
$weight="bold" $weight="bold"
as="h2" as="h2"
$theme="primary" $theme="primary"
$margin={{ bottom: 'none' }} $margin={{ bottom: 'small' }}
> >
{t('Documents')} {t('Documents')}
</Text> </Text>
@@ -102,95 +207,7 @@ export const DocsGrid = () => {
{error && <TextErrors causes={error.cause} />} {error && <TextErrors causes={error.cause} />}
<DataGrid <DataGrid
columns={[ columns={columns}
{
headerName: '',
id: 'visibility',
size: 95,
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
{row.link_reach === LinkReach.PUBLIC && (
<Text
$weight="bold"
$background={colorsTokens()['primary-600']}
$color="white"
$padding="xtiny"
$radius="3px"
>
{t('Public')}
</Text>
)}
</StyledLink>
);
},
},
{
headerName: t('Document name'),
field: 'title',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold" $theme="primary">
{row.title}
</Text>
</StyledLink>
);
},
},
{
headerName: t('Created at'),
field: 'created_at',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold">{formatDate(row.created_at)}</Text>
</StyledLink>
);
},
},
{
headerName: t('Updated at'),
field: 'updated_at',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold">{formatDate(row.updated_at)}</Text>
</StyledLink>
);
},
},
{
headerName: t('Your role'),
id: 'your_role',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold">
{transRole(currentDocRole(row.abilities))}
</Text>
</StyledLink>
);
},
},
{
headerName: t('Members'),
id: 'users_number',
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
<Text $weight="bold">{row.accesses.length}</Text>
</StyledLink>
);
},
},
{
id: 'column-actions',
renderCell: ({ row }) => {
return <DocsGridActions doc={row} />;
},
},
]}
rows={docs} rows={docs}
isLoading={isLoading} isLoading={isLoading}
pagination={pagination} pagination={pagination}

View File

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { Box } from '@/components'; import { Box } from '@/components';
import { useCreateDoc, useTrans } from '@/features/docs/doc-management/'; import { useCreateDoc, useTrans } from '@/features/docs/doc-management/';
import { useResponsiveStore } from '@/stores';
import { DocsGrid } from './DocsGrid'; import { DocsGrid } from './DocsGrid';
@@ -12,6 +13,7 @@ export const DocsGridContainer = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { untitledDocument } = useTrans(); const { untitledDocument } = useTrans();
const router = useRouter(); const router = useRouter();
const { isMobile } = useResponsiveStore();
const { mutate: createDoc } = useCreateDoc({ const { mutate: createDoc } = useCreateDoc({
onSuccess: (doc) => { onSuccess: (doc) => {
@@ -25,7 +27,11 @@ export const DocsGridContainer = () => {
return ( return (
<Box $overflow="auto"> <Box $overflow="auto">
<Box $align="flex-end" $justify="center" $margin="big"> <Box
$align="flex-end"
$justify="center"
$margin={isMobile ? 'small' : 'big'}
>
<Button onClick={handleCreateDoc}>{t('Create a new document')}</Button> <Button onClick={handleCreateDoc}>{t('Create a new document')}</Button>
</Box> </Box>
<DocsGrid /> <DocsGrid />

View File

@@ -11,6 +11,7 @@ import { Box, IconBG, Text, TextErrors } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { Doc, Role } from '@/features/docs/doc-management'; import { Doc, Role } from '@/features/docs/doc-management';
import { ChooseRole } from '@/features/docs/members/members-add/'; import { ChooseRole } from '@/features/docs/members/members-add/';
import { useResponsiveStore } from '@/stores';
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api'; import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
import { Invitation } from '../types'; import { Invitation } from '../types';
@@ -31,6 +32,7 @@ export const InvitationItem = ({
const canDelete = invitation.abilities.destroy; const canDelete = invitation.abilities.destroy;
const canUpdate = invitation.abilities.partial_update; const canUpdate = invitation.abilities.partial_update;
const { t } = useTranslation(); const { t } = useTranslation();
const { isSmallMobile, screenWidth } = useResponsiveStore();
const [localRole, setLocalRole] = useState(role); const [localRole, setLocalRole] = useState(role);
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const { toast } = useToastProvider(); const { toast } = useToastProvider();
@@ -62,8 +64,7 @@ export const InvitationItem = ({
return ( return (
<Box $width="100%" $gap="0.7rem"> <Box $width="100%" $gap="0.7rem">
<Box $direction="row" $gap="1rem"> <Box $direction="row" $gap="1rem" $wrap="wrap">
<IconBG iconName="account_circle" $size="2rem" />
<Box <Box
$align="center" $align="center"
$direction="row" $direction="row"
@@ -71,8 +72,10 @@ export const InvitationItem = ({
$justify="space-between" $justify="space-between"
$width="100%" $width="100%"
$wrap="wrap" $wrap="wrap"
$css={`flex: ${isSmallMobile ? '100%' : '70%'};`}
> >
<Box> <IconBG iconName="account_circle" $size="2rem" />
<Box $css="flex:1;">
<Text <Text
$size="t" $size="t"
$background={colorsTokens()['info-600']} $background={colorsTokens()['info-600']}
@@ -85,8 +88,15 @@ export const InvitationItem = ({
</Text> </Text>
<Text $justify="center">{invitation.email}</Text> <Text $justify="center">{invitation.email}</Text>
</Box> </Box>
<Box $direction="row" $gap="1rem" $align="center"> <Box
<Box $minWidth="13rem"> $direction="row"
$gap="1rem"
$align="center"
$justify="space-between"
$css="flex:1;"
$wrap={screenWidth < 400 ? 'wrap' : 'nowrap'}
>
<Box $minWidth="13rem" $css={isSmallMobile ? 'flex:1;' : ''}>
<ChooseRole <ChooseRole
label={t('Role')} label={t('Role')}
defaultRole={localRole} defaultRole={localRole}
@@ -103,25 +113,27 @@ export const InvitationItem = ({
/> />
</Box> </Box>
{doc.abilities.manage_accesses && ( {doc.abilities.manage_accesses && (
<Button <Box $margin={isSmallMobile ? 'auto' : ''}>
color="tertiary-text" <Button
icon={ color="tertiary-text"
<Text icon={
$isMaterialIcon <Text
$theme={!canDelete ? 'greyscale' : 'primary'} $isMaterialIcon
$variation={!canDelete ? '500' : 'text'} $theme={!canDelete ? 'greyscale' : 'primary'}
> $variation={!canDelete ? '500' : 'text'}
delete >
</Text> delete
} </Text>
disabled={!canDelete} }
onClick={() => disabled={!canDelete}
removeDocInvitation({ onClick={() =>
docId: doc.id, removeDocInvitation({
invitationId: invitation.id, docId: doc.id,
}) invitationId: invitation.id,
} })
/> }
/>
</Box>
)} )}
</Box> </Box>
</Box> </Box>

View File

@@ -6,6 +6,7 @@ import { APIError } from '@/api';
import { Box, Card, InfiniteScroll, TextErrors } from '@/components'; import { Box, Card, InfiniteScroll, TextErrors } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { Doc, currentDocRole } from '@/features/docs/doc-management'; import { Doc, currentDocRole } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { useDocInvitationsInfinite } from '../api'; import { useDocInvitationsInfinite } from '../api';
import { Invitation } from '../types'; import { Invitation } from '../types';
@@ -26,6 +27,7 @@ const InvitationListState = ({
doc, doc,
}: InvitationListStateProps) => { }: InvitationListStateProps) => {
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const { isSmallMobile } = useResponsiveStore();
if (error) { if (error) {
return <TextErrors causes={error.cause} />; return <TextErrors causes={error.cause} />;
@@ -49,7 +51,7 @@ const InvitationListState = ({
key={`${invitation.id}-${index}`} key={`${invitation.id}-${index}`}
$background={!(index % 2) ? 'white' : colorsTokens()['greyscale-000']} $background={!(index % 2) ? 'white' : colorsTokens()['greyscale-000']}
$direction="row" $direction="row"
$padding="small" $padding={isSmallMobile ? 'tiny' : 'small'}
$align="center" $align="center"
$gap="1rem" $gap="1rem"
$radius="4px" $radius="4px"

View File

@@ -11,6 +11,7 @@ import { Box, Card, IconBG } from '@/components';
import { Doc, Role } from '@/features/docs/doc-management'; import { Doc, Role } from '@/features/docs/doc-management';
import { useCreateDocInvitation } from '@/features/docs/members/invitation-list/'; import { useCreateDocInvitation } from '@/features/docs/members/invitation-list/';
import { useLanguage } from '@/i18n/hooks/useLanguage'; import { useLanguage } from '@/i18n/hooks/useLanguage';
import { useResponsiveStore } from '@/stores';
import { useCreateDocAccess } from '../api'; import { useCreateDocAccess } from '../api';
import { import {
@@ -37,6 +38,7 @@ interface ModalAddMembersProps {
export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => { export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
const { contentLanguage } = useLanguage(); const { contentLanguage } = useLanguage();
const { t } = useTranslation(); const { t } = useTranslation();
const { isSmallMobile } = useResponsiveStore();
const [selectedUsers, setSelectedUsers] = useState<OptionsSelect>([]); const [selectedUsers, setSelectedUsers] = useState<OptionsSelect>([]);
const [selectedRole, setSelectedRole] = useState<Role>(); const [selectedRole, setSelectedRole] = useState<Role>();
const { toast } = useToastProvider(); const { toast } = useToastProvider();
@@ -145,7 +147,12 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
$wrap="wrap" $wrap="wrap"
> >
<IconBG iconName="group_add" /> <IconBG iconName="group_add" />
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 70%;"> <Box
$gap="0.7rem"
$direction="row"
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
$css="flex: 70%;"
>
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 80%;"> <Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 80%;">
<Box $css="flex: auto;" $width="15rem"> <Box $css="flex: auto;" $width="15rem">
<SearchUsers <SearchUsers

View File

@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
import { Box, IconBG, Text, TextErrors } from '@/components'; import { Box, IconBG, Text, TextErrors } from '@/components';
import { Access, Doc, Role } from '@/features/docs/doc-management'; import { Access, Doc, Role } from '@/features/docs/doc-management';
import { ChooseRole } from '@/features/docs/members/members-add/'; import { ChooseRole } from '@/features/docs/members/members-add/';
import { useResponsiveStore } from '@/stores';
import { useDeleteDocAccess, useUpdateDocAccess } from '../api'; import { useDeleteDocAccess, useUpdateDocAccess } from '../api';
import { useWhoAmI } from '../hooks/useWhoAmI'; import { useWhoAmI } from '../hooks/useWhoAmI';
@@ -31,6 +32,7 @@ export const MemberItem = ({
}: MemberItemProps) => { }: MemberItemProps) => {
const { isMyself, isLastOwner, isOtherOwner } = useWhoAmI(access); const { isMyself, isLastOwner, isOtherOwner } = useWhoAmI(access);
const { t } = useTranslation(); const { t } = useTranslation();
const { isSmallMobile, screenWidth } = useResponsiveStore();
const [localRole, setLocalRole] = useState(role); const [localRole, setLocalRole] = useState(role);
const { toast } = useToastProvider(); const { toast } = useToastProvider();
const router = useRouter(); const router = useRouter();
@@ -71,8 +73,7 @@ export const MemberItem = ({
return ( return (
<Box $width="100%"> <Box $width="100%">
<Box $direction="row" $gap="1rem"> <Box $direction="row" $gap="1rem" $wrap="wrap">
<IconBG iconName="account_circle" $size="2rem" />
<Box <Box
$align="center" $align="center"
$direction="row" $direction="row"
@@ -80,10 +81,21 @@ export const MemberItem = ({
$justify="space-between" $justify="space-between"
$width="100%" $width="100%"
$wrap="wrap" $wrap="wrap"
$css={`flex: ${isSmallMobile ? '100%' : '70%'};`}
> >
<Text $justify="center">{access.user.email}</Text> <IconBG iconName="account_circle" $size="2rem" />
<Box $direction="row" $gap="1rem" $align="center"> <Text $justify="center" $css="flex:1;">
<Box $minWidth="13rem"> {access.user.email}
</Text>
<Box
$direction="row"
$gap="1rem"
$align="center"
$justify="space-between"
$css="flex:1;"
$wrap={screenWidth < 400 ? 'wrap' : 'nowrap'}
>
<Box $minWidth="13rem" $css={isSmallMobile ? 'flex:1;' : ''}>
<ChooseRole <ChooseRole
label={t('Role')} label={t('Role')}
defaultRole={localRole} defaultRole={localRole}
@@ -100,22 +112,24 @@ export const MemberItem = ({
/> />
</Box> </Box>
{doc.abilities.manage_accesses && ( {doc.abilities.manage_accesses && (
<Button <Box $margin={isSmallMobile ? 'auto' : ''}>
color="tertiary-text" <Button
icon={ color="tertiary-text"
<Text icon={
$isMaterialIcon <Text
$theme={isNotAllowed ? 'greyscale' : 'primary'} $isMaterialIcon
$variation={isNotAllowed ? '500' : 'text'} $theme={isNotAllowed ? 'greyscale' : 'primary'}
> $variation={isNotAllowed ? '500' : 'text'}
delete >
</Text> delete
} </Text>
disabled={isNotAllowed} }
onClick={() => disabled={isNotAllowed}
removeDocAccess({ docId: doc.id, accessId: access.id }) onClick={() =>
} removeDocAccess({ docId: doc.id, accessId: access.id })
/> }
/>
</Box>
)} )}
</Box> </Box>
</Box> </Box>

View File

@@ -6,6 +6,7 @@ import { APIError } from '@/api';
import { Box, Card, InfiniteScroll, TextErrors } from '@/components'; import { Box, Card, InfiniteScroll, TextErrors } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { Access, Doc, currentDocRole } from '@/features/docs/doc-management'; import { Access, Doc, currentDocRole } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { useDocAccessesInfinite } from '../api'; import { useDocAccessesInfinite } from '../api';
@@ -25,6 +26,7 @@ const MemberListState = ({
doc, doc,
}: MemberListStateProps) => { }: MemberListStateProps) => {
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const { isSmallMobile } = useResponsiveStore();
if (error) { if (error) {
return <TextErrors causes={error.cause} />; return <TextErrors causes={error.cause} />;
@@ -48,7 +50,7 @@ const MemberListState = ({
key={`${access.id}-${index}`} key={`${access.id}-${index}`}
$background={!(index % 2) ? 'white' : colorsTokens()['greyscale-000']} $background={!(index % 2) ? 'white' : colorsTokens()['greyscale-000']}
$direction="row" $direction="row"
$padding="small" $padding={isSmallMobile ? 'tiny' : 'small'}
$align="center" $align="center"
$gap="1rem" $gap="1rem"
$radius="4px" $radius="4px"