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"