Skip to content

Commit 27c17e4

Browse files
authored
Merge pull request #257 from beNative/tisi/fix-electron-backend-appendlogtofile-error
Handle rich text parsing errors and append logs via IPC
2 parents 6f1d512 + a272abd commit 27c17e4

5 files changed

Lines changed: 118 additions & 62 deletions

File tree

components/RichTextEditor.tsx

Lines changed: 96 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
UNDO_COMMAND,
5757
type EditorState,
5858
type LexicalEditor,
59+
$createTextNode,
5960
} from 'lexical';
6061
import IconButton from './IconButton';
6162
import ContextMenuComponent, { type MenuItem as ContextMenuItem } from './ContextMenu';
@@ -285,19 +286,26 @@ const ToolbarPlugin: React.FC<{
285286
});
286287
}, [editor]);
287288

288-
const toggleLink = useCallback(() => {
289-
if (readOnly) {
290-
return;
291-
}
292-
if (isLink) {
293-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
294-
return;
295-
}
296-
const url = window.prompt('Enter URL');
297-
if (url) {
298-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
299-
}
300-
}, [editor, isLink, readOnly]);
289+
const toggleLink = useCallback(() => {
290+
if (readOnly) {
291+
return;
292+
}
293+
if (isLink) {
294+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
295+
return;
296+
}
297+
298+
const promptFn = typeof window.prompt === 'function' ? window.prompt.bind(window) : null;
299+
if (!promptFn) {
300+
console.warn('Link insertion prompt is unavailable in this environment.');
301+
return;
302+
}
303+
304+
const url = promptFn('Enter URL');
305+
if (url) {
306+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
307+
}
308+
}, [editor, isLink, readOnly]);
301309

302310
const insertImage = useCallback(
303311
(payload: ImagePayload) => {
@@ -617,34 +625,7 @@ const HtmlContentSynchronizer: React.FC<{ html: string; lastAppliedHtmlRef: Reac
617625
const [editor] = useLexicalComposerContext();
618626

619627
useEffect(() => {
620-
const normalizedIncoming = html.trim();
621-
622-
editor.update(() => {
623-
const root = $getRoot();
624-
const currentHtml = $generateHtmlFromNodes(editor).trim();
625-
626-
if (currentHtml === normalizedIncoming) {
627-
lastAppliedHtmlRef.current = normalizedIncoming;
628-
return;
629-
}
630-
631-
if (normalizedIncoming === lastAppliedHtmlRef.current && currentHtml !== '') {
632-
return;
633-
}
634-
635-
root.clear();
636-
637-
if (!normalizedIncoming) {
638-
lastAppliedHtmlRef.current = '';
639-
return;
640-
}
641-
642-
const parser = new DOMParser();
643-
const dom = parser.parseFromString(normalizedIncoming, 'text/html');
644-
const nodes = $generateNodesFromDOM(editor, dom);
645-
nodes.forEach(node => root.append(node));
646-
lastAppliedHtmlRef.current = normalizedIncoming;
647-
});
628+
applyHtmlToEditor(editor, html, lastAppliedHtmlRef);
648629
}, [editor, html, lastAppliedHtmlRef]);
649630

650631
return null;
@@ -799,6 +780,68 @@ const ClipboardImagePlugin: React.FC<{ readOnly: boolean }> = ({ readOnly }) =>
799780
return null;
800781
};
801782

783+
const sanitizeDomFromHtml = (html: string): Document => {
784+
const parser = new DOMParser();
785+
const dom = parser.parseFromString(html, 'text/html');
786+
dom.querySelectorAll('script,style').forEach(node => node.remove());
787+
return dom;
788+
};
789+
790+
const fallbackToPlainText = (text: string) => {
791+
const parser = new DOMParser();
792+
const dom = parser.parseFromString(text, 'text/html');
793+
return (dom.body.textContent || '').trim();
794+
};
795+
796+
const applyHtmlToEditor = (
797+
editor: LexicalEditor,
798+
html: string,
799+
lastAppliedHtmlRef: React.MutableRefObject<string>,
800+
) => {
801+
const normalizedIncoming = html.trim();
802+
803+
editor.update(() => {
804+
const root = $getRoot();
805+
const currentHtml = $generateHtmlFromNodes(editor).trim();
806+
807+
if (currentHtml === normalizedIncoming) {
808+
lastAppliedHtmlRef.current = normalizedIncoming;
809+
return;
810+
}
811+
812+
if (normalizedIncoming === lastAppliedHtmlRef.current && currentHtml !== '') {
813+
return;
814+
}
815+
816+
root.clear();
817+
818+
if (!normalizedIncoming) {
819+
lastAppliedHtmlRef.current = '';
820+
root.append($createParagraphNode());
821+
return;
822+
}
823+
824+
try {
825+
const dom = sanitizeDomFromHtml(normalizedIncoming);
826+
const nodes = $generateNodesFromDOM(editor, dom);
827+
if (nodes.length === 0) {
828+
const paragraph = $createParagraphNode();
829+
paragraph.append($createTextNode(''));
830+
root.append(paragraph);
831+
} else {
832+
nodes.forEach(node => root.append(node));
833+
}
834+
lastAppliedHtmlRef.current = normalizedIncoming;
835+
} catch (error) {
836+
console.error('Failed to sync HTML content into the rich text editor.', error);
837+
const paragraph = $createParagraphNode();
838+
paragraph.append($createTextNode(fallbackToPlainText(normalizedIncoming)));
839+
root.append(paragraph);
840+
lastAppliedHtmlRef.current = paragraph.getTextContent();
841+
}
842+
});
843+
};
844+
802845
const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(
803846
({ html, onChange, readOnly = false, onScroll, onFocusChange }, ref) => {
804847
const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -860,13 +903,17 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(
860903
const handleChange = useCallback(
861904
(editorState: EditorState, editor: LexicalEditor) => {
862905
editorState.read(() => {
863-
const generated = $generateHtmlFromNodes(editor);
864-
const normalized = generated.trim();
865-
if (normalized === lastAppliedHtmlRef.current) {
866-
return;
906+
try {
907+
const generated = $generateHtmlFromNodes(editor);
908+
const normalized = generated.trim();
909+
if (normalized === lastAppliedHtmlRef.current) {
910+
return;
911+
}
912+
lastAppliedHtmlRef.current = normalized;
913+
onChange(normalized);
914+
} catch (error) {
915+
console.error('Failed to serialize rich text content to HTML.', error);
867916
}
868-
lastAppliedHtmlRef.current = normalized;
869-
onChange(normalized);
870917
});
871918
},
872919
[onChange],
@@ -911,7 +958,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(
911958
editable: !readOnly,
912959
theme: RICH_TEXT_THEME,
913960
onError: (error: Error) => {
914-
throw error;
961+
console.error('Rich text editor encountered an error.', error);
915962
},
916963
nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, ImageNode],
917964
editorState: (editor: LexicalEditor) => {
@@ -920,15 +967,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(
920967
lastAppliedHtmlRef.current = '';
921968
return;
922969
}
923-
const parser = new DOMParser();
924-
const dom = parser.parseFromString(initialHtml, 'text/html');
925-
editor.update(() => {
926-
const root = $getRoot();
927-
root.clear();
928-
const nodes = $generateNodesFromDOM(editor, dom);
929-
nodes.forEach(node => root.append(node));
930-
lastAppliedHtmlRef.current = initialHtml;
931-
});
970+
applyHtmlToEditor(editor, initialHtml, lastAppliedHtmlRef);
932971
},
933972
}),
934973
[readOnly],

electron/main.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,17 @@ ipcMain.handle('app:get-version', () => app.getVersion());
656656
// Fix: Error on line 145 is resolved by importing 'platform' from 'process'.
657657
ipcMain.handle('app:get-platform', () => platform);
658658
ipcMain.handle('app:get-log-path', () => log.transports.file.getFile().path);
659+
ipcMain.handle('log:append', async (_, content: string) => {
660+
const logFilePath = log.transports.file.getFile().path;
661+
try {
662+
await fs.mkdir(path.dirname(logFilePath), { recursive: true });
663+
await fs.appendFile(logFilePath, content, 'utf-8');
664+
return { success: true, filePath: logFilePath };
665+
} catch (error) {
666+
console.error('Failed to append to log file:', error);
667+
return { success: false, error: error instanceof Error ? error.message : String(error) };
668+
}
669+
});
659670
ipcMain.handle('app:open-executable-folder', async () => {
660671
const execDir = path.dirname(process.execPath);
661672
try {

electron/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
6060
getAppVersion: () => ipcRenderer.invoke('app:get-version'),
6161
getPlatform: () => ipcRenderer.invoke('app:get-platform'),
6262
getLogPath: () => ipcRenderer.invoke('app:get-log-path'),
63+
appendLog: (content: string) => ipcRenderer.invoke('log:append', content),
6364
openExecutableFolder: () => ipcRenderer.invoke('app:open-executable-folder'),
6465
renderPlantUML: (diagram: string, format: 'svg' = 'svg') => ipcRenderer.invoke('plantuml:render-svg', diagram, format),
6566
updaterSetAllowPrerelease: (allow: boolean) => ipcRenderer.send('updater:set-allow-prerelease', allow),

services/storageService.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,14 @@ export const storageService = {
6767
* @param content The string content to append.
6868
*/
6969
appendLogToFile: async (content: string): Promise<void> => {
70-
// This feature is not fully implemented in the electron backend.
71-
// Logging a warning to avoid silent failures.
72-
if (window.electronAPI) {
73-
console.warn('appendLogToFile is not implemented in the Electron backend.');
70+
if (window.electronAPI?.appendLog) {
71+
const result = await window.electronAPI.appendLog(content);
72+
if (!result?.success && !result?.canceled) {
73+
throw new Error(result?.error || 'Failed to append log content.');
74+
}
75+
return;
7476
}
77+
78+
console.warn('Appending logs is only supported in the desktop application.');
7579
},
76-
};
80+
};

types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ declare global {
3636
getAppVersion: () => Promise<string>;
3737
getPlatform: () => Promise<string>;
3838
getLogPath: () => Promise<string>;
39+
appendLog: (content: string) => Promise<{ success: boolean; error?: string; canceled?: boolean; filePath?: string }>;
3940
openExecutableFolder: () => Promise<{ success: boolean; path?: string; error?: string }>;
4041
renderPlantUML: (
4142
diagram: string,

0 commit comments

Comments
 (0)