diff --git a/CHANGELOG.md b/CHANGELOG.md
index 708cde87..9288f36f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,11 @@ and this project adheres to
## Added
- 🎨(frontend) better conversion editor to pdf #151
+- ✨(frontend) Versioning #147
+
+## Fixed
+
+- 🐛(y-webrtc) fix prob connection #147
## [1.1.0] - 2024-07-15
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/common.ts b/src/frontend/apps/e2e/__tests__/app-impress/common.ts
index e620e735..8ca94fc0 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/common.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/common.ts
@@ -140,3 +140,34 @@ export const goToGridDoc = async (
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();
+ }
+ });
+};
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
index 4654945c..d27cb052 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
-import { createDoc, goToGridDoc } from './common';
+import { createDoc, goToGridDoc, mockedDocument } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -153,34 +153,17 @@ test.describe('Doc Editor', () => {
});
test('it cannot edit if viewer', async ({ page }) => {
- 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,
- },
- });
- } else {
- await route.continue();
- }
+ await mockedDocument(page, {
+ 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,
+ },
});
await goToGridDoc(page);
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
index 0da98bd5..eb6dfa51 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
-import { goToGridDoc } from './common';
+import { goToGridDoc, mockedDocument } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -8,57 +8,42 @@ test.beforeEach(async ({ page }) => {
test.describe('Doc Header', () => {
test('it checks the element are correctly displayed', async ({ page }) => {
- 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: [
- {
- 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',
+ await mockedDocument(page, {
+ accesses: [
+ {
+ id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
+ role: 'owner',
+ user: {
+ email: 'super@owner.com',
},
- });
- } 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);
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts
index 96184095..eb3096ca 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import pdf from 'pdf-parse';
-import { createDoc, goToGridDoc } from './common';
+import { createDoc, goToGridDoc, mockedDocument } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -200,34 +200,17 @@ test.describe('Doc Tools', () => {
});
test('it checks the options available if administrator', async ({ page }) => {
- 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: 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 mockedDocument(page, {
+ 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,
+ },
});
await goToGridDoc(page);
@@ -250,34 +233,17 @@ test.describe('Doc Tools', () => {
});
test('it checks the options available if editor', async ({ page }) => {
- 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: 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 mockedDocument(page, {
+ 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,
+ },
});
await goToGridDoc(page);
@@ -300,34 +266,17 @@ test.describe('Doc Tools', () => {
});
test('it checks the options available if reader', async ({ page }) => {
- 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,
- },
- });
- } else {
- await route.continue();
- }
+ await mockedDocument(page, {
+ 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,
+ },
});
await goToGridDoc(page);
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts
new file mode 100644
index 00000000..bc1f28d0
--- /dev/null
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts
@@ -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();
+ });
+});
diff --git a/src/frontend/apps/impress/conf/default.conf b/src/frontend/apps/impress/conf/default.conf
index e2ab0b11..cb639442 100644
--- a/src/frontend/apps/impress/conf/default.conf
+++ b/src/frontend/apps/impress/conf/default.conf
@@ -8,6 +8,10 @@ server {
try_files $uri index.html $uri/ =404;
}
+ location ~ ^/docs/(.*)/versions/(.*)/$ {
+ error_page 404 /docs/[id]/versions/[versionId]/;
+ }
+
location /docs/ {
error_page 404 /docs/[id]/;
}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
index da092f09..b1447397 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
@@ -8,6 +8,7 @@ import { WebrtcProvider } from 'y-webrtc';
import { Box } from '@/components';
import { useAuthStore } from '@/core/auth';
import { Doc } from '@/features/docs/doc-management';
+import { Version } from '@/features/docs/doc-versioning/';
import useSaveDoc from '../hook/useSaveDoc';
import { useDocStore } from '../stores';
@@ -17,31 +18,46 @@ import { BlockNoteToolbar } from './BlockNoteToolbar';
interface BlockNoteEditorProps {
doc: Doc;
+ version?: Version;
}
-export const BlockNoteEditor = ({ doc }: BlockNoteEditorProps) => {
+export const BlockNoteEditor = ({ doc, version }: BlockNoteEditorProps) => {
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) {
- createProvider(doc.id, doc.content);
return null;
}
- return ;
+ return ;
};
interface BlockNoteContentProps {
doc: Doc;
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 { 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(() => {
if (storedEditor) {
return storedEditor;
@@ -60,8 +76,8 @@ export const BlockNoteContent = ({ doc, provider }: BlockNoteContentProps) => {
}, [provider, storedEditor, userData?.email]);
useEffect(() => {
- setStore(doc.id, { editor });
- }, [setStore, doc.id, editor]);
+ setStore(storeId, { editor });
+ }, [setStore, storeId, editor]);
return (
{
diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx
index 7f134879..e8660739 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx
@@ -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 { 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 { Doc } from '@/features/docs/doc-management';
+import {
+ Panel,
+ Versions,
+ useDocVersion,
+} from '@/features/docs/doc-versioning/';
import { BlockNoteEditor } from './BlockNoteEditor';
@@ -12,24 +21,101 @@ interface DocEditorProps {
}
export const DocEditor = ({ doc }: DocEditorProps) => {
+ const {
+ query: { versionId },
+ } = useRouter();
+
+ const { t } = useTranslation();
+
+ const isVersion = versionId && typeof versionId === 'string';
+
+ const { colorsTokens } = useCunninghamTheme();
+
return (
<>
{!doc.abilities.partial_update && (
- {`Read only, you don't have the right to update this document.`}
+
+ {t(`Read only, you don't have the right to update this document.`)}
+
)}
-
+
+ {t(`You cannot edit document version.`)}
+
+
+ )}
+
-
-
+
+ {isVersion ? (
+
+ ) : (
+
+ )}
+
+ {doc.abilities.versions_list && }
+
>
);
};
+
+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 (
+
+
+ wifi_off
+
+ ) : undefined
+ }
+ />
+
+ );
+ }
+
+ if (isLoading || !version) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useDocStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useDocStore.tsx
index 4784ac12..db877db8 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useDocStore.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useDocStore.tsx
@@ -4,7 +4,7 @@ import * as Y from 'yjs';
import { create } from 'zustand';
import { signalingUrl } from '@/core';
-import { Base64, Doc } from '@/features/docs/doc-management';
+import { Base64 } from '@/features/docs/doc-management';
interface DocStore {
provider: WebrtcProvider;
@@ -13,43 +13,39 @@ interface DocStore {
export interface UseDocStore {
docsStore: {
- [docId: Doc['id']]: DocStore;
+ [storeId: string]: DocStore;
};
- createProvider: (docId: Doc['id'], initialDoc: Base64) => WebrtcProvider;
- setStore: (docId: Doc['id'], props: Partial) => void;
+ createProvider: (storeId: string, initialDoc: Base64) => WebrtcProvider;
+ setStore: (storeId: string, props: Partial) => void;
}
-const initialState = {
- docsStore: {},
-};
-
export const useDocStore = create((set, get) => ({
- docsStore: initialState.docsStore,
- createProvider: (docId: string, initialDoc: Base64) => {
+ docsStore: {},
+ createProvider: (storeId: string, initialDoc: Base64) => {
const doc = new Y.Doc({
- guid: docId,
+ guid: storeId,
});
if (initialDoc) {
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
}
- const provider = new WebrtcProvider(docId, doc, {
- signaling: [signalingUrl(docId)],
+ const provider = new WebrtcProvider(storeId, doc, {
+ signaling: [signalingUrl(storeId)],
maxConns: 5,
});
- get().setStore(docId, { provider });
+ get().setStore(storeId, { provider });
return provider;
},
- setStore: (docId, props) => {
+ setStore: (storeId, props) => {
set(({ docsStore }) => {
return {
docsStore: {
...docsStore,
- [docId]: {
- ...docsStore[docId],
+ [storeId]: {
+ ...docsStore[storeId],
...props,
},
},
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
index 4a842548..68582013 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
@@ -25,7 +25,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
return (
diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/api/index.ts b/src/frontend/apps/impress/src/features/docs/doc-versioning/api/index.ts
index fad889b3..d300be2c 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-versioning/api/index.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/api/index.ts
@@ -1 +1,2 @@
export * from './useDocVersions';
+export * from './useDocVersion';
diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/Panel.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/Panel.tsx
new file mode 100644
index 00000000..a370760b
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/Panel.tsx
@@ -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 (
+
+ setIsOpen(!isOpen)}
+ $radius="2px"
+ />
+
+
+
+ {t('VERSIONS')}
+
+
+
+
+
+ );
+};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionItem.tsx
new file mode 100644
index 00000000..6dfea4c8
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionItem.tsx
@@ -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 (
+
+ {
+ if (isActive) {
+ e.preventDefault();
+ }
+ }}
+ >
+
+
+
+ description
+
+
+ {text}
+
+
+
+
+
+ );
+};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionList.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionList.tsx
new file mode 100644
index 00000000..9adea71d
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionList.tsx
@@ -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 | null;
+ versions?: Versions[];
+ doc: Doc;
+}
+
+const VersionListState = ({
+ isLoading,
+ error,
+ versions,
+ doc,
+}: VersionListStateProps) => {
+ const { t } = useTranslation();
+ const { formatDate } = useDate();
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+ {versions?.map((version) => (
+
+ ))}
+ {error && (
+
+
+ wifi_off
+
+ ) : undefined
+ }
+ />
+
+ )}
+ >
+ );
+};
+
+interface VersionListProps {
+ doc: Doc;
+}
+
+export const VersionList = ({ doc }: VersionListProps) => {
+ const {
+ data,
+ error,
+ isLoading,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = useDocVersionsInfiniteQuery({
+ docId: doc.id,
+ });
+ const containerRef = useRef(null);
+ const versions = useMemo(() => {
+ return data?.pages.reduce((acc, page) => {
+ return acc.concat(page.results);
+ }, [] as Versions[]);
+ }, [data?.pages]);
+
+ return (
+
+ {
+ void fetchNextPage();
+ }}
+ scrollContainer={containerRef.current}
+ as="ul"
+ $padding="none"
+ $margin={{ top: 'none' }}
+ role="listbox"
+ >
+
+
+
+ );
+};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/index.ts
new file mode 100644
index 00000000..8960d84f
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/index.ts
@@ -0,0 +1 @@
+export * from './Panel';
diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/index.ts b/src/frontend/apps/impress/src/features/docs/doc-versioning/index.ts
new file mode 100644
index 00000000..314dad0c
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/index.ts
@@ -0,0 +1,3 @@
+export * from './api';
+export * from './components';
+export * from './types';
diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/types.ts b/src/frontend/apps/impress/src/features/docs/doc-versioning/types.ts
index 4c60b988..d6246c78 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-versioning/types.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/types.ts
@@ -10,4 +10,5 @@ export interface Versions {
export interface Version {
content: Doc['content'];
last_modified: string;
+ id: string;
}
diff --git a/src/frontend/apps/impress/src/hook/useDate.tsx b/src/frontend/apps/impress/src/hook/useDate.tsx
index bfe2bd4c..5b1b78be 100644
--- a/src/frontend/apps/impress/src/hook/useDate.tsx
+++ b/src/frontend/apps/impress/src/hook/useDate.tsx
@@ -1,7 +1,7 @@
import { DateTime, DateTimeFormatOptions } from 'luxon';
import { useTranslation } from 'react-i18next';
-const format: DateTimeFormatOptions = {
+const formatDefault: DateTimeFormatOptions = {
month: '2-digit',
day: '2-digit',
year: 'numeric',
@@ -12,7 +12,10 @@ const format: DateTimeFormatOptions = {
export const useDate = () => {
const { i18n } = useTranslation();
- const formatDate = (date: string): string => {
+ const formatDate = (
+ date: string,
+ format: DateTimeFormatOptions = formatDefault,
+ ): string => {
return DateTime.fromISO(date)
.setLocale(i18n.language)
.toLocaleString(format);
diff --git a/src/frontend/apps/impress/src/pages/docs/[id].tsx b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
similarity index 73%
rename from src/frontend/apps/impress/src/pages/docs/[id].tsx
rename to src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
index c3164be3..72c200d0 100644
--- a/src/frontend/apps/impress/src/pages/docs/[id].tsx
+++ b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
@@ -1,15 +1,15 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter as useNavigate } from 'next/navigation';
import { useRouter } from 'next/router';
-import { ReactElement } from 'react';
-import { Box, Text, TextErrors } from '@/components/';
-import { DocEditor } from '@/features/docs/doc-editor';
+import { Box, Text } from '@/components';
+import { TextErrors } from '@/components/TextErrors';
+import { DocEditor } from '@/features/docs';
import { useDoc } from '@/features/docs/doc-management';
import { MainLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
-const Page: NextPageWithLayout = () => {
+export function DocLayout() {
const {
query: { id },
} = useRouter();
@@ -18,14 +18,18 @@ const Page: NextPageWithLayout = () => {
return null;
}
- return ;
-};
+ return (
+
+
+
+ );
+}
interface DocProps {
id: string;
}
-const Doc = ({ id }: DocProps) => {
+const DocPage = ({ id }: DocProps) => {
const { data: doc, isLoading, isError, error } = useDoc({ id });
const navigate = useNavigate();
@@ -41,7 +45,7 @@ const Doc = ({ id }: DocProps) => {
causes={error.cause}
icon={
error.status === 502 ? (
-
+
wifi_off
) : undefined
@@ -62,8 +66,12 @@ const Doc = ({ id }: DocProps) => {
return ;
};
-Page.getLayout = function getLayout(page: ReactElement) {
- return {page};
+const Page: NextPageWithLayout = () => {
+ return null;
+};
+
+Page.getLayout = function getLayout() {
+ return ;
};
export default Page;
diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/versions/[versionId].tsx b/src/frontend/apps/impress/src/pages/docs/[id]/versions/[versionId].tsx
new file mode 100644
index 00000000..0eed2ba3
--- /dev/null
+++ b/src/frontend/apps/impress/src/pages/docs/[id]/versions/[versionId].tsx
@@ -0,0 +1,13 @@
+import { NextPageWithLayout } from '@/types/next';
+
+import { DocLayout } from '../index';
+
+const Page: NextPageWithLayout = () => {
+ return null;
+};
+
+Page.getLayout = function getLayout() {
+ return ;
+};
+
+export default Page;
diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/versions/index.tsx b/src/frontend/apps/impress/src/pages/docs/[id]/versions/index.tsx
new file mode 100644
index 00000000..0eed2ba3
--- /dev/null
+++ b/src/frontend/apps/impress/src/pages/docs/[id]/versions/index.tsx
@@ -0,0 +1,13 @@
+import { NextPageWithLayout } from '@/types/next';
+
+import { DocLayout } from '../index';
+
+const Page: NextPageWithLayout = () => {
+ return null;
+};
+
+Page.getLayout = function getLayout() {
+ return ;
+};
+
+export default Page;
diff --git a/src/frontend/apps/impress/src/pages/globals.css b/src/frontend/apps/impress/src/pages/globals.css
index 20e745a5..930d8359 100644
--- a/src/frontend/apps/impress/src/pages/globals.css
+++ b/src/frontend/apps/impress/src/pages/globals.css
@@ -31,3 +31,13 @@ main ::-webkit-scrollbar-thumb:hover,
.ReactModalPortal ::-webkit-scrollbar-thumb:hover {
background-color: #a8bbbf;
}
+
+.btn-unstyled {
+ background: none;
+ color: inherit;
+ border: none;
+ padding: 0;
+ font: inherit;
+ cursor: pointer;
+ outline: inherit;
+}