diff --git a/CHANGELOG.md b/CHANGELOG.md index fc298dd29..53dcc7c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to ## Added - ✨(ci) add security scan #291 +- ✨(frontend) Activate versions feature #240 ## Changed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts index 4ad46ec19..07d4b58cb 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createDoc } from './common'; +import { createDoc, goToGridDoc } from './common'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -33,6 +33,7 @@ test.describe('Doc Table Content', () => { await editor.getByText('Hello').dblclick(); await page.getByRole('button', { name: 'Strike' }).click(); + await page.locator('.bn-block-outer').first().click(); await page.locator('.bn-block-outer').last().click(); // Create space to fill the viewport @@ -92,4 +93,45 @@ test.describe('Doc Table Content', () => { await expect(editor.getByText('Hello World')).not.toBeInViewport(); await expect(superW).toHaveAttribute('aria-selected', 'true'); }); + + test('it checks that table contents panel is opened automaticaly if more that 2 headings', async ({ + page, + browserName, + }) => { + const [randomDoc] = await createDoc( + page, + 'doc-table-content', + browserName, + 1, + ); + + await expect(page.locator('h2').getByText(randomDoc)).toBeVisible(); + await expect(page.getByLabel('Open the panel')).toBeHidden(); + + const editor = page.locator('.ProseMirror'); + + await editor.locator('.bn-block-outer').last().fill('/'); + await page.getByText('Heading 1').click(); + await page.keyboard.type('Hello World', { delay: 100 }); + + await page.keyboard.press('Enter'); + + await editor.locator('.bn-block-outer').last().fill('/'); + await page.getByText('Heading 2').click(); + await page.keyboard.type('Super World', { delay: 100 }); + + await goToGridDoc(page, { + title: randomDoc, + }); + + await expect(page.getByLabel('Close the panel')).toBeVisible(); + + const panel = page.getByLabel('Document panel'); + await expect(panel.getByText('Hello World')).toBeVisible(); + await expect(panel.getByText('Super World')).toBeVisible(); + + await page.getByLabel('Close the panel').click(); + + await expect(panel).toHaveAttribute('aria-hidden', 'true'); + }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts new file mode 100644 index 000000000..e853dda59 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts @@ -0,0 +1,205 @@ +import { expect, test } from '@playwright/test'; + +import { createDoc, goToGridDoc, mockedDocument } from './common'; + +test.beforeEach(async ({ page }) => { + await page.goto('/'); +}); + +test.describe('Doc Version', () => { + test('it displays the doc versions', async ({ page, browserName }) => { + const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1); + + await expect(page.locator('h2').getByText(randomDoc)).toBeVisible(); + + await page.getByLabel('Open the document options').click(); + await page + .getByRole('button', { + name: 'Version history', + }) + .click(); + + const panel = page.getByLabel('Document panel'); + + await expect(panel.getByText('Current version')).toBeVisible(); + expect(await panel.locator('li').count()).toBe(1); + + await page.locator('.ProseMirror.bn-editor').click(); + await page.locator('.ProseMirror.bn-editor').last().fill('Hello World'); + + await goToGridDoc(page, { + title: randomDoc, + }); + + await expect(page.getByText('Hello World')).toBeVisible(); + + await page + .locator('.ProseMirror .bn-block') + .getByText('Hello World') + .fill('It will create a version'); + + await goToGridDoc(page, { + title: randomDoc, + }); + + await expect(page.getByText('Hello World')).toBeHidden(); + await expect(page.getByText('It will create a version')).toBeVisible(); + + await page.getByLabel('Open the document options').click(); + await page + .getByRole('button', { + name: 'Version history', + }) + .click(); + + await expect(panel.getByText('Current version')).toBeVisible(); + expect(await panel.locator('li').count()).toBe(2); + + await panel.locator('li').nth(1).click(); + await expect( + page.getByText('Read only, you cannot edit document versions.'), + ).toBeVisible(); + await expect(page.getByText('Hello World')).toBeVisible(); + await expect(page.getByText('It will create a version')).toBeHidden(); + + await panel.getByText('Current version').click(); + await expect(page.getByText('Hello World')).toBeHidden(); + await expect(page.getByText('It will create a version')).toBeVisible(); + }); + + test('it does not display the doc versions if not allowed', async ({ + page, + }) => { + await mockedDocument(page, { + abilities: { + versions_list: false, + partial_update: true, + }, + }); + + await goToGridDoc(page); + + await expect(page.locator('h2').getByText('Mocked document')).toBeVisible(); + + await page.getByLabel('Open the document options').click(); + await expect( + page.getByRole('button', { name: 'Version history' }), + ).toBeHidden(); + + await page.getByRole('button', { name: 'Table of content' }).click(); + + await expect( + page.getByLabel('Document panel').getByText('Versions'), + ).toBeHidden(); + }); + + test('it restores the doc version', async ({ page, browserName }) => { + const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1); + + await expect(page.locator('h2').getByText(randomDoc)).toBeVisible(); + + await page.locator('.bn-block-outer').last().click(); + await page.locator('.bn-block-outer').last().fill('Hello'); + + await goToGridDoc(page, { + title: randomDoc, + }); + + await expect(page.getByText('Hello')).toBeVisible(); + await page.locator('.bn-block-outer').last().click(); + await page.keyboard.press('Enter'); + await page.locator('.bn-block-outer').last().fill('World'); + + await goToGridDoc(page, { + title: randomDoc, + }); + + await expect(page.getByText('World')).toBeVisible(); + + await page.getByLabel('Open the document options').click(); + await page + .getByRole('button', { + name: 'Version history', + }) + .click(); + + const panel = page.getByLabel('Document panel'); + await panel.locator('li').nth(1).click(); + await expect(page.getByText('World')).toBeHidden(); + + await panel.getByLabel('Open the version options').click(); + await page.getByText('Restore the version').click(); + + await expect(page.getByText('Restore this version?')).toBeVisible(); + + await page + .getByRole('button', { + name: 'Restore', + }) + .click(); + + await expect(panel.locator('li')).toHaveCount(3); + + await panel.getByText('Current version').click(); + await expect(page.getByText('Hello')).toBeVisible(); + await expect(page.getByText('World')).toBeHidden(); + }); + + test('it restores the doc version from button title', async ({ + page, + browserName, + }) => { + const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1); + + await expect(page.locator('h2').getByText(randomDoc)).toBeVisible(); + + await page.locator('.bn-block-outer').last().click(); + await page.locator('.bn-block-outer').last().fill('Hello'); + + await goToGridDoc(page, { + title: randomDoc, + }); + + await expect(page.getByText('Hello')).toBeVisible(); + await page.locator('.bn-block-outer').last().click(); + await page.keyboard.press('Enter'); + await page.locator('.bn-block-outer').last().fill('World'); + + await goToGridDoc(page, { + title: randomDoc, + }); + + await expect(page.getByText('World')).toBeVisible(); + + await page.getByLabel('Open the document options').click(); + await page + .getByRole('button', { + name: 'Version history', + }) + .click(); + + const panel = page.getByLabel('Document panel'); + await panel.locator('li').nth(1).click(); + await expect(page.getByText('World')).toBeHidden(); + + await page + .getByRole('button', { + name: 'Restore this version', + }) + .click(); + + await expect(page.getByText('Restore this version?')).toBeVisible(); + + await page + .getByRole('button', { + name: 'Restore', + }) + .click(); + + await expect(panel.locator('li')).toHaveCount(3); + + await panel.getByText('Current version').click(); + await expect(page.getByText('Hello')).toBeVisible(); + await expect(page.getByText('World')).toBeHidden(); + }); +}); diff --git a/src/frontend/apps/impress/src/components/Panel.tsx b/src/frontend/apps/impress/src/components/Panel.tsx deleted file mode 100644 index b154ef6f9..000000000 --- a/src/frontend/apps/impress/src/components/Panel.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { PropsWithChildren, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Box, Card, IconBG, Text } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; - -interface PanelProps { - title?: string; - setIsPanelOpen: (isOpen: boolean) => void; -} - -export const Panel = ({ - children, - title, - setIsPanelOpen, -}: PropsWithChildren) => { - const { t } = useTranslation(); - const { colorsTokens } = useCunninghamTheme(); - - const [isOpen, setIsOpen] = useState(false); - - useEffect(() => { - setIsOpen(true); - }, []); - - const closedOverridingStyles = !isOpen && { - $width: '0', - $maxWidth: '0', - $minWidth: '0', - }; - - const transition = 'all 0.5s ease-in-out'; - - return ( - - - - { - setIsOpen(false); - setTimeout(() => { - setIsPanelOpen(false); - }, 400); - }} - $radius="2px" - /> - {title && ( - - {title} - - )} - - {children} - - - ); -}; 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 a5610d381..58342c3c5 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 @@ -5,19 +5,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Box, Card, Text, TextErrors } from '@/components'; -import { Panel } from '@/components/Panel'; import { useCunninghamTheme } from '@/cunningham'; import { DocHeader } from '@/features/docs/doc-header'; import { Doc } from '@/features/docs/doc-management'; -import { TableContent } from '@/features/docs/doc-table-content'; -import { - VersionList, - Versions, - useDocVersion, - useDocVersionStore, -} from '@/features/docs/doc-versioning/'; +import { useHeading } from '@/features/docs/doc-table-content'; +import { Versions, useDocVersion } from '@/features/docs/doc-versioning/'; import { BlockNoteEditor } from './BlockNoteEditor'; +import { IconOpenPanelEditor, PanelEditor } from './PanelEditor'; interface DocEditorProps { doc: Doc; @@ -27,8 +22,8 @@ export const DocEditor = ({ doc }: DocEditorProps) => { const { query: { versionId }, } = useRouter(); - const { isPanelVersionOpen, setIsPanelVersionOpen } = useDocVersionStore(); const { t } = useTranslation(); + const headings = useHeading(doc.id); const isVersion = versionId && typeof versionId === 'string'; @@ -56,21 +51,22 @@ export const DocEditor = ({ doc }: DocEditorProps) => { $height="100%" $direction="row" $margin={{ all: 'small', top: 'none' }} - $gap="1rem" + $css="overflow-x: clip;" > - + {isVersion ? ( ) : ( )} + - {doc.abilities.versions_list && isPanelVersionOpen && ( - - - - )} - + ); 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 new file mode 100644 index 000000000..1d81731a0 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/PanelEditor.tsx @@ -0,0 +1,187 @@ +import React, { PropsWithChildren, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Box, BoxButton, Card, IconBG, Text } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; +import { Doc } from '@/features/docs//doc-management'; +import { HeadingBlock, TableContent } from '@/features/docs/doc-table-content'; +import { VersionList } from '@/features/docs/doc-versioning'; + +import { usePanelEditorStore } from '../stores/usePanelEditorStore'; + +interface PanelProps { + doc: Doc; + headings: HeadingBlock[]; +} + +export const PanelEditor = ({ + doc, + headings, +}: PropsWithChildren) => { + const { t } = useTranslation(); + const { colorsTokens } = useCunninghamTheme(); + + const { isPanelTableContentOpen, setIsPanelTableContentOpen, isPanelOpen } = + usePanelEditorStore(); + + return ( + + + + + setIsPanelTableContentOpen(true)} + $zIndex={1} + > + + {t('Table of content')} + + + {doc.abilities.versions_list && ( + setIsPanelTableContentOpen(false)} + $zIndex={1} + > + + {t('Versions')} + + + )} + + {isPanelTableContentOpen && ( + + )} + {!isPanelTableContentOpen && doc.abilities.versions_list && ( + + )} + + + ); +}; + +interface IconOpenPanelEditorProps { + headings: HeadingBlock[]; +} + +export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => { + const { t } = useTranslation(); + const { setIsPanelOpen, isPanelOpen, setIsPanelTableContentOpen } = + usePanelEditorStore(); + const [hasBeenOpen, setHasBeenOpen] = useState(isPanelOpen); + + const setClosePanel = () => { + setHasBeenOpen(true); + setIsPanelOpen(!isPanelOpen); + }; + + // Open the panel if there are more than 1 heading + useEffect(() => { + if (headings?.length && headings.length > 1 && !hasBeenOpen) { + setIsPanelTableContentOpen(true); + setIsPanelOpen(true); + setHasBeenOpen(true); + } + }, [headings, setIsPanelTableContentOpen, setIsPanelOpen, hasBeenOpen]); + + // If open from the doc header we set the state as well + useEffect(() => { + if (isPanelOpen && !hasBeenOpen) { + setHasBeenOpen(true); + } + }, [hasBeenOpen, isPanelOpen]); + + // Close the panel unmount + useEffect(() => { + return () => { + setIsPanelOpen(false); + }; + }, [setIsPanelOpen]); + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/index.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/index.ts index e742cba56..8efa05869 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/index.ts @@ -1 +1,2 @@ export * from './useDocStore'; +export * from './usePanelEditorStore'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/usePanelEditorStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/usePanelEditorStore.tsx new file mode 100644 index 000000000..64833d03d --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/usePanelEditorStore.tsx @@ -0,0 +1,19 @@ +import { create } from 'zustand'; + +export interface UsePanelEditorStore { + isPanelOpen: boolean; + setIsPanelOpen: (isOpen: boolean) => void; + isPanelTableContentOpen: boolean; + setIsPanelTableContentOpen: (isOpen: boolean) => void; +} + +export const usePanelEditorStore = create((set) => ({ + isPanelOpen: false, + isPanelTableContentOpen: true, + setIsPanelTableContentOpen: (isPanelTableContentOpen) => { + set(() => ({ isPanelTableContentOpen })); + }, + setIsPanelOpen: (isPanelOpen) => { + set(() => ({ isPanelOpen })); + }, +})); 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 fea1033bf..5d70f5e8a 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,14 +3,13 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, DropButton, IconOptions, Text } from '@/components'; +import { usePanelEditorStore } from '@/features/docs/doc-editor/'; import { Doc, ModalRemoveDoc, ModalShare, ModalUpdateDoc, } from '@/features/docs/doc-management'; -import { useDocTableContentStore } from '@/features/docs/doc-table-content'; -import { useDocVersionStore } from '@/features/docs/doc-versioning'; import { ModalPDF } from './ModalExport'; @@ -25,8 +24,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false); const [isModalPDFOpen, setIsModalPDFOpen] = useState(false); const [isDropOpen, setIsDropOpen] = useState(false); - const { setIsPanelVersionOpen } = useDocVersionStore(); - const { setIsPanelTableContentOpen } = useDocTableContentStore(); + const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore(); return ( { {t('Delete document')} )} + {doc.abilities.versions_list && ( + + )}