From 9d7a82d38150152ce4c7e703873c575edc35efec Mon Sep 17 00:00:00 2001 From: JayDataEngineer <262154717+JayDataEngineer@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:00:13 -0400 Subject: [PATCH] fix: sync Editor editable state with readOnly prop (#523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rich-text Editor (used for card descriptions) creates its Tiptap instance once via useEditor with an empty dependency array. This means the initial value of `editable: !readOnly` is captured at mount time and never updated, and the onChange/onBlur callbacks are frozen as the first-render closures. In CardPage, the description Editor mounts as soon as the card query resolves but before the permissions query resolves, so `readOnly` is `true` and onChange/onBlur are `undefined` at that moment. When permissions resolve a moment later and `canEdit` flips to `true`, the Editor never becomes editable — leaving the description stuck read-only even for workspace admins. Fix mirrors the existing pattern in PlainTextEditor.tsx: - Use refs for onChange/onBlur so the editor reads the latest values - Add a useEffect that calls editor.setEditable(!readOnly) when the readOnly prop changes Co-authored-by: Jay --- apps/web/src/components/Editor.tsx | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/Editor.tsx b/apps/web/src/components/Editor.tsx index f80d23f7..3afa1a05 100644 --- a/apps/web/src/components/Editor.tsx +++ b/apps/web/src/components/Editor.tsx @@ -457,6 +457,17 @@ export default function Editor({ }) { const containerRef = useRef(null); + // useEditor is created once (empty deps below), so keep the latest callbacks + // in refs to avoid the editor capturing stale closures on re-render. + const onChangeRef = useRef(onChange); + const onBlurRef = useRef(onBlur); + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + useEffect(() => { + onBlurRef.current = onBlur; + }, [onBlur]); + const editor = useEditor( { extensions: [ @@ -553,7 +564,7 @@ export default function Editor({ ...(enableYouTubeEmbed ? [YouTubeNode] : []), ], content, - onUpdate: ({ editor }) => onChange?.(editor.getHTML()), + onUpdate: ({ editor }) => onChangeRef.current?.(editor.getHTML()), onBlur: ({ event }) => { if ( document @@ -563,7 +574,7 @@ export default function Editor({ return; // Only trigger onBlur if the click was outside both the editor and menu if (!containerRef.current?.contains(event.relatedTarget as Node)) { - onBlur?.(); + onBlurRef.current?.(); } }, editorProps: { @@ -587,6 +598,16 @@ export default function Editor({ } }, [content, editor]); + // useEditor captures `readOnly` once at creation time (empty deps above), so + // explicitly sync `editable` when the prop changes. Without this the editor + // gets stuck read-only when `readOnly` flips from true to false after mount + // (e.g. card permissions resolving slower than the card data on first load). + useEffect(() => { + if (!editor) return; + if (editor.isEditable === !readOnly) return; + editor.setEditable(!readOnly); + }, [editor, readOnly]); + return (