✨(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:
@@ -14,6 +14,7 @@ and this project adheres to
|
|||||||
- ✨Add image attachments with access control
|
- ✨Add image attachments with access control
|
||||||
- ✨(frontend) Upload image to a document #211
|
- ✨(frontend) Upload image to a document #211
|
||||||
- ✨(frontend) Versions #217
|
- ✨(frontend) Versions #217
|
||||||
|
- ✨(frontend) Summary #223
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -29,6 +29,7 @@ const BoxButton = forwardRef<HTMLDivElement, BoxType>(
|
|||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
font-family: inherit;
|
||||||
${$css || ''}
|
${$css || ''}
|
||||||
`}
|
`}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Panel } from '@/components/Panel';
|
|||||||
import { useCunninghamTheme } from '@/cunningham';
|
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 { Summary, useDocSummaryStore } from '@/features/docs/doc-summary';
|
||||||
import {
|
import {
|
||||||
VersionList,
|
VersionList,
|
||||||
Versions,
|
Versions,
|
||||||
@@ -27,6 +28,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
|||||||
query: { versionId },
|
query: { versionId },
|
||||||
} = useRouter();
|
} = useRouter();
|
||||||
const { isPanelVersionOpen, setIsPanelVersionOpen } = useDocVersionStore();
|
const { isPanelVersionOpen, setIsPanelVersionOpen } = useDocVersionStore();
|
||||||
|
const { isPanelSummaryOpen, setIsPanelSummaryOpen } = useDocSummaryStore();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -70,6 +72,11 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
|||||||
<VersionList doc={doc} />
|
<VersionList doc={doc} />
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
{isPanelSummaryOpen && (
|
||||||
|
<Panel title={t('SUMMARY')} setIsPanelOpen={setIsPanelSummaryOpen}>
|
||||||
|
<Summary doc={doc} />
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ModalShare,
|
ModalShare,
|
||||||
ModalUpdateDoc,
|
ModalUpdateDoc,
|
||||||
} from '@/features/docs/doc-management';
|
} from '@/features/docs/doc-management';
|
||||||
|
import { useDocSummaryStore } from '@/features/docs/doc-summary';
|
||||||
import { useDocVersionStore } from '@/features/docs/doc-versioning';
|
import { useDocVersionStore } from '@/features/docs/doc-versioning';
|
||||||
|
|
||||||
import { ModalPDF } from './ModalExport';
|
import { ModalPDF } from './ModalExport';
|
||||||
@@ -25,6 +26,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
|
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
|
||||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||||
const { setIsPanelVersionOpen } = useDocVersionStore();
|
const { setIsPanelVersionOpen } = useDocVersionStore();
|
||||||
|
const { setIsPanelSummaryOpen } = useDocSummaryStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -83,6 +85,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPanelVersionOpen(true);
|
setIsPanelVersionOpen(true);
|
||||||
|
setIsPanelSummaryOpen(false);
|
||||||
setIsDropOpen(false);
|
setIsDropOpen(false);
|
||||||
}}
|
}}
|
||||||
color="primary-text"
|
color="primary-text"
|
||||||
@@ -92,6 +95,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
<Text $theme="primary">{t('Version history')}</Text>
|
<Text $theme="primary">{t('Version history')}</Text>
|
||||||
</Button>
|
</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
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsModalPDFOpen(true);
|
setIsModalPDFOpen(true);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './Summary';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './components';
|
||||||
|
export * from './stores';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './useDocSummaryStore';
|
||||||
@@ -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 }));
|
||||||
|
},
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user