From 85a56cac59d0bb3c855f0ce2b75f5148fbafb93f Mon Sep 17 00:00:00 2001 From: Gerhard Scheikl Date: Sat, 9 May 2026 17:17:55 +0200 Subject: [PATCH] fix(settings): preserve stored logo + persist editor changes on save --- app/components/RichTextEditor.tsx | 13 +++++++++++-- app/routes/app.settings.tsx | 11 ++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/components/RichTextEditor.tsx b/app/components/RichTextEditor.tsx index c9f9766..9027f41 100644 --- a/app/components/RichTextEditor.tsx +++ b/app/components/RichTextEditor.tsx @@ -58,6 +58,12 @@ export function RichTextEditor({ const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); + // Mirror editor HTML into local state so the hidden always + // reflects the latest content. Without this, React doesn't re-render + // when TipTap's content changes and the form submits stale HTML. + const initialHtml = swapCidToLogo(defaultValue || "

", logoDataUrl); + const [html, setHtml] = useState(initialHtml); + const editor = useEditor({ immediatelyRender: false, extensions: [ @@ -85,6 +91,9 @@ export function RichTextEditor({ Color, ], content: swapCidToLogo(defaultValue || "

", logoDataUrl), + onUpdate: ({ editor }) => { + setHtml(editor.getHTML()); + }, editorProps: { attributes: { class: "wysiwyg-editor", @@ -96,7 +105,7 @@ export function RichTextEditor({ // Keep editor disposed cleanly on unmount. useEffect(() => () => editor?.destroy(), [editor]); - const html = swapLogoToCid(editor?.getHTML() ?? defaultValue, logoDataUrl); + const submittedHtml = swapLogoToCid(html, logoDataUrl); if (!mounted) { // Server / pre-hydration fallback: a textarea so the value is still @@ -219,7 +228,7 @@ export function RichTextEditor({ /> - + {variables.length > 0 ? (
Insert variable: diff --git a/app/routes/app.settings.tsx b/app/routes/app.settings.tsx index a563c1b..f6bcaed 100644 --- a/app/routes/app.settings.tsx +++ b/app/routes/app.settings.tsx @@ -101,12 +101,21 @@ export const action = async ({ request }: ActionFunctionArgs) => { // 2. Remove the current logo (`removeLogo=on`). // 3. Provide an external URL via the `logoUrl` field. // If a file is uploaded it wins over a manually-entered URL. - let resolvedLogoUrl = str("logoUrl"); + // Look up the existing logoUrl so we don't accidentally clear it when + // the user just edited unrelated fields (the visible URL field is hidden + // for stored uploads, so it submits empty in that case). + const existing = await db.shopSettings.findUnique({ + where: { shopDomain: session.shop }, + select: { logoUrl: true }, + }); + const submittedLogoUrl = str("logoUrl"); const removeLogo = bool("removeLogo"); const logoFile = form.get("logoFile"); const hasUpload = logoFile && typeof logoFile === "object" && "size" in logoFile && (logoFile as File).size > 0; + let resolvedLogoUrl = submittedLogoUrl || existing?.logoUrl || ""; + if (removeLogo && !hasUpload) { await deleteStoredLogo(session.shop); resolvedLogoUrl = "";