From 6a0a04ccaa3f282d5485e1c11593fb9b94652167 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Tue, 26 Nov 2024 12:03:32 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=A5(frontend)=20remove=20files=20f?= =?UTF-8?q?rom=20bad=20rebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We had already deleted this file but it must have reappeared with a bad rebase --- .../apps/impress/src/__tests__/pages.test.tsx | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 src/frontend/apps/impress/src/__tests__/pages.test.tsx diff --git a/src/frontend/apps/impress/src/__tests__/pages.test.tsx b/src/frontend/apps/impress/src/__tests__/pages.test.tsx deleted file mode 100644 index 81f837aa9..000000000 --- a/src/frontend/apps/impress/src/__tests__/pages.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; - -import { AppWrapper } from '@/tests/utils'; - -import Page from '../pages'; - -jest.mock('next/router', () => ({ - useRouter() { - return { - push: jest.fn(), - }; - }, -})); - -jest.mock('@sentry/nextjs', () => ({ - captureException: jest.fn(), - captureMessage: jest.fn(), - setUser: jest.fn(), -})); - -describe('Page', () => { - it('checks Page rendering', () => { - render(, { wrapper: AppWrapper }); - - expect( - screen.getByRole('button', { - name: /Create a new document/i, - }), - ).toBeInTheDocument(); - }); -}); From 290089feb51a8a8598d8fba58e153b91c1d6497e Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Mon, 25 Nov 2024 12:08:49 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8(frontend)=20update=20doc=20header?= =?UTF-8?q?=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modification of the header style to be consistent with the new UI : - We replace the option menu with the DropdownMenu component - We add a dowload button - We put an input in place of an editable div. --- CHANGELOG.md | 1 + .../apps/impress/src/components/Icon.tsx | 20 +- .../separators/HorizontalSeparator.tsx | 31 +++ .../src/cunningham/cunningham-style.css | 4 +- .../docs/doc-header/components/DocHeader.tsx | 142 +++++-------- .../docs/doc-header/components/DocTitle.tsx | 98 +++------ .../docs/doc-header/components/DocToolBox.tsx | 197 ++++++++++-------- .../doc-versioning/components/VersionItem.tsx | 7 +- 8 files changed, 238 insertions(+), 262 deletions(-) create mode 100644 src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f0034b6b9..79cfaebb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to - ✨(frontend) add sentry #424 - ✨(frontend) add crisp chatbot #450 - 💄(frontend) update DocsGridOptions component #432 +- 💄(frontend) update DocHeader ui #446 ## Changed diff --git a/src/frontend/apps/impress/src/components/Icon.tsx b/src/frontend/apps/impress/src/components/Icon.tsx index b5d443818..224f87b6b 100644 --- a/src/frontend/apps/impress/src/components/Icon.tsx +++ b/src/frontend/apps/impress/src/components/Icon.tsx @@ -1,3 +1,5 @@ +import { css } from 'styled-components'; + import { Text, TextType } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; @@ -40,23 +42,21 @@ export const IconBG = ({ iconName, ...textProps }: IconBGProps) => { ); }; -interface IconOptionsProps { - isOpen: boolean; - 'aria-label': string; -} +type IconOptionsProps = TextType & { + isHorizontal?: boolean; +}; -export const IconOptions = ({ isOpen, ...props }: IconOptionsProps) => { +export const IconOptions = ({ isHorizontal, ...props }: IconOptionsProps) => { return ( - more_vert + {isHorizontal ? 'more_horiz' : 'more_vert'} ); }; diff --git a/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx b/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx new file mode 100644 index 000000000..b660e2595 --- /dev/null +++ b/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx @@ -0,0 +1,31 @@ +import { useCunninghamTheme } from '@/cunningham'; + +import { Box } from '../Box'; + +export enum SeparatorVariant { + LIGHT = 'light', + DARK = 'dark', +} + +type Props = { + variant?: SeparatorVariant; +}; + +export const HorizontalSeparator = ({ + variant = SeparatorVariant.LIGHT, +}: Props) => { + const { colorsTokens } = useCunninghamTheme(); + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css index eb40fed50..4765ad486 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-style.css +++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css @@ -352,8 +352,8 @@ input:-webkit-autofill:focus { } .c__button--nano { - padding: 0 var(--c--theme--spacings--2xs) !important; - gap: var(--c--theme--spacings--2xs) !important; + padding: 0 var(--c--theme--spacings--2xs); + gap: var(--c--theme--spacings--2xs); } .c__button--medium { 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 1698bf3d4..0062c81a5 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,20 +1,19 @@ -import { Fragment } from 'react'; +import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; -import { Box, Card, StyledLink, Text } from '@/components'; +import { Box, Icon, Text } from '@/components'; +import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator'; import { useCunninghamTheme } from '@/cunningham'; import { Doc, - Role, + LinkReach, currentDocRole, useTrans, } from '@/features/docs/doc-management'; import { Versions } from '@/features/docs/doc-versioning'; -import { useDate } from '@/hook'; import { useResponsiveStore } from '@/stores'; -import { DocTagPublic } from './DocTagPublic'; import { DocTitle } from './DocTitle'; import { DocToolBox } from './DocToolBox'; @@ -24,105 +23,78 @@ interface DocHeaderProps { } export const DocHeader = ({ doc, versionId }: DocHeaderProps) => { - const { colorsTokens } = useCunninghamTheme(); + const { colorsTokens, spacingsTokens } = useCunninghamTheme(); + const { isDesktop } = useResponsiveStore(); + const spacings = spacingsTokens(); + const colors = colorsTokens(); + const { t } = useTranslation(); - const { formatDate } = useDate(); + const docIsPublic = doc.link_reach === LinkReach.PUBLIC; + const { transRole } = useTrans(); - const { isMobile, isSmallMobile } = useResponsiveStore(); return ( <> - - - - - home - - + {docIsPublic && ( + aria-label={t('Public document')} + $color={colors['primary-600']} + $background={colors['primary-100']} + $radius={spacings['3xs']} + $direction="row" + $padding="xs" + $flex={1} + $align="center" + $gap={spacings['3xs']} + $css={css` + border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd); + `} + > + + {t('Public document')} + + )} + - + + + + {isDesktop && ( + <> + + {transRole(currentDocRole(doc.abilities))} ·  + + + {t('Last update: {{update}}', { + update: DateTime.fromISO(doc.updated_at).toRelative(), + })} + + + )} + {!isDesktop && ( + + {DateTime.fromISO(doc.updated_at).toRelative()} + + )} + + - - - - - {t('Created at')} {formatDate(doc.created_at)} - - - {t('Owners:')}{' '} - - {doc.accesses - .filter( - (access) => access.role === Role.OWNER && access.user.email, - ) - .map((access, index, accesses) => ( - - {access.user.full_name || access.user.email}{' '} - {index < accesses.length - 1 ? ' / ' : ''} - - ))} - - - - - {t('Your role:')}{' '} - {transRole(currentDocRole(doc.abilities))} - - - + + ); }; 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 6c6a9dcf9..e10f8bd10 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 @@ -5,12 +5,12 @@ import { VariantType, useToastProvider, } from '@openfun/cunningham-react'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; import { Box, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { useHeadingStore } from '@/features/docs/doc-editor'; import { Doc, KEY_DOC, @@ -19,7 +19,6 @@ import { useUpdateDoc, } from '@/features/docs/doc-management'; import { useBroadcastStore, useResponsiveStore } from '@/stores'; -import { isFirefox } from '@/utils/userAgent'; interface DocTitleProps { doc: Doc; @@ -32,7 +31,7 @@ export const DocTitle = ({ doc }: DocTitleProps) => { return ( {doc.title} @@ -44,20 +43,18 @@ export const DocTitle = ({ doc }: DocTitleProps) => { }; const DocTitleInput = ({ doc }: DocTitleProps) => { + const { isDesktop } = useResponsiveStore(); const { t } = useTranslation(); const { colorsTokens } = useCunninghamTheme(); const [titleDisplay, setTitleDisplay] = useState(doc.title); const { toast } = useToastProvider(); const { untitledDocument } = useTrans(); const isUntitled = titleDisplay === untitledDocument; - const { headings } = useHeadingStore(); - const headingText = headings?.[0]?.contentText; - const debounceRef = useRef(); - const { isMobile } = useResponsiveStore(); + const { broadcast } = useBroadcastStore(); const { mutate: updateDoc } = useUpdateDoc({ - listInvalideQueries: [KEY_LIST_DOC], + listInvalideQueries: [KEY_DOC, KEY_LIST_DOC], onSuccess(data) { if (data.title !== untitledDocument) { toast(t('Document title updated successfully'), VariantType.SUCCESS); @@ -81,10 +78,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { // If mutation we update if (sanitizedTitle !== doc.title) { - if (debounceRef.current) { - clearTimeout(debounceRef.current); - debounceRef.current = undefined; - } + setTitleDisplay(sanitizedTitle); updateDoc({ id: doc.id, title: sanitizedTitle }); } }, @@ -98,74 +92,38 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { } }; - const handleOnClick = () => { - if (isUntitled) { - setTitleDisplay(''); - } - }; - - useEffect(() => { - setTitleDisplay(doc.title); - }, [doc.title]); - - useEffect(() => { - if ((!debounceRef.current && !isUntitled) || !headingText) { - return; - } - - setTitleDisplay(headingText); - - if (debounceRef.current) { - clearTimeout(debounceRef.current); - } - - debounceRef.current = setTimeout(() => { - handleTitleSubmit(headingText); - debounceRef.current = undefined; - }, 3000); - }, [isUntitled, handleTitleSubmit, headingText]); - return ( <> - handleTitleSubmit(e.currentTarget.textContent || '') - } + as="span" + role="textbox" + contentEditable + defaultValue={isUntitled ? undefined : titleDisplay} onKeyDownCapture={handleKeyDown} suppressContentEditableWarning={true} - $color={ - isUntitled - ? colorsTokens()['greyscale-200'] - : colorsTokens()['greyscale-text'] + aria-label={t('doc title input')} + onBlurCapture={(event) => + handleTitleSubmit(event.target.textContent || '') } - $css={` - ${isUntitled && 'font-style: italic;'} - cursor: text; - font-size: ${isMobile ? '1.2rem' : '1.5rem'}; - transition: box-shadow 0.5s, border-color 0.5s; - border: 1px dashed transparent; - - &:hover { - border-color: rgba(0, 123, 255, 0.25); - border-style: dashed; + $color={colorsTokens()['greyscale-text']} + $margin={{ left: '-2px', right: '10px' }} + $css={css` + &[contenteditable='true']:empty:not(:focus):before { + content: '${untitledDocument}'; + color: grey; + pointer-events: none; + font-style: italic; } + font-size: ${isDesktop + ? css`var(--c--theme--font--sizes--h2)` + : css`var(--c--theme--font--sizes--sm)`}; + font-weight: 700; - &:focus { - outline: none; - border-color: transparent; - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); - } + outline: none; `} > - {titleDisplay} + {isUntitled ? '' : titleDisplay} 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 47bf9b787..3db2bea86 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 @@ -3,11 +3,19 @@ import { VariantType, useToastProvider, } from '@openfun/cunningham-react'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; -import { Box, DropButton, IconOptions } from '@/components'; +import { + Box, + DropdownMenu, + DropdownMenuOption, + Icon, + IconOptions, +} from '@/components'; import { useAuthStore } from '@/core'; +import { useCunninghamTheme } from '@/cunningham'; import { useEditorStore, usePanelEditorStore, @@ -29,10 +37,15 @@ interface DocToolBoxProps { export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => { const { t } = useTranslation(); + const { spacingsTokens, colorsTokens } = useCunninghamTheme(); + + const spacings = spacingsTokens(); + const colors = colorsTokens(); + 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(); @@ -40,6 +53,66 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => { const { editor } = useEditorStore(); const { toast } = useToastProvider(); + const options: DropdownMenuOption[] = [ + ...(isSmallMobile + ? [ + { + label: t('Share'), + icon: 'upload', + callback: () => { + setIsModalShareOpen(true); + }, + }, + { + label: t('Export'), + icon: 'download', + callback: () => { + setIsModalPDFOpen(true); + }, + }, + ] + : []), + { + label: t('Version history'), + icon: 'history', + disabled: !doc.abilities.versions_list, + callback: () => { + setIsPanelOpen(true); + setIsPanelTableContentOpen(false); + }, + }, + { + label: t('Table of contents'), + icon: 'summarize', + callback: () => { + setIsPanelOpen(true); + setIsPanelTableContentOpen(true); + }, + }, + { + label: t('Copy as {{format}}', { format: 'Markdown' }), + icon: 'content_copy', + callback: () => { + void copyCurrentEditorToClipboard('markdown'); + }, + }, + { + label: t('Copy as {{format}}', { format: 'HTML' }), + icon: 'content_copy', + callback: () => { + void copyCurrentEditorToClipboard('html'); + }, + }, + { + label: t('Delete document'), + icon: 'delete', + disabled: !doc.abilities.destroy, + callback: () => { + setIsModalRemoveOpen(true); + }, + }, + ]; + const copyCurrentEditorToClipboard = async ( asFormat: 'html' | 'markdown', ) => { @@ -84,9 +157,10 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => { )} - - {authenticated && ( + + {authenticated && !isSmallMobile && ( )} - - } - onOpenChange={(isOpen) => setIsDropOpen(isOpen)} - isOpen={isDropOpen} - > - - {doc.abilities.versions_list && ( - - )} - - - {doc.abilities.destroy && ( - - )} - - - - + {!isSmallMobile && ( + - } onClose={() => onClose()} rightActions={ - + <> + + + } size={ModalSize.MEDIUM} title={ - - - picture_as_pdf - - - {t('Export')} - - + + {t('Download')} + } > { aria-label={t('Content modal to export the document')} $gap="1.5rem" > - - - {t( - 'Export your document, it will be inserted in the selected template.', - )} - - - + + {t( + 'Upload your docs to a Microsoft Word, Open Office or PDF document.', + )} + + setFormat(options.target.value as DocDownloadFormat) + } + /> {isPending && (