🛂(frontend) block edition only when not alone
We added a system to know if a user is alone on a document or not. We adapt the frontend to block the edition only when the user is not alone on the document.
This commit is contained in:
committed by
Manuel Raynaud
parent
9a8f952210
commit
55979e4370
@@ -50,9 +50,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
||||
const isConnectedToCollabServer = provider.isSynced;
|
||||
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
|
||||
|
||||
useSaveDoc(doc.id, provider.document, !readOnly);
|
||||
useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer);
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.resolvedLanguage;
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('useSaveDoc', () => {
|
||||
|
||||
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('useSaveDoc', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
renderHook(() => useSaveDoc(docId, yDoc, false), {
|
||||
renderHook(() => useSaveDoc(docId, yDoc, false, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('useSaveDoc', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('useSaveDoc', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
@@ -164,7 +164,7 @@ describe('useSaveDoc', () => {
|
||||
const docId = 'test-doc-id';
|
||||
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,12 @@ import { toBase64 } from '../utils';
|
||||
|
||||
const SAVE_INTERVAL = 60000;
|
||||
|
||||
const useSaveDoc = (docId: string, yDoc: Y.Doc, canSave: boolean) => {
|
||||
const useSaveDoc = (
|
||||
docId: string,
|
||||
yDoc: Y.Doc,
|
||||
canSave: boolean,
|
||||
isConnectedToCollabServer: boolean,
|
||||
) => {
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
onSuccess: () => {
|
||||
@@ -49,10 +54,18 @@ const useSaveDoc = (docId: string, yDoc: Y.Doc, canSave: boolean) => {
|
||||
updateDoc({
|
||||
id: docId,
|
||||
content: toBase64(Y.encodeStateAsUpdate(yDoc)),
|
||||
websocket: isConnectedToCollabServer,
|
||||
});
|
||||
|
||||
return true;
|
||||
}, [canSave, yDoc, docId, isLocalChange, updateDoc]);
|
||||
}, [
|
||||
canSave,
|
||||
isLocalChange,
|
||||
updateDoc,
|
||||
docId,
|
||||
yDoc,
|
||||
isConnectedToCollabServer,
|
||||
]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const AlertNetwork = () => {
|
||||
<Box $direction="row" $gap={spacingsTokens['2xs']} $align="center">
|
||||
<Icon iconName="mobiledata_off" $theme="warning" $variation="600" />
|
||||
<Text $theme="warning" $variation="600" $weight={500}>
|
||||
{t('Your network do not allow you to edit')}
|
||||
{t('Others are editing. Your network prevent changes.')}
|
||||
</Text>
|
||||
</Box>
|
||||
<BoxButton
|
||||
@@ -50,7 +50,7 @@ export const AlertNetwork = () => {
|
||||
$margin={{ top: 'auto' }}
|
||||
/>
|
||||
<Text $theme="warning" $variation="600" $weight="500" $size="xs">
|
||||
{t('Know more')}
|
||||
{t('Learn more')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
</Box>
|
||||
@@ -74,8 +74,8 @@ export const AlertNetworkModal = ({ onClose }: AlertNetworkModalProps) => {
|
||||
onClose={() => onClose()}
|
||||
rightActions={
|
||||
<>
|
||||
<Button aria-label={t('OK')} onClick={onClose}>
|
||||
{t('OK')}
|
||||
<Button aria-label={t('OK')} onClick={onClose} color="danger">
|
||||
{t('I understand')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -88,24 +88,39 @@ export const AlertNetworkModal = ({ onClose }: AlertNetworkModalProps) => {
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
>
|
||||
{t("Why can't I edit?")}
|
||||
{t("Why you can't edit the document?")}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
aria-label={t('Content modal to explain why the user cannot edit')}
|
||||
className="--docs--modal-alert-network"
|
||||
$margin={{ top: 'xs' }}
|
||||
$margin={{ top: 'md' }}
|
||||
>
|
||||
<Text $size="sm" $variation="600">
|
||||
{t(
|
||||
'The network configuration of your workstation or internet connection does not allow editing shared documents.',
|
||||
'Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.',
|
||||
)}
|
||||
</Text>
|
||||
<Text $size="sm" $variation="600" $margin={{ top: 'xs' }}>
|
||||
{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.',
|
||||
)}
|
||||
<Text
|
||||
$size="sm"
|
||||
$variation="600"
|
||||
$margin={{ top: 'xs' }}
|
||||
$weight="bold"
|
||||
$display="inline"
|
||||
>
|
||||
{t("This means you can't edit until others leave.")}{' '}
|
||||
<Text
|
||||
$size="sm"
|
||||
$variation="600"
|
||||
$margin={{ top: 'xs' }}
|
||||
$weight="normal"
|
||||
$display="inline"
|
||||
>
|
||||
{t(
|
||||
'If you wish to be able to co-edit in real-time, contact your Information Systems Security Manager about allowing WebSockets.',
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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<DocCanEditResponse> => {
|
||||
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<DocCanEditResponse>;
|
||||
};
|
||||
|
||||
export const KEY_CAN_EDIT = 'doc-can-edit';
|
||||
|
||||
export function useDocCanEdit(
|
||||
param: string,
|
||||
queryConfig?: UseQueryOptions<
|
||||
DocCanEditResponse,
|
||||
APIError,
|
||||
DocCanEditResponse
|
||||
>,
|
||||
) {
|
||||
return useQuery<DocCanEditResponse, APIError, DocCanEditResponse>({
|
||||
queryKey: [KEY_CAN_EDIT, param],
|
||||
queryFn: () => docCanEdit(param),
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
UseMutationOptions,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Doc } from '@/features/docs';
|
||||
|
||||
import { KEY_CAN_EDIT } from './useDocCanEdit';
|
||||
|
||||
export type UpdateDocParams = Pick<Doc, 'id'> &
|
||||
Partial<Pick<Doc, 'content' | 'title'>>;
|
||||
Partial<Pick<Doc, 'content' | 'title'>> & {
|
||||
websocket?: boolean;
|
||||
};
|
||||
|
||||
export const updateDoc = async ({
|
||||
id,
|
||||
@@ -24,25 +32,30 @@ export const updateDoc = async ({
|
||||
return response.json() as Promise<Doc>;
|
||||
};
|
||||
|
||||
interface UpdateDocProps {
|
||||
onSuccess?: (data: Doc) => void;
|
||||
type UseUpdateDoc = UseMutationOptions<Doc, APIError, Partial<Doc>> & {
|
||||
listInvalideQueries?: string[];
|
||||
}
|
||||
};
|
||||
|
||||
export function useUpdateDoc({
|
||||
onSuccess,
|
||||
listInvalideQueries,
|
||||
}: UpdateDocProps = {}) {
|
||||
export function useUpdateDoc(queryConfig?: UseUpdateDoc) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Doc, APIError, UpdateDocParams>({
|
||||
mutationFn: updateDoc,
|
||||
onSuccess: (data) => {
|
||||
listInvalideQueries?.forEach((queryKey) => {
|
||||
...queryConfig,
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryConfig?.listInvalideQueries?.forEach((queryKey) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [queryKey],
|
||||
});
|
||||
});
|
||||
onSuccess?.(data);
|
||||
|
||||
if (queryConfig?.onSuccess) {
|
||||
void queryConfig.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_CAN_EDIT],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, 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 } from '../types';
|
||||
|
||||
@@ -13,31 +14,30 @@ export const useIsCollaborativeEditable = (doc: Doc) => {
|
||||
const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
|
||||
const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED;
|
||||
const docHasMember = doc.nb_accesses_direct > 1;
|
||||
const isUserReader = !doc.abilities.partial_update;
|
||||
const isShared = docIsPublic || docIsAuth || docHasMember;
|
||||
const [isEditable, setIsEditable] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { isOffline } = useIsOffline();
|
||||
const _isEditable = isUserReader || isConnected || !isShared || isOffline;
|
||||
const [isEditable, setIsEditable] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(!_isEditable);
|
||||
|
||||
const {
|
||||
data: { can_edit } = { can_edit: _isEditable },
|
||||
isLoading: isLoadingCanEdit,
|
||||
} = useDocCanEdit(doc.id, {
|
||||
enabled: !_isEditable,
|
||||
queryKey: [KEY_CAN_EDIT, doc.id],
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* Connection can take a few seconds
|
||||
*/
|
||||
useEffect(() => {
|
||||
const _isEditable = isConnected || !isShared || isOffline;
|
||||
setIsLoading(true);
|
||||
|
||||
if (_isEditable) {
|
||||
setIsEditable(true);
|
||||
setIsLoading(false);
|
||||
if (isLoadingCanEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setIsEditable(false);
|
||||
setIsLoading(false);
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isConnected, isOffline, isShared]);
|
||||
setIsEditable(can_edit);
|
||||
setIsLoading(false);
|
||||
}, [can_edit, isLoadingCanEdit]);
|
||||
|
||||
if (!conf?.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user