(frontend) summary feature

Add the summary feature to the doc.
We will be able to access part of the doc quickly
from the summary.
This commit is contained in:
Anthony LC
2024-09-03 15:46:07 +02:00
committed by Anthony LC
parent b83875fc97
commit 85044fd665
10 changed files with 201 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ and this project adheres to
- ✨Add image attachments with access control
- ✨(frontend) Upload image to a document #211
- ✨(frontend) Versions #217
- ✨(frontend) Summary #223
## Changed

View File

@@ -0,0 +1,64 @@
import { expect, test } from '@playwright/test';
import { createDoc } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc Summary', () => {
test('it checks the doc summary', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-summary', browserName, 1);
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', {
name: 'Summary',
})
.click();
const panel = page.getByLabel('Document panel');
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');
await page.locator('.bn-block-outer').last().click();
// Create space to fill the viewport
for (let i = 0; i < 6; i++) {
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');
await page.locator('.bn-block-outer').last().click();
// Create space to fill the viewport
for (let i = 0; i < 4; i++) {
await page.keyboard.press('Enter');
}
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Heading 3').click();
await page.keyboard.type('Another World');
await expect(panel.getByText('Hello World')).toBeVisible();
await expect(panel.getByText('Super World')).toBeVisible();
await panel.getByText('Another World').click();
await expect(editor.getByText('Hello World')).not.toBeInViewport();
await panel.getByText('Back to top').click();
await expect(editor.getByText('Hello World')).toBeInViewport();
await panel.getByText('Go to bottom').click();
await expect(editor.getByText('Hello World')).not.toBeInViewport();
});
});

View File

@@ -29,6 +29,7 @@ const BoxButton = forwardRef<HTMLDivElement, BoxType>(
border: none;
outline: none;
transition: all 0.2s ease-in-out;
font-family: inherit;
${$css || ''}
`}
{...props}

View File

@@ -9,6 +9,7 @@ import { Panel } from '@/components/Panel';
import { useCunninghamTheme } from '@/cunningham';
import { DocHeader } from '@/features/docs/doc-header';
import { Doc } from '@/features/docs/doc-management';
import { Summary, useDocSummaryStore } from '@/features/docs/doc-summary';
import {
VersionList,
Versions,
@@ -27,6 +28,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
query: { versionId },
} = useRouter();
const { isPanelVersionOpen, setIsPanelVersionOpen } = useDocVersionStore();
const { isPanelSummaryOpen, setIsPanelSummaryOpen } = useDocSummaryStore();
const { t } = useTranslation();
@@ -70,6 +72,11 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
<VersionList doc={doc} />
</Panel>
)}
{isPanelSummaryOpen && (
<Panel title={t('SUMMARY')} setIsPanelOpen={setIsPanelSummaryOpen}>
<Summary doc={doc} />
</Panel>
)}
</Box>
</>
);

View File

@@ -9,6 +9,7 @@ import {
ModalShare,
ModalUpdateDoc,
} from '@/features/docs/doc-management';
import { useDocSummaryStore } from '@/features/docs/doc-summary';
import { useDocVersionStore } from '@/features/docs/doc-versioning';
import { ModalPDF } from './ModalExport';
@@ -25,6 +26,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
const [isDropOpen, setIsDropOpen] = useState(false);
const { setIsPanelVersionOpen } = useDocVersionStore();
const { setIsPanelSummaryOpen } = useDocSummaryStore();
return (
<Box
@@ -83,6 +85,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<Button
onClick={() => {
setIsPanelVersionOpen(true);
setIsPanelSummaryOpen(false);
setIsDropOpen(false);
}}
color="primary-text"
@@ -92,6 +95,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<Text $theme="primary">{t('Version history')}</Text>
</Button>
)}
<Button
onClick={() => {
setIsPanelSummaryOpen(true);
setIsPanelVersionOpen(false);
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">summarize</span>}
size="small"
>
<Text $theme="primary">{t('Summary')}</Text>
</Button>
<Button
onClick={() => {
setIsModalPDFOpen(true);

View File

@@ -0,0 +1,96 @@
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, BoxButton, Text } from '@/components';
import { useDocStore } from '../../doc-editor';
import { Doc } from '../../doc-management';
interface SummaryProps {
doc: Doc;
}
export const Summary = ({ doc }: SummaryProps) => {
const { docsStore } = useDocStore();
const { t } = useTranslation();
const editor = docsStore?.[doc.id].editor;
const headingFiltering = useCallback(
() => editor?.document.filter((block) => block.type === 'heading'),
[editor?.document],
);
const [headings, setHeadings] = useState(headingFiltering());
if (!editor) {
return null;
}
editor.onEditorContentChange(() => {
setHeadings(headingFiltering());
});
return (
<Box $overflow="auto" $padding="small">
{headings?.map((heading) => (
<BoxButton
key={heading.id}
onClick={() => {
editor.focus();
editor?.setTextCursorPosition(heading.id, 'end');
document
.querySelector(`[data-id="${heading.id}"]`)
?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}}
style={{ textAlign: 'left' }}
>
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
{heading.content?.[0]?.type === 'text' && heading.content?.[0]?.text
? `- ${heading.content[0].text}`
: ''}
</Text>
</BoxButton>
))}
<Box
$height="1px"
$width="auto"
$background="#e5e5e5"
$margin={{ vertical: 'small' }}
$css="flex: none;"
/>
<BoxButton
onClick={() => {
editor.focus();
document.querySelector(`[data-id="initialBlockId"]`)?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}}
>
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
{t('Back to top')}
</Text>
</BoxButton>
<BoxButton
onClick={() => {
editor.focus();
document
.querySelector(
`.bn-editor > .bn-block-group > .bn-block-outer:last-child`,
)
?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}}
>
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
{t('Go to bottom')}
</Text>
</BoxButton>
</Box>
);
};

View File

@@ -0,0 +1 @@
export * from './Summary';

View File

@@ -0,0 +1,2 @@
export * from './components';
export * from './stores';

View File

@@ -0,0 +1 @@
export * from './useDocSummaryStore';

View File

@@ -0,0 +1,13 @@
import { create } from 'zustand';
export interface UseDocSummaryStore {
isPanelSummaryOpen: boolean;
setIsPanelSummaryOpen: (isOpen: boolean) => void;
}
export const useDocSummaryStore = create<UseDocSummaryStore>((set) => ({
isPanelSummaryOpen: false,
setIsPanelSummaryOpen: (isPanelSummaryOpen) => {
set(() => ({ isPanelSummaryOpen }));
},
}));