diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7acabdb3..3b611656 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project adheres to
- 🚩(frontend) version MIT only #911
- ✨(backend) integrate maleware_detection from django-lasuite #936
- 🩺(CI) add lint spell mistakes #954
+- 🛂(frontend) block edition to not connected users #945
## Changed
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/common.ts b/src/frontend/apps/e2e/__tests__/app-impress/common.ts
index 8217dc1b..09e1ce74 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/common.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/common.ts
@@ -102,7 +102,7 @@ export const verifyDocName = async (page: Page, docName: string) => {
export const addNewMember = async (
page: Page,
index: number,
- role: 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader',
+ role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
fillText: string = 'user ',
) => {
const responsePromiseSearchUser = page.waitForResponse(
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 d1a8d856..87d13c80 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
@@ -4,6 +4,8 @@ import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import {
+ CONFIG,
+ addNewMember,
createDoc,
goToGridDoc,
mockedDocument,
@@ -363,7 +365,7 @@ test.describe('Doc Editor', () => {
partial_update: true,
retrieve: true,
},
- link_reach: 'public',
+ link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
@@ -453,6 +455,55 @@ test.describe('Doc Editor', () => {
expect(svgBuffer.toString()).toContain('Hello svg');
});
+ test('it checks block editing when not connected to collab server', async ({
+ page,
+ }) => {
+ await page.route('**/api/v1.0/config/', async (route) => {
+ const request = route.request();
+ if (request.method().includes('GET')) {
+ await route.fulfill({
+ json: {
+ ...CONFIG,
+ COLLABORATION_WS_URL: 'ws://localhost:5555/collaboration/ws/',
+ },
+ });
+ } else {
+ await route.continue();
+ }
+ });
+
+ await page.goto('/');
+
+ void page
+ .getByRole('button', {
+ name: 'New doc',
+ })
+ .click();
+
+ const card = page.getByLabel('It is the card information');
+ await expect(
+ card.getByText('Your network do not allow you to edit'),
+ ).toBeHidden();
+ const editor = page.locator('.ProseMirror');
+
+ await expect(editor).toHaveAttribute('contenteditable', 'true');
+
+ await page.getByRole('button', { name: 'Share' }).click();
+
+ await addNewMember(page, 0, 'Editor', 'impress');
+
+ // Close the modal
+ await page.getByRole('button', { name: 'close' }).first().click();
+
+ await expect(
+ card.getByText('Your network do not allow you to edit'),
+ ).toBeVisible({
+ timeout: 10000,
+ });
+
+ await expect(editor).toHaveAttribute('contenteditable', 'false');
+ });
+
test('it checks if callout custom block', async ({ page, browserName }) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
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 a927578c..e4240a02 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
@@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
import * as Y from 'yjs';
import { Box, TextErrors } from '@/components';
-import { Doc } from '@/docs/doc-management';
+import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
import { useAuth } from '@/features/auth';
import { useUploadFile } from '../hook';
@@ -49,7 +49,9 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { setEditor } = useEditorStore();
const { t } = useTranslation();
- const readOnly = !doc.abilities.partial_update;
+ const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
+ const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
+
useSaveDoc(doc.id, provider.document, !readOnly);
const { i18n } = useTranslation();
const lang = i18n.resolvedLanguage;
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 d7e23aa5..6f07096e 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
@@ -25,7 +25,6 @@ interface DocEditorProps {
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
const { isDesktop } = useResponsiveStore();
-
const isVersion = !!versionId && typeof versionId === 'string';
const { colorsTokens } = useCunninghamTheme();
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertNetwork.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertNetwork.tsx
new file mode 100644
index 00000000..ac230310
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertNetwork.tsx
@@ -0,0 +1,113 @@
+import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
+import { t } from 'i18next';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { css } from 'styled-components';
+
+import { Box, BoxButton, Icon, Text } from '@/components';
+import { useCunninghamTheme } from '@/cunningham';
+
+export const AlertNetwork = () => {
+ const { t } = useTranslation();
+ const { colorsTokens, spacingsTokens } = useCunninghamTheme();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+ {t('Your network do not allow you to edit')}
+
+
+ setIsModalOpen(true)}
+ >
+
+
+ {t('Know more')}
+
+
+
+
+ {isModalOpen && (
+ setIsModalOpen(false)} />
+ )}
+ >
+ );
+};
+
+interface AlertNetworkModalProps {
+ onClose: () => void;
+}
+
+export const AlertNetworkModal = ({ onClose }: AlertNetworkModalProps) => {
+ return (
+ onClose()}
+ rightActions={
+ <>
+
+ >
+ }
+ size={ModalSize.MEDIUM}
+ title={
+
+ {t("Why can't I edit?")}
+
+ }
+ >
+
+
+ {t(
+ 'The network configuration of your workstation or internet connection does not allow editing shared documents.',
+ )}
+
+
+ {t(
+ 'Docs use WebSockets to enable real-time editing. These communication channels allow instant and bidirectional exchanges between your browser and our servers. To access collaborative editing, please contact your IT department to enable WebSockets.',
+ )}
+
+
+
+ );
+};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertPublic.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertPublic.tsx
new file mode 100644
index 00000000..de01fa57
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertPublic.tsx
@@ -0,0 +1,39 @@
+import { useTranslation } from 'react-i18next';
+import { css } from 'styled-components';
+
+import { Box, Icon, Text } from '@/components';
+import { useCunninghamTheme } from '@/cunningham';
+
+export const AlertPublic = ({ isPublicDoc }: { isPublicDoc: boolean }) => {
+ const { t } = useTranslation();
+ const { colorsTokens, spacingsTokens } = useCunninghamTheme();
+
+ return (
+
+
+
+ {isPublicDoc
+ ? t('Public document')
+ : t('Document accessible to any connected person')}
+
+
+ );
+};
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 8aebcd17..cae72309 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
@@ -1,17 +1,20 @@
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
-import { css } from 'styled-components';
-import { Box, HorizontalSeparator, Icon, Text } from '@/components';
+import { Box, HorizontalSeparator, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
LinkReach,
+ Role,
currentDocRole,
+ useIsCollaborativeEditable,
useTrans,
} from '@/docs/doc-management';
import { useResponsiveStore } from '@/stores';
+import { AlertNetwork } from './AlertNetwork';
+import { AlertPublic } from './AlertPublic';
import { DocTitle } from './DocTitle';
import { DocToolBox } from './DocToolBox';
@@ -20,51 +23,26 @@ interface DocHeaderProps {
}
export const DocHeader = ({ doc }: DocHeaderProps) => {
- const { colorsTokens, spacingsTokens } = useCunninghamTheme();
+ const { spacingsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
-
const { t } = useTranslation();
+ const { transRole } = useTrans();
+ const { isEditable } = useIsCollaborativeEditable(doc);
const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED;
- const { transRole } = useTrans();
-
return (
<>
+ {!isEditable && }
{(docIsPublic || docIsAuth) && (
-
-
-
- {docIsPublic
- ? t('Public document')
- : t('Document accessible to any connected person')}
-
-
+
)}
{
{isDesktop && (
<>
-
- {transRole(currentDocRole(doc.abilities))} ·
+
+ {transRole(
+ isEditable
+ ? currentDocRole(doc.abilities)
+ : Role.READER,
+ )}
+ ·
{t('Last update: {{update}}', {
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts
index a1937ba0..96968e38 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts
@@ -1,3 +1,4 @@
export * from './useCollaboration';
-export * from './useTrans';
export * from './useCopyDocLink';
+export * from './useIsCollaborativeEditable';
+export * from './useTrans';
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useIsCollaborativeEditable.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useIsCollaborativeEditable.tsx
new file mode 100644
index 00000000..aebf2bf1
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useIsCollaborativeEditable.tsx
@@ -0,0 +1,41 @@
+import { useEffect, useState } from 'react';
+
+import { useProviderStore } from '../stores';
+import { Doc, LinkReach } from '../types';
+
+export const useIsCollaborativeEditable = (doc: Doc) => {
+ const { isConnected } = useProviderStore();
+
+ const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
+ const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED;
+ const docHasMember = doc.nb_accesses_direct > 1;
+ const isShared = docIsPublic || docIsAuth || docHasMember;
+ const [isEditable, setIsEditable] = useState(true);
+ const [isLoading, setIsLoading] = useState(true);
+
+ /**
+ * Connection can take a few seconds
+ */
+ useEffect(() => {
+ const _isEditable = isConnected || !isShared;
+ setIsLoading(true);
+
+ if (_isEditable) {
+ setIsEditable(true);
+ setIsLoading(false);
+ return;
+ }
+
+ const timer = setTimeout(() => {
+ setIsEditable(false);
+ setIsLoading(false);
+ }, 5000);
+
+ return () => clearTimeout(timer);
+ }, [isConnected, isShared]);
+
+ return {
+ isEditable,
+ isLoading,
+ };
+};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx
index fb78eb6e..ae7ed151 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx
@@ -1,4 +1,4 @@
-import { HocuspocusProvider } from '@hocuspocus/provider';
+import { HocuspocusProvider, WebSocketStatus } from '@hocuspocus/provider';
import * as Y from 'yjs';
import { create } from 'zustand';
@@ -12,10 +12,12 @@ export interface UseCollaborationStore {
) => HocuspocusProvider;
destroyProvider: () => void;
provider: HocuspocusProvider | undefined;
+ isConnected: boolean;
}
const defaultValues = {
provider: undefined,
+ isConnected: false,
};
export const useProviderStore = create((set, get) => ({
@@ -33,6 +35,11 @@ export const useProviderStore = create((set, get) => ({
url: wsUrl,
name: storeId,
document: doc,
+ onStatus: ({ status }) => {
+ set({
+ isConnected: status === WebSocketStatus.Connected,
+ });
+ },
});
set({