diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts index 526e25a74..fb237161a 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts @@ -5,7 +5,6 @@ import { expect, test } from '@playwright/test'; import { createDoc, overrideConfig, verifyDocName } from './utils-common'; import { openSuggestionMenu, writeInEditor } from './utils-editor'; import { connectOtherUserToDoc, updateShareLink } from './utils-share'; -import { createRootSubPage } from './utils-sub-pages'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -111,147 +110,7 @@ test.describe('Doc Collaboration', () => { await cleanup(); }); - // TODO(yhub): re-enable when yhub exposes a connection-info API - the test - // asserts `can_edit=false` while another user is connected to the - // collaborative server, but `get_document_connection_info` is currently - // stubbed to report no connections. - test.skip('it checks block editing when not connected to collab server', async ({ - page, - browserName, - }) => { - test.slow(); - - /** - * The good port is 3002, but we want to simulate a not connected - * collaborative server. - * So we use a port that is not used by the collaborative server. - * The server will not be able to connect to the collaborative server. - */ - await overrideConfig(page, { - COLLABORATION_WS_URL: 'ws://localhost:5555/ws/docs', - COLLABORATION_WS_NOT_CONNECTED_READ_ONLY: true, - }); - - await page.goto('/'); - - const [parentTitle] = await createDoc( - page, - 'editing-blocking', - browserName, - 1, - ); - - const card = page.getByLabel('It is the card information'); - await expect( - card.getByText('Others are editing. Your network prevent changes.'), - ).toBeHidden(); - const editor = page.locator('.ProseMirror'); - - await expect(editor).toHaveAttribute('contenteditable', 'true'); - - let responseCanEditPromise = page.waitForResponse( - (response) => - response.url().includes(`/can-edit/`) && response.status() === 200, - ); - - await page.getByRole('button', { name: 'Share' }).click(); - - await updateShareLink(page, 'Public', 'Editing'); - - // Close the modal - await page.getByRole('button', { name: 'close' }).first().click(); - - const urlParentDoc = page.url(); - - const { name: childTitle } = await createRootSubPage( - page, - browserName, - 'editing-blocking - child', - ); - - let responseCanEdit = await responseCanEditPromise; - expect(responseCanEdit.ok()).toBeTruthy(); - let jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean }; - expect(jsonCanEdit.can_edit).toBeTruthy(); - - const urlChildDoc = page.url(); - - /** - * We open another browser that will connect to the collaborative server - * and will block the current browser to edit the doc. - */ - const { otherPage, cleanup } = await connectOtherUserToDoc({ - browserName, - docUrl: urlChildDoc, - docTitle: childTitle, - withoutSignIn: true, - }); - - const webSocketPromise = otherPage.waitForEvent( - 'websocket', - (webSocket) => { - return webSocket.url().includes(`${process.env.COLLABORATION_WS_URL}/`); - }, - ); - - await otherPage.goto(urlChildDoc); - - const webSocket = await webSocketPromise; - expect(webSocket.url()).toContain(`${process.env.COLLABORATION_WS_URL}/`); - - await verifyDocName(otherPage, childTitle); - - await page.reload(); - - responseCanEdit = await page.waitForResponse( - (response) => - response.url().includes(`/can-edit/`) && response.status() === 200, - ); - expect(responseCanEdit.ok()).toBeTruthy(); - - jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean }; - expect(jsonCanEdit.can_edit).toBeFalsy(); - - await expect( - card.getByText('Others are editing. Your network prevent changes.'), - ).toBeVisible({ - timeout: 10000, - }); - - await expect(editor).toHaveAttribute('contenteditable', 'false'); - - await expect( - page.getByRole('textbox', { name: 'Document title' }), - ).toBeHidden(); - await expect(page.getByRole('heading', { name: childTitle })).toBeVisible(); - - await page.goto(urlParentDoc); - - await verifyDocName(page, parentTitle); - - await page.getByRole('button', { name: 'Share' }).click(); - - await page.getByTestId('doc-access-mode').click(); - await page.getByRole('menuitemradio', { name: 'Reading' }).click(); - - // Close the modal - await page.getByRole('button', { name: 'close' }).first().click(); - - await page.goto(urlChildDoc); - - await expect(editor).toHaveAttribute('contenteditable', 'true'); - - await expect( - page.getByRole('textbox', { name: 'Document title' }), - ).toContainText(childTitle); - await expect(page.getByRole('heading', { name: childTitle })).toBeHidden(); - - await expect( - card.getByText('Others are editing. Your network prevent changes.'), - ).toBeHidden(); - - await cleanup(); - }); + // TODO(yhub): Add test to check that no connected websocket users can collaborate test('checks disconnection and reconnection when changing tab visibility', async ({ page, diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/DocEditor.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/DocEditor.spec.tsx index e27870b94..9db868131 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/DocEditor.spec.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/DocEditor.spec.tsx @@ -25,7 +25,6 @@ vi.mock('../../doc-management', async () => { const actual = await vi.importActual('../../doc-management'); return { ...actual, - useIsCollaborativeEditable: () => ({ isEditable: true, isLoading: false }), useProviderStore: () => ({ provider: { roomname: 'test-doc-id', 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 f24db166b..79bedea1f 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 @@ -8,7 +8,6 @@ import { Doc, LinkReach, getDocLinkReach, - useIsCollaborativeEditable, useProviderStore, } from '@/docs/doc-management'; import { useAuth } from '@/features/auth/'; @@ -85,10 +84,8 @@ interface DocEditorProps { export const DocEditor = ({ doc }: DocEditorProps) => { useCollaboration(doc.id); - const { isEditable, isLoading } = useIsCollaborativeEditable(doc); const isDeletedDoc = !!doc.deleted_at; - const readOnly = - !doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc; + const readOnly = !doc.abilities.partial_update || isDeletedDoc; const { trackEvent } = useAnalytics(); const [hasTracked, setHasTracked] = useState(false); const { authenticated } = useAuth(); 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 deleted file mode 100644 index a0837feeb..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/AlertNetwork.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react'; -import { t } from 'i18next'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Box, BoxButton, Card, Icon, Text } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; - -export const AlertNetwork = () => { - const { t } = useTranslation(); - const { spacingsTokens } = useCunninghamTheme(); - const [isModalOpen, setIsModalOpen] = useState(false); - - return ( - <> - - - - - - {t('Others are editing. Your network prevent changes.')} - - - setIsModalOpen(true)} - $withThemeInherited - > - - - {t('Learn more')} - - - - - {isModalOpen && ( - setIsModalOpen(false)} /> - )} - - ); -}; - -interface AlertNetworkModalProps { - onClose: () => void; -} - -export const AlertNetworkModal = ({ onClose }: AlertNetworkModalProps) => { - return ( - onClose()} - aria-label={t("Why you can't edit the document?")} - rightActions={ - <> - - - } - size={ModalSize.MEDIUM} - title={ - - {t("Why you can't edit the document?")} - - } - > - - - {t( - 'Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.', - )} - - - {t("This means you can't edit until others leave.")}{' '} - - {t( - 'If you wish to be able to co-edit in real-time, contact your Information Systems Security Manager about allowing WebSockets.', - )} - - - - - ); -}; 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 6e24e17fb..6e0e1e72c 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 @@ -10,10 +10,8 @@ import { getEmojiAndTitle, useDocTitleUpdate, useDocUtils, - useIsCollaborativeEditable, } from '@/docs/doc-management'; -import { AlertNetwork } from './AlertNetwork'; import { AlertRestore } from './AlertRestore'; import { DocHeaderInfo } from './DocHeaderInfo'; import { DocTitle } from './DocTitle'; @@ -24,7 +22,6 @@ interface DocHeaderProps { export const DocHeader = ({ doc }: DocHeaderProps) => { const { t } = useTranslation(); - const { isEditable } = useIsCollaborativeEditable(doc); const isDeletedDoc = !!doc.deleted_at; // Emoji Management const { emoji } = getEmojiAndTitle(doc.title ?? ''); @@ -57,11 +54,10 @@ export const DocHeader = ({ doc }: DocHeaderProps) => { {isDeletedDoc && } - {!isEditable && } diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx index c5f7f8d73..530e0e83b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx @@ -9,7 +9,6 @@ import { LinkReach, Role, getDocLinkReach, - useIsCollaborativeEditable, useTrans, } from '@/docs/doc-management'; import { useDate } from '@/hooks'; @@ -20,7 +19,6 @@ interface DocHeaderInfoProps { export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => { const { transRole } = useTrans(); - const { isEditable } = useIsCollaborativeEditable(doc); const { relativeDate, calculateDaysLeft } = useDate(); const { data: config } = useConfig(); @@ -50,12 +48,16 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => { $variation="tertiary" $size="s" $weight="bold" - $theme={isEditable ? 'neutral' : 'warning'} + $theme={doc.abilities.partial_update ? 'neutral' : 'warning'} $direction="row" $margin="0" > - {transRole(isEditable ? doc.user_role || doc.link_role : Role.READER)} + {transRole( + doc.abilities.partial_update + ? doc.user_role || doc.link_role + : Role.READER, + )}  ยท  diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx index 23a06c107..b7e38b830 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx @@ -11,7 +11,6 @@ import { useDocStore, useDocTitleUpdate, useDocUtils, - useIsCollaborativeEditable, useTrans, } from '@/docs/doc-management'; import SimpleFileIcon from '@/features/docs/doc-management/assets/simple-document.svg'; @@ -24,8 +23,7 @@ interface DocTitleProps { } export const DocTitle = ({ doc }: DocTitleProps) => { - const { isEditable, isLoading } = useIsCollaborativeEditable(doc); - const readOnly = !doc.abilities.partial_update || !isEditable || isLoading; + const readOnly = !doc.abilities.partial_update; if (readOnly) { return ; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocCanEdit.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocCanEdit.tsx deleted file mode 100644 index 8847ef94e..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocCanEdit.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { UseQueryOptions, useQuery } from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; - -type DocCanEditResponse = { can_edit: boolean }; - -export const docCanEdit = async (id: string): Promise => { - const response = await fetchAPI(`documents/${id}/can-edit/`); - - if (!response.ok) { - throw new APIError('Failed to get the doc', await errorCauses(response)); - } - - return response.json() as Promise; -}; - -export const KEY_CAN_EDIT = 'doc-can-edit'; - -export function useDocCanEdit( - param: string, - queryConfig?: UseQueryOptions< - DocCanEditResponse, - APIError, - DocCanEditResponse - >, -) { - return useQuery({ - queryKey: [KEY_CAN_EDIT, param], - queryFn: () => docCanEdit(param), - ...queryConfig, - }); -} diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocContentUpdate.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocContentUpdate.tsx index 23cb7402e..8f845a29b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocContentUpdate.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocContentUpdate.tsx @@ -9,7 +9,6 @@ import { APIError, errorCauses, fetchAPI } from '@/api'; import { Doc } from '../types'; -import { KEY_CAN_EDIT } from './useDocCanEdit'; import { KEY_DOC_CONTENT } from './useDocContent'; export interface UpdateDocContentParams { @@ -112,12 +111,6 @@ export function useDocContentUpdate(queryConfig?: UseDocContentUpdate) { ); } - // If error it means the user is probably not allowed to edit the doc - // so we invalidate the canEdit query to update the UI accordingly - void queryClient.invalidateQueries({ - queryKey: [KEY_CAN_EDIT], - }); - if (queryConfig?.onError) { queryConfig.onError(error, variables, onMutateResult, context); } 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 ba5d9640d..eb2fc20ea 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 @@ -2,5 +2,4 @@ export * from './useCopyDocLink'; export * from './useCreateChildDocTree'; export * from './useDocTitleUpdate'; export * from './useDocUtils'; -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 deleted file mode 100644 index d2d2f172a..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useIsCollaborativeEditable.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -import { useConfig } from '@/core'; -import { useIsOffline } from '@/features/service-worker'; - -import { KEY_CAN_EDIT, useDocCanEdit } from '../api/useDocCanEdit'; -import { useProviderStore } from '../stores'; -import { Doc, LinkReach, LinkRole } from '../types'; - -export const useIsCollaborativeEditable = (doc: Doc) => { - const { isConnected } = useProviderStore(); - const { data: conf } = useConfig(); - - const docIsPublic = - doc.computed_link_reach === LinkReach.PUBLIC && - doc.computed_link_role === LinkRole.EDITOR; - const docIsAuth = - doc.computed_link_reach === LinkReach.AUTHENTICATED && - doc.computed_link_role === LinkRole.EDITOR; - const docHasMember = - doc.nb_accesses_direct > 1 || doc.nb_accesses_ancestors > 1; - const isUserReader = !doc.abilities.partial_update; - const isShared = docIsPublic || docIsAuth || docHasMember; - const { isOffline } = useIsOffline(); - const _isEditable = isUserReader || isConnected || !isShared || isOffline; - const [isEditable, setIsEditable] = useState(true); - const [isLoading, setIsLoading] = useState(!_isEditable); - const timeout = useRef(null); - const { data: editingRight, isLoading: isLoadingCanEdit } = useDocCanEdit( - doc.id, - { - enabled: !_isEditable, - queryKey: [KEY_CAN_EDIT, doc.id], - staleTime: 0, - }, - ); - - useEffect(() => { - if (isLoadingCanEdit || _isEditable || !editingRight) { - return; - } - - // Connection to the WebSocket can take some time, so we set a timeout to ensure the loading state is cleared after a reasonable time. - timeout.current = setTimeout(() => { - setIsEditable(editingRight.can_edit); - setIsLoading(false); - }, 1500); - - return () => { - if (timeout.current) { - clearTimeout(timeout.current); - } - }; - }, [editingRight, isLoadingCanEdit, _isEditable]); - - useEffect(() => { - if (!_isEditable) { - return; - } - - if (timeout.current) { - clearTimeout(timeout.current); - } - - setIsEditable(true); - setIsLoading(false); - }, [_isEditable]); - - if (!conf?.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY) { - return { - isEditable: true, - isLoading: false, - }; - } - - return { - isEditable, - isLoading, - }; -};