From 82a76a3ede1f5518e61fc0336377df38e3721bd1 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 8 Oct 2024 16:30:26 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=B1(frontend)=20docs=20mobile=20friend?= =?UTF-8?q?ly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We adapt the docs component to be mobile friendly. --- CHANGELOG.md | 1 + .../apps/e2e/__tests__/app-impress/common.ts | 13 +- .../__tests__/app-impress/doc-create.spec.ts | 11 +- .../__tests__/app-impress/doc-grid.spec.ts | 96 ++++++++- .../__tests__/app-impress/doc-header.spec.ts | 32 +++ .../app-impress/doc-visibility.spec.ts | 4 +- .../src/cunningham/cunningham-style.css | 17 ++ .../doc-editor/components/BlockNoteEditor.tsx | 44 +++- .../docs/doc-editor/components/DocEditor.tsx | 9 +- .../doc-editor/components/PanelEditor.tsx | 34 ++- .../docs/doc-header/components/DocHeader.tsx | 63 +++--- .../doc-header/components/DocTagPublic.tsx | 4 + .../docs/doc-header/components/DocTitle.tsx | 13 +- .../docs/doc-header/components/DocToolBox.tsx | 140 +++++++----- .../components/DocVisibility.tsx | 87 ++++---- .../doc-management/components/ModalShare.tsx | 19 +- .../doc-table-content/components/Heading.tsx | 12 +- .../components/TableContent.tsx | 39 ++-- .../docs/docs-grid/components/DocsGrid.tsx | 201 ++++++++++-------- .../components/DocsGridContainer.tsx | 8 +- .../components/InvitationItem.tsx | 60 +++--- .../components/InvitationList.tsx | 4 +- .../members-add/components/AddMembers.tsx | 9 +- .../members-list/components/MemberItem.tsx | 56 +++-- .../members-list/components/MemberList.tsx | 4 +- 25 files changed, 662 insertions(+), 318 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 128b505cb..b510b51ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to - ✨(frontend) Activate versions feature #240 - ✨(frontend) one-click document creation #275 - ✨(frontend) edit title inline #275 +- 📱(frontend) mobile responsive #304 - 🌐(frontend) Update translation #308 ## Changed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/common.ts b/src/frontend/apps/e2e/__tests__/app-impress/common.ts index be29d06b4..7dddccd1c 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/common.ts @@ -113,13 +113,14 @@ export const goToGridDoc = async ( const header = page.locator('header').first(); await header.locator('h2').getByText('Docs').click(); - const datagrid = page - .getByLabel('Datagrid of the documents page 1') - .getByRole('table'); + const datagrid = page.getByLabel('Datagrid of the documents page 1'); + const datagridTable = datagrid.getByRole('table'); - await expect(datagrid.getByLabel('Loading data')).toBeHidden(); + await expect(datagrid.getByLabel('Loading data')).toBeHidden({ + timeout: 5000, + }); - const rows = datagrid.getByRole('row'); + const rows = datagridTable.getByRole('row'); const row = title ? rows.filter({ hasText: title, @@ -132,7 +133,7 @@ export const goToGridDoc = async ( expect(docTitle).toBeDefined(); - await docTitleCell.click(); + await row.getByRole('link').first().click(); return docTitle as string; }; diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts index f0fcd335b..5226fb0fd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts @@ -18,12 +18,13 @@ test.describe('Doc Create', () => { const header = page.locator('header').first(); await header.locator('h2').getByText('Docs').click(); - const datagrid = page - .getByLabel('Datagrid of the documents page 1') - .getByRole('table'); + const datagrid = page.getByLabel('Datagrid of the documents page 1'); + const datagridTable = datagrid.getByRole('table'); - await expect(datagrid.getByLabel('Loading data')).toBeHidden(); - await expect(datagrid.getByText(docTitle)).toBeVisible({ + await expect(datagrid.getByLabel('Loading data')).toBeHidden({ + timeout: 5000, + }); + await expect(datagridTable.getByText(docTitle)).toBeVisible({ timeout: 5000, }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts index c83ed044c..4d8e2973e 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts @@ -117,7 +117,9 @@ test.describe('Documents Grid', () => { .getByRole('cell') .nth(cellNumber); - await expect(datagrid.getByLabel('Loading data')).toBeHidden(); + await expect(datagrid.getByLabel('Loading data')).toBeHidden({ + timeout: 5000, + }); // Initial state await expect(docNameRow1).toHaveText(/.*/); @@ -134,7 +136,9 @@ test.describe('Documents Grid', () => { const responseOrderingAsc = await responsePromiseOrderingAsc; expect(responseOrderingAsc.ok()).toBeTruthy(); - await expect(datagrid.getByLabel('Loading data')).toBeHidden(); + await expect(datagrid.getByLabel('Loading data')).toBeHidden({ + timeout: 5000, + }); await expect(docNameRow1).toHaveText(/.*/); await expect(docNameRow2).toHaveText(/.*/); @@ -155,7 +159,9 @@ test.describe('Documents Grid', () => { const responseOrderingDesc = await responsePromiseOrderingDesc; expect(responseOrderingDesc.ok()).toBeTruthy(); - await expect(datagrid.getByLabel('Loading data')).toBeHidden(); + await expect(datagrid.getByLabel('Loading data')).toBeHidden({ + timeout: 5000, + }); await expect(docNameRow1).toHaveText(/.*/); await expect(docNameRow2).toHaveText(/.*/); @@ -244,3 +250,87 @@ test.describe('Documents Grid', () => { 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: 5000, + }); + + 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'); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts index e2685b61e..d02168de1 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts @@ -385,3 +385,35 @@ test.describe('Doc Header', () => { ).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(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index 08a4bdae1..7a4e8bdb2 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -23,7 +23,9 @@ test.describe('Doc Visibility', () => { .getByLabel('Datagrid of the documents page 1') .getByRole('table'); - await expect(datagrid.getByLabel('Loading data')).toBeHidden(); + await expect(datagrid.getByLabel('Loading data')).toBeHidden({ + timeout: 5000, + }); await expect(datagrid.getByText(docTitle)).toBeVisible(); diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css index 69f737aea..587708310 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-style.css +++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css @@ -507,6 +507,23 @@ input:-webkit-autofill:focus { 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 */ diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 5d531a057..64e103dec 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -18,10 +18,14 @@ import { randomColor } from '../utils'; import { BlockNoteToolbar } from './BlockNoteToolbar'; -const cssEditor = ` +const cssEditor = (readonly: boolean) => ` &, & > .bn-container, & .ProseMirror { height:100% }; + & .bn-editor { + padding-right: 30px; + ${readonly && `padding-left: 30px;`} + }; & .collaboration-cursor__caret.ProseMirror-widget{ word-wrap: initial; } @@ -30,6 +34,35 @@ const cssEditor = ` padding: 2px; 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 { @@ -70,8 +103,9 @@ export const BlockNoteContent = ({ const isVersion = doc.id !== storeId; const { userData } = useAuthStore(); 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 { mutateAsync: createDocAttachment, @@ -130,7 +164,7 @@ export const BlockNoteContent = ({ }, [editor, resetHeadings, setHeadings]); return ( - + {isErrorAttachment && ( diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx index c1f4198dd..4782aee1a 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx @@ -9,6 +9,7 @@ import { useCunninghamTheme } from '@/cunningham'; import { DocHeader } from '@/features/docs/doc-header'; import { Doc } from '@/features/docs/doc-management'; import { Versions, useDocVersion } from '@/features/docs/doc-versioning/'; +import { useResponsiveStore } from '@/stores'; import { useHeadingStore } from '../stores'; @@ -25,6 +26,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => { } = useRouter(); const { t } = useTranslation(); const { headings } = useHeadingStore(); + const { isMobile } = useResponsiveStore(); const isVersion = versionId && typeof versionId === 'string'; @@ -51,11 +53,12 @@ export const DocEditor = ({ doc }: DocEditorProps) => { $background={colorsTokens()['primary-bg']} $height="100%" $direction="row" - $margin={{ all: 'small', top: 'none' }} + $margin={{ all: isMobile ? 'tiny' : 'small', top: 'none' }} $css="overflow-x: clip;" + $position="relative" > { ) : ( )} - + {!isMobile && } diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/PanelEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/PanelEditor.tsx index 60ce65b8a..0d0321b47 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/PanelEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/PanelEditor.tsx @@ -6,6 +6,7 @@ import { useCunninghamTheme } from '@/cunningham'; import { Doc } from '@/features/docs/doc-management'; import { TableContent } from '@/features/docs/doc-table-content'; import { VersionList } from '@/features/docs/doc-versioning'; +import { useResponsiveStore } from '@/stores'; import { usePanelEditorStore } from '../stores'; import { HeadingBlock } from '../types'; @@ -21,7 +22,7 @@ export const PanelEditor = ({ }: PropsWithChildren) => { const { t } = useTranslation(); const { colorsTokens } = useCunninghamTheme(); - + const { isMobile } = useResponsiveStore(); const { isPanelTableContentOpen, setIsPanelTableContentOpen, isPanelOpen } = usePanelEditorStore(); @@ -29,12 +30,12 @@ export const PanelEditor = ({ + {isMobile && } { const { setIsPanelOpen, isPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore(); const [hasBeenOpen, setHasBeenOpen] = useState(isPanelOpen); + const { isMobile } = useResponsiveStore(); const setClosePanel = () => { setHasBeenOpen(true); @@ -142,12 +154,18 @@ export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => { // Open the panel if there are more than 1 heading useEffect(() => { - if (headings?.length && headings.length > 1 && !hasBeenOpen) { + if (headings?.length && headings.length > 1 && !hasBeenOpen && !isMobile) { setIsPanelTableContentOpen(true); setIsPanelOpen(true); setHasBeenOpen(true); } - }, [headings, setIsPanelTableContentOpen, setIsPanelOpen, hasBeenOpen]); + }, [ + headings, + setIsPanelTableContentOpen, + setIsPanelOpen, + hasBeenOpen, + isMobile, + ]); // If open from the doc header we set the state as well useEffect(() => { @@ -169,7 +187,7 @@ export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => { aria-label={isPanelOpen ? t('Close the panel') : t('Open the panel')} $background="transparent" $size="h2" - $zIndex={1} + $zIndex={10} $hasTransition="slow" $css={` cursor: pointer; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx index 3c563707b..2e6f007c7 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx @@ -1,5 +1,4 @@ -import { Button } from '@openfun/cunningham-react'; -import React, { Fragment, useState } from 'react'; +import React, { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, Card, StyledLink, Text } from '@/components'; @@ -10,8 +9,9 @@ import { currentDocRole, useTrans, } 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 { useResponsiveStore } from '@/stores'; import { DocTagPublic } from './DocTagPublic'; import { DocTitle } from './DocTitle'; @@ -27,15 +27,19 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => { const { t } = useTranslation(); const { formatDate } = useDate(); const { transRole } = useTrans(); - const [isModalVersionOpen, setIsModalVersionOpen] = useState(false); + const { isMobile, isSmallMobile } = useResponsiveStore(); return ( <> - + { $width="1px" $height="70%" $background={colorsTokens()['greyscale-100']} - $margin={{ horizontal: 'small' }} + $margin={{ horizontal: 'tiny' }} /> - + - {versionId && ( - - )} + - - + {t('Created at')} {formatDate(doc.created_at)} @@ -106,13 +116,6 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => { - {isModalVersionOpen && versionId && ( - setIsModalVersionOpen(false)} - docId={doc.id} - versionId={versionId} - /> - )} ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTagPublic.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTagPublic.tsx index 19f29bf11..4f7ab3ce4 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTagPublic.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTagPublic.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import { Doc, LinkReach } from '@/features/docs/doc-management'; +import { useResponsiveStore } from '@/stores'; interface DocTagPublicProps { doc: Doc; @@ -11,6 +12,7 @@ interface DocTagPublicProps { export const DocTagPublic = ({ doc }: DocTagPublicProps) => { const { colorsTokens } = useCunninghamTheme(); const { t } = useTranslation(); + const { isSmallMobile } = useResponsiveStore(); if (doc?.link_reach !== LinkReach.PUBLIC) { return null; @@ -24,6 +26,8 @@ export const DocTagPublic = ({ doc }: DocTagPublicProps) => { $padding="xtiny" $radius="3px" $size="s" + $position={isSmallMobile ? 'absolute' : 'initial'} + $css={isSmallMobile ? 'right: 10px;' : ''} > {t('Public')} diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx index 09980586e..41b8447ae 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx @@ -19,6 +19,7 @@ import { useTrans, useUpdateDoc, } from '@/features/docs/doc-management'; +import { useResponsiveStore } from '@/stores'; import { isFirefox } from '@/utils/userAgent'; const DocTitleStyle = createGlobalStyle` @@ -32,9 +33,15 @@ interface DocTitleProps { } export const DocTitle = ({ doc }: DocTitleProps) => { + const { isMobile } = useResponsiveStore(); + if (!doc.abilities.partial_update) { return ( - + {doc.title} ); @@ -53,6 +60,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { const { headings } = useHeadingStore(); const headingText = headings?.[0]?.contentText; const debounceRef = useRef(); + const { isMobile } = useResponsiveStore(); const { mutate: updateDoc } = useUpdateDoc({ listInvalideQueries: [KEY_DOC, KEY_LIST_DOC], @@ -124,7 +132,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { as="h2" $radius="4px" $padding={{ horizontal: 'tiny', vertical: '4px' }} - $align="center" $margin="none" contentEditable={isFirefox() ? 'true' : 'plaintext-only'} onClick={handleOnClick} @@ -141,7 +148,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { $css={` ${isUntitled && 'font-style: italic;'} cursor: text; - font-size: 1.5rem; + font-size: ${isMobile ? '1.2rem' : '1.5rem'}; transition: box-shadow 0.5s, border-color 0.5s; border: 1px dashed transparent; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx index df3054ac3..98dfd6386 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx @@ -9,98 +9,121 @@ import { ModalRemoveDoc, ModalShare, } from '@/features/docs/doc-management'; +import { useResponsiveStore } from '@/stores'; + +import { ModalVersion, Versions } from '../../doc-versioning'; import { ModalPDF } from './ModalExport'; interface DocToolBoxProps { doc: Doc; + versionId?: Versions['version_id']; } -export const DocToolBox = ({ doc }: DocToolBoxProps) => { +export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => { const { t } = useTranslation(); const [isModalShareOpen, setIsModalShareOpen] = useState(false); const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false); const [isModalPDFOpen, setIsModalPDFOpen] = useState(false); const [isDropOpen, setIsDropOpen] = useState(false); const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore(); + const [isModalVersionOpen, setIsModalVersionOpen] = useState(false); + const { isSmallMobile } = useResponsiveStore(); return ( - - - } - onOpenChange={(isOpen) => setIsDropOpen(isOpen)} - isOpen={isDropOpen} - > - - {doc.abilities.versions_list && ( + {versionId && ( + + + + )} + + + + } + onOpenChange={(isOpen) => setIsDropOpen(isOpen)} + isOpen={isDropOpen} + > + + {doc.abilities.versions_list && ( + + )} - )} - - - {doc.abilities.destroy && ( - )} - - + {doc.abilities.destroy && ( + + )} + + + {isModalShareOpen && ( setIsModalShareOpen(false)} doc={doc} /> )} @@ -110,6 +133,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { {isModalRemoveOpen && ( setIsModalRemoveOpen(false)} doc={doc} /> )} + {isModalVersionOpen && versionId && ( + setIsModalVersionOpen(false)} + docId={doc.id} + versionId={versionId} + /> + )} ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocVisibility.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocVisibility.tsx index 61788869b..8d7f96286 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocVisibility.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocVisibility.tsx @@ -43,48 +43,57 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => { $direction="row" $align="center" $justify="space-between" + $gap="1rem" > - - - { - 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') - } - /> - - + }} + color="primary" + icon={copy} + > + {t('Copy link')} + + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalShare.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalShare.tsx index a45236fe5..33a8aa566 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalShare.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalShare.tsx @@ -5,6 +5,7 @@ import { Box, Card, SideModal, Text } from '@/components'; import { InvitationList } from '@/features/docs/members/invitation-list'; import { AddMembers } from '@/features/docs/members/members-add'; import { MemberList } from '@/features/docs/members/members-list'; +import { useResponsiveStore } from '@/stores'; import { Doc } from '../types'; import { currentDocRole } from '../utils'; @@ -20,6 +21,15 @@ const ModalShareStyle = createGlobalStyle` padding: 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) => { + const { isMobile, isSmallMobile } = useResponsiveStore(); + const width = isSmallMobile ? '100vw' : isMobile ? '90vw' : '70vw'; + return ( <> - + { const [isHover, setIsHover] = useState(isHighlight); const { colorsTokens } = useCunninghamTheme(); + const { isMobile } = useResponsiveStore(); return ( setIsHover(true)} onMouseLeave={() => setIsHover(false)} onClick={() => { - editor.focus(); + // With mobile the focus open the keyboard and the scroll is not working + if (!isMobile) { + editor.focus(); + } + editor.setTextCursorPosition(headingId, 'end'); document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({ behavior: 'smooth', diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx index bbaa6560d..e16141d7d 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { Box, BoxButton, Text } from '@/components'; import { HeadingBlock, useDocStore } from '@/features/docs/doc-editor'; import { Doc } from '@/features/docs/doc-management'; +import { useResponsiveStore } from '@/stores'; import { Heading } from './Heading'; @@ -14,6 +15,7 @@ interface TableContentProps { export const TableContent = ({ doc, headings }: TableContentProps) => { const { docsStore } = useDocStore(); + const { isMobile } = useResponsiveStore(); const { t } = useTranslation(); const editor = docsStore?.[doc.id]?.editor; const [headingIdHighlight, setHeadingIdHighlight] = useState(); @@ -66,17 +68,20 @@ export const TableContent = ({ doc, headings }: TableContentProps) => { return ( - - {headings?.map((heading) => ( - - ))} + + {headings?.map( + (heading) => + heading.contentText && ( + + ), + )} { /> { - editor.focus(); + // With mobile the focus open the keyboard and the scroll is not working + if (!isMobile) { + editor.focus(); + } + document.querySelector(`.bn-editor`)?.scrollIntoView({ behavior: 'smooth', block: 'start', @@ -101,7 +110,11 @@ export const TableContent = ({ doc, headings }: TableContentProps) => { { - editor.focus(); + // With mobile the focus open the keyboard and the scroll is not working + if (!isMobile) { + editor.focus(); + } + document .querySelector( `.bn-editor > .bn-block-group > .bn-block-outer:last-child`, diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx index ea755d2e3..3aa67e42d 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx @@ -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 { useTranslation } from 'react-i18next'; import { createGlobalStyle } from 'styled-components'; @@ -15,6 +20,7 @@ import { useTrans, } from '@/features/docs/doc-management'; import { useDate } from '@/hook/'; +import { useResponsiveStore } from '@/stores'; import { PAGE_SIZE } from '../conf'; @@ -62,6 +68,7 @@ export const DocsGrid = () => { ]); const { page, pageSize, setPagesCount } = pagination; const [docs, setDocs] = useState([]); + const { isMobile } = useResponsiveStore(); const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined; @@ -82,19 +89,117 @@ export const DocsGrid = () => { setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0); }, [data?.count, pageSize, setPagesCount]); + const columns: Column[] = [ + { + headerName: '', + id: 'visibility', + size: 95, + renderCell: ({ row }) => { + return ( + row.link_reach === LinkReach.PUBLIC && ( + + + {t('Public')} + + + ) + ); + }, + }, + { + headerName: t('Document name'), + field: 'title', + renderCell: ({ row }) => { + return ( + + + {row.title} + + + ); + }, + }, + { + headerName: t('Created at'), + field: 'created_at', + renderCell: ({ row }) => { + return ( + + {formatDate(row.created_at)} + + ); + }, + }, + { + headerName: t('Updated at'), + field: 'updated_at', + renderCell: ({ row }) => { + return ( + + {formatDate(row.updated_at)} + + ); + }, + }, + { + headerName: t('Your role'), + id: 'your_role', + renderCell: ({ row }) => { + return ( + + + {transRole(currentDocRole(row.abilities))} + + + ); + }, + }, + { + headerName: t('Members'), + id: 'users_number', + renderCell: ({ row }) => { + return ( + + {row.accesses.length} + + ); + }, + }, + { + id: 'column-actions', + renderCell: ({ row }) => { + return ; + }, + }, + ]; + + // 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 ( {t('Documents')} @@ -102,95 +207,7 @@ export const DocsGrid = () => { {error && } { - return ( - - {row.link_reach === LinkReach.PUBLIC && ( - - {t('Public')} - - )} - - ); - }, - }, - { - headerName: t('Document name'), - field: 'title', - renderCell: ({ row }) => { - return ( - - - {row.title} - - - ); - }, - }, - { - headerName: t('Created at'), - field: 'created_at', - renderCell: ({ row }) => { - return ( - - {formatDate(row.created_at)} - - ); - }, - }, - { - headerName: t('Updated at'), - field: 'updated_at', - renderCell: ({ row }) => { - return ( - - {formatDate(row.updated_at)} - - ); - }, - }, - { - headerName: t('Your role'), - id: 'your_role', - renderCell: ({ row }) => { - return ( - - - {transRole(currentDocRole(row.abilities))} - - - ); - }, - }, - { - headerName: t('Members'), - id: 'users_number', - renderCell: ({ row }) => { - return ( - - {row.accesses.length} - - ); - }, - }, - { - id: 'column-actions', - renderCell: ({ row }) => { - return ; - }, - }, - ]} + columns={columns} rows={docs} isLoading={isLoading} pagination={pagination} diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridContainer.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridContainer.tsx index a3534dc2f..e42388420 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridContainer.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridContainer.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { Box } from '@/components'; import { useCreateDoc, useTrans } from '@/features/docs/doc-management/'; +import { useResponsiveStore } from '@/stores'; import { DocsGrid } from './DocsGrid'; @@ -12,6 +13,7 @@ export const DocsGridContainer = () => { const { t } = useTranslation(); const { untitledDocument } = useTrans(); const router = useRouter(); + const { isMobile } = useResponsiveStore(); const { mutate: createDoc } = useCreateDoc({ onSuccess: (doc) => { @@ -25,7 +27,11 @@ export const DocsGridContainer = () => { return ( - + diff --git a/src/frontend/apps/impress/src/features/docs/members/invitation-list/components/InvitationItem.tsx b/src/frontend/apps/impress/src/features/docs/members/invitation-list/components/InvitationItem.tsx index 12b29ac64..e92358c8d 100644 --- a/src/frontend/apps/impress/src/features/docs/members/invitation-list/components/InvitationItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/members/invitation-list/components/InvitationItem.tsx @@ -11,6 +11,7 @@ import { Box, IconBG, Text, TextErrors } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import { Doc, Role } from '@/features/docs/doc-management'; import { ChooseRole } from '@/features/docs/members/members-add/'; +import { useResponsiveStore } from '@/stores'; import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api'; import { Invitation } from '../types'; @@ -31,6 +32,7 @@ export const InvitationItem = ({ const canDelete = invitation.abilities.destroy; const canUpdate = invitation.abilities.partial_update; const { t } = useTranslation(); + const { isSmallMobile, screenWidth } = useResponsiveStore(); const [localRole, setLocalRole] = useState(role); const { colorsTokens } = useCunninghamTheme(); const { toast } = useToastProvider(); @@ -62,8 +64,7 @@ export const InvitationItem = ({ return ( - - + - + + {invitation.email} - - + + {doc.abilities.manage_accesses && ( -