diff --git a/libs/shared/lib/components/textEditor/TextEditor.tsx b/libs/shared/lib/components/textEditor/TextEditor.tsx index 5c8714452eddfb9eee04a1cb365085e4d5789064..32664ad31d9879a10a3c6fc12e02a3c27d1055e1 100644 --- a/libs/shared/lib/components/textEditor/TextEditor.tsx +++ b/libs/shared/lib/components/textEditor/TextEditor.tsx @@ -1,43 +1,61 @@ -import { useRef } from 'react'; +import { useEffect, useRef, useCallback } from 'react'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; -import { LexicalEditor, EditorState } from 'lexical'; -import { $generateHtmlFromNodes } from '@lexical/html'; -import { MyOnChangePlugin } from './plugins/StateChangePlugin'; +import { SerializedEditorState} from 'lexical'; import { ToolbarPlugin } from './plugins/ToolbarPlugin'; import { PreviewPlugin } from './plugins/PreviewPlugin'; import { InsertVariablesPlugin } from './plugins/InsertVariablesPlugin'; import { ErrorHandler } from './ErrorHandler'; import { Placeholder } from './Placeholder'; import { VariableNode } from './VariableNode'; -import { fontFamily } from 'html2canvas/dist/types/css/property-descriptors/font-family'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { SaveButtonPlugin } from './plugins/SaveButtonPlugin'; type TextEditorProps = { - editorState: EditorState | null; - setEditorState: (value: EditorState) => void; + children: React.ReactNode; + editorState: SerializedEditorState | null; + setEditorState: (value: SerializedEditorState) => void; showToolbar: boolean; placeholder?: string; + handleSave: (elements: SerializedEditorState) => void; }; -export function TextEditor({ editorState, setEditorState, showToolbar, placeholder }: TextEditorProps) { - function onChange(editorState: EditorState, editor: LexicalEditor) { - setEditorState(editorState); - editor.read(() => { - // TODO: +function InitialStateLoader({ editorState }: { editorState: SerializedEditorState | null}) { + const [editor] = useLexicalComposerContext(); + const previousEditorStateRef = useRef<string | null>(null); + + useEffect(() => { + if (!editor || !editorState) return; + queueMicrotask(() => { + editor.setEditorState(editor.parseEditorState(editorState)); }); - } + + }, [editor, editorState]); + + return null; +} + +export function TextEditor({ editorState, showToolbar, placeholder, handleSave, children }: TextEditorProps) { + const contentEditableRef = useRef<HTMLDivElement>(null); + const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null); + const isUpdatingRef = useRef(false); + + useEffect(() => { + return () => { + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current); + } + }; + }, []); const initialConfig = { namespace: 'MyEditor', - editorState: editorState ? JSON.stringify(editorState) : null, onError: ErrorHandler, nodes: [VariableNode], }; - const contentEditableRef = useRef<HTMLDivElement>(null); - return ( <LexicalComposer initialConfig={initialConfig}> <div className="editor-toolbar flex items-center bg-secondary-50 rounded mt-4 space-x-2"> @@ -54,8 +72,13 @@ export function TextEditor({ editorState, setEditorState, showToolbar, placehold </div> <div className="preview min-h-24 p-3 hidden"></div> </div> - <MyOnChangePlugin onChange={onChange} /> <InsertVariablesPlugin /> + <InitialStateLoader editorState={editorState} /> + <div className='flex justify-end mt-3'> + {children} + <SaveButtonPlugin + onChange={handleSave}></SaveButtonPlugin> + </div> </LexicalComposer> ); -} +} \ No newline at end of file diff --git a/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx index c854fe4a2ebea1024d67c4def3f102c1ce979d42..c996a62e5ebc5cc325492cd4d778c268104de790 100644 --- a/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx +++ b/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx @@ -44,6 +44,7 @@ export const InsertVariablesPlugin = () => { <> {nodeTypes.map((nodeType) => ( <Input + key={nodeType} type="dropdown" label={`${nodeType} variable`} value="" diff --git a/libs/shared/lib/components/textEditor/plugins/SaveButtonPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/SaveButtonPlugin.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b4c8944edfff8b4088fe520121dc13e7bd75e763 --- /dev/null +++ b/libs/shared/lib/components/textEditor/plugins/SaveButtonPlugin.tsx @@ -0,0 +1,18 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { Button } from '../../buttons'; +import { SerializedEditorState } from 'lexical'; + +export function SaveButtonPlugin({ onChange }: { onChange: (elements: SerializedEditorState) => void }) { + const [editor] = useLexicalComposerContext(); + + function handleSave() { + editor.read(() => { + const data = editor.getEditorState(); + const jsonData = data.toJSON(); + + onChange(jsonData); + }) + } + + return <Button label="Save" variantType="primary" className="ml-2" onClick={handleSave} />; +} diff --git a/libs/shared/lib/insight-sharing/FormAlert.tsx b/libs/shared/lib/insight-sharing/FormAlert.tsx index e9ebe20f761eab8c54718543efdbc804b9ab15e9..e010d447486b2ddc4f9bd8fc8f4f33b304805011 100644 --- a/libs/shared/lib/insight-sharing/FormAlert.tsx +++ b/libs/shared/lib/insight-sharing/FormAlert.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '../components/accordion'; import { TextEditor } from '../components/textEditor'; import { @@ -10,10 +10,9 @@ import { } from '@graphpolaris/shared/lib/data-access/store/insightSharingSlice'; import { useAppDispatch, useSessionCache } from '@graphpolaris/shared/lib/data-access'; import { Button, Input, LoadingSpinner } from '../components'; -import { EditorState } from 'lexical'; -import { MonitorType } from './components/Sidebar'; import { wsUpdateInsight, wsCreateInsight, wsDeleteInsight } from '@graphpolaris/shared/lib/data-access/broker/wsInsightSharing'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { SerializedEditorState } from 'lexical'; type Props = { insight: InsightResponse; @@ -29,27 +28,36 @@ export function FormAlert(props: Props) { const [description, setDescription] = useState(props.insight.description); const [recipients, setRecipients] = useState<string[]>(props.insight.recipients || []); const [recipientInput, setRecipientInput] = useState<string>(''); - const [editorState, setEditorState] = useState<EditorState | null>(null); + const [editorState, setEditorState] = useState<SerializedEditorState | null>(null); useEffect(() => { - setName(props.insight.name); - setDescription(props.insight.description); - setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); - setRecipients(props.insight.recipients || []); - if (props.insight.template) { - try { - const parsedTemplate = JSON.parse(props.insight.template); - setEditorState(parsedTemplate); - } catch (e) { + let isMounted = true; + + if (isMounted) { + setName(props.insight.name); + setDescription(props.insight.description); + setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); + setRecipients(props.insight.recipients || []); + if (props.insight.template) { + try { + const parsedTemplate = JSON.parse(props.insight.template); + setEditorState(parsedTemplate); + } catch (e) { + setEditorState(null); + console.error('Failed to parse template:', e); + } + } else { setEditorState(null); - console.error('Failed to parse template:', e); } - } else { - setEditorState(null); } + + return () => { + isMounted = false; + setEditorState(null); + }; }, [props.insight]); - const handleSave = async () => { + const handleSave = async (element: SerializedEditorState) => { if (!name || name.trim() === '') { dispatch(addError('Name is required')); return; @@ -59,7 +67,7 @@ export function FormAlert(props: Props) { name, description, recipients, - template: JSON.stringify(editorState), + template: JSON.stringify(element), saveStateId: session.currentSaveState || '', type: 'alert' as const, frequency: '', @@ -151,20 +159,17 @@ export function FormAlert(props: Props) { <span className="font-semibold">Alerting text</span> </AccordionHead> <AccordionBody> - <TextEditor - key={props.insight.id || 'new'} - editorState={editorState} - setEditorState={setEditorState} - showToolbar={true} - placeholder="Start typing your alert template..." - /> + <TextEditor + key={`editor-${props.insight.id}`} + editorState={editorState} + setEditorState={setEditorState} + showToolbar={true} + placeholder="Start typing your alert template..." + handleSave={handleSave} + ><Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /></TextEditor> </AccordionBody> </AccordionItem> </Accordion> - <div className="flex justify-end mt-2"> - <Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /> - <Button label="Save" variantType="primary" className="ml-2" onClick={handleSave} /> - </div> </div> ); } diff --git a/libs/shared/lib/insight-sharing/FormReport.tsx b/libs/shared/lib/insight-sharing/FormReport.tsx index de77445955f305551a60cf19c78c201a0b356ed9..7597375748b34c231dcd56b115ca613bbaaa1ccb 100644 --- a/libs/shared/lib/insight-sharing/FormReport.tsx +++ b/libs/shared/lib/insight-sharing/FormReport.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Button, Input, LoadingSpinner } from '../components'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../components/accordion'; import { TextEditor } from '../components/textEditor'; @@ -9,8 +9,7 @@ import { InsightRequest, } from '@graphpolaris/shared/lib/data-access/store/insightSharingSlice'; import { useAppDispatch, useSessionCache } from '@graphpolaris/shared/lib/data-access'; -import { MonitorType } from './components/Sidebar'; -import { EditorState } from 'lexical'; +import { SerializedEditorState } from 'lexical'; import { wsCreateInsight, wsDeleteInsight, wsUpdateInsight } from '@graphpolaris/shared/lib/data-access/broker/wsInsightSharing'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; @@ -29,25 +28,33 @@ export function FormReport(props: Props) { const [recipients, setRecipients] = useState(props.insight.recipients || []); const [recipientInput, setRecipientInput] = useState<string>(''); const [frequency, setFrequency] = useState(props.insight.frequency || 'Daily'); - const [editorState, setEditorState] = useState<EditorState | null>(null); + const [editorState, setEditorState] = useState<SerializedEditorState| null>(null); useEffect(() => { - setName(props.insight.name); - setDescription(props.insight.description); - setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); - setRecipients(props.insight.recipients || []); - setFrequency(props.insight.frequency || 'Daily'); - if (props.insight.template) { - try { - const parsedTemplate = JSON.parse(props.insight.template); - setEditorState(parsedTemplate); - } catch (e) { + let isMounted = true; + + if (isMounted) { + setName(props.insight.name); + setDescription(props.insight.description); + setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); + setRecipients(props.insight.recipients || []); + if (props.insight.template) { + try { + const parsedTemplate = JSON.parse(props.insight.template); + setEditorState(parsedTemplate); + } catch (e) { + setEditorState(null); + console.error('Failed to parse template:', e); + } + } else { setEditorState(null); - console.error('Failed to parse template:', e); } - } else { - setEditorState(null); } + + return () => { + isMounted = false; + setEditorState(null); + }; }, [props.insight]); const handleSetFrequency = (value: string | number) => { @@ -62,7 +69,7 @@ export function FormReport(props: Props) { setDescription(value as string); }; - const handleSave = async () => { + const handleSave = async (elements: SerializedEditorState) => { if (!name || name.trim() === '') { dispatch(addError('Please enter a name for the report.')); return; @@ -73,7 +80,7 @@ export function FormReport(props: Props) { description, recipients, frequency, - template: JSON.stringify(editorState), + template: JSON.stringify(elements), saveStateId: session.currentSaveState || '', type: 'report' as const, }; @@ -177,20 +184,17 @@ export function FormReport(props: Props) { <span className="font-semibold">Email Template</span> </AccordionHead> <AccordionBody> - <TextEditor - key={props.insight.id || 'new'} - editorState={editorState} - setEditorState={setEditorState} - showToolbar={true} - placeholder="Start typing your report template..." - /> + <TextEditor + key={`editor-${props.insight.id}`} + editorState={editorState} + setEditorState={setEditorState} + showToolbar={true} + placeholder="Start typing your alert template..." + handleSave={handleSave} + ><Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /></TextEditor> </AccordionBody> </AccordionItem> </Accordion> - <div className="flex justify-end mt-2"> - <Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /> - <Button label="Save" variantType="primary" className="ml-2" onClick={handleSave} /> - </div> </div> ); }