✨(frontend) feature versioning
We can now see the version of the document and navigate to different versions. Collaboration is possible per version.
This commit is contained in:
@@ -11,6 +11,11 @@ and this project adheres to
|
|||||||
## Added
|
## Added
|
||||||
|
|
||||||
- 🎨(frontend) better conversion editor to pdf #151
|
- 🎨(frontend) better conversion editor to pdf #151
|
||||||
|
- ✨(frontend) Versioning #147
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- 🐛(y-webrtc) fix prob connection #147
|
||||||
|
|
||||||
## [1.1.0] - 2024-07-15
|
## [1.1.0] - 2024-07-15
|
||||||
|
|
||||||
|
|||||||
@@ -140,3 +140,34 @@ export const goToGridDoc = async (
|
|||||||
|
|
||||||
return docTitle as string;
|
return docTitle as string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mockedDocument = async (page: Page, json: object) => {
|
||||||
|
await page.route('**/documents/**/', async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
if (request.method().includes('GET') && !request.url().includes('page=')) {
|
||||||
|
await route.fulfill({
|
||||||
|
json: {
|
||||||
|
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||||
|
content: '',
|
||||||
|
title: 'Mocked document',
|
||||||
|
accesses: [],
|
||||||
|
abilities: {
|
||||||
|
destroy: false, // Means not owner
|
||||||
|
versions_destroy: false,
|
||||||
|
versions_list: true,
|
||||||
|
versions_retrieve: true,
|
||||||
|
manage_accesses: false, // Means not admin
|
||||||
|
update: false,
|
||||||
|
partial_update: false, // Means not editor
|
||||||
|
retrieve: true,
|
||||||
|
},
|
||||||
|
is_public: false,
|
||||||
|
created_at: '2021-09-01T09:00:00Z',
|
||||||
|
...json,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, goToGridDoc } from './common';
|
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -153,34 +153,17 @@ test.describe('Doc Editor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it cannot edit if viewer', async ({ page }) => {
|
test('it cannot edit if viewer', async ({ page }) => {
|
||||||
await page.route('**/documents/**/', async (route) => {
|
await mockedDocument(page, {
|
||||||
const request = route.request();
|
abilities: {
|
||||||
if (
|
destroy: false, // Means not owner
|
||||||
request.method().includes('GET') &&
|
versions_destroy: false,
|
||||||
!request.url().includes('page=')
|
versions_list: true,
|
||||||
) {
|
versions_retrieve: true,
|
||||||
await route.fulfill({
|
manage_accesses: false, // Means not admin
|
||||||
json: {
|
update: false,
|
||||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
partial_update: false, // Means not editor
|
||||||
content: '',
|
retrieve: true,
|
||||||
title: 'Mocked document',
|
},
|
||||||
accesses: [],
|
|
||||||
abilities: {
|
|
||||||
destroy: false, // Means not owner
|
|
||||||
versions_destroy: false,
|
|
||||||
versions_list: true,
|
|
||||||
versions_retrieve: true,
|
|
||||||
manage_accesses: false, // Means not admin
|
|
||||||
update: false,
|
|
||||||
partial_update: false, // Means not editor
|
|
||||||
retrieve: true,
|
|
||||||
},
|
|
||||||
is_public: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await route.continue();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { goToGridDoc } from './common';
|
import { goToGridDoc, mockedDocument } from './common';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -8,57 +8,42 @@ test.beforeEach(async ({ page }) => {
|
|||||||
|
|
||||||
test.describe('Doc Header', () => {
|
test.describe('Doc Header', () => {
|
||||||
test('it checks the element are correctly displayed', async ({ page }) => {
|
test('it checks the element are correctly displayed', async ({ page }) => {
|
||||||
await page.route('**/documents/**/', async (route) => {
|
await mockedDocument(page, {
|
||||||
const request = route.request();
|
accesses: [
|
||||||
if (
|
{
|
||||||
request.method().includes('GET') &&
|
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||||
!request.url().includes('page=')
|
role: 'owner',
|
||||||
) {
|
user: {
|
||||||
await route.fulfill({
|
email: 'super@owner.com',
|
||||||
json: {
|
|
||||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
|
||||||
content: '',
|
|
||||||
title: 'Mocked document',
|
|
||||||
accesses: [
|
|
||||||
{
|
|
||||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
|
||||||
role: 'owner',
|
|
||||||
user: {
|
|
||||||
email: 'super@owner.com',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
|
||||||
role: 'admin',
|
|
||||||
user: {
|
|
||||||
email: 'super@admin.com',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
|
||||||
role: 'owner',
|
|
||||||
user: {
|
|
||||||
email: 'super2@owner.com',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
abilities: {
|
|
||||||
destroy: true, // Means owner
|
|
||||||
versions_destroy: true,
|
|
||||||
versions_list: true,
|
|
||||||
versions_retrieve: true,
|
|
||||||
manage_accesses: true,
|
|
||||||
update: true,
|
|
||||||
partial_update: true,
|
|
||||||
retrieve: true,
|
|
||||||
},
|
|
||||||
is_public: true,
|
|
||||||
created_at: '2021-09-01T09:00:00Z',
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
} else {
|
{
|
||||||
await route.continue();
|
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||||
}
|
role: 'admin',
|
||||||
|
user: {
|
||||||
|
email: 'super@admin.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||||
|
role: 'owner',
|
||||||
|
user: {
|
||||||
|
email: 'super2@owner.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
abilities: {
|
||||||
|
destroy: true, // Means owner
|
||||||
|
versions_destroy: true,
|
||||||
|
versions_list: true,
|
||||||
|
versions_retrieve: true,
|
||||||
|
manage_accesses: true,
|
||||||
|
update: true,
|
||||||
|
partial_update: true,
|
||||||
|
retrieve: true,
|
||||||
|
},
|
||||||
|
is_public: true,
|
||||||
|
created_at: '2021-09-01T09:00:00Z',
|
||||||
});
|
});
|
||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
|
|||||||
import cs from 'convert-stream';
|
import cs from 'convert-stream';
|
||||||
import pdf from 'pdf-parse';
|
import pdf from 'pdf-parse';
|
||||||
|
|
||||||
import { createDoc, goToGridDoc } from './common';
|
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -200,34 +200,17 @@ test.describe('Doc Tools', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it checks the options available if administrator', async ({ page }) => {
|
test('it checks the options available if administrator', async ({ page }) => {
|
||||||
await page.route('**/documents/**/', async (route) => {
|
await mockedDocument(page, {
|
||||||
const request = route.request();
|
abilities: {
|
||||||
if (
|
destroy: false, // Means not owner
|
||||||
request.method().includes('GET') &&
|
versions_destroy: true,
|
||||||
!request.url().includes('page=')
|
versions_list: true,
|
||||||
) {
|
versions_retrieve: true,
|
||||||
await route.fulfill({
|
manage_accesses: true, // Means admin
|
||||||
json: {
|
update: true,
|
||||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
partial_update: true,
|
||||||
content: '',
|
retrieve: true,
|
||||||
title: 'Mocked document',
|
},
|
||||||
accesses: [],
|
|
||||||
abilities: {
|
|
||||||
destroy: false, // Means not owner
|
|
||||||
versions_destroy: true,
|
|
||||||
versions_list: true,
|
|
||||||
versions_retrieve: true,
|
|
||||||
manage_accesses: true, // Means admin
|
|
||||||
update: true,
|
|
||||||
partial_update: true,
|
|
||||||
retrieve: true,
|
|
||||||
},
|
|
||||||
is_public: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await route.continue();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
@@ -250,34 +233,17 @@ test.describe('Doc Tools', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it checks the options available if editor', async ({ page }) => {
|
test('it checks the options available if editor', async ({ page }) => {
|
||||||
await page.route('**/documents/**/', async (route) => {
|
await mockedDocument(page, {
|
||||||
const request = route.request();
|
abilities: {
|
||||||
if (
|
destroy: false, // Means not owner
|
||||||
request.method().includes('GET') &&
|
versions_destroy: true,
|
||||||
!request.url().includes('page=')
|
versions_list: true,
|
||||||
) {
|
versions_retrieve: true,
|
||||||
await route.fulfill({
|
manage_accesses: false, // Means not admin
|
||||||
json: {
|
update: true,
|
||||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
partial_update: true, // Means editor
|
||||||
content: '',
|
retrieve: true,
|
||||||
title: 'Mocked document',
|
},
|
||||||
accesses: [],
|
|
||||||
abilities: {
|
|
||||||
destroy: false, // Means not owner
|
|
||||||
versions_destroy: true,
|
|
||||||
versions_list: true,
|
|
||||||
versions_retrieve: true,
|
|
||||||
manage_accesses: false, // Means not admin
|
|
||||||
update: true,
|
|
||||||
partial_update: true, // Means editor
|
|
||||||
retrieve: true,
|
|
||||||
},
|
|
||||||
is_public: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await route.continue();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
@@ -300,34 +266,17 @@ test.describe('Doc Tools', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it checks the options available if reader', async ({ page }) => {
|
test('it checks the options available if reader', async ({ page }) => {
|
||||||
await page.route('**/documents/**/', async (route) => {
|
await mockedDocument(page, {
|
||||||
const request = route.request();
|
abilities: {
|
||||||
if (
|
destroy: false, // Means not owner
|
||||||
request.method().includes('GET') &&
|
versions_destroy: false,
|
||||||
!request.url().includes('page=')
|
versions_list: true,
|
||||||
) {
|
versions_retrieve: true,
|
||||||
await route.fulfill({
|
manage_accesses: false, // Means not admin
|
||||||
json: {
|
update: false,
|
||||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
partial_update: false, // Means not editor
|
||||||
content: '',
|
retrieve: true,
|
||||||
title: 'Mocked document',
|
},
|
||||||
accesses: [],
|
|
||||||
abilities: {
|
|
||||||
destroy: false, // Means not owner
|
|
||||||
versions_destroy: false,
|
|
||||||
versions_list: true,
|
|
||||||
versions_retrieve: true,
|
|
||||||
manage_accesses: false, // Means not admin
|
|
||||||
update: false,
|
|
||||||
partial_update: false, // Means not editor
|
|
||||||
retrieve: true,
|
|
||||||
},
|
|
||||||
is_public: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await route.continue();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
const panel = page.getByLabel('Document version 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-editor')
|
||||||
|
.last()
|
||||||
|
.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 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('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 expect(page.getByLabel('Document version panel')).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,10 @@ server {
|
|||||||
try_files $uri index.html $uri/ =404;
|
try_files $uri index.html $uri/ =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ ^/docs/(.*)/versions/(.*)/$ {
|
||||||
|
error_page 404 /docs/[id]/versions/[versionId]/;
|
||||||
|
}
|
||||||
|
|
||||||
location /docs/ {
|
location /docs/ {
|
||||||
error_page 404 /docs/[id]/;
|
error_page 404 /docs/[id]/;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { WebrtcProvider } from 'y-webrtc';
|
|||||||
import { Box } from '@/components';
|
import { Box } from '@/components';
|
||||||
import { useAuthStore } from '@/core/auth';
|
import { useAuthStore } from '@/core/auth';
|
||||||
import { Doc } from '@/features/docs/doc-management';
|
import { Doc } from '@/features/docs/doc-management';
|
||||||
|
import { Version } from '@/features/docs/doc-versioning/';
|
||||||
|
|
||||||
import useSaveDoc from '../hook/useSaveDoc';
|
import useSaveDoc from '../hook/useSaveDoc';
|
||||||
import { useDocStore } from '../stores';
|
import { useDocStore } from '../stores';
|
||||||
@@ -17,31 +18,46 @@ import { BlockNoteToolbar } from './BlockNoteToolbar';
|
|||||||
|
|
||||||
interface BlockNoteEditorProps {
|
interface BlockNoteEditorProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
|
version?: Version;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockNoteEditor = ({ doc }: BlockNoteEditorProps) => {
|
export const BlockNoteEditor = ({ doc, version }: BlockNoteEditorProps) => {
|
||||||
const { createProvider, docsStore } = useDocStore();
|
const { createProvider, docsStore } = useDocStore();
|
||||||
const provider = docsStore?.[doc.id]?.provider;
|
const storeId = version?.id || doc.id;
|
||||||
|
const initialContent = version?.content || doc.content;
|
||||||
|
const provider = docsStore?.[storeId]?.provider;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!provider || provider.doc.guid !== storeId) {
|
||||||
|
createProvider(storeId, initialContent);
|
||||||
|
}
|
||||||
|
}, [createProvider, initialContent, provider, storeId]);
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
createProvider(doc.id, doc.content);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BlockNoteContent doc={doc} provider={provider} />;
|
return <BlockNoteContent doc={doc} provider={provider} storeId={storeId} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface BlockNoteContentProps {
|
interface BlockNoteContentProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
provider: WebrtcProvider;
|
provider: WebrtcProvider;
|
||||||
|
storeId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockNoteContent = ({ doc, provider }: BlockNoteContentProps) => {
|
export const BlockNoteContent = ({
|
||||||
|
doc,
|
||||||
|
provider,
|
||||||
|
storeId,
|
||||||
|
}: BlockNoteContentProps) => {
|
||||||
|
const isVersion = doc.id !== storeId;
|
||||||
const { userData } = useAuthStore();
|
const { userData } = useAuthStore();
|
||||||
const { setStore, docsStore } = useDocStore();
|
const { setStore, docsStore } = useDocStore();
|
||||||
useSaveDoc(doc.id, provider.doc, doc.abilities.partial_update);
|
const canSave = doc.abilities.partial_update && !isVersion;
|
||||||
|
useSaveDoc(doc.id, provider.doc, canSave);
|
||||||
|
|
||||||
const storedEditor = docsStore?.[doc.id]?.editor;
|
const storedEditor = docsStore?.[storeId]?.editor;
|
||||||
const editor = useMemo(() => {
|
const editor = useMemo(() => {
|
||||||
if (storedEditor) {
|
if (storedEditor) {
|
||||||
return storedEditor;
|
return storedEditor;
|
||||||
@@ -60,8 +76,8 @@ export const BlockNoteContent = ({ doc, provider }: BlockNoteContentProps) => {
|
|||||||
}, [provider, storedEditor, userData?.email]);
|
}, [provider, storedEditor, userData?.email]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStore(doc.id, { editor });
|
setStore(storeId, { editor });
|
||||||
}, [setStore, doc.id, editor]);
|
}, [setStore, storeId, editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -77,7 +93,7 @@ export const BlockNoteContent = ({ doc, provider }: BlockNoteContentProps) => {
|
|||||||
<BlockNoteView
|
<BlockNoteView
|
||||||
editor={editor}
|
editor={editor}
|
||||||
formattingToolbar={false}
|
formattingToolbar={false}
|
||||||
editable={doc.abilities.partial_update}
|
editable={doc.abilities.partial_update && !isVersion}
|
||||||
theme="light"
|
theme="light"
|
||||||
>
|
>
|
||||||
<BlockNoteToolbar />
|
<BlockNoteToolbar />
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { Alert, VariantType } from '@openfun/cunningham-react';
|
import { Alert, Loader, VariantType } from '@openfun/cunningham-react';
|
||||||
|
import { useRouter as useNavigate } from 'next/navigation';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Box, Card } from '@/components';
|
import { Box, Card, Text, TextErrors } from '@/components';
|
||||||
|
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 {
|
||||||
|
Panel,
|
||||||
|
Versions,
|
||||||
|
useDocVersion,
|
||||||
|
} from '@/features/docs/doc-versioning/';
|
||||||
|
|
||||||
import { BlockNoteEditor } from './BlockNoteEditor';
|
import { BlockNoteEditor } from './BlockNoteEditor';
|
||||||
|
|
||||||
@@ -12,24 +21,101 @@ interface DocEditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DocEditor = ({ doc }: DocEditorProps) => {
|
export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||||
|
const {
|
||||||
|
query: { versionId },
|
||||||
|
} = useRouter();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isVersion = versionId && typeof versionId === 'string';
|
||||||
|
|
||||||
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocHeader doc={doc} />
|
<DocHeader doc={doc} />
|
||||||
{!doc.abilities.partial_update && (
|
{!doc.abilities.partial_update && (
|
||||||
<Box $margin={{ all: 'small', top: 'none' }}>
|
<Box $margin={{ all: 'small', top: 'none' }}>
|
||||||
<Alert
|
<Alert type={VariantType.WARNING}>
|
||||||
type={VariantType.WARNING}
|
{t(`Read only, you don't have the right to update this document.`)}
|
||||||
>{`Read only, you don't have the right to update this document.`}</Alert>
|
</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Card
|
{isVersion && (
|
||||||
$margin={{ all: 'big', top: 'none' }}
|
<Box $margin={{ all: 'small', top: 'none' }}>
|
||||||
$padding="big"
|
<Alert type={VariantType.WARNING}>
|
||||||
$css="flex:1;"
|
{t(`You cannot edit document version.`)}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
$background={colorsTokens()['primary-bg']}
|
||||||
|
$height="100%"
|
||||||
|
$direction="row"
|
||||||
|
$margin={{ all: 'small', top: 'none' }}
|
||||||
|
$gap="1rem"
|
||||||
$overflow="auto"
|
$overflow="auto"
|
||||||
>
|
>
|
||||||
<BlockNoteEditor doc={doc} />
|
<Card $padding="big" $css="flex:1;" $overflow="auto">
|
||||||
</Card>
|
{isVersion ? (
|
||||||
|
<DocVersionEditor doc={doc} versionId={versionId} />
|
||||||
|
) : (
|
||||||
|
<BlockNoteEditor doc={doc} />
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
{doc.abilities.versions_list && <Panel doc={doc} />}
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface DocVersionEditorProps {
|
||||||
|
doc: Doc;
|
||||||
|
versionId: Versions['version_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocVersionEditor = ({ doc, versionId }: DocVersionEditorProps) => {
|
||||||
|
const {
|
||||||
|
data: version,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useDocVersion({
|
||||||
|
docId: doc.id,
|
||||||
|
versionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (isError && error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
navigate.replace(`/404`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box $margin="large">
|
||||||
|
<TextErrors
|
||||||
|
causes={error.cause}
|
||||||
|
icon={
|
||||||
|
error.status === 502 ? (
|
||||||
|
<Text className="material-icons" $theme="danger">
|
||||||
|
wifi_off
|
||||||
|
</Text>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading || !version) {
|
||||||
|
return (
|
||||||
|
<Box $align="center" $justify="center" $height="100%">
|
||||||
|
<Loader />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BlockNoteEditor doc={doc} version={version} />;
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as Y from 'yjs';
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { signalingUrl } from '@/core';
|
import { signalingUrl } from '@/core';
|
||||||
import { Base64, Doc } from '@/features/docs/doc-management';
|
import { Base64 } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
interface DocStore {
|
interface DocStore {
|
||||||
provider: WebrtcProvider;
|
provider: WebrtcProvider;
|
||||||
@@ -13,43 +13,39 @@ interface DocStore {
|
|||||||
|
|
||||||
export interface UseDocStore {
|
export interface UseDocStore {
|
||||||
docsStore: {
|
docsStore: {
|
||||||
[docId: Doc['id']]: DocStore;
|
[storeId: string]: DocStore;
|
||||||
};
|
};
|
||||||
createProvider: (docId: Doc['id'], initialDoc: Base64) => WebrtcProvider;
|
createProvider: (storeId: string, initialDoc: Base64) => WebrtcProvider;
|
||||||
setStore: (docId: Doc['id'], props: Partial<DocStore>) => void;
|
setStore: (storeId: string, props: Partial<DocStore>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
docsStore: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDocStore = create<UseDocStore>((set, get) => ({
|
export const useDocStore = create<UseDocStore>((set, get) => ({
|
||||||
docsStore: initialState.docsStore,
|
docsStore: {},
|
||||||
createProvider: (docId: string, initialDoc: Base64) => {
|
createProvider: (storeId: string, initialDoc: Base64) => {
|
||||||
const doc = new Y.Doc({
|
const doc = new Y.Doc({
|
||||||
guid: docId,
|
guid: storeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (initialDoc) {
|
if (initialDoc) {
|
||||||
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
|
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = new WebrtcProvider(docId, doc, {
|
const provider = new WebrtcProvider(storeId, doc, {
|
||||||
signaling: [signalingUrl(docId)],
|
signaling: [signalingUrl(storeId)],
|
||||||
maxConns: 5,
|
maxConns: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
get().setStore(docId, { provider });
|
get().setStore(storeId, { provider });
|
||||||
|
|
||||||
return provider;
|
return provider;
|
||||||
},
|
},
|
||||||
setStore: (docId, props) => {
|
setStore: (storeId, props) => {
|
||||||
set(({ docsStore }) => {
|
set(({ docsStore }) => {
|
||||||
return {
|
return {
|
||||||
docsStore: {
|
docsStore: {
|
||||||
...docsStore,
|
...docsStore,
|
||||||
[docId]: {
|
[storeId]: {
|
||||||
...docsStore[docId],
|
...docsStore[storeId],
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
$margin="big"
|
$margin="small"
|
||||||
aria-label={t('It is the card information about the document.')}
|
aria-label={t('It is the card information about the document.')}
|
||||||
>
|
>
|
||||||
<Box $padding="small" $direction="row" $align="center">
|
<Box $padding="small" $direction="row" $align="center">
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './useDocVersions';
|
export * from './useDocVersions';
|
||||||
|
export * from './useDocVersion';
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Box, Card, IconBG, Text } from '@/components';
|
||||||
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { Doc } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
|
import { VersionList } from './VersionList';
|
||||||
|
|
||||||
|
interface PanelProps {
|
||||||
|
doc: Doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Panel = ({ doc }: PanelProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
|
const closedOverridingStyles = !isOpen && {
|
||||||
|
$width: '0',
|
||||||
|
$maxWidth: '0',
|
||||||
|
$minWidth: '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const transition = 'all 0.5s ease-in-out';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
$width="100%"
|
||||||
|
$maxWidth="20rem"
|
||||||
|
$position="relative"
|
||||||
|
$css={`
|
||||||
|
transition: ${transition};
|
||||||
|
${
|
||||||
|
!isOpen &&
|
||||||
|
`
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
aria-label={t('Document version panel')}
|
||||||
|
{...closedOverridingStyles}
|
||||||
|
>
|
||||||
|
<IconBG
|
||||||
|
iconName="menu_open"
|
||||||
|
aria-label={
|
||||||
|
isOpen
|
||||||
|
? t('Close the document version panel')
|
||||||
|
: t('Open the document version panel')
|
||||||
|
}
|
||||||
|
$background="white"
|
||||||
|
$size="h2"
|
||||||
|
$zIndex={1}
|
||||||
|
$css={`
|
||||||
|
cursor: pointer;
|
||||||
|
left: ${isOpen ? '0' : '-3.3'}rem;
|
||||||
|
top: 0.1rem;
|
||||||
|
transform: rotate(${isOpen ? '180' : '0'}deg);
|
||||||
|
transition: ${transition};
|
||||||
|
user-select: none;
|
||||||
|
`}
|
||||||
|
$position="absolute"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
$radius="2px"
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
$overflow="hidden"
|
||||||
|
$css={`
|
||||||
|
opacity: ${isOpen ? '1' : '0'};
|
||||||
|
transition: ${transition};
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
$padding={{ all: 'small' }}
|
||||||
|
$direction="row"
|
||||||
|
$align="center"
|
||||||
|
$justify="center"
|
||||||
|
$css={`border-top: 2px solid ${colorsTokens()['primary-600']};`}
|
||||||
|
>
|
||||||
|
<Text $weight="bold" $size="l" $theme="primary">
|
||||||
|
{t('VERSIONS')}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<VersionList doc={doc} />
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Box, StyledLink, Text } from '@/components';
|
||||||
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
|
||||||
|
import { Versions } from '../types';
|
||||||
|
|
||||||
|
interface VersionItemProps {
|
||||||
|
text: string;
|
||||||
|
link: string;
|
||||||
|
versionId?: Versions['version_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VersionItem = ({ versionId, text, link }: VersionItemProps) => {
|
||||||
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
const {
|
||||||
|
query: { versionId: currentId },
|
||||||
|
} = useRouter();
|
||||||
|
|
||||||
|
const isActive = versionId === currentId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
as="li"
|
||||||
|
$background={isActive ? colorsTokens()['primary-300'] : 'transparent'}
|
||||||
|
$css={`
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
border-bottom: 1px solid ${colorsTokens()['primary-100']};
|
||||||
|
&:hover{
|
||||||
|
border-left: 4px solid ${colorsTokens()['primary-400']};
|
||||||
|
background: ${colorsTokens()['primary-300']};
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
$hasTransition
|
||||||
|
$minWidth="13rem"
|
||||||
|
>
|
||||||
|
<StyledLink
|
||||||
|
href={link}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (isActive) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
$padding={{ vertical: '0.7rem', horizontal: 'small' }}
|
||||||
|
$align="center"
|
||||||
|
$direction="row"
|
||||||
|
$justify="space-between"
|
||||||
|
$width="100%"
|
||||||
|
>
|
||||||
|
<Box $direction="row" $gap="0.5rem" $align="center">
|
||||||
|
<Text $isMaterialIcon $size="24px" $theme="primary">
|
||||||
|
description
|
||||||
|
</Text>
|
||||||
|
<Text $weight="bold" $theme="primary" $size="m">
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</StyledLink>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { Loader } from '@openfun/cunningham-react';
|
||||||
|
import React, { useMemo, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { APIError } from '@/api';
|
||||||
|
import { Box, InfiniteScroll, Text, TextErrors } from '@/components';
|
||||||
|
import { Doc } from '@/features/docs/doc-management';
|
||||||
|
import { useDate } from '@/hook';
|
||||||
|
|
||||||
|
import { useDocVersionsInfiniteQuery } from '../api/useDocVersions';
|
||||||
|
import { Versions } from '../types';
|
||||||
|
|
||||||
|
import { VersionItem } from './VersionItem';
|
||||||
|
|
||||||
|
interface VersionListStateProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: APIError<unknown> | null;
|
||||||
|
versions?: Versions[];
|
||||||
|
doc: Doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VersionListState = ({
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
versions,
|
||||||
|
doc,
|
||||||
|
}: VersionListStateProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { formatDate } = useDate();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Box $align="center" $margin="large">
|
||||||
|
<Loader />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<VersionItem
|
||||||
|
text={t('Current version')}
|
||||||
|
versionId={undefined}
|
||||||
|
link={`/docs/${doc.id}/`}
|
||||||
|
/>
|
||||||
|
{versions?.map((version) => (
|
||||||
|
<VersionItem
|
||||||
|
key={version.version_id}
|
||||||
|
versionId={version.version_id}
|
||||||
|
text={formatDate(version.last_modified, {
|
||||||
|
dateStyle: 'long',
|
||||||
|
timeStyle: 'short',
|
||||||
|
})}
|
||||||
|
link={`/docs/${doc.id}/versions/${version.version_id}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{error && (
|
||||||
|
<Box
|
||||||
|
$justify="center"
|
||||||
|
$margin={{ vertical: 'big', horizontal: 'auto' }}
|
||||||
|
>
|
||||||
|
<TextErrors
|
||||||
|
causes={error.cause}
|
||||||
|
icon={
|
||||||
|
error.status === 502 ? (
|
||||||
|
<Text $isMaterialIcon $theme="danger">
|
||||||
|
wifi_off
|
||||||
|
</Text>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface VersionListProps {
|
||||||
|
doc: Doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VersionList = ({ doc }: VersionListProps) => {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
} = useDocVersionsInfiniteQuery({
|
||||||
|
docId: doc.id,
|
||||||
|
});
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const versions = useMemo(() => {
|
||||||
|
return data?.pages.reduce((acc, page) => {
|
||||||
|
return acc.concat(page.results);
|
||||||
|
}, [] as Versions[]);
|
||||||
|
}, [data?.pages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box $css="overflow-y: auto; overflow-x: hidden;" ref={containerRef}>
|
||||||
|
<InfiniteScroll
|
||||||
|
hasMore={hasNextPage}
|
||||||
|
isLoading={isFetchingNextPage}
|
||||||
|
next={() => {
|
||||||
|
void fetchNextPage();
|
||||||
|
}}
|
||||||
|
scrollContainer={containerRef.current}
|
||||||
|
as="ul"
|
||||||
|
$padding="none"
|
||||||
|
$margin={{ top: 'none' }}
|
||||||
|
role="listbox"
|
||||||
|
>
|
||||||
|
<VersionListState
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={error}
|
||||||
|
versions={versions}
|
||||||
|
doc={doc}
|
||||||
|
/>
|
||||||
|
</InfiniteScroll>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './Panel';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './api';
|
||||||
|
export * from './components';
|
||||||
|
export * from './types';
|
||||||
@@ -10,4 +10,5 @@ export interface Versions {
|
|||||||
export interface Version {
|
export interface Version {
|
||||||
content: Doc['content'];
|
content: Doc['content'];
|
||||||
last_modified: string;
|
last_modified: string;
|
||||||
|
id: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DateTime, DateTimeFormatOptions } from 'luxon';
|
import { DateTime, DateTimeFormatOptions } from 'luxon';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const format: DateTimeFormatOptions = {
|
const formatDefault: DateTimeFormatOptions = {
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -12,7 +12,10 @@ const format: DateTimeFormatOptions = {
|
|||||||
export const useDate = () => {
|
export const useDate = () => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
const formatDate = (date: string): string => {
|
const formatDate = (
|
||||||
|
date: string,
|
||||||
|
format: DateTimeFormatOptions = formatDefault,
|
||||||
|
): string => {
|
||||||
return DateTime.fromISO(date)
|
return DateTime.fromISO(date)
|
||||||
.setLocale(i18n.language)
|
.setLocale(i18n.language)
|
||||||
.toLocaleString(format);
|
.toLocaleString(format);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Loader } from '@openfun/cunningham-react';
|
import { Loader } from '@openfun/cunningham-react';
|
||||||
import { useRouter as useNavigate } from 'next/navigation';
|
import { useRouter as useNavigate } from 'next/navigation';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { ReactElement } from 'react';
|
|
||||||
|
|
||||||
import { Box, Text, TextErrors } from '@/components/';
|
import { Box, Text } from '@/components';
|
||||||
import { DocEditor } from '@/features/docs/doc-editor';
|
import { TextErrors } from '@/components/TextErrors';
|
||||||
|
import { DocEditor } from '@/features/docs';
|
||||||
import { useDoc } from '@/features/docs/doc-management';
|
import { useDoc } from '@/features/docs/doc-management';
|
||||||
import { MainLayout } from '@/layouts';
|
import { MainLayout } from '@/layouts';
|
||||||
import { NextPageWithLayout } from '@/types/next';
|
import { NextPageWithLayout } from '@/types/next';
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
export function DocLayout() {
|
||||||
const {
|
const {
|
||||||
query: { id },
|
query: { id },
|
||||||
} = useRouter();
|
} = useRouter();
|
||||||
@@ -18,14 +18,18 @@ const Page: NextPageWithLayout = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Doc id={id} />;
|
return (
|
||||||
};
|
<MainLayout>
|
||||||
|
<DocPage id={id} />
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface DocProps {
|
interface DocProps {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Doc = ({ id }: DocProps) => {
|
const DocPage = ({ id }: DocProps) => {
|
||||||
const { data: doc, isLoading, isError, error } = useDoc({ id });
|
const { data: doc, isLoading, isError, error } = useDoc({ id });
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -41,7 +45,7 @@ const Doc = ({ id }: DocProps) => {
|
|||||||
causes={error.cause}
|
causes={error.cause}
|
||||||
icon={
|
icon={
|
||||||
error.status === 502 ? (
|
error.status === 502 ? (
|
||||||
<Text className="material-icons" $theme="danger">
|
<Text $isMaterialIcon $theme="danger">
|
||||||
wifi_off
|
wifi_off
|
||||||
</Text>
|
</Text>
|
||||||
) : undefined
|
) : undefined
|
||||||
@@ -62,8 +66,12 @@ const Doc = ({ id }: DocProps) => {
|
|||||||
return <DocEditor doc={doc} />;
|
return <DocEditor doc={doc} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
Page.getLayout = function getLayout(page: ReactElement) {
|
const Page: NextPageWithLayout = () => {
|
||||||
return <MainLayout>{page}</MainLayout>;
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout() {
|
||||||
|
return <DocLayout />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { NextPageWithLayout } from '@/types/next';
|
||||||
|
|
||||||
|
import { DocLayout } from '../index';
|
||||||
|
|
||||||
|
const Page: NextPageWithLayout = () => {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout() {
|
||||||
|
return <DocLayout />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { NextPageWithLayout } from '@/types/next';
|
||||||
|
|
||||||
|
import { DocLayout } from '../index';
|
||||||
|
|
||||||
|
const Page: NextPageWithLayout = () => {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout() {
|
||||||
|
return <DocLayout />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -31,3 +31,13 @@ main ::-webkit-scrollbar-thumb:hover,
|
|||||||
.ReactModalPortal ::-webkit-scrollbar-thumb:hover {
|
.ReactModalPortal ::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: #a8bbbf;
|
background-color: #a8bbbf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-unstyled {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: inherit;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user