From 05c449bab8b12a17b55cbc32f88044c449da83e6 Mon Sep 17 00:00:00 2001 From: Samuel JD <40059405+samueljd@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:20:45 +0530 Subject: [PATCH] fix/s5-error (#380) fix previous file rendering when current file is erroring out fix b tag error and validation on import. --- package.json | 2 +- .../TextEditor/BibleNavigationX/index.js | 9 +- .../EditorPage/TextEditor/EditorMenuBar.jsx | 2 + .../EditorPage/TextEditor/ErrorScreen.jsx | 48 +++++ .../EditorPage/TextEditor/cacheUtils.js | 51 +++--- .../EditorPage/TextEditor/conversionUtils.js | 13 ++ .../EditorPage/TextEditor/index.jsx | 62 +++++-- .../src/components/Projects/ImportPopUp.js | 21 +-- .../src/components/SnackBar/AutoSnackBar.js | 172 ++++++++++++++++++ renderer/src/components/SnackBar/index.js | 1 + yarn.lock | 10 +- 11 files changed, 337 insertions(+), 54 deletions(-) create mode 100644 renderer/src/components/EditorPage/TextEditor/ErrorScreen.jsx create mode 100644 renderer/src/components/SnackBar/AutoSnackBar.js diff --git a/package.json b/package.json index 2adf3bb67..0a1c60395 100644 --- a/package.json +++ b/package.json @@ -225,7 +225,7 @@ "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-scripts": "5.0.1", - "sj-usfm-grammar": "^3.0.4", + "sj-usfm-grammar": "3.0.8", "styled-components": "^5.3.6", "tc-ui-toolkit": "5.3.3", "terser-webpack-plugin": "^5.3.10", diff --git a/renderer/src/components/EditorPage/TextEditor/BibleNavigationX/index.js b/renderer/src/components/EditorPage/TextEditor/BibleNavigationX/index.js index 38540a6c2..c178ab18e 100644 --- a/renderer/src/components/EditorPage/TextEditor/BibleNavigationX/index.js +++ b/renderer/src/components/EditorPage/TextEditor/BibleNavigationX/index.js @@ -11,7 +11,7 @@ import SelectChapter from './SelectChapter'; export default function BibleNavigationX(props) { const { - chapterNumber, setChapterNumber, setBook, loading, bookAvailable, booksInProject, + chapterNumber, setChapterNumber, setBook, loading, bookAvailable, booksInProject, parseError, } = props; const { @@ -60,6 +60,13 @@ export default function BibleNavigationX(props) { setReference(); }, [bookId, chapter]); + useEffect(() => { + if (parseError) { + setOpenBook(false); + setOpenChapter(false); + } + }, [parseError]); + useEffect(() => { if (openBook === false && openChapter === false) { setCloseNavigation(true); diff --git a/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx b/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx index 6bc2ea428..0f7121d05 100644 --- a/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx +++ b/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx @@ -22,6 +22,7 @@ export default function EditorMenuBar(props) { loading, bookAvailable, booksInProject, + parseError, } = props; const { t } = useTranslation(); @@ -53,6 +54,7 @@ export default function EditorMenuBar(props) { loading={loading} bookAvailable={bookAvailable} booksInProject={booksInProject} + parseError={parseError} />
+
+
+
+
+ + + + + + + + + +
+

+ Some USFM tags appear to be in unexpected locations. Kindly check the + + USFM documentation + + +

+ {/*

Kindly check for errors in the USFM file

*/} +
+
+
+
+ ); +} diff --git a/renderer/src/components/EditorPage/TextEditor/cacheUtils.js b/renderer/src/components/EditorPage/TextEditor/cacheUtils.js index 4a8dd4331..5f1b8075b 100644 --- a/renderer/src/components/EditorPage/TextEditor/cacheUtils.js +++ b/renderer/src/components/EditorPage/TextEditor/cacheUtils.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import * as path from 'path'; import * as crypto from 'crypto'; import { convertUsfmToUsj } from './conversionUtils'; @@ -84,32 +85,40 @@ export async function handleCache(filePath, usfmContent, projectCachePath, fileC const oldHash = fileCacheMap[filePath]; async function processAndCacheUSJ() { - const { usj, error } = await convertUsfmToUsj(usfmContent); - if (error) { - // eslint-disable-next-line no-console - console.error('Error parsing USFM', error); - return { error }; + try { + const { usj, error } = await convertUsfmToUsj(usfmContent); + if (error) { + console.error('Error parsing USFM:', error); + return { error, usj: null }; // Return consistent error object + } + writeCache(newHash, usj, projectCachePath); + updateCacheMapToFile(fileCacheMapPath, filePath, newHash); + return { usj, error: null }; // Always include error field + } catch (err) { + console.error('Error in processAndCacheUSJ:', err); + return { error: err.message, usj: null }; } - writeCache(newHash, usj, projectCachePath); - updateCacheMapToFile(fileCacheMapPath, filePath, newHash); - return { usj }; } - if (!oldHash) { - // eslint-disable-next-line no-console - console.log('No existing hash found. Creating new cache entry.'); - return processAndCacheUSJ(); - } + try { + if (!oldHash) { + console.log('No existing hash found. Creating new cache entry.'); + return processAndCacheUSJ(); + } - if (isCacheValid(oldHash, projectCachePath) && oldHash === newHash) { - // eslint-disable-next-line no-console - console.log('Cache hit'); - return { usj: await readCache(oldHash, projectCachePath) }; + if (isCacheValid(oldHash, projectCachePath) && oldHash === newHash) { + console.log('Cache hit'); + const cachedUsj = await readCache(oldHash, projectCachePath); + return { usj: cachedUsj, error: null }; + } + + console.log('Cache miss or content changed'); + deleteOldCacheFile(oldHash, projectCachePath); + return processAndCacheUSJ(); + } catch (err) { + console.error('Error in handleCache:', err); + return { error: err.message, usj: null }; } - // eslint-disable-next-line no-console - console.log('Cache miss or content changed'); - deleteOldCacheFile(oldHash, projectCachePath); - return processAndCacheUSJ(); } export async function updateCache(filePath, usj, usfm, fileCacheMapPath, projectCachePath) { diff --git a/renderer/src/components/EditorPage/TextEditor/conversionUtils.js b/renderer/src/components/EditorPage/TextEditor/conversionUtils.js index c9c57bbaa..8dfd0c2ab 100644 --- a/renderer/src/components/EditorPage/TextEditor/conversionUtils.js +++ b/renderer/src/components/EditorPage/TextEditor/conversionUtils.js @@ -34,6 +34,19 @@ export async function convertUsjToUsfm(usj) { return usfm; } +export async function validateUsfm(usfm) { + if (!usfmParserInstance) { + usfmParserInstance = await initializeParser(); + } + try { + const { isValid, validUSFM, bookCode } = usfmParserInstance.validate(usfm); + // console.log('USFM validation, isValid:', isValid, 'validUSFM:', validUSFM, bookCode); + return { isValid, validUSFM, bookCode }; + } catch (e) { + return { isValid: false, error: e }; + } +} + initializeParser() .then(() => { // eslint-disable-next-line no-console diff --git a/renderer/src/components/EditorPage/TextEditor/index.jsx b/renderer/src/components/EditorPage/TextEditor/index.jsx index eb9e75f9d..47574e05f 100644 --- a/renderer/src/components/EditorPage/TextEditor/index.jsx +++ b/renderer/src/components/EditorPage/TextEditor/index.jsx @@ -3,13 +3,15 @@ import React, { } from 'react'; import { ReferenceContext } from '@/components/context/ReferenceContext'; import { debounce } from 'lodash'; - import { LoadingSpinner } from '@/components/LoadingSpinner'; +import { useAutoSnackbar } from '@/components/SnackBar'; +import { useTranslation } from 'react-i18next'; import { useReadUsfmFile } from './hooks/useReadUsfmFile'; import EditorMenuBar from './EditorMenuBar'; import LexicalEditor from './LexicalEditor'; import { updateCacheNSaveFile } from './updateAndSave'; import EmptyScreen from './EmptyScreen'; +import ErrorScreen from './ErrorScreen'; const defaultScrRef = { bookCode: 'PSA', @@ -24,16 +26,23 @@ export default function TextEditor() { const [usjInput, setUsjInput] = useState(); const [scrRef, setScrRef] = useState(defaultScrRef); const [navRef, setNavRef] = useState(); + const [parseError, setParseError] = useState(false); const { state: { - bookId: defaultBookId, selectedFont, editorFontSize, projectScriptureDir, - // chapter, - // verse, + bookId: defaultBookId, + selectedFont, + editorFontSize, + projectScriptureDir, }, actions: { - handleSelectedFont, onChangeChapter, onChangeVerse, handleEditorFontSize, + handleSelectedFont, + onChangeChapter, + onChangeVerse, + handleEditorFontSize, }, } = useContext(ReferenceContext); + const { showSnackbar } = useAutoSnackbar(); + const { t } = useTranslation(); const [book, setBook] = useState(defaultBookId); const { @@ -41,15 +50,37 @@ export default function TextEditor() { } = useReadUsfmFile(book); useEffect(() => { - if (cachedData.error) { - console.error('Error parsing USFM', cachedData.error); + if (loading) { + showSnackbar(`Preparing ${book.toUpperCase()} file`, 'update'); + } + const { usj, error } = cachedData; + if (!loading) { + if (error) { + console.error('Error parsing USFM:', error); + setParseError(true); + showSnackbar( + t('dynamic-msg-load-ref-bible-snack-fail', { + refName: book.toUpperCase(), + }), + 'failure', + ); + setUsjInput(null); + return; + } + } + if (!usj || Object.entries(usj).length === 0) { + setParseError(false); + setUsjInput(null); return; } - const { usj } = cachedData; - if (!usj && usj?.entries(usj).length === 0) { return; } - // console.log(usj); + setParseError(false); setUsjInput(usj); - }, [book, cachedData]); + !loading + && showSnackbar( + t('dynamic-msg-load-ref-bible-snack', { refName: book.toUpperCase() }), + 'success', + ); + }, [cachedData, loading]); useEffect(() => { setScrRef({ @@ -93,6 +124,7 @@ export default function TextEditor() { handleEditorFontSize, bookAvailable, booksInProject, + parseError, }; const props = { @@ -105,7 +137,6 @@ export default function TextEditor() { scrRef, setScrRef, bookId: book, - }; return (
@@ -114,8 +145,11 @@ export default function TextEditor() { ) : ( <> - {!bookAvailable && } - {bookAvailable && usjInput && } + {parseError && } + {!parseError && !bookAvailable && } + {!parseError && bookAvailable && usjInput && ( + + )} )}
diff --git a/renderer/src/components/Projects/ImportPopUp.js b/renderer/src/components/Projects/ImportPopUp.js index d11e5821c..1b6005558 100644 --- a/renderer/src/components/Projects/ImportPopUp.js +++ b/renderer/src/components/Projects/ImportPopUp.js @@ -8,6 +8,7 @@ import { DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/outline'; import { SnackBar } from '@/components/SnackBar'; import { ProjectContext } from '@/components/context/ProjectContext'; import { readUsfm } from '@/components/Projects/utils/readUsfm'; +import { validateUsfm } from '@/components/EditorPage/TextEditor/conversionUtils'; import styles from './ImportPopUp.module.css'; import * as logger from '../../logger'; import CloseIcon from '@/illustrations/close-button-black.svg'; @@ -44,8 +45,8 @@ export default function ImportPopUp(props) { } = useContext(ProjectContext); const compareArrays = (a, b) => a.length === b.length - && a.every((element) => b.indexOf(element) !== -1) - && b.every((element) => a.indexOf(element) !== -1); + && a.every((element) => b.indexOf(element) !== -1) + && b.every((element) => a.indexOf(element) !== -1); function close() { logger.debug('ImportPopUp.js', 'Closing the Import UI'); @@ -114,19 +115,17 @@ export default function ImportPopUp(props) { const fs = window.require('fs'); const files = []; const bookCodeList = []; - folderPath.forEach((filePath) => { + folderPath.forEach(async (filePath) => { switch (projectType) { case 'Translation': { const usfm = fs.readFileSync(filePath, 'utf8'); - const myUsfmParser = new grammar.USFMParser(usfm, grammar.LEVEL.RELAXED); - const isJsonValid = myUsfmParser.validate(); - if (isJsonValid) { + const { isValid, validUSFM, bookCode } = await validateUsfm(usfm); + if (isValid) { // If importing a USFM file then ask user for replace of USFM with the new content or not replaceConformation(true); logger.debug('ImportPopUp.js', 'Valid USFM file.'); - const jsonOutput = myUsfmParser.toJSON(); - files.push({ id: jsonOutput.book.bookCode, content: usfm }); - bookCodeList.push(jsonOutput.book.bookCode); + files.push({ id: bookCode, content: validUSFM }); + bookCodeList.push(bookCode); } else { logger.warn('ImportPopUp.js', 'Invalid USFM file.'); setNotify('failure'); @@ -186,7 +185,7 @@ export default function ImportPopUp(props) { const fileExt = filename.split('.').pop()?.toLowerCase(); if (fileExt === 'txt' || fileExt === 'usfm' || fileExt === 'text' || fileExt === 'sfm' - || fileExt === undefined) { + || fileExt === undefined) { const myUsfmParser = new grammar.USFMParser(file, grammar.LEVEL.RELAXED); const isJsonValid = myUsfmParser.validate(); // if the USFM is valid @@ -238,7 +237,7 @@ export default function ImportPopUp(props) { title: t('label-other'), }; if (bookCodeList.length === advanceSettings.canonSpecification[2].length - && compareArrays(advanceSettings.currentScope, bookCodeList)) { + && compareArrays(advanceSettings.currentScope, bookCodeList)) { newCanonSpecification.title = advanceSettings.canonSpecification[2].title; newCanonSpecification.id = advanceSettings.canonSpecification[2].id; } else if (bookCodeList.length === advanceSettings.canonSpecification[1].length diff --git a/renderer/src/components/SnackBar/AutoSnackBar.js b/renderer/src/components/SnackBar/AutoSnackBar.js new file mode 100644 index 000000000..9986921ca --- /dev/null +++ b/renderer/src/components/SnackBar/AutoSnackBar.js @@ -0,0 +1,172 @@ +import { + Fragment, useState, useEffect, useRef, +} from 'react'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { Popover, Transition } from '@headlessui/react'; +import PropTypes from 'prop-types'; +import { createRoot } from 'react-dom/client'; + +const colors = { + success: '#82E0AA', + failure: '#F5B7B1', + warning: '#F8C471', + info: '#85C1E9', + update: '#D5D8DC', +}; + +const AutoSnackBar = ({ + snackText, + snackType, + isOpen, + onClose, +}) => { + const handleTransitionEnd = () => { + if (!isOpen) { + onClose(); + } + }; + + return ( + + + +
+ +

+ {snackText} +

+
+
+
+
+ ); +}; + +AutoSnackBar.propTypes = { + snackText: PropTypes.string, + snackType: PropTypes.string, + isOpen: PropTypes.bool, + onClose: PropTypes.func, +}; + +export const useAutoSnackbar = () => { + const [snackbar, setSnackbar] = useState({ + isOpen: false, + snackText: '', + snackType: 'success', + }); + const [timeLeft, setTimeLeft] = useState(null); + const portalRef = useRef(null); + const rootRef = useRef(null); + const timerRef = useRef(null); + const mountedRef = useRef(false); + + useEffect(() => { + mountedRef.current = true; + + if (!portalRef.current) { + portalRef.current = document.createElement('div'); + portalRef.current.id = 'snackbar-portal'; + document.body.appendChild(portalRef.current); + } + + if (!rootRef.current && portalRef.current) { + rootRef.current = createRoot(portalRef.current); + } + + return () => { + mountedRef.current = false; + if (timerRef.current) { + clearInterval(timerRef.current); + } + + if (rootRef.current) { + rootRef.current.unmount(); + rootRef.current = null; + } + + if (portalRef.current && document.body.contains(portalRef.current)) { + document.body.removeChild(portalRef.current); + portalRef.current = null; + } + }; + }, []); + + useEffect(() => { + if (mountedRef.current && rootRef.current) { + rootRef.current.render( + { + if (mountedRef.current) { + setSnackbar((prev) => ({ ...prev, isOpen: false })); + setTimeLeft(null); + } + }} + />, + ); + } + }, [snackbar]); + + useEffect(() => { + if (timeLeft === 0 && mountedRef.current) { + setSnackbar((prev) => ({ ...prev, isOpen: false })); + setTimeLeft(null); + return; + } + + if (!timeLeft) { return; } + + timerRef.current = setInterval(() => { + if (mountedRef.current) { + setTimeLeft((prev) => prev - 1); + } + }, 1000); + + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + }; + }, [timeLeft]); + + const showSnackbar = (text, type = 'success') => { + if (mountedRef.current) { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + + setSnackbar({ + snackText: text, + snackType: type, + isOpen: true, + }); + setTimeLeft(type === 'failure' ? 15 : 8); + } + }; + + return { showSnackbar }; +}; diff --git a/renderer/src/components/SnackBar/index.js b/renderer/src/components/SnackBar/index.js index 04948b4cb..3c8ad9563 100644 --- a/renderer/src/components/SnackBar/index.js +++ b/renderer/src/components/SnackBar/index.js @@ -1 +1,2 @@ export { default as SnackBar } from './SnackBar'; +export { useAutoSnackbar } from './AutoSnackBar'; diff --git a/yarn.lock b/yarn.lock index 5c64310a2..5b72f82d1 100755 --- a/yarn.lock +++ b/yarn.lock @@ -20882,12 +20882,10 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -sj-usfm-grammar@^3.0.0, sj-usfm-grammar@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/sj-usfm-grammar/-/sj-usfm-grammar-3.0.4.tgz#aceda791291a858befd248430d947ccb15c4fbf7" - integrity sha512-HjEDlRt9kJslnjmZ7tGMSQQG8Rvs3n4qfZKPOIABkorx4vpP+rKMSteyEhmv4pXFFPLw37/kZPtTBX1Ui+jPRw== - dependencies: - sj-usfm-grammar "^3.0.0" +sj-usfm-grammar@3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/sj-usfm-grammar/-/sj-usfm-grammar-3.0.8.tgz#13b8f2e009058d6dd582c72d6dadc3a553ecbcf3" + integrity sha512-GThlgmxJKcs+abG/az0YVlTesGEAHkuRH5yy5Sj/Y5Xzq6SCL4EnZl2mdoemFEzvSjfPDE0tohKtHzCnmrohTQ== slash@^1.0.0: version "1.0.0"