Skip to content

Commit 85044fd

Browse files
committed
✨(frontend) summary feature
Add the summary feature to the doc. We will be able to access part of the doc quickly from the summary.
1 parent b83875f commit 85044fd

File tree

10 files changed

+201
-0
lines changed

10 files changed

+201
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to
1414
- ✨Add image attachments with access control
1515
- ✨(frontend) Upload image to a document #211
1616
- ✨(frontend) Versions #217
17+
- ✨(frontend) Summary #223
1718

1819
## Changed
1920

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
import { createDoc } from './common';
4+
5+
test.beforeEach(async ({ page }) => {
6+
await page.goto('/');
7+
});
8+
9+
test.describe('Doc Summary', () => {
10+
test('it checks the doc summary', async ({ page, browserName }) => {
11+
const [randomDoc] = await createDoc(page, 'doc-summary', browserName, 1);
12+
13+
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
14+
15+
await page.getByLabel('Open the document options').click();
16+
await page
17+
.getByRole('button', {
18+
name: 'Summary',
19+
})
20+
.click();
21+
22+
const panel = page.getByLabel('Document panel');
23+
const editor = page.locator('.ProseMirror');
24+
25+
await editor.locator('.bn-block-outer').last().fill('/');
26+
await page.getByText('Heading 1').click();
27+
await page.keyboard.type('Hello World');
28+
29+
await page.locator('.bn-block-outer').last().click();
30+
31+
// Create space to fill the viewport
32+
for (let i = 0; i < 6; i++) {
33+
await page.keyboard.press('Enter');
34+
}
35+
36+
await editor.locator('.bn-block-outer').last().fill('/');
37+
await page.getByText('Heading 2').click();
38+
await page.keyboard.type('Super World');
39+
40+
await page.locator('.bn-block-outer').last().click();
41+
42+
// Create space to fill the viewport
43+
for (let i = 0; i < 4; i++) {
44+
await page.keyboard.press('Enter');
45+
}
46+
47+
await editor.locator('.bn-block-outer').last().fill('/');
48+
await page.getByText('Heading 3').click();
49+
await page.keyboard.type('Another World');
50+
51+
await expect(panel.getByText('Hello World')).toBeVisible();
52+
await expect(panel.getByText('Super World')).toBeVisible();
53+
54+
await panel.getByText('Another World').click();
55+
56+
await expect(editor.getByText('Hello World')).not.toBeInViewport();
57+
58+
await panel.getByText('Back to top').click();
59+
await expect(editor.getByText('Hello World')).toBeInViewport();
60+
61+
await panel.getByText('Go to bottom').click();
62+
await expect(editor.getByText('Hello World')).not.toBeInViewport();
63+
});
64+
});

src/frontend/apps/impress/src/components/BoxButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const BoxButton = forwardRef<HTMLDivElement, BoxType>(
2929
border: none;
3030
outline: none;
3131
transition: all 0.2s ease-in-out;
32+
font-family: inherit;
3233
${$css || ''}
3334
`}
3435
{...props}

src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Panel } from '@/components/Panel';
99
import { useCunninghamTheme } from '@/cunningham';
1010
import { DocHeader } from '@/features/docs/doc-header';
1111
import { Doc } from '@/features/docs/doc-management';
12+
import { Summary, useDocSummaryStore } from '@/features/docs/doc-summary';
1213
import {
1314
VersionList,
1415
Versions,
@@ -27,6 +28,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
2728
query: { versionId },
2829
} = useRouter();
2930
const { isPanelVersionOpen, setIsPanelVersionOpen } = useDocVersionStore();
31+
const { isPanelSummaryOpen, setIsPanelSummaryOpen } = useDocSummaryStore();
3032

3133
const { t } = useTranslation();
3234

@@ -70,6 +72,11 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
7072
<VersionList doc={doc} />
7173
</Panel>
7274
)}
75+
{isPanelSummaryOpen && (
76+
<Panel title={t('SUMMARY')} setIsPanelOpen={setIsPanelSummaryOpen}>
77+
<Summary doc={doc} />
78+
</Panel>
79+
)}
7380
</Box>
7481
</>
7582
);

src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ModalShare,
1010
ModalUpdateDoc,
1111
} from '@/features/docs/doc-management';
12+
import { useDocSummaryStore } from '@/features/docs/doc-summary';
1213
import { useDocVersionStore } from '@/features/docs/doc-versioning';
1314

1415
import { ModalPDF } from './ModalExport';
@@ -25,6 +26,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
2526
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
2627
const [isDropOpen, setIsDropOpen] = useState(false);
2728
const { setIsPanelVersionOpen } = useDocVersionStore();
29+
const { setIsPanelSummaryOpen } = useDocSummaryStore();
2830

2931
return (
3032
<Box
@@ -83,6 +85,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
8385
<Button
8486
onClick={() => {
8587
setIsPanelVersionOpen(true);
88+
setIsPanelSummaryOpen(false);
8689
setIsDropOpen(false);
8790
}}
8891
color="primary-text"
@@ -92,6 +95,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
9295
<Text $theme="primary">{t('Version history')}</Text>
9396
</Button>
9497
)}
98+
<Button
99+
onClick={() => {
100+
setIsPanelSummaryOpen(true);
101+
setIsPanelVersionOpen(false);
102+
setIsDropOpen(false);
103+
}}
104+
color="primary-text"
105+
icon={<span className="material-icons">summarize</span>}
106+
size="small"
107+
>
108+
<Text $theme="primary">{t('Summary')}</Text>
109+
</Button>
95110
<Button
96111
onClick={() => {
97112
setIsModalPDFOpen(true);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React, { useCallback, useState } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
4+
import { Box, BoxButton, Text } from '@/components';
5+
6+
import { useDocStore } from '../../doc-editor';
7+
import { Doc } from '../../doc-management';
8+
9+
interface SummaryProps {
10+
doc: Doc;
11+
}
12+
13+
export const Summary = ({ doc }: SummaryProps) => {
14+
const { docsStore } = useDocStore();
15+
const { t } = useTranslation();
16+
17+
const editor = docsStore?.[doc.id].editor;
18+
const headingFiltering = useCallback(
19+
() => editor?.document.filter((block) => block.type === 'heading'),
20+
[editor?.document],
21+
);
22+
23+
const [headings, setHeadings] = useState(headingFiltering());
24+
25+
if (!editor) {
26+
return null;
27+
}
28+
29+
editor.onEditorContentChange(() => {
30+
setHeadings(headingFiltering());
31+
});
32+
33+
return (
34+
<Box $overflow="auto" $padding="small">
35+
{headings?.map((heading) => (
36+
<BoxButton
37+
key={heading.id}
38+
onClick={() => {
39+
editor.focus();
40+
editor?.setTextCursorPosition(heading.id, 'end');
41+
document
42+
.querySelector(`[data-id="${heading.id}"]`)
43+
?.scrollIntoView({
44+
behavior: 'smooth',
45+
block: 'start',
46+
});
47+
}}
48+
style={{ textAlign: 'left' }}
49+
>
50+
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
51+
{heading.content?.[0]?.type === 'text' && heading.content?.[0]?.text
52+
? `- ${heading.content[0].text}`
53+
: ''}
54+
</Text>
55+
</BoxButton>
56+
))}
57+
<Box
58+
$height="1px"
59+
$width="auto"
60+
$background="#e5e5e5"
61+
$margin={{ vertical: 'small' }}
62+
$css="flex: none;"
63+
/>
64+
<BoxButton
65+
onClick={() => {
66+
editor.focus();
67+
document.querySelector(`[data-id="initialBlockId"]`)?.scrollIntoView({
68+
behavior: 'smooth',
69+
block: 'start',
70+
});
71+
}}
72+
>
73+
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
74+
{t('Back to top')}
75+
</Text>
76+
</BoxButton>
77+
<BoxButton
78+
onClick={() => {
79+
editor.focus();
80+
document
81+
.querySelector(
82+
`.bn-editor > .bn-block-group > .bn-block-outer:last-child`,
83+
)
84+
?.scrollIntoView({
85+
behavior: 'smooth',
86+
block: 'start',
87+
});
88+
}}
89+
>
90+
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
91+
{t('Go to bottom')}
92+
</Text>
93+
</BoxButton>
94+
</Box>
95+
);
96+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Summary';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './components';
2+
export * from './stores';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useDocSummaryStore';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { create } from 'zustand';
2+
3+
export interface UseDocSummaryStore {
4+
isPanelSummaryOpen: boolean;
5+
setIsPanelSummaryOpen: (isOpen: boolean) => void;
6+
}
7+
8+
export const useDocSummaryStore = create<UseDocSummaryStore>((set) => ({
9+
isPanelSummaryOpen: false,
10+
setIsPanelSummaryOpen: (isPanelSummaryOpen) => {
11+
set(() => ({ isPanelSummaryOpen }));
12+
},
13+
}));

0 commit comments

Comments
 (0)