Fix body field mismatch when switching between items in edit dialog

The RichTextEditor was not updating its content when switching from one
item to another because initializedRef was only reset when value became
null. Now uses prevValueRef to properly detect value changes.
This commit is contained in:
Ilia Sharin
2026-08-20 13:01:55 -04:00
parent f9ae92f605
commit 95a628249d
4 changed files with 28 additions and 38 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 334 KiB

+6 -13
View File
@@ -10,27 +10,20 @@ function RichTextEditor({ value, onChange, placeholder = "Add details...", colla
const [copyMenuPos, setCopyMenuPos] = useStateD({ x: 0, y: 0 }); const [copyMenuPos, setCopyMenuPos] = useStateD({ x: 0, y: 0 });
const toolbarRef = useRefD(null); const toolbarRef = useRefD(null);
const isEditingRef = useRefD(false); const isEditingRef = useRefD(false);
const initializedRef = useRefD(false); const prevValueRef = useRefD(value);
// Initialize editor content only when value changes externally (not from our own edits) // Initialize editor content only when value changes externally (not from our own edits)
useEffectD(() => { useEffectD(() => {
if (editorRef.current && value !== undefined && !isEditingRef.current) { if (editorRef.current && !isEditingRef.current) {
const html = MarkdownUtils.toHtml(value || ''); // Update if value changed from previous or editor is empty
// Only set innerHTML on first load or when value is reset externally if (prevValueRef.current !== value || editorRef.current.innerHTML === '') {
if (!initializedRef.current || editorRef.current.innerHTML === '') { const html = MarkdownUtils.toHtml(value || '');
editorRef.current.innerHTML = html; editorRef.current.innerHTML = html;
initializedRef.current = true; prevValueRef.current = value;
} }
} }
}, [value]); }, [value]);
// Reset initialized flag when dialog closes/opens (value becomes null/undefined)
useEffectD(() => {
if (value === null || value === undefined) {
initializedRef.current = false;
}
}, [value]);
const handleInput = useCallbackD(() => { const handleInput = useCallbackD(() => {
if (editorRef.current) { if (editorRef.current) {
isEditingRef.current = true; isEditingRef.current = true;
+8 -6
View File
@@ -79,12 +79,13 @@ const Parser = {
// Check for body line (starts with whitespace + "> ") // Check for body line (starts with whitespace + "> ")
const bodyMatch = line.match(/^(\s*)>\s?(.*)$/); const bodyMatch = line.match(/^(\s*)>\s?(.*)$/);
if (bodyMatch) { if (bodyMatch) {
// Body line - attach to current item at matching depth // Body line - attach to the most recent item in the stack
const bodyDepth = bodyMatch[1].length / 2; // Body indent is one level deeper than its parent item (e.g., item at depth 0 has body at depth 1)
const bodyIndent = bodyMatch[1].length / 2;
const bodyLine = bodyMatch[2]; const bodyLine = bodyMatch[2];
// Find the item at this depth // Find the item whose depth is bodyIndent - 1 (body is indented under its item)
for (let j = stack.length - 1; j >= 0; j--) { for (let j = stack.length - 1; j >= 0; j--) {
if (stack[j].depth === bodyDepth && stack[j].item) { if (stack[j].item && stack[j].depth === bodyIndent - 1) {
if (!stack[j].item.body) stack[j].item.body = ''; if (!stack[j].item.body) stack[j].item.body = '';
else stack[j].item.body += '\n'; else stack[j].item.body += '\n';
stack[j].item.body += bodyLine; stack[j].item.body += bodyLine;
@@ -165,10 +166,11 @@ const Parser = {
const metaStr = meta.length ? ` *(${meta.join(', ')})*` : ''; const metaStr = meta.length ? ` *(${meta.join(', ')})*` : '';
const prefix = it.priority ? `[${it.priority}] ` : ''; const prefix = it.priority ? `[${it.priority}] ` : '';
lines.push(`${indent}- [${GLYPH[it.status] || ' '}] ${prefix}${it.title}${metaStr}`); lines.push(`${indent}- [${GLYPH[it.status] || ' '}] ${prefix}${it.title}${metaStr}`);
// Serialize body as indented blockquote lines // Serialize body as indented blockquote lines (one level deeper than item)
if (it.body) { if (it.body) {
const bodyIndent = ' '.repeat(depth + 1);
for (const bodyLine of it.body.split('\n')) { for (const bodyLine of it.body.split('\n')) {
lines.push(`${indent}> ${bodyLine}`); lines.push(`${bodyIndent}> ${bodyLine}`);
} }
} }
if (it.children?.length) { if (it.children?.length) {
+14 -19
View File
@@ -2802,12 +2802,13 @@ const Parser = {
// Check for body line (starts with whitespace + "> ") // Check for body line (starts with whitespace + "> ")
const bodyMatch = line.match(/^(\s*)>\s?(.*)$/); const bodyMatch = line.match(/^(\s*)>\s?(.*)$/);
if (bodyMatch) { if (bodyMatch) {
// Body line - attach to current item at matching depth // Body line - attach to the most recent item in the stack
const bodyDepth = bodyMatch[1].length / 2; // Body indent is one level deeper than its parent item (e.g., item at depth 0 has body at depth 1)
const bodyIndent = bodyMatch[1].length / 2;
const bodyLine = bodyMatch[2]; const bodyLine = bodyMatch[2];
// Find the item at this depth // Find the item whose depth is bodyIndent - 1 (body is indented under its item)
for (let j = stack.length - 1; j >= 0; j--) { for (let j = stack.length - 1; j >= 0; j--) {
if (stack[j].depth === bodyDepth && stack[j].item) { if (stack[j].item && stack[j].depth === bodyIndent - 1) {
if (!stack[j].item.body) stack[j].item.body = '';else stack[j].item.body += '\n'; if (!stack[j].item.body) stack[j].item.body = '';else stack[j].item.body += '\n';
stack[j].item.body += bodyLine; stack[j].item.body += bodyLine;
break; break;
@@ -2904,10 +2905,11 @@ const Parser = {
const metaStr = meta.length ? ` *(${meta.join(', ')})*` : ''; const metaStr = meta.length ? ` *(${meta.join(', ')})*` : '';
const prefix = it.priority ? `[${it.priority}] ` : ''; const prefix = it.priority ? `[${it.priority}] ` : '';
lines.push(`${indent}- [${GLYPH[it.status] || ' '}] ${prefix}${it.title}${metaStr}`); lines.push(`${indent}- [${GLYPH[it.status] || ' '}] ${prefix}${it.title}${metaStr}`);
// Serialize body as indented blockquote lines // Serialize body as indented blockquote lines (one level deeper than item)
if (it.body) { if (it.body) {
const bodyIndent = ' '.repeat(depth + 1);
for (const bodyLine of it.body.split('\n')) { for (const bodyLine of it.body.split('\n')) {
lines.push(`${indent}> ${bodyLine}`); lines.push(`${bodyIndent}> ${bodyLine}`);
} }
} }
if (it.children?.length) { if (it.children?.length) {
@@ -4647,26 +4649,19 @@ function RichTextEditor({
}); });
const toolbarRef = useRefD(null); const toolbarRef = useRefD(null);
const isEditingRef = useRefD(false); const isEditingRef = useRefD(false);
const initializedRef = useRefD(false); const prevValueRef = useRefD(value);
// Initialize editor content only when value changes externally (not from our own edits) // Initialize editor content only when value changes externally (not from our own edits)
useEffectD(() => { useEffectD(() => {
if (editorRef.current && value !== undefined && !isEditingRef.current) { if (editorRef.current && !isEditingRef.current) {
const html = MarkdownUtils.toHtml(value || ''); // Update if value changed from previous or editor is empty
// Only set innerHTML on first load or when value is reset externally if (prevValueRef.current !== value || editorRef.current.innerHTML === '') {
if (!initializedRef.current || editorRef.current.innerHTML === '') { const html = MarkdownUtils.toHtml(value || '');
editorRef.current.innerHTML = html; editorRef.current.innerHTML = html;
initializedRef.current = true; prevValueRef.current = value;
} }
} }
}, [value]); }, [value]);
// Reset initialized flag when dialog closes/opens (value becomes null/undefined)
useEffectD(() => {
if (value === null || value === undefined) {
initializedRef.current = false;
}
}, [value]);
const handleInput = useCallbackD(() => { const handleInput = useCallbackD(() => {
if (editorRef.current) { if (editorRef.current) {
isEditingRef.current = true; isEditingRef.current = true;