diff --git a/docs/Development/Offline Merge Text Translation/Offline-Merge-TextTransaltion.svg b/docs/Development/Offline Merge Text Translation/Offline-Merge-TextTransaltion.svg new file mode 100644 index 000000000..5730939d0 --- /dev/null +++ b/docs/Development/Offline Merge Text Translation/Offline-Merge-TextTransaltion.svg @@ -0,0 +1,890 @@ + + + + + + + + + Text + Offline Merge + + + + Import Same + Project + + + + Replace / Merge + + + + + + + + + + + + + + + + + Parse USFMs and + Find conflicts + + + + + + + + + + + + + + + + + Merge + + + + Commit Current State with TimeStamp + + + + + + + + + + + + + + + + UI + process data and Shows Conflicted Booksand Chapter + + + + + + + + + + + + + + + + Send Data to + UI + + + + store the parsed + JSONwith + orginal , incoming and merged JsonsUpdate on chapter conflict + finish.merge/projectName + + + + + + + + + + + + + ROM + 1 + 2 + 3 + 4 + Chapter Done + + + + + Finish Click + onChapter Conflict Done + + + + + + + + + + + + + + + + + + + + + + + + + Book Count : 3 + + PSA + + + + + + + 1 + + + + 2 + + + + 5 + ACT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Finish + + + + Complete All + Conflict + + + + + + + + + + + + + + + + Get + Merged Json from BE + + + + + + + + + + + + + + + + Convert JSON To + USFMBook by Book + + + + + + + + + + + + + + + + + + + + + + + + + + + + Replace in the + original Project Source + + + + Remove .merge and + project + + + + check for Inprogress Merge + + + + + + + + + + + + + + + + + NO + + + + + + + Yes + + + + + + + + + + + + + + + + load existing + configand + data + Load Existing + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Not + Found data + + + + Error Load Data + /Config + Corrupted + + + + Update MD5 and + Metadata + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + If conflict + + + + + + + + + + + + + + + + Yes + + + + Commit Finish Conflict Status with TimeStamp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + update mergeJson + content based + onselection / + reset + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/Development/Offline Merge Text Translation/development.md b/docs/Development/Offline Merge Text Translation/development.md new file mode 100644 index 000000000..50c39281c --- /dev/null +++ b/docs/Development/Offline Merge Text Translation/development.md @@ -0,0 +1,18 @@ +## Offline Merge Transaltion + +- Offline Merge Feature for Text Translation +- Merge Option available on Import Existing Project ( Flavour Text Translation ) +- The Merge Can be resumed later from the check point ( Check Point will be the completion of each chapter. The system will only take care of chapter by chapter . You can not stop and continue from middle of a chapter ) +- The merge will be corrupted if back end data is modified / removed manually. Can not be restored +- One commit will be created on merge start with all the changes before the merge ( timestamp and commit message can be used to identify the commit) +- One commit will be created after the merge with all the merged changes ( timestamp and commit message can be used to identify) +- This commit can be used to revert back to earlier stage ( **advanced option** ) +- Can not re work on chapter or book once you finish the conflicts with book / chapter +- updating `metadata.json` with new USFM's metadata on finish merge conflict for entire project +- Merge UI ( options) + - Accept all new changes + - Keep All existing changes + - Revert All changes + - Accept single changes ( current or new ) + - Chapter Completion ( Check point ) + - Merge Conflict Completion ( on completion : replace the original USFM with new merge USFM ) diff --git a/package.json b/package.json index 78dfc3661..fc21a2746 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "bible-reference-rcl": "1.1.0", "clsx": "1.1.1", "crypto-js": "^4.1.1", + "diff-match-patch": "^1.0.5", "dotenv": "^16.3.1", "electron-is-dev": "^2.0.0", "electron-log": "4.4.7", diff --git a/renderer/src/components/Projects/ProjectList.js b/renderer/src/components/Projects/ProjectList.js index ece2c0528..3e9dd95c6 100644 --- a/renderer/src/components/Projects/ProjectList.js +++ b/renderer/src/components/Projects/ProjectList.js @@ -53,7 +53,7 @@ export default function ProjectList() { }; const closeEditProject = async () => { - logger.debug('ProjectList.js', 'Closing edit project page and updating the values'); + logger.debug('ProjectList.js', 'Closing edit project page and updating the valuesd'); setCallEditProject(false); await FetchProjects(); }; diff --git a/renderer/src/components/Projects/ProjectRow.js b/renderer/src/components/Projects/ProjectRow.js index 310f91eb9..867033621 100644 --- a/renderer/src/components/Projects/ProjectRow.js +++ b/renderer/src/components/Projects/ProjectRow.js @@ -204,6 +204,7 @@ const ProjectRow = ({ )} + {({ active }) => ( + + + + +
+ + {/* saved conflicts - resume */} + {savedConflictsBooks?.length > 0 && ( + +
+ +

Resume Conflict Resolution

+
+ + {savedConflictsBooks.map((book) => ( + + ))} +
+
+ )} +
+ + ); +} + +export default ImportUsfmUI; diff --git a/renderer/src/components/TextTranslationMerge/TranslationMergNavBar.jsx b/renderer/src/components/TextTranslationMerge/TranslationMergNavBar.jsx new file mode 100644 index 000000000..68d541cca --- /dev/null +++ b/renderer/src/components/TextTranslationMerge/TranslationMergNavBar.jsx @@ -0,0 +1,70 @@ +// import { Cog8ToothIcon } from '@heroicons/react/24/outline'; +// import { useTranslation } from 'react-i18next'; + +function TranslationMergNavBar({ + conflictedBooks, selectedBook, setSelectedBook, resolvedBooks, disableSelection, conflictedChapters, selectedChapter, setSelectedChapter, +}) { + // const { t } = useTranslation(); + return ( +
+
+ + + Books : + {' '} + {`${conflictedBooks?.length} `} + + {/* */} +
+ +
+
    + {conflictedBooks?.map((book) => { + const bookId = book.split('/'); + return ( +
  • + + {/* chapter selection */} + {selectedBook === book && !resolvedBooks.includes(book) && ( + +
    + {conflictedChapters?.map((chNo) => ( + + ))} +
    + )} + +
  • + ); + })} +
+
+
+ ); +} + +export default TranslationMergNavBar; diff --git a/renderer/src/components/TextTranslationMerge/TranslationMergeUI.jsx b/renderer/src/components/TextTranslationMerge/TranslationMergeUI.jsx new file mode 100644 index 000000000..510f3f9b8 --- /dev/null +++ b/renderer/src/components/TextTranslationMerge/TranslationMergeUI.jsx @@ -0,0 +1,581 @@ +/* eslint-disable no-nested-ternary */ +import React, { + useRef, Fragment, useState, useEffect, +} from 'react'; +import { Dialog, Transition } from '@headlessui/react'; +import { useTranslation } from 'react-i18next'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import ConfirmationModal from '@/layouts/editor/ConfirmationModal'; +import { readUsfmFile } from '@/core/projects/userSettings'; +import localforage from 'localforage'; +import { flushSync } from 'react-dom'; +import TranslationMergNavBar from './TranslationMergNavBar'; +// import * as logger from '../../logger'; +import LoadingScreen from '../Loading/LoadingScreen'; +import UsfmConflictEditor from './UsfmConflictEditor'; +import { processAndIdentiyVerseChangeinUSFMJsons } from './processUsfmObjs'; +import packageInfo from '../../../../package.json'; +import { commitChanges } from '../Sync/Isomorphic/utils'; +/* eslint-disable import/no-unresolved , import/extensions */ +import { useGrammartoPerf } from '@/hooks2/useGrammartoPerf'; + +const grammar = require('usfm-grammar'); +const path = require('path'); +const md5 = require('md5'); + +function TranslationMergeUI({ conflictData, closeMergeWindow, triggerSnackBar }) { + const { t } = useTranslation(); + const cancelButtonRef = useRef(null); + const [model, setModel] = React.useState({ + openModel: false, + title: '', + confirmMessage: '', + buttonName: '', + }); + + const [selectedChapter, setSelectedChapter] = useState(); + const [resolvedBooks, setResolvedBooks] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [usfmJsons, setUsfmJsons] = useState({}); + const [selectedBook, setSelectedBook] = useState(); + const [conflictedChapters, setConflictedChapters] = useState({}); + const [chapterResolveDone, setChapterResolveDone] = useState(false); + const [finishedConflict, setFinishedConflict] = useState([]); + // const [resolvedChapters, setResolvedChapters] = useState({}); + + const [currentPerfInputArr, setCurrentPerfInputArr] = useState([]); + const [currentPerfResolveBookCode, setCurrentPerfResolveBookCode] = useState(''); + const [generatedPerfUSFM, setGeneratedPerfUSFM] = useState(); + + useGrammartoPerf(currentPerfInputArr, currentPerfResolveBookCode, setGeneratedPerfUSFM); + + const removeSection = async (abort = false) => { + if (abort === false) { + // TODO : allow to close and continue later + } else { + // popup with warning + setModel({ + openModel: true, + title: t('modal-title-abort-conflict-resolution'), + confirmMessage: t('text-msg-abort-conflict-resolution'), + buttonName: t('label-abort'), + }); + } + }; + + const modalClose = () => { + setModel({ + openModel: false, + title: '', + confirmMessage: '', + buttonName: '', + }); + }; + + const handleStartOver = () => { + console.log('start over called ----'); + modalClose(); + }; + + const handleOnAbortMerge = (buttonName) => { + console.log({ buttonName }, model); + if (model.buttonName === t('label-abort')) { + setError(''); + modalClose(); + closeMergeWindow(); + } else { + handleStartOver(); + } + }; + + async function parseUsfm(usfm) { + const myUsfmParser = new grammar.USFMParser(usfm, grammar.LEVEL.RELAXED); + const isJsonValid = myUsfmParser.validate(); + return { valid: isJsonValid, data: myUsfmParser.toJSON() }; + } + + async function parseJsonToUsfm(json) { + const myUsfmParser = new grammar.JSONParser(json); + const usfm = myUsfmParser.toUSFM(); + return usfm; + } + + // INFO : Previous function to handle all books together ( JSON => usfm all together at the end ) + // const handleFinishedResolution = async () => { + // const fs = window.require('fs'); + + // flushSync(() => { + // setLoading(true); + // setFinishedConflict(false); + // }); + + // const resolvedBooks = { ...usfmJsons }; + // delete resolvedBooks.conflictMeta; + + // const currentSourceMeta = usfmJsons?.conflictMeta?.currentMeta; + + // // TODO : Disable all clicks when loading is true + + // const sourceIngredientPath = path.join(usfmJsons.conflictMeta.sourceProjectPath); + // // loop over the resolved books + // // eslint-disable-next-line no-restricted-syntax + // for (const bookName of Object.keys(resolvedBooks)) { + // const resolvedMergeJson = resolvedBooks[bookName]?.mergeJson; + // // eslint-disable-next-line no-await-in-loop + // const generatedUSFM = await parseJsonToUsfm(resolvedMergeJson); + + // // TODO : convert here to PERF + + // const perfUSFM = ''; + + // // overwrite the source file with new file + // fs.writeFileSync(path.join(sourceIngredientPath, 'ingredients', `${resolvedMergeJson.book.bookCode}.usfm`), generatedUSFM); + + // // get and update the usfms ingredients + // const stat = fs.statSync(path.join(sourceIngredientPath, 'ingredients', `${resolvedMergeJson.book.bookCode}.usfm`)); + // currentSourceMeta.ingredients[bookName].checksum.md5 = md5(generatedUSFM); + // currentSourceMeta.ingredients[bookName].size = stat.size; + // } + + // // write updated metadata here + // fs.writeFileSync(path.join(sourceIngredientPath, 'metadata.json'), JSON.stringify(currentSourceMeta)); + + // // remove .merge/project + // await fs.rmSync(usfmJsons.conflictMeta.projectMergePath, { recursive: true, force: true }); + // // commit all changes after merge finish + // const commitAuthor = { name: 'scribeInternal', email: 'scribe@bridgeconn.com' }; + // const backupMessage = `Scribe Internal Commit After Text Merge Finish : ${usfmJsons.conflictMeta.projectFullName} : ${new Date()}`; + // await commitChanges(fs, usfmJsons.conflictMeta.sourceProjectPath, commitAuthor, backupMessage, true); + + // setLoading(false); + // triggerSnackBar('success', 'Conflict Resolved Successfully'); + // closeMergeWindow(); + // }; + + const handleFinishMergeProcess = async () => { + try { + setLoading(true); + console.log('Done everything '); + const fs = window.require('fs'); + // remove temp merge path of project + await fs.rmSync(usfmJsons.conflictMeta.projectMergePath, { recursive: true, force: true }); + setLoading(false); + triggerSnackBar('success', 'Conflict Resolved Successfully'); + closeMergeWindow(); + } catch (err) { + console.error('Error Finish Process : ', err); + setLoading(false); + } + }; + + const handleFinishedBookResolution = async () => { + const resolvedBooks = { ...usfmJsons }; + delete resolvedBooks.conflictMeta; + console.log('handle finish book resolution ===> ', resolvedBooks); + // work on single book + const resolvedMergeJson = resolvedBooks[selectedBook]?.mergeJson; + const generatedUSFM = await parseJsonToUsfm(resolvedMergeJson); + + if (generatedUSFM && resolvedMergeJson.book.bookCode) { + setCurrentPerfInputArr([{ + selectors: { org: 'unfoldingWord', lang: 'en', abbr: 'ult' }, + bookCode: resolvedMergeJson.book.bookCode.toLowerCase(), + data: generatedUSFM, + }]); + setCurrentPerfResolveBookCode(resolvedMergeJson.book.bookCode.toUpperCase()); + } else { + console.error('Can not generate usfm of current book : ', selectedBook); + } + }; + + // Function to write back the current usfmJSON data as config in the .merge + const writeBackConflictConfigData = async (projectFullName, configData) => { + try { + const fs = window.require('fs'); + const newpath = localStorage.getItem('userPath'); + const path = require('path'); + localforage.getItem('userProfile').then((user) => { + const USFMMergeDirPath = path.join(newpath, packageInfo.name, 'users', user?.username, '.merge-usfm'); + if (!fs.existsSync(path.join(USFMMergeDirPath, projectFullName))) { + fs.mkdirSync(path.join(USFMMergeDirPath, projectFullName), { recursive: true }); + } + fs.writeFileSync(path.join(USFMMergeDirPath, projectFullName, 'usfmJsons.json'), JSON.stringify(configData)); + setLoading(false); + }); + } catch (err) { + console.error('Error Writeback config : ', err); + setLoading(false); + } + }; + + const resolveAndMarkDoneChapter = async () => { + try { + setChapterResolveDone(false); + // remove current chapter from conflicted list + const restOfTheChapters = conflictedChapters[selectedBook]?.filter((chNo) => chNo !== selectedChapter); + setConflictedChapters((prev) => ({ ...prev, [selectedBook]: restOfTheChapters || [] })); + let isBookResolved = false; + if (!restOfTheChapters) { + setResolvedBooks((prev) => [...prev, selectedBook]); + // isBookResolved = true; + } else if (restOfTheChapters?.length === 0) { + // completed conflicts for that particualr book + flushSync(() => { + setLoading(true); + }); + await handleFinishedBookResolution(selectedBook); + setResolvedBooks((prev) => [...prev, selectedBook]); + isBookResolved = true; + } else { + // current book have pending chapter , // Switch to next chapter or book + setSelectedChapter(restOfTheChapters[0]); + } + // store the jsons to the backend (/.merge/projectName/BookID.json) + + const { projectFullName } = usfmJsons.conflictMeta; + + // resolved chapters of each book and resolved books in the conflictMeta and store in the BE + const currentUSFMJsonsData = JSON.parse(JSON.stringify(usfmJsons)); + // initial time (if resolved chapters exist for the project) + if (currentUSFMJsonsData.conflictMeta?.resolvedStatus) { + currentUSFMJsonsData.conflictMeta.resolvedStatus[selectedBook] = { conflictedChapters: restOfTheChapters, isBookResolved }; + } else { + currentUSFMJsonsData.conflictMeta.resolvedStatus = { [selectedBook]: { conflictedChapters: restOfTheChapters, isBookResolved } }; + } + setUsfmJsons(currentUSFMJsonsData); + await writeBackConflictConfigData(projectFullName, currentUSFMJsonsData); + } catch (err) { + console.error('Failed resolve book : ', err); + setLoading(false); + } + }; + + /** + * + * @param {*} selectedBook + * function checks the conflict in the book , indenitify the conflicted chapters, generate usfmJson + */ + const checkForConflictInSelectedBook = async (selectedBook) => { + // parse imported + const fs = window.require('fs'); + const IncomingUsfm = fs.readFileSync(path.join(usfmJsons.conflictMeta.incomingPath, selectedBook), 'utf8'); + if (IncomingUsfm) { + const importedJson = await parseUsfm(IncomingUsfm); + if (!importedJson.valid) { + setError('Imported Usfm is invalid'); + } else { + // Parse current project same book + // const importedBookCode = `${importedJson.data.book.bookCode.toLowerCase()}.usfm`; + + setError(''); + + setUsfmJsons((prev) => ({ ...prev, [selectedBook]: { ...prev[selectedBook], imported: importedJson.data } })); + + // setSelectedBookId(importedJson.data.book.bookCode.toLowerCase()); + // const currentBookPath = Object.keys(usfmJsons.conflictMeta.currentMeta.ingredients).find((code) => code.toLowerCase().endsWith(importedBookCode)); + const { projectFullName } = usfmJsons.conflictMeta; + const currentBookUsfm = await readUsfmFile(selectedBook, projectFullName); + // console.log('FOUND ====> ', { currentBookPath, currentBookUsfm }); + if (currentBookUsfm) { + const currentJson = await parseUsfm(currentBookUsfm); + // generate the merge object with current , incoming , merge verses + const processOutArr = await processAndIdentiyVerseChangeinUSFMJsons(currentJson.data, importedJson.data).catch((err) => { + console.log('process usfm : ', err); + }); + const mergeJson = processOutArr[0]; + const conflcitedChapters = processOutArr[1]; + console.log('processOutArr[1] : ', processOutArr[1]); + currentJson && currentJson?.valid && setUsfmJsons((prev) => ({ ...prev, [selectedBook]: { ...prev[selectedBook], current: currentJson.data, mergeJson } })); + // if processOutArr leng = 0 ; there is not actual conflict on content. so do auto resolve for the book + if (conflcitedChapters?.length > 0) { + setConflictedChapters((prev) => ({ ...prev, [selectedBook]: processOutArr[1] || [] })); + if (conflcitedChapters && conflcitedChapters?.length > 0) { + setSelectedChapter(conflcitedChapters[0]); + } + } else { + // resolve the book automatically ; the conflict in md5 only not on content + await resolveAndMarkDoneChapter(); + triggerSnackBar('info', 'No conflict in verse level. The conflict may be because of extra tags.'); + } + setLoading(false); + } + } + } else { + setError('unable to read imported USFM'); + } + setLoading(false); + }; + + /** + * existing project merge + * read usfm json data from backend + */ + const getInprogressMergeProject = async (data) => { + const fs = window.require('fs'); + if (fs.existsSync(path.join(data.projectMergePath, 'usfmJsons.json'))) { + let usfmJsonsContent = fs.readFileSync(path.join(data.projectMergePath, 'usfmJsons.json'), 'utf8'); + usfmJsonsContent = JSON.parse(usfmJsonsContent); + if (usfmJsonsContent) { + setUsfmJsons(usfmJsonsContent); + // check the books is already resolved or not => then select current book and unresolved chapters + let bookToSelect; + const resolvedBooksArr = []; + const conflictedChsOfBooks = {}; + + // loading config - config have data of atleast 1 chapter of any book resolved ( checkpoint ) + if (usfmJsonsContent.conflictMeta.resolvedStatus) { + // eslint-disable-next-line no-restricted-syntax + for (const book in usfmJsonsContent.conflictMeta.resolvedStatus) { + if (book in usfmJsonsContent.conflictMeta.resolvedStatus) { + const currentBook = usfmJsonsContent.conflictMeta.resolvedStatus[book]; + if (!currentBook.isBookResolved && !bookToSelect) { + bookToSelect = book; + } + if (currentBook.isBookResolved) { + resolvedBooksArr.push(book); + } + conflictedChsOfBooks[book] = currentBook.conflictedChapters || []; + } + } + + // bookToSelect == undefined => All books in the resolved Status are completly resolved + if (!bookToSelect) { + const pendingBook = usfmJsonsContent.conflictMeta.files.find((bukName) => !(bukName in usfmJsonsContent.conflictMeta.resolvedStatus)); + // if pendingBook == undefined means all files conflict are resolved + bookToSelect = pendingBook || usfmJsonsContent.conflictMeta.files[0]; + } + } else { + // checkpoint - stored on initially and not worked on any chapter + bookToSelect = usfmJsonsContent.conflictMeta.files[0]; + } + + setSelectedBook(bookToSelect); + setResolvedBooks(resolvedBooksArr); + setConflictedChapters(conflictedChsOfBooks); + } else { + console.error('Inprogress project config is corrupted'); + } + } else { + console.error('Unable to get the inprogress config'); + } + }; + + console.log({ + conflictData, conflictedChapters, resolvedBooks, finishedConflict, + }); + + /** + * Function overwrite the org usmf with generated perf usfm + * update config of the merge + * update metadata with new perf data + * reset perf states + */ + const writeBackPerfUSFMandUpdateConfig = async (generatedPerfUSFM) => { + try { + console.log('generated perf in useEffect &&&&&&&&&&&&&&&&&&&&&&&& : ', generatedPerfUSFM); + const fs = window.require('fs'); + setChapterResolveDone(false); + setCurrentPerfInputArr([]); + + // work on single book + const sourceIngredientPath = path.join(usfmJsons.conflictMeta.sourceProjectPath); + fs.writeFileSync(path.join(sourceIngredientPath, 'ingredients', `${currentPerfResolveBookCode.toUpperCase()}.usfm`), generatedPerfUSFM); + + const stat = fs.statSync(path.join(sourceIngredientPath, 'ingredients', `${currentPerfResolveBookCode.toUpperCase()}.usfm`)); + + // read source meta - update the for the current book - write back + const sourceMeta = fs.readFileSync(path.join(sourceIngredientPath, 'metadata.json')); + const sourceMetaJson = JSON.parse(sourceMeta); + sourceMetaJson.ingredients[selectedBook].checksum.md5 = md5(generatedPerfUSFM); + console.log('Updated MD5 ================================> ', { selectedBook }, md5(generatedPerfUSFM)); + sourceMetaJson.ingredients[selectedBook].size = stat.size; + fs.writeFileSync(path.join(sourceIngredientPath, 'metadata.json'), JSON.stringify(sourceMetaJson)); + + // commit for the overwritten usfm + const commitAuthor = { name: 'scribeInternal', email: 'scribe@bridgeconn.com' }; + const backupMessage = `Scribe Internal Commit - conflict resolved for book : ${currentPerfResolveBookCode.toUpperCase()} : ${new Date()}`; + await commitChanges(fs, usfmJsons.conflictMeta.sourceProjectPath, commitAuthor, backupMessage, true); + + setCurrentPerfResolveBookCode(''); + setLoading(false); + } catch (err) { + console.error('error writeBackPerfUSFMandUpdateConfig : ', err); + setLoading(false); + } + }; + + // perf updation handle + useEffect(() => { + if (generatedPerfUSFM) { + writeBackPerfUSFMandUpdateConfig(generatedPerfUSFM); + } + }, [generatedPerfUSFM]); + + // useEffect to trigger completed all conflict Resolution + useEffect(() => { + if (resolvedBooks.length >= usfmJsons?.conflictMeta?.files?.length) { + setFinishedConflict(true); + } else { + setFinishedConflict(false); + } + }, [resolvedBooks]); + + // store conflict data to usfm jsons meta + useEffect(() => { + /** + * check the project merge is new or existing + */ + if (conflictData.data.isNewProjectMerge) { + setUsfmJsons((prev) => ({ ...prev, conflictMeta: conflictData.data })); + setSelectedBook(conflictData?.data?.files[0]); + } else { + getInprogressMergeProject(conflictData.data); + } + }, [conflictData]); + + // handle conflict check for a book on book nav + useEffect(() => { + if (!loading && usfmJsons?.conflictMeta) { + // if (!loading && usfmJsons?.conflictMeta && selectedBook) { + (async () => { + setLoading(true); + if (conflictedChapters[selectedBook]?.length > 0) { + // Auto Select first Chapter of the selected Book + if (conflictedChapters[selectedBook] && conflictedChapters[selectedBook].length > 0) { + setSelectedChapter(conflictedChapters[selectedBook][0]); + } + setLoading(false); + } else { + await checkForConflictInSelectedBook(selectedBook); + await writeBackConflictConfigData(usfmJsons.conflictMeta.projectFullName, usfmJsons); + } + })(); + } else { + // INFO : loading is showing : some other process is going on + } + }, [selectedBook, usfmJsons.conflictMeta]); + + return ( + <> + + removeSection(true)} + > + + +
+
+

{t('label-resolve-conflict')}

+
+ {/* close btn section */} + +
+ + {/* contents section */} +
+
+ + + +
+
+ + {loading ? () : ( + + usfmJsons[selectedBook]?.current && usfmJsons[selectedBook]?.imported && ( +
+ +
+ ) + )} + +
+
+

{error}

+ {usfmJsons[selectedBook]?.current && usfmJsons[selectedBook]?.imported && ( + finishedConflict ? ( + + ) : ( + + !resolvedBooks.includes(selectedBook) && ( + + ) + ) + )} +
+
+
+
+
+
+
+ + modalClose()} + confirmMessage={model.confirmMessage} + buttonName={model.buttonName} + closeModal={() => handleOnAbortMerge(model.buttonName)} + /> + + + ); +} + +export default TranslationMergeUI; diff --git a/renderer/src/components/TextTranslationMerge/UsfmConflictEditor.jsx b/renderer/src/components/TextTranslationMerge/UsfmConflictEditor.jsx new file mode 100644 index 000000000..d1ca1c3ff --- /dev/null +++ b/renderer/src/components/TextTranslationMerge/UsfmConflictEditor.jsx @@ -0,0 +1,286 @@ +/* eslint-disable no-nested-ternary */ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ArrowSmallDownIcon, ArrowSmallUpIcon, ArrowPathRoundedSquareIcon, +} from '@heroicons/react/20/solid'; + +/* eslint-disable no-unused-vars */ +function UsfmConflictEditor({ + usfmJsons, currentProjectMeta, selectedChapter, setUsfmJsons, setChapterResolveDone, resolvedChapters, selectedBook, resolvedBooks, conflictedChapters, +}) { + const [resolveAllActive, setResolveALlActive] = useState(true); + const [resetAlll, setResetAll] = useState(false); + const { t } = useTranslation(); + + console.log({ + usfmJsons, currentProjectMeta, selectedChapter, + }); + + const resolveAllTogether = (type) => { + usfmJsons[selectedBook].mergeJson.chapters.slice(selectedChapter - 1, selectedChapter)[0] + .contents.forEach((verseObj) => { + if (verseObj?.resolved?.status === false) { + let currentData = {}; + if (type === 'current') { + currentData = { verseText: verseObj.verseText, contents: verseObj.contents, verseNumber: verseObj.verseNumber }; + } else { + currentData = verseObj.incoming; + } + + // assign to resolved section + verseObj.resolved.status = true; + verseObj.resolved.resolvedContent = currentData; + // set the current data to the verse => contents and verse => verseText + verseObj.contents = currentData.contents; + verseObj.verseText = currentData.verseText; + } + }); + setUsfmJsons((prev) => ({ ...prev, [selectedBook]: { ...prev[selectedBook], mergeJson: usfmJsons[selectedBook].mergeJson } })); + setResolveALlActive(false); + setResetAll(true); + }; + + const handleResetSingle = (verseNum) => { + const currentVerseObj = usfmJsons[selectedBook].mergeJson.chapters.slice(selectedChapter - 1, selectedChapter)[0] + .contents.find((item) => item?.verseNumber === verseNum); + currentVerseObj.resolved.status = false; + currentVerseObj.resolved.resolvedContent = null; + // reset the default contents and verseText with current data + currentVerseObj.contents = currentVerseObj.current.contents; + currentVerseObj.verseText = currentVerseObj.current.verseText; + setUsfmJsons((prev) => ({ ...prev, [selectedBook]: { ...prev[selectedBook], mergeJson: usfmJsons[selectedBook].mergeJson } })); + setResolveALlActive(true); + setResetAll(false); + setChapterResolveDone(false); + }; + + const resetAllResolved = () => { + usfmJsons[selectedBook].mergeJson.chapters.slice(selectedChapter - 1, selectedChapter)[0] + .contents.forEach((verseObj) => { + if (verseObj?.resolved?.status === true) { + verseObj.resolved.status = false; + verseObj.resolved.resolvedContent = null; + // reset the default contents and verseText with current data + verseObj.contents = verseObj.current.contents; + verseObj.verseText = verseObj.current.verseText; + } + }); + setUsfmJsons((prev) => ({ ...prev, [selectedBook]: { ...prev[selectedBook], mergeJson: usfmJsons[selectedBook].mergeJson } })); + setResolveALlActive(true); + setResetAll(false); + setChapterResolveDone(false); + }; + + const handleResolveSingle = (type, verseNum) => { + const currentVerseObj = usfmJsons[selectedBook].mergeJson.chapters.slice(selectedChapter - 1, selectedChapter)[0] + .contents.find((item) => item?.verseNumber === verseNum); + let currentData = {}; + if (type === 'current') { + // INFO: the the resolved data is now storing in contents and verseText , which can be used to USFM generation easily + currentData = { verseText: currentVerseObj.verseText, contents: currentVerseObj.contents, verseNumber: verseNum }; + // the content & verse text by default have the current content + } else { + currentData = currentVerseObj.incoming; + } + + // assign to resolved section + currentVerseObj.resolved.status = true; + currentVerseObj.resolved.resolvedContent = currentData; + // set the current data to the verse => contents and verse => verseText + currentVerseObj.contents = currentData.contents; + currentVerseObj.verseText = currentData.verseText; + setUsfmJsons((prev) => ({ ...prev, [selectedBook]: { ...prev[selectedBook], mergeJson: usfmJsons[selectedBook].mergeJson } })); + }; + + useEffect(() => { + // check all resolved in the current ch + let resolvedStatus = false; + for (let index = 0; index < usfmJsons[selectedBook].mergeJson.chapters.slice(selectedChapter - 1, selectedChapter)[0].contents.length; index++) { + const verseObj = usfmJsons[selectedBook].mergeJson.chapters.slice(selectedChapter - 1, selectedChapter)[0].contents[index]; + if (verseObj?.resolved && verseObj?.resolved?.status === false) { + resolvedStatus = false; + setChapterResolveDone(false); + setResetAll(false); + setResolveALlActive(true); + break; + } else { + resolvedStatus = true; + } + } + if (resolvedStatus) { + setChapterResolveDone(true); + setResetAll(true); + setResolveALlActive(false); + } + }, [usfmJsons, selectedChapter]); + + return ( +
+ {/* Header and Buttons */} +
+
+
+ {t('label-comparison')} +
+
+ + + + + {/* */} + + + +
+
+ {/*
+ +
*/} +
+ + {/* --------------------------------------------- testing -------------------------------------------- */} + +
+ {selectedChapter && usfmJsons[selectedBook].mergeJson + && usfmJsons[selectedBook]?.mergeJson?.chapters?.slice(selectedChapter - 1, selectedChapter)[0].contents.map((item, index) => ( + item.verseNumber + && ( +
+ {item.verseNumber} + {/* conflict is / was there */} + {(item?.resolved) ? ( + + // conflict resolved section (Data from resolved.resolvedContent) + item.resolved.status ? ( +
+
+ {/*
{item.resolved.resolvedContent.verseText}
*/} +
{item.verseText}
+ {conflictedChapters?.includes(selectedChapter) && ( + handleResetSingle(item.verseNumber)} + /> + )} +
+
+ ) + : ( + // conflict Exist Show Both data +
+
+
handleResolveSingle('current', item.verseNumber)} + > + {item.current.verseText} +
+
handleResolveSingle('incoming', item.verseNumber)} + > + {item.incoming.verseText} +
+
+ +
+ {/* current */} +
handleResolveSingle('current', item.verseNumber)} + // onMouseEnter={() => setHoveredId('current')} + // onMouseLeave={() => setHoveredId('')} + title={t('tooltip-merge-orginal-btn')} + className="bg-black w-6 h-6 rounded-full flex justify-center items-center" + > + +
+ {/* Both */} + {/*
{ }} + onMouseEnter={() => setHoveredId('both')} + onMouseLeave={() => setHoveredId('')} + title={t('tooltip-merge-both-btn')} + className="bg-blue-500 w-6 h-6 rounded-full flex justify-center items-center" + > + +
*/} + {/* Incoming */} +
handleResolveSingle('incoming', item.verseNumber)} + // onMouseEnter={() => setHoveredId('incoming')} + // onMouseLeave={() => setHoveredId('')} + title={t('tooltip-merge-new-btn')} + className="bg-success w-6 h-6 rounded-full flex justify-center items-center" + > + +
+
+
+ ) + ) + : ( +
+ {item.verseText} +
+ )} +
+ ) + ))} +
+ +
+ ); +} + +export default UsfmConflictEditor; diff --git a/renderer/src/components/TextTranslationMerge/mergeTextTranslationProject.js b/renderer/src/components/TextTranslationMerge/mergeTextTranslationProject.js new file mode 100644 index 000000000..4778b8e60 --- /dev/null +++ b/renderer/src/components/TextTranslationMerge/mergeTextTranslationProject.js @@ -0,0 +1,113 @@ +import updateTranslationSB from '@/core/burrito/updateTranslationSB'; +import packageInfo from '../../../../package.json'; +import { commitChanges } from '../Sync/Isomorphic/utils'; + +export const mergeTextTranslationProject = async (incomingPath, currentUser, setConflictPopup, setProcessMerge, incomingMeta, triggerSnackBar, startOver = false) => { + try { + // update the metadata of current md5 --- updateTranslationSB (src/core/burrito/) + console.log({ + incomingPath, currentUser, incomingMeta, startOver, +}); + const fse = window.require('fs-extra'); + const fs = window.require('fs'); + const path = require('path'); + const newpath = localStorage.getItem('userPath'); + + await updateTranslationSB(currentUser, { name: incomingMeta.projectName, id: incomingMeta.id }, false).then(async (updatedCurrentMeta) => { + console.log({ updatedCurrentMeta }); + // compare md5s of incoming and current ingredients + const incomingIngredients = incomingMeta.ingredientsObj; + const currentIngredients = updatedCurrentMeta.ingredients; + const conflictedIngFilePaths = []; + + Object.entries(incomingIngredients).forEach(([key, val]) => { + if (val.scope) { + // if scope then its is usfm + const bookId = Object.keys(val.scope)[0]; + console.log('bookId : ', bookId); + const currentMd5 = currentIngredients[key]?.checksum?.md5; + const incomingMd5 = val.checksum.md5; + if (currentMd5 && incomingMd5) { + if (currentMd5 !== incomingMd5) { + console.log('MD5s xxxxxxxxxxxxxx : ', { bookId, currentMd5, incomingMd5 }, currentMd5 === incomingIngredients); + conflictedIngFilePaths.push(key); + } + } else { + // error no book in incoming + throw new Error('Can not proceed Merge, Project have scope difference.'); + } + } + }); + + console.log('conflict', { conflictedIngFilePaths }); + if (conflictedIngFilePaths.length > 0) { + /** + * Check the current Project is new or inprogres + * Check the current ProjectName in => ./merge/ProjectName + * move imported project to backup folder + * create a GIT Backup before start merge : Create a COMMIT with Proper Msg and Timestamp + * TODO: Idea is to manual git reset to commit based on timestamp + */ + const USFMMergeDirPath = path.join(newpath, packageInfo.name, 'users', currentUser, '.merge-usfm'); + const projectDirName = `${incomingMeta.projectName}_${incomingMeta.id[0]}`; + const sourceProjectPath = path.join(newpath, packageInfo.name, 'users', currentUser, 'projects', projectDirName); + let existingIncomingMeta; + let isNewProjectMerge = true; + console.log({ isNewProjectMerge }); + if (!fs.existsSync(path.join(USFMMergeDirPath, projectDirName))) { + console.log('not exist directory ==========='); + fs.mkdirSync(path.join(USFMMergeDirPath, projectDirName), { recursive: true }); + await fse.copy(incomingPath, path.join(USFMMergeDirPath, projectDirName, 'incoming')); + console.log('After copy 0000000000000000000'); + // commit existing changes before merge start + const commitAuthor = { name: 'scribeInternal', email: 'scribe@bridgeconn.com' }; + const backupMessage = `Scribe Internal Commit Before Text Merge Start : ${projectDirName} : ${new Date()} , startOver : ${startOver}`; + await commitChanges(fs, sourceProjectPath, commitAuthor, backupMessage, true); + } else { + isNewProjectMerge = false; + // read existing meta of incoming instead of using the new because the merge is + console.log('exist directory ===========xxxxxxxxxx'); + if (fs.existsSync(path.join(path.join(USFMMergeDirPath, projectDirName, 'incoming', 'metadata.json')))) { + existingIncomingMeta = fs.readFileSync(path.join(path.join(USFMMergeDirPath, projectDirName, 'incoming', 'metadata.json')), 'utf-8'); + existingIncomingMeta = JSON.parse(existingIncomingMeta); + } else { + throw new Error('Can not proceed Merge, Unable to find the metadata for imported Project'); + } + } + + // conflcit section - set values and open conflict window + setConflictPopup({ + open: true, + data: { + projectType: 'textTranslation', + files: conflictedIngFilePaths, + incomingPath: path.join(USFMMergeDirPath, projectDirName, 'incoming'), + incomingMeta: isNewProjectMerge ? incomingMeta : existingIncomingMeta, + currentMeta: updatedCurrentMeta, + projectId: incomingMeta.id[0], + projectName: incomingMeta.projectName, + projectFullName: projectDirName, + sourceProjectPath, + projectMergePath: path.join(USFMMergeDirPath, projectDirName), + currentUser, + isNewProjectMerge, + }, + }); + } else { + setProcessMerge(false); + triggerSnackBar('success', 'No Conflict Found'); + console.log('No Conflict =================>'); + } + + setProcessMerge(false); + }); + + // identify conflicted books + // rest of the codes are in the current implementation ofr book wise chapter conflict + // + } catch (err) { + setProcessMerge(false); + console.error('Failue in MergeText Process : ', err); + throw new Error(err); + } +}; diff --git a/renderer/src/components/TextTranslationMerge/processUsfmObjs.js b/renderer/src/components/TextTranslationMerge/processUsfmObjs.js new file mode 100644 index 000000000..5a67bfad3 --- /dev/null +++ b/renderer/src/components/TextTranslationMerge/processUsfmObjs.js @@ -0,0 +1,35 @@ +async function processAndIdentiyVerseChangeinUSFMJsons(currentJson, IncomingJson) { + // process USFM JSONs and generate comparaison object + const mergeTempJson = JSON.parse(JSON.stringify(currentJson)); + const conflictedChapters = []; + + return new Promise((resolve, reject) => { + const comparisonResult = async () => { + mergeTempJson.chapters.forEach((chapter, index) => { + const IncomingChap = IncomingJson.chapters[index]; + chapter.contents.forEach((content) => { + if (content.verseNumber) { + const IncomingVerse = IncomingChap.contents.find((ch) => ch.verseNumber === content.verseNumber); + /** + * handle conflict detection coniditons + * conflict only if + * - both have valid string ([a-zA-Z0-9]) and not same + * - valid content in incoming , current can be anything + * */ + if ((content.verseText !== IncomingVerse.verseText) && (/[a-zA-Z0-9]/.test(IncomingVerse.verseText))) { + // add incoming data + content.current = JSON.parse(JSON.stringify(content)); + content.incoming = IncomingVerse; + content.resolved = { status: false, resolvedContent: null }; + !conflictedChapters.includes(chapter.chapterNumber) && conflictedChapters.push(chapter.chapterNumber); + } + } + }); + }); + // console.log({ mergeTempJson, conflictedChapters }); + }; + comparisonResult().then(() => resolve([mergeTempJson, conflictedChapters])).catch((err) => reject(err)); + }); +} + +export { processAndIdentiyVerseChangeinUSFMJsons }; diff --git a/renderer/src/core/burrito/importBurrito.js b/renderer/src/core/burrito/importBurrito.js index c85f14110..5bd3c4d3d 100644 --- a/renderer/src/core/burrito/importBurrito.js +++ b/renderer/src/core/burrito/importBurrito.js @@ -90,8 +90,9 @@ export const viewBurrito = async (filePath, currentUser, resource) => { let sb = fs.readFileSync(path.join(filePath, 'metadata.json')); const metadata = JSON.parse(sb); // Fixing the issue of previous version of AG. The dateCreated was left empty and it will fail the validation. + const agId = Object.keys(metadata?.identification?.primary?.scribe); + result.id = agId; if (!metadata?.meta?.dateCreated) { - const agId = Object.keys(metadata?.identification?.primary?.scribe); metadata.meta.dateCreated = metadata?.identification?.primary?.scribe[agId[0]].timestamp; sb = JSON.stringify(metadata); } @@ -103,6 +104,7 @@ export const viewBurrito = async (filePath, currentUser, resource) => { result.version = metadata.meta.version; result.burritoType = `${metadata.type?.flavorType?.name} / ${metadata.type?.flavorType?.flavor?.name}`; result.ingredients = Object.keys(metadata.ingredients).map((key) => key); + result.ingredientsObj = metadata.ingredients; result.primaryKey = metadata.identification.primary; result.publicDomain = metadata.copyright?.publicDomain; result.language = metadata.languages.map((lang) => lang.name.en); diff --git a/renderer/src/core/burrito/updateTranslationSB.js b/renderer/src/core/burrito/updateTranslationSB.js index 7ae82d664..228d42a1b 100644 --- a/renderer/src/core/burrito/updateTranslationSB.js +++ b/renderer/src/core/burrito/updateTranslationSB.js @@ -88,7 +88,7 @@ const updateTranslationSB = async (username, project, updateBurrito) => new Prom try { logger.debug('updateTranslationSB.js', 'Updating the metadata.json (burrito) file.'); fs.writeFileSync(path.join(folder, 'metadata.json'), JSON.stringify(metadata)); - resolve(true); + resolve(metadata); } catch { logger.error('updateTranslationSB.js', 'Failed to update the metadata.json (burrito) file.'); resolve(false); diff --git a/renderer/src/core/projects/userSettings.js b/renderer/src/core/projects/userSettings.js index 4ea179167..b0e8b4fb4 100644 --- a/renderer/src/core/projects/userSettings.js +++ b/renderer/src/core/projects/userSettings.js @@ -109,3 +109,26 @@ export const saveUserSettings = async (userSettingsJson) => { throw new Error(err?.message || err); } }; + +// this will read usfm file based on path name / bookname (eg : ingredients/MAT.usfm) +export const readUsfmFile = async (filename, projectName) => { + try { + logger.debug('userSettings.js', 'In readUsfm file'); + const currentUser = await localForage.getItem('userProfile'); + const newpath = localStorage.getItem('userPath'); + const fs = window.require('fs'); + const path = require('path'); + const file = path.join(newpath, packageInfo.name, 'users', currentUser.username, 'projects', projectName, filename); + if (fs.existsSync(file)) { + const usfm = await fs.readFileSync(file, 'utf-8'); + if (usfm) { + logger.debug('userSettings.js', 'read usfm file successfully'); + return usfm; + } + + throw new Error('failed to read usfm file'); + } + } catch (err) { + throw new Error(err?.message || err); + } +}; diff --git a/renderer/src/hooks/useGrammartoPerf.js b/renderer/src/hooks/useGrammartoPerf.js new file mode 100644 index 000000000..aa7c787f8 --- /dev/null +++ b/renderer/src/hooks/useGrammartoPerf.js @@ -0,0 +1,77 @@ +import { useProskomma, useImport, useCatalog } from 'proskomma-react-hooks'; +import { + useDeepCompareEffect, + useDeepCompareMemo, +} from 'use-deep-compare'; +import EpiteleteHtml from 'epitelete-html'; +import htmlMap from '../components/EditorPage/TextEditor/hooks/htmlmap'; + +export const useGrammartoPerf = (perfArr = [], selectedBook = '', setGeneratedPerfUSFM = () => {}) => { + let selectedDocument; + let refName; + + console.log({ perfArr }); + + const { proskomma, stateId, newStateId } = useProskomma({ verbose: false }); + + console.log({ proskomma, stateId, newStateId }); + + const { done } = useImport({ + proskomma, + stateId, + newStateId, + documents: perfArr, + }); + + console.log({ done }); + + const { catalog } = useCatalog({ proskomma, stateId, verbose: false }); + const { id: docSetId, documents } = (done && catalog.docSets[0]) || {}; + + console.log(' ----------------- ------------ >', { catalog, docSetId, documents }); + + if (done) { + selectedDocument = documents?.find( + (doc) => { + console.log('finidng book code ===> ', doc.bookCode, selectedBook); + return doc.bookCode === selectedBook; + }, + ); + } + + console.log({ selectedDocument }); + + // const { bookCode, h: bookName } = selectedDocument || {}; + const { bookCode } = selectedDocument || {}; + const ready = (docSetId && bookCode) || false; + + console.log({ bookCode, ready }); + + const epiteleteHtml = useDeepCompareMemo( + () => ready + && new EpiteleteHtml({ + proskomma, + docSetId, + htmlMap, + options: { historySize: 100 }, + }), + [proskomma, ready, docSetId, refName], + ); + + useDeepCompareEffect(() => { + console.log('Triggering useDeepCompareEffect ==========> '); + if (epiteleteHtml) { + console.log('Insde useDeepCompareEffect ......................', bookCode); + // const fs = window.require('fs'); + epiteleteHtml.readHtml(bookCode, { cloning: false }, htmlMap).then(async (_htmlPerf) => { + console.log('INSIDE READ HTML =============> ', _htmlPerf); + const usfmPerfString = await epiteleteHtml?.readUsfm(bookCode); + // await fs.writeFileSync(`${bookCode}-TEST.usfm`, usfmPerfString); + console.log(' Conversion done for PER USFM ============> ', usfmPerfString); + setGeneratedPerfUSFM(usfmPerfString); + + // remove htmlMap for default classes + }); + } + }, [epiteleteHtml, bookCode]); +}; diff --git a/renderer/src/layouts/projects/Import/ConflictResolverUI.jsx b/renderer/src/layouts/projects/Import/ConflictResolverUI.jsx index 988678625..fa557270e 100644 --- a/renderer/src/layouts/projects/Import/ConflictResolverUI.jsx +++ b/renderer/src/layouts/projects/Import/ConflictResolverUI.jsx @@ -188,14 +188,14 @@ function ConflictResolverUI({ conflictData, setConflictPopup }) { onClick={() => removeSection()} > {finishingMerge - ? ( -
- ) - : <>{t('label-done')}} + ? ( +
+ ) + : <>{t('label-done')}}
)} diff --git a/renderer/src/layouts/projects/Import/mergeProject.js b/renderer/src/layouts/projects/Import/mergeProject.js index 45a2e8a78..01b1a4f1b 100644 --- a/renderer/src/layouts/projects/Import/mergeProject.js +++ b/renderer/src/layouts/projects/Import/mergeProject.js @@ -167,6 +167,7 @@ export const mergeProject = async (incomingPath, currentUser, setConflictPopup, setConflictPopup({ open: true, data: { + projectType: 'textStories', files: mergeStatus.data, mergeDirPath, projectPath: targetPath, diff --git a/renderer/src/layouts/projects/ImportProjectPopUp.js b/renderer/src/layouts/projects/ImportProjectPopUp.js index 436bd2fe1..8e5e50782 100644 --- a/renderer/src/layouts/projects/ImportProjectPopUp.js +++ b/renderer/src/layouts/projects/ImportProjectPopUp.js @@ -18,7 +18,9 @@ import * as logger from '../../logger'; import ConfirmationModal from '../editor/ConfirmationModal'; import burrito from '../../lib/BurritoTemplete.json'; import { mergeProject } from './Import/mergeProject'; +import packageInfo from '../../../../package.json'; import { LoadingSpinner } from '@/components/LoadingSpinner'; +import { mergeTextTranslationProject } from '@/components/TextTranslationMerge/mergeTextTranslationProject'; export default function ImportProjectPopUp(props) { const { @@ -45,6 +47,12 @@ export default function ImportProjectPopUp(props) { title: '', confirmMessage: '', buttonName: '', + buttonName2: { + active: false, + loading: false, + name: null, + action: () => {}, + } }); const { action: { FetchProjects } } = useContext(AutographaContext); const { @@ -59,12 +67,16 @@ export default function ImportProjectPopUp(props) { completedSteps: 0, }); + const triggerSnackBar = (status, message) => { + setNotify(status); + setSnackText(message); + setOpenSnackBar(true); + }; + async function close(triggeredFrom) { logger.debug('ImportProjectPopUp.js', `Closing the Dialog box : Triggered from : ${triggeredFrom}`); removeExtractedZipDir() setValid(false); - setSbData() - setFolderPath() closePopUp(false); setShow(false); setImportProgress((prev)=>({...prev, importStarted:false, completedSteps: 0, totalSteps: 4})) @@ -102,6 +114,7 @@ export default function ImportProjectPopUp(props) { } else { logger.debug('ImportProjectPopUp.js', 'Didn\'t select any project'); setSbData({}); + setFolderPath() close("else"); } setFolderPath(selectedFolderPath); @@ -129,7 +142,14 @@ export default function ImportProjectPopUp(props) { title: '', confirmMessage: '', buttonName: '', + buttonName2: { + active: false, + loading: false, + name: null, + action: () => {}, + } }); + setProcessMerge(false) setImportProgress((prev)=>({...prev, importStarted:false, completedSteps: 0, totalSteps: 4})) }; @@ -152,6 +172,7 @@ export default function ImportProjectPopUp(props) { if (status[0].type === 'success') { setSbData({}); close("Success"); + setFolderPath() FetchProjects(); router.push('/projects'); } @@ -172,12 +193,37 @@ export default function ImportProjectPopUp(props) { callImport(false); } }; + + const startTextTranslationMergeProcess = async (startOver=false) => { + try { + if(startOver) { + const path = require('path'); + const fs = window.require('fs'); + const newpath = localStorage.getItem('userPath'); + const USFMMergeDirPath = path.join(newpath, packageInfo.name, 'users', currentUser, '.merge-usfm'); + const projectDirName = `${sbData.projectName}_${sbData.id[0]}`; + await fs.rmSync(path.join(USFMMergeDirPath,projectDirName), { recursive: true, force: true }); + } + // start conflict checks continue / start over + await mergeTextTranslationProject(folderPath, currentUser, setConflictPopup, setProcessMerge, sbData, triggerSnackBar, startOver) + console.log("completed merge idenitfy process ------"); + setSbData({}); + setFolderPath() + } catch(err) { + setSbData({}); + console.error("error in merge process : ", err); + } + } const callFunction = () => { if (model.buttonName === 'Replace') { setMerge(false); checkBurritoVersion(); - } else { + } + else if(model.buttonName === t('label-startover')) { + startTextTranslationMergeProcess(true) + } + else { callImport(true); } }; @@ -187,29 +233,72 @@ export default function ImportProjectPopUp(props) { logger.debug('importProjectPopUp.js', 'call for merge'); setProcessMerge(true) modelClose(); - await mergeProject(folderPath, currentUser, setConflictPopup, setModel, setProcessMerge); + if (sbData?.burritoType === 'gloss / textStories'){ + await mergeProject(folderPath, currentUser, setConflictPopup, setModel, setProcessMerge); + }else if (sbData?.burritoType === 'scripture / textTranslation') { + console.log("Started Indentify Merge conflicts ------"); + try { + // confirm the user need to comtinue or start over the conflict process before move + const path = require('path'); + const fs = window.require('fs'); + const newpath = localStorage.getItem('userPath'); + const USFMMergeDirPath = path.join(newpath, packageInfo.name, 'users', currentUser, '.merge-usfm'); + const projectDirName = `${sbData.projectName}_${sbData.id[0]}`; + if(fs.existsSync(path.join(USFMMergeDirPath, projectDirName))) { + console.log("in IF ###############"); + setModel({ + openModel: true, + title: "Confirm", + confirmMessage: "You already have a conflict resolution in progress. Do you want to continue or start over.", + buttonName: t('label-startover'), + buttonName2 : { + active: true, + loading: false, + name:t('label-continue'), + action: () => startTextTranslationMergeProcess(false), + } + }); + } else { + console.log("in ELSE ###############"); + await startTextTranslationMergeProcess(false) + } + } catch(err) { + setMerge(false) + setProcessMerge(false) + console.log("error merge fucntion : ", err); + } + } setMerge(false) - setSbData({}); close() logger.debug('importProjectPopUp.js', 'git merge process done'); } + console.log({sbData, model}); + const importProject = async () => { logger.debug('ImportProjectPopUp.js', 'Inside importProject'); if (folderPath) { setImportProgress((prev)=>({...prev, importStarted:true, completedSteps: prev.completedSteps + 1 })) setValid(false); + let IsMergeOption = false if (sbData.duplicate === true) { logger.warn('ImportProjectPopUp.js', 'Project already available'); // currently MERGE feature only Enabled for OBS projects - if (sbData?.burritoType === 'gloss / textStories'){ + if (sbData?.burritoType === 'gloss / textStories' || sbData?.burritoType === 'scripture / textTranslation'){ setMerge(true) + IsMergeOption = true } setModel({ openModel: true, title: t('modal-title-replace-resource'), confirmMessage: t('dynamic-msg-confirm-replace-resource'), buttonName: t('btn-replace'), + buttonName2 : { + active: IsMergeOption, + loading: processMerge, + name:t('label-merge'), + action: () => MergeFunction(), + } }); } else { logger.debug('ImportProjectPopUp.js', 'Its a new project'); @@ -262,7 +351,7 @@ export default function ImportProjectPopUp(props) { {t('label-import-project')}