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 27dee6f0..3d502fad 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 @@ -132,6 +132,10 @@ test.describe('Doc Editor', () => { }) .click(); + await expect( + page.getByText(`Your document "${doc}" has been saved.`), + ).toBeVisible(); + const card = page.getByLabel('Create new document card').first(); await expect( card.getByRole('heading', { diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx index f02fe2ea..319133b6 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx @@ -1,5 +1,7 @@ +import { VariantType, useToastProvider } from '@openfun/cunningham-react'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import * as Y from 'yjs'; import { useUpdateDoc } from '@/features/docs/doc-management/'; @@ -7,7 +9,19 @@ import { useUpdateDoc } from '@/features/docs/doc-management/'; import { toBase64 } from '../utils'; const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => { - const { mutate: updateDoc } = useUpdateDoc(); + const { toast } = useToastProvider(); + const { t } = useTranslation(); + + const { mutate: updateDoc } = useUpdateDoc({ + onSuccess: (data) => { + toast( + t('Your document "{{docTitle}}" has been saved.', { + docTitle: data.title, + }), + VariantType.SUCCESS, + ); + }, + }); const [initialDoc, setInitialDoc] = useState( toBase64(Y.encodeStateAsUpdate(doc)), ); @@ -40,23 +54,23 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => { }; }, [doc]); + /** + * Check if the doc has been updated and can be saved. + */ + const shouldSave = useCallback(() => { + const newDoc = toBase64(Y.encodeStateAsUpdate(doc)); + return initialDoc !== newDoc && canSave; + }, [canSave, doc, initialDoc]); + const saveDoc = useCallback(() => { const newDoc = toBase64(Y.encodeStateAsUpdate(doc)); - - /** - * Save only if the doc has changed. - */ - if (initialDoc === newDoc || !canSave) { - return; - } - setInitialDoc(newDoc); updateDoc({ id: docId, content: newDoc, }); - }, [initialDoc, docId, doc, updateDoc, canSave]); + }, [doc, docId, updateDoc]); const timeout = useRef(); const router = useRouter(); @@ -66,8 +80,26 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => { clearTimeout(timeout.current); } - const onSave = () => { + const onSave = (e?: Event) => { + if (!shouldSave()) { + return; + } + saveDoc(); + + /** + * Firefox does not trigger the request everytime the user leaves the page. + * Plus the request is not intercepted by the service worker. + * So we prevent the default behavior to have the popup asking the user + * if he wants to leave the page, by adding the popup, we let the time to the + * request to be sent, and intercepted by the service worker (for the offline part). + */ + const isFirefox = + navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + + if (typeof e !== 'undefined' && e.preventDefault && isFirefox) { + e.preventDefault(); + } }; // Save every minute @@ -82,7 +114,7 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => { removeEventListener('beforeunload', onSave); router.events.off('routeChangeStart', onSave); }; - }, [router.events, saveDoc]); + }, [router.events, saveDoc, shouldSave]); }; export default useSaveDoc;