diff --git a/app/components/IPRWYSIWYGEditor/index.tsx b/app/components/IPRWYSIWYGEditor/index.tsx index 396a5b220..b1c80feab 100644 --- a/app/components/IPRWYSIWYGEditor/index.tsx +++ b/app/components/IPRWYSIWYGEditor/index.tsx @@ -1,5 +1,6 @@ import React, { forwardRef, useCallback, useEffect, + useImperativeHandle, useMemo, useState, } from 'react'; @@ -39,11 +40,11 @@ const usePageLeaveWarning = (enabled) => { return ''; }; useEffect(() => { - if (!enabled) return; + if (!enabled) return undefined; window.addEventListener('beforeunload', handler); + return () => window.removeEventListener('beforeunload', handler); }, [enabled]); - return () => window.removeEventListener('beforeunload', handler); }; const MenuBarButton = forwardRef( @@ -194,14 +195,14 @@ type IPRWYSIWYGEditorProps = { title?: string; }; -const IPRWYSIWYGEditor = ({ +const IPRWYSIWYGEditor = forwardRef(({ alertLeave = false, text, isOpen, onClose, onSave, title = 'IPR WYSIWYG Editor', -}: IPRWYSIWYGEditorProps): JSX.Element => { +}: IPRWYSIWYGEditorProps, editorRef): JSX.Element => { const [isDirty, setIsDirty] = useState(false); const shouldWarnWhenUseLeave = alertLeave && isOpen && isDirty; @@ -214,6 +215,8 @@ const IPRWYSIWYGEditor = ({ }, }); + useImperativeHandle(editorRef, () => ({ editor, isDirty, setIsDirty }), [editor, isDirty, setIsDirty]); + useEffect(() => { if (editor && text) { editor.commands.setContent(text); @@ -224,19 +227,21 @@ const IPRWYSIWYGEditor = ({ if (editor) { onClose(editor.isEmpty ? '' : editor.getHTML()); } + setIsDirty(false); }, [editor, onClose]); const handleOnSave = useCallback(() => { if (editor) { onSave(editor.isEmpty ? '' : editor.getHTML()); - setIsDirty(false); } + setIsDirty(false); }, [editor, onSave]); const handleOnClose = useCallback(() => { - onClose(null); // Reset the editor text, since we don't deal with the state in React editor.commands.setContent(text); + onClose(null); + setIsDirty(false); }, [onClose, editor, text]); const saveButton = useMemo(() => { @@ -266,7 +271,7 @@ const IPRWYSIWYGEditor = ({ /> ); -}; +}); export default IPRWYSIWYGEditor; export { IPRWYSIWYGEditorProps, MenuBar }; diff --git a/app/views/ReportView/components/AnalystComments/index.tsx b/app/views/ReportView/components/AnalystComments/index.tsx index 50f65f0cb..7cec973ba 100644 --- a/app/views/ReportView/components/AnalystComments/index.tsx +++ b/app/views/ReportView/components/AnalystComments/index.tsx @@ -1,5 +1,5 @@ import React, { - useEffect, useState, useContext, useCallback, useMemo, + useEffect, useState, useContext, useCallback, useMemo, useRef, } from 'react'; import { Typography, @@ -24,6 +24,9 @@ import IPRWYSIWYGEditor from '@/components/IPRWYSIWYGEditor'; import './index.scss'; import { useQuery, useQueryClient } from 'react-query'; +import { Editor } from '@tiptap/react'; + +const AUTO_SAVE_INTERVAL = 30 * 1000; // Autosaves per 30s const useComments = (report?: ReportType) => useQuery({ queryKey: ['report-comments', report?.ident], @@ -57,6 +60,7 @@ const AnalystComments = ({ const { canEdit } = useReport(); const { showConfirmDialog } = useConfirmDialog(); + const editorRef = useRef<{ editor: Editor, isDirty: boolean | null }>(); const [comments, setComments] = useState(''); const [signatures, setSignatures] = useState(); const [signatureTypes, setSignatureTypes] = useState(DEFAULT_SIGNATURE_TYPES); @@ -76,7 +80,9 @@ const AnalystComments = ({ commentsQuery.error || signaturesQuery.error || signatureTypesQuery.error }`); } + }, [commentsQuery.error, isError, signatureTypesQuery.error, signaturesQuery.error]); + useEffect(() => { if (!isApiLoading) { setComments(commentsQuery.data); setSignatures(signaturesQuery.data); @@ -84,7 +90,35 @@ const AnalystComments = ({ setIsComponentLoading(false); if (loadedDispatch) loadedDispatch({ type: 'analyst-comments' }); } - }, [setIsComponentLoading, isApiLoading, isError, commentsQuery.data, signaturesQuery.data, signatureTypesQuery.data, loadedDispatch, commentsQuery.error, signaturesQuery.error, signatureTypesQuery.error]); + }, [setIsComponentLoading, isApiLoading, isError, commentsQuery.data, signaturesQuery.data, signatureTypesQuery.data, loadedDispatch]); + + // Try to load previously unsaved analyst comments + useEffect(() => { + if (isEditorOpen && !isApiLoading) { + const savedComments = localStorage.getItem(`${report.ident}-analyst_comments`); + if (savedComments) { + snackbar.info('Loaded previously unsaved analyst comments, please remember to save.'); + localStorage.removeItem(`${report.ident}-analyst_comments`); + editorRef.current.editor.commands.setContent(savedComments); + } + } + }, [isApiLoading, isEditorOpen, report.ident]); + + // Intervally saves in-edit analyst comments + useEffect(() => { + const interval = setInterval(() => { + const editor = editorRef.current?.editor; + const isDirty = editorRef.current?.isDirty; + if (!editor) return; + + // When user is actively editing + if (isEditorOpen && isDirty) { + localStorage.setItem(`${report.ident}-analyst_comments`, editorRef.current.editor.getHTML()); + } + }, AUTO_SAVE_INTERVAL); + + return () => clearInterval(interval); + }, [isEditorOpen, report.ident]); const handleSign = useCallback(async (signed: boolean) => { setIsSigned(signed); @@ -154,13 +188,21 @@ const AnalystComments = ({ ); const handleEditorSave = useCallback( - (editedComments?: string) => handleEditorAction(editedComments, false), - [handleEditorAction], + (editedComments?: string) => { + // Clear sessionStoarge because the user already saved + localStorage.removeItem(`${report.ident}-analyst_comments`); + return handleEditorAction(editedComments, false); + }, + [handleEditorAction, report.ident], ); const handleEditorClose = useCallback( - (editedComments?: string) => handleEditorAction(editedComments, true), - [handleEditorAction], + (editedComments?: string) => { + // Clear sessionStoarge because the user decided to not save + localStorage.removeItem(`${report.ident}-analyst_comments`); + return handleEditorAction(editedComments, true); + }, + [handleEditorAction, report.ident], ); const signatureSection = useMemo(() => signatureTypes.map((sigType) => { @@ -206,6 +248,7 @@ const AnalystComments = ({