refactor: improve css editor loading suspense

This commit is contained in:
Carlos Valente
2026-03-20 21:17:56 +01:00
committed by Carlos Valente
parent 129efc33b9
commit b2b7855114
4 changed files with 59 additions and 62 deletions
@@ -1,4 +1,5 @@
.wrapper { .wrapper {
min-height: 500px;
max-height: 500px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
} }
@@ -1,71 +1,38 @@
import Prism from 'virtual:prismjs'; import Prism from 'virtual:prismjs';
import { forwardRef, memo, useEffect, useImperativeHandle, useState } from 'react'; import { memo } from 'react';
import Editor from 'react-simple-code-editor'; import Editor from 'react-simple-code-editor';
import 'prismjs/components/prism-css'; import 'prismjs/components/prism-css';
import 'prismjs/themes/prism-tomorrow.min.css'; import 'prismjs/themes/prism-tomorrow.min.css';
import style from './StyleEditor.module.scss'; import style from './StyleEditor.module.scss';
interface CodeEditorProps { interface CodeEditorProps {
language: string; language: string;
initialValue: string; value: string;
isDirty: boolean; onChange: (value: string) => void;
setIsDirty: (value: boolean) => void;
} }
const CodeEditor = forwardRef((props: CodeEditorProps, cssRef) => { export default memo(CodeEditor);
const { language, initialValue, isDirty, setIsDirty } = props; function CodeEditor({ language, onChange, value }: CodeEditorProps) {
const [code, setCode] = useState(initialValue);
const highlight = (code: string) => { const highlight = (code: string) => {
const grammar = Prism.languages[language]; const grammar = Prism.languages[language];
return grammar ? Prism.highlight(code, grammar, language) : code; return grammar ? Prism.highlight(code, grammar, language) : code;
}; };
const handleChange = (newCode: string) => {
setCode(newCode);
};
useImperativeHandle(cssRef, () => {
return {
getCss: () => code,
};
});
// add contents to editor on mount and any change in initialValue
useEffect(() => {
setCode(initialValue);
}, [initialValue]);
// handle dirty state on change
useEffect(() => {
if (initialValue.trim() !== code.trim() && !isDirty && code.length !== 0) {
setIsDirty(true);
}
if (initialValue.trim() === code.trim() && isDirty) {
setIsDirty(false);
}
}, [initialValue, code, isDirty, setIsDirty]);
return ( return (
<div className={style.wrapper}> <div className={style.wrapper}>
<Editor <Editor
value={code} value={value}
padding={15} padding={15}
onValueChange={handleChange} onValueChange={onChange}
highlight={highlight} highlight={highlight}
style={{ style={{
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: 12, fontSize: 12,
minHeight: 500, minHeight: 500,
background: '#2d2d2d', // Background of tomorrow theme background: 'transparent',
}} }}
/> />
</div> </div>
); );
}); }
CodeEditor.displayName = 'StyleEditor';
export default memo(CodeEditor);
@@ -13,3 +13,12 @@
gap: 1rem; gap: 1rem;
flex-direction: column; flex-direction: column;
} }
.editorBody {
position: relative;
min-height: 500px;
background-color: $gray-1350;
border: 1px solid $gray-1100;
border-radius: 3px;
overflow: hidden;
}
@@ -1,4 +1,4 @@
import { Suspense, lazy, useEffect, useRef, useState } from 'react'; import { Suspense, lazy, useEffect, useState } from 'react';
import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../../common/api/assets'; import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../../common/api/assets';
import Button from '../../../../../common/components/buttons/Button'; import Button from '../../../../../common/components/buttons/Button';
@@ -15,24 +15,21 @@ interface CodeEditorModalProps {
onClose: () => void; onClose: () => void;
} }
interface CSSRef {
getCss: () => string;
}
export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProps) { export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProps) {
const [css, setCSS] = useState(''); const [savedCss, setSavedCss] = useState('');
const [isDirty, setIsDirty] = useState(false); const [draftCss, setDraftCss] = useState('');
const [isLoadingCss, setIsLoadingCss] = useState(false);
const [saveLoading, setSaveLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false);
const [resetLoading, setResetLoading] = useState(false); const [resetLoading, setResetLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const cssRef = useRef<CSSRef>(null); const isDirty = savedCss.trim() !== draftCss.trim();
const handleRestore = async () => { const handleRestore = async () => {
try { try {
setResetLoading(true); setResetLoading(true);
const defaultCss = await restoreCSSContents(); const defaultCss = await restoreCSSContents();
setCSS(defaultCss); setDraftCss(defaultCss);
} catch (_error) { } catch (_error) {
/** no error handling for now */ /** no error handling for now */
} finally { } finally {
@@ -43,11 +40,8 @@ export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProp
const handleSave = async () => { const handleSave = async () => {
try { try {
setSaveLoading(true); setSaveLoading(true);
if (cssRef.current) { await postCSSContents(draftCss);
await postCSSContents(cssRef.current.getCss()); setSavedCss(draftCss);
setCSS(cssRef.current.getCss());
setIsDirty(false);
}
} catch (_error) { } catch (_error) {
/** no error handling for now */ /** no error handling for now */
} finally { } finally {
@@ -55,22 +49,43 @@ export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProp
} }
}; };
const clear = () => setCSS(''); const clear = () => setDraftCss('');
useEffect(() => { useEffect(() => {
let isCancelled = false;
async function fetchServerCSS() { async function fetchServerCSS() {
// check for isOpen to fetch recent css // check for isOpen to fetch recent css
if (isOpen) { if (isOpen) {
try { try {
setError(null);
setIsLoadingCss(true);
const css = await getCSSContents(); const css = await getCSSContents();
setCSS(css); if (isCancelled) {
return;
}
setSavedCss(css);
setDraftCss(css);
} catch (_error) { } catch (_error) {
if (isCancelled) {
return;
}
setSavedCss('');
setDraftCss('');
setError('Failed to load CSS from server'); setError('Failed to load CSS from server');
/** no error handling for now */ /** no error handling for now */
} finally {
if (!isCancelled) {
setIsLoadingCss(false);
}
} }
} }
} }
fetchServerCSS(); fetchServerCSS();
return () => {
isCancelled = true;
};
}, [isOpen]); }, [isOpen]);
return ( return (
@@ -81,9 +96,14 @@ export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProp
showCloseButton showCloseButton
showBackdrop showBackdrop
bodyElements={ bodyElements={
<Suspense fallback={null}> <div className={style.editorBody}>
<CodeEditor ref={cssRef} initialValue={css} language='css' isDirty={isDirty} setIsDirty={setIsDirty} /> {!isLoadingCss && (
</Suspense> <Suspense fallback={<Panel.Loader isLoading />}>
<CodeEditor value={draftCss} onChange={setDraftCss} language='css' />
</Suspense>
)}
<Panel.Loader isLoading={isLoadingCss} />
</div>
} }
footerElements={ footerElements={
<div className={style.column}> <div className={style.column}>