🚸(frontend) separate viewers from editors
We are now totally separating the viewers with the editors. We will not load the provider when we are in viewer mode, meaning the viewers will not be aware of other users and will not show their cursors anymore. We still get the document updates in real-time.
This commit is contained in:
@@ -10,16 +10,17 @@ and this project adheres to
|
|||||||
|
|
||||||
- ✨(frontend) create skeleton component for DocEditor #1491
|
- ✨(frontend) create skeleton component for DocEditor #1491
|
||||||
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
|
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
|
||||||
|
- ✨(frontend) ajustable left panel #1456
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- ♻️(frontend) adapt custom blocks to new implementation #1375
|
- ♻️(frontend) adapt custom blocks to new implementation #1375
|
||||||
- ♻️(backend) increase user short_name field length
|
- ♻️(backend) increase user short_name field length
|
||||||
|
- 🚸(frontend) separate viewers from editors #1509
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 🐛(frontend) fix duplicate document entries in grid #1479
|
- 🐛(frontend) fix duplicate document entries in grid #1479
|
||||||
- 🐛(frontend) show full nested doc names with ajustable bar #1456
|
|
||||||
- 🐛(backend) fix trashbin list
|
- 🐛(backend) fix trashbin list
|
||||||
- ♿(frontend) improve accessibility:
|
- ♿(frontend) improve accessibility:
|
||||||
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
|
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
keyCloakSignIn,
|
keyCloakSignIn,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
import { writeInEditor } from './utils-editor';
|
import { getEditor, writeInEditor } from './utils-editor';
|
||||||
import { addNewMember, connectOtherUserToDoc } from './utils-share';
|
import { addNewMember, connectOtherUserToDoc } from './utils-share';
|
||||||
import { createRootSubPage } from './utils-sub-pages';
|
import { createRootSubPage } from './utils-sub-pages';
|
||||||
|
|
||||||
@@ -182,15 +182,14 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc Visibility: Public', () => {
|
test.describe('Doc Visibility: Public', () => {
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
});
|
||||||
|
|
||||||
test('It checks a public doc in read only mode', async ({
|
test('It checks a public doc in read only mode', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto('/');
|
|
||||||
await keyCloakSignIn(page, browserName);
|
|
||||||
|
|
||||||
const [docTitle] = await createDoc(
|
const [docTitle] = await createDoc(
|
||||||
page,
|
page,
|
||||||
'Public read only',
|
'Public read only',
|
||||||
@@ -200,6 +199,8 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
|
await writeInEditor({ page, text: 'Hello Public Viewonly' });
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const selectVisibility = page.getByTestId('doc-visibility');
|
const selectVisibility = page.getByTestId('doc-visibility');
|
||||||
await selectVisibility.click();
|
await selectVisibility.click();
|
||||||
@@ -241,49 +242,63 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
await expect(page.getByTestId('search-docs-button')).toBeVisible();
|
await expect(page.getByTestId('search-docs-button')).toBeVisible();
|
||||||
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const docUrl = page.url();
|
||||||
|
|
||||||
await page
|
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||||
.getByRole('button', {
|
browserName,
|
||||||
name: 'Logout',
|
docUrl,
|
||||||
})
|
withoutSignIn: true,
|
||||||
.click();
|
});
|
||||||
|
|
||||||
await expectLoginPage(page);
|
await expect(otherPage.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
|
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
|
||||||
await page.goto(urlDoc);
|
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
|
||||||
|
await expect(
|
||||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
otherPage.getByRole('button', { name: 'Share' }),
|
||||||
await expect(page.getByTestId('search-docs-button')).toBeHidden();
|
).toBeVisible();
|
||||||
await expect(page.getByTestId('new-doc-button')).toBeHidden();
|
const card = otherPage.getByLabel('It is the card information');
|
||||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
|
||||||
const card = page.getByLabel('It is the card information');
|
|
||||||
await expect(card).toBeVisible();
|
await expect(card).toBeVisible();
|
||||||
await expect(card.getByText('Reader')).toBeVisible();
|
await expect(card.getByText('Reader')).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
const otherEditor = await getEditor({ page: otherPage });
|
||||||
|
await expect(otherEditor).toHaveAttribute('contenteditable', 'false');
|
||||||
|
await expect(otherEditor.getByText('Hello Public Viewonly')).toBeVisible();
|
||||||
|
|
||||||
|
// Cursor and selection of the anonymous user are not visible
|
||||||
|
await otherEditor.getByText('Hello Public').selectText();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(
|
page.locator('.collaboration-cursor-custom__base'),
|
||||||
|
).toBeHidden();
|
||||||
|
await expect(page.locator('.ProseMirror-yjs-selection')).toBeHidden();
|
||||||
|
|
||||||
|
// Can still see changes made by others
|
||||||
|
await writeInEditor({ page, text: 'Can you see it ?' });
|
||||||
|
await expect(otherEditor.getByText('Can you see it ?')).toBeVisible();
|
||||||
|
|
||||||
|
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await expect(
|
||||||
|
otherPage.getByText(
|
||||||
'You can view this document but need additional access to see its members or modify settings.',
|
'You can view this document but need additional access to see its members or modify settings.',
|
||||||
),
|
),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Request access' }),
|
otherPage.getByRole('button', { name: 'Request access' }),
|
||||||
).toBeHidden();
|
).toBeHidden();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('It checks a public doc in editable mode', async ({
|
test('It checks a public doc in editable mode', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto('/');
|
|
||||||
await keyCloakSignIn(page, browserName);
|
|
||||||
|
|
||||||
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
|
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
|
await writeInEditor({ page, text: 'Hello Public Editable' });
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const selectVisibility = page.getByTestId('doc-visibility');
|
const selectVisibility = page.getByTestId('doc-visibility');
|
||||||
await selectVisibility.click();
|
await selectVisibility.click();
|
||||||
@@ -317,20 +332,47 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
cardContainer.getByText('Public document', { exact: true }),
|
cardContainer.getByText('Public document', { exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const docUrl = page.url();
|
||||||
|
|
||||||
await page
|
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||||
.getByRole('button', {
|
browserName,
|
||||||
name: 'Logout',
|
docUrl,
|
||||||
})
|
withoutSignIn: true,
|
||||||
.click();
|
docTitle,
|
||||||
|
});
|
||||||
|
|
||||||
await expectLoginPage(page);
|
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
|
||||||
|
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
|
||||||
|
|
||||||
await page.goto(urlDoc);
|
const otherEditor = await getEditor({ page: otherPage });
|
||||||
|
await expect(otherEditor).toHaveAttribute('contenteditable', 'true');
|
||||||
|
await expect(otherEditor.getByText('Hello Public Editable')).toBeVisible();
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
// We can see the collaboration cursor of the anonymous user
|
||||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
await otherEditor.getByText('Hello Public').selectText();
|
||||||
|
await expect(
|
||||||
|
page.locator('.collaboration-cursor-custom__base').getByText('Anonymous'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
otherPage.getByRole('button', { name: 'Share' }),
|
||||||
|
).toBeVisible();
|
||||||
|
const card = otherPage.getByLabel('It is the card information');
|
||||||
|
await expect(card).toBeVisible();
|
||||||
|
await expect(card.getByText('Editor')).toBeVisible();
|
||||||
|
|
||||||
|
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await expect(
|
||||||
|
otherPage.getByText(
|
||||||
|
'You can view this document but need additional access to see its members or modify settings.',
|
||||||
|
),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
otherPage.getByRole('button', { name: 'Request access' }),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ export const expectLoginPage = async (page: Page) =>
|
|||||||
).toBeVisible({
|
).toBeVisible({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// language helper
|
// language helper
|
||||||
export const TestLanguage = {
|
export const TestLanguage = {
|
||||||
English: {
|
English: {
|
||||||
|
|||||||
@@ -17,11 +17,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { Box, TextErrors } from '@/components';
|
import { Box, TextErrors } from '@/components';
|
||||||
import {
|
import { Doc, useProviderStore } from '@/docs/doc-management';
|
||||||
Doc,
|
|
||||||
useIsCollaborativeEditable,
|
|
||||||
useProviderStore,
|
|
||||||
} from '@/docs/doc-management';
|
|
||||||
import { useAuth } from '@/features/auth';
|
import { useAuth } from '@/features/auth';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -32,7 +28,6 @@ import {
|
|||||||
useUploadStatus,
|
useUploadStatus,
|
||||||
} from '../hook';
|
} from '../hook';
|
||||||
import { useEditorStore } from '../stores';
|
import { useEditorStore } from '../stores';
|
||||||
import { cssEditor } from '../styles';
|
|
||||||
import { DocsBlockNoteEditor } from '../types';
|
import { DocsBlockNoteEditor } from '../types';
|
||||||
import { randomColor } from '../utils';
|
import { randomColor } from '../utils';
|
||||||
|
|
||||||
@@ -85,25 +80,19 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isSynced: isConnectedToCollabServer } = useProviderStore();
|
const { isSynced: isConnectedToCollabServer } = useProviderStore();
|
||||||
|
|
||||||
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
|
||||||
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
|
|
||||||
const isDeletedDoc = !!doc.deleted_at;
|
|
||||||
|
|
||||||
useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer);
|
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const lang = i18n.resolvedLanguage;
|
const lang = i18n.resolvedLanguage;
|
||||||
|
|
||||||
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
|
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
|
||||||
|
|
||||||
const collabName = readOnly
|
const collabName = user?.full_name || user?.email || t('Anonymous');
|
||||||
? 'Reader'
|
|
||||||
: user?.full_name || user?.email || t('Anonymous');
|
|
||||||
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
|
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
|
||||||
|
|
||||||
const editor: DocsBlockNoteEditor = useCreateBlockNote(
|
const editor: DocsBlockNoteEditor = useCreateBlockNote(
|
||||||
{
|
{
|
||||||
collaboration: {
|
collaboration: {
|
||||||
provider,
|
provider: provider,
|
||||||
fragment: provider.document.getXmlFragment('document-store'),
|
fragment: provider.document.getXmlFragment('document-store'),
|
||||||
user: {
|
user: {
|
||||||
name: collabName,
|
name: collabName,
|
||||||
@@ -117,10 +106,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
renderCursor: (user: { color: string; name: string }) => {
|
renderCursor: (user: { color: string; name: string }) => {
|
||||||
const cursorElement = document.createElement('span');
|
const cursorElement = document.createElement('span');
|
||||||
|
|
||||||
if (user.name === 'Reader') {
|
|
||||||
return cursorElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursorElement.classList.add('collaboration-cursor-custom__base');
|
cursorElement.classList.add('collaboration-cursor-custom__base');
|
||||||
const caretElement = document.createElement('span');
|
const caretElement = document.createElement('span');
|
||||||
caretElement.classList.add('collaboration-cursor-custom__caret');
|
caretElement.classList.add('collaboration-cursor-custom__caret');
|
||||||
@@ -181,12 +166,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
}, [setEditor, editor]);
|
}, [setEditor, editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
$padding={{ top: 'md' }}
|
|
||||||
$background="white"
|
|
||||||
$css={cssEditor(readOnly, isDeletedDoc)}
|
|
||||||
className="--docs--editor-container"
|
|
||||||
>
|
|
||||||
{errorAttachment && (
|
{errorAttachment && (
|
||||||
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
|
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
|
||||||
<TextErrors
|
<TextErrors
|
||||||
@@ -201,24 +181,21 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
editor={editor}
|
editor={editor}
|
||||||
formattingToolbar={false}
|
formattingToolbar={false}
|
||||||
slashMenu={false}
|
slashMenu={false}
|
||||||
editable={!readOnly}
|
|
||||||
theme="light"
|
theme="light"
|
||||||
>
|
>
|
||||||
<BlockNoteSuggestionMenu />
|
<BlockNoteSuggestionMenu />
|
||||||
<BlockNoteToolbar />
|
<BlockNoteToolbar />
|
||||||
</BlockNoteView>
|
</BlockNoteView>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface BlockNoteEditorVersionProps {
|
interface BlockNoteReaderProps {
|
||||||
initialContent: Y.XmlFragment;
|
initialContent: Y.XmlFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockNoteEditorVersion = ({
|
export const BlockNoteReader = ({ initialContent }: BlockNoteReaderProps) => {
|
||||||
initialContent,
|
const { setEditor } = useEditorStore();
|
||||||
}: BlockNoteEditorVersionProps) => {
|
|
||||||
const readOnly = true;
|
|
||||||
const editor = useCreateBlockNote(
|
const editor = useCreateBlockNote(
|
||||||
{
|
{
|
||||||
collaboration: {
|
collaboration: {
|
||||||
@@ -234,9 +211,23 @@ export const BlockNoteEditorVersion = ({
|
|||||||
[initialContent],
|
[initialContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEditor(editor);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setEditor(undefined);
|
||||||
|
};
|
||||||
|
}, [setEditor, editor]);
|
||||||
|
|
||||||
|
useHeadings(editor);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box $css={cssEditor(readOnly, true)} className="--docs--editor-container">
|
<BlockNoteView
|
||||||
<BlockNoteView editor={editor} editable={!readOnly} theme="light" />
|
editor={editor}
|
||||||
</Box>
|
editable={false}
|
||||||
|
theme="light"
|
||||||
|
formattingToolbar={false}
|
||||||
|
slashMenu={false}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,21 +3,31 @@ import { css } from 'styled-components';
|
|||||||
|
|
||||||
import { Box, Loading } from '@/components';
|
import { Box, Loading } from '@/components';
|
||||||
import { DocHeader } from '@/docs/doc-header/';
|
import { DocHeader } from '@/docs/doc-header/';
|
||||||
import { Doc, useProviderStore } from '@/docs/doc-management';
|
import {
|
||||||
|
Doc,
|
||||||
|
useIsCollaborativeEditable,
|
||||||
|
useProviderStore,
|
||||||
|
} from '@/docs/doc-management';
|
||||||
import { TableContent } from '@/docs/doc-table-content/';
|
import { TableContent } from '@/docs/doc-table-content/';
|
||||||
import { useSkeletonStore } from '@/features/skeletons';
|
import { useSkeletonStore } from '@/features/skeletons';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { BlockNoteEditor } from './BlockNoteEditor';
|
import { cssEditor } from '../styles';
|
||||||
|
|
||||||
|
import { BlockNoteEditor, BlockNoteReader } from './BlockNoteEditor';
|
||||||
|
|
||||||
interface DocEditorContainerProps {
|
interface DocEditorContainerProps {
|
||||||
docHeader: React.ReactNode;
|
docHeader: React.ReactNode;
|
||||||
docEditor: React.ReactNode;
|
docEditor: React.ReactNode;
|
||||||
|
isDeletedDoc: boolean;
|
||||||
|
readOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocEditorContainer = ({
|
export const DocEditorContainer = ({
|
||||||
docHeader,
|
docHeader,
|
||||||
docEditor,
|
docEditor,
|
||||||
|
isDeletedDoc,
|
||||||
|
readOnly,
|
||||||
}: DocEditorContainerProps) => {
|
}: DocEditorContainerProps) => {
|
||||||
const { isDesktop } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
|
|
||||||
@@ -44,7 +54,14 @@ export const DocEditorContainer = ({
|
|||||||
className="--docs--doc-editor-content"
|
className="--docs--doc-editor-content"
|
||||||
>
|
>
|
||||||
<Box $css="flex:1;" $position="relative" $width="100%">
|
<Box $css="flex:1;" $position="relative" $width="100%">
|
||||||
{docEditor}
|
<Box
|
||||||
|
$padding={{ top: 'md' }}
|
||||||
|
$background="white"
|
||||||
|
$css={cssEditor(readOnly, isDeletedDoc)}
|
||||||
|
className="--docs--editor-container"
|
||||||
|
>
|
||||||
|
{docEditor}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -59,6 +76,8 @@ interface DocEditorProps {
|
|||||||
export const DocEditor = ({ doc }: DocEditorProps) => {
|
export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||||
const { isDesktop } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
const { provider, isReady } = useProviderStore();
|
const { provider, isReady } = useProviderStore();
|
||||||
|
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
||||||
|
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
|
||||||
const { setIsSkeletonVisible } = useSkeletonStore();
|
const { setIsSkeletonVisible } = useSkeletonStore();
|
||||||
const isProviderReady = isReady && provider;
|
const isProviderReady = isReady && provider;
|
||||||
|
|
||||||
@@ -87,7 +106,19 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
|||||||
)}
|
)}
|
||||||
<DocEditorContainer
|
<DocEditorContainer
|
||||||
docHeader={<DocHeader doc={doc} />}
|
docHeader={<DocHeader doc={doc} />}
|
||||||
docEditor={<BlockNoteEditor doc={doc} provider={provider} />}
|
docEditor={
|
||||||
|
readOnly ? (
|
||||||
|
<BlockNoteReader
|
||||||
|
initialContent={provider.document.getXmlFragment(
|
||||||
|
'document-store',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<BlockNoteEditor doc={doc} provider={provider} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isDeletedDoc={!!doc.deleted_at}
|
||||||
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ describe('useSaveDoc', () => {
|
|||||||
|
|
||||||
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
||||||
|
|
||||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||||
wrapper: AppWrapper,
|
wrapper: AppWrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,37 +62,6 @@ describe('useSaveDoc', () => {
|
|||||||
addEventListenerSpy.mockRestore();
|
addEventListenerSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not save when canSave is false', () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const yDoc = new Y.Doc();
|
|
||||||
const docId = 'test-doc-id';
|
|
||||||
|
|
||||||
fetchMock.patch('http://test.jest/api/v1.0/documents/test-doc-id/', {
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: 'test-doc-id',
|
|
||||||
content: 'test-content',
|
|
||||||
title: 'test-title',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
renderHook(() => useSaveDoc(docId, yDoc, false, true), {
|
|
||||||
wrapper: AppWrapper,
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
// Trigger a local update
|
|
||||||
yDoc.getMap('test').set('key', 'value');
|
|
||||||
|
|
||||||
// Advance timers to trigger the save interval
|
|
||||||
vi.advanceTimersByTime(61000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Since canSave is false, no API call should be made
|
|
||||||
expect(fetchMock.calls().length).toBe(0);
|
|
||||||
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should save when there are local changes', async () => {
|
it('should save when there are local changes', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const yDoc = new Y.Doc();
|
const yDoc = new Y.Doc();
|
||||||
@@ -106,7 +75,7 @@ describe('useSaveDoc', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||||
wrapper: AppWrapper,
|
wrapper: AppWrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,7 +112,7 @@ describe('useSaveDoc', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||||
wrapper: AppWrapper,
|
wrapper: AppWrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,7 +132,7 @@ describe('useSaveDoc', () => {
|
|||||||
const docId = 'test-doc-id';
|
const docId = 'test-doc-id';
|
||||||
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
||||||
|
|
||||||
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||||
wrapper: AppWrapper,
|
wrapper: AppWrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const SAVE_INTERVAL = 60000;
|
|||||||
export const useSaveDoc = (
|
export const useSaveDoc = (
|
||||||
docId: string,
|
docId: string,
|
||||||
yDoc: Y.Doc,
|
yDoc: Y.Doc,
|
||||||
canSave: boolean,
|
|
||||||
isConnectedToCollabServer: boolean,
|
isConnectedToCollabServer: boolean,
|
||||||
) => {
|
) => {
|
||||||
const { mutate: updateDoc } = useUpdateDoc({
|
const { mutate: updateDoc } = useUpdateDoc({
|
||||||
@@ -47,7 +46,7 @@ export const useSaveDoc = (
|
|||||||
}, [yDoc]);
|
}, [yDoc]);
|
||||||
|
|
||||||
const saveDoc = useCallback(() => {
|
const saveDoc = useCallback(() => {
|
||||||
if (!canSave || !isLocalChange) {
|
if (!isLocalChange) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,14 +57,7 @@ export const useSaveDoc = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}, [
|
}, [isLocalChange, updateDoc, docId, yDoc, isConnectedToCollabServer]);
|
||||||
canSave,
|
|
||||||
isLocalChange,
|
|
||||||
updateDoc,
|
|
||||||
docId,
|
|
||||||
yDoc,
|
|
||||||
isConnectedToCollabServer,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { Box, Text, TextErrors } from '@/components';
|
import { Box, Text, TextErrors } from '@/components';
|
||||||
import { BlockNoteEditorVersion, DocEditorContainer } from '@/docs/doc-editor/';
|
import { BlockNoteReader, DocEditorContainer } from '@/docs/doc-editor/';
|
||||||
import { Doc, base64ToBlocknoteXmlFragment } from '@/docs/doc-management';
|
import { Doc, base64ToBlocknoteXmlFragment } from '@/docs/doc-management';
|
||||||
import { Versions, useDocVersion } from '@/docs/doc-versioning/';
|
import { Versions, useDocVersion } from '@/docs/doc-versioning/';
|
||||||
|
|
||||||
@@ -77,7 +77,9 @@ export const DocVersionEditor = ({
|
|||||||
return (
|
return (
|
||||||
<DocEditorContainer
|
<DocEditorContainer
|
||||||
docHeader={<DocVersionHeader />}
|
docHeader={<DocVersionHeader />}
|
||||||
docEditor={<BlockNoteEditorVersion initialContent={initialContent} />}
|
docEditor={<BlockNoteReader initialContent={initialContent} />}
|
||||||
|
isDeletedDoc={false}
|
||||||
|
readOnly={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user