From 240a1c09f5c6f4ea238312455cf4d8e472e7f379 Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 4 Dec 2025 09:43:12 -0600 Subject: [PATCH] Add field-level inline editing for discussion guide items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users can now edit individual questions and prompts directly without entering section edit mode. Edit buttons appear on hover for each item, allowing inline editing of content, time limits, and probe questions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../DiscussionGuideViewer.tsx | 269 +++++++++++++++++- 1 file changed, 262 insertions(+), 7 deletions(-) diff --git a/src/components/focus-group-session/DiscussionGuideViewer.tsx b/src/components/focus-group-session/DiscussionGuideViewer.tsx index 2e4466ce..baf10d22 100644 --- a/src/components/focus-group-session/DiscussionGuideViewer.tsx +++ b/src/components/focus-group-session/DiscussionGuideViewer.tsx @@ -342,6 +342,19 @@ const DiscussionGuideViewer: React.FC = React.memo(( const [editingSubsectionId, setEditingSubsectionId] = useState(null); const [tempSubsectionTitle, setTempSubsectionTitle] = useState(''); + // State for item-level inline editing + interface EditingItemState { + sectionId: string; + subsectionId?: string | null; + itemId: string; + itemType: 'question' | 'activity'; + } + const [editingItem, setEditingItem] = useState(null); + const [editingItemContent, setEditingItemContent] = useState(''); + const [editingItemProbes, setEditingItemProbes] = useState([]); + const [editingItemTimeLimit, setEditingItemTimeLimit] = useState(undefined); + const [isSavingItem, setIsSavingItem] = useState(false); + // Drag and drop state const [activeId, setActiveId] = useState(null); @@ -351,12 +364,12 @@ const DiscussionGuideViewer: React.FC = React.memo(( useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); - // Notify parent about editing state + // Notify parent about editing state (section or item level) useEffect(() => { if (onEditingChange) { - onEditingChange(!!editingSectionId); + onEditingChange(!!editingSectionId || !!editingItem); } - }, [editingSectionId, onEditingChange]); + }, [editingSectionId, editingItem, onEditingChange]); // Sync editing section with main guide when it changes useEffect(() => { @@ -370,6 +383,11 @@ const DiscussionGuideViewer: React.FC = React.memo(( // Functions for inline section editing const startEditingSection = (section: DiscussionGuideSection) => { + // Prevent section editing if item is being edited + if (editingItem) { + toast.info('Please save or cancel item editing first'); + return; + } setEditingSectionId(section.id); setEditingSection({ ...section }); // Ensure the section is open when editing starts @@ -381,6 +399,95 @@ const DiscussionGuideViewer: React.FC = React.memo(( setEditingSection(null); }; + // Functions for item-level inline editing + const startEditingItem = useCallback(( + sectionId: string, + itemId: string, + itemType: 'question' | 'activity', + item: DiscussionGuideItem, + subsectionId?: string | null + ) => { + // Prevent item editing if section is being edited + if (editingSectionId) { + toast.info('Please save or cancel section editing first'); + return; + } + setEditingItem({ sectionId, subsectionId, itemId, itemType }); + setEditingItemContent(item.content); + setEditingItemProbes(item.probes || []); + setEditingItemTimeLimit(item.time_limit); + }, [editingSectionId]); + + const cancelEditingItem = useCallback(() => { + setEditingItem(null); + setEditingItemContent(''); + setEditingItemProbes([]); + setEditingItemTimeLimit(undefined); + }, []); + + const saveEditingItem = useCallback(async () => { + if (!editingItem || !structuredGuide || !onSave) return; + + setIsSavingItem(true); + try { + const updatedGuide = { + ...structuredGuide, + sections: structuredGuide.sections.map(section => { + if (section.id !== editingItem.sectionId) return section; + + const updateItem = (item: DiscussionGuideItem): DiscussionGuideItem => { + if (item.id !== editingItem.itemId) return item; + return { + ...item, + content: editingItemContent, + probes: editingItem.itemType === 'question' ? editingItemProbes : item.probes, + time_limit: editingItemTimeLimit + }; + }; + + if (editingItem.subsectionId) { + // Update in subsection + return { + ...section, + subsections: section.subsections?.map(sub => { + if (sub.id !== editingItem.subsectionId) return sub; + return { + ...sub, + questions: editingItem.itemType === 'question' + ? sub.questions?.map(updateItem) + : sub.questions, + activities: editingItem.itemType === 'activity' + ? sub.activities?.map(updateItem) + : sub.activities + }; + }) + }; + } + + // Update at section level + return { + ...section, + questions: editingItem.itemType === 'question' + ? section.questions?.map(updateItem) + : section.questions, + activities: editingItem.itemType === 'activity' + ? section.activities?.map(updateItem) + : section.activities + }; + }) + }; + + await onSave(updatedGuide); + cancelEditingItem(); + toast.success('Item updated successfully'); + } catch (error) { + console.error('Error saving item:', error); + toast.error('Failed to save item'); + } finally { + setIsSavingItem(false); + } + }, [editingItem, editingItemContent, editingItemProbes, editingItemTimeLimit, structuredGuide, onSave, cancelEditingItem]); + const updateEditingSection = useCallback((updates: Partial) => { setEditingSection(prevSection => { if (!prevSection) return prevSection; @@ -1040,12 +1147,136 @@ const DiscussionGuideViewer: React.FC = React.memo(( ); } + // Check if this specific item is being edited (item-level editing) + const subsectionId = subsectionIndex !== undefined ? section?.subsections?.[subsectionIndex]?.id : null; + const isItemEditing = editingItem?.itemId === item.id && + editingItem?.sectionId === section?.id && + editingItem?.subsectionId === subsectionId; + + // Render item-level inline edit mode + if (isItemEditing) { + return ( +
+
+ +
+ +
+ {/* Badge row with time limit editor */} +
+ + {itemType === 'activity' ? ( + <> + + {typeof item.type === 'string' ? item.type.replace('_', ' ') : String(item.type || 'unknown')} + + ) : ( + <> + + {typeof item.type === 'string' ? item.type.replace('_', ' ') : String(item.type || 'unknown')} + + )} + + +
+ + setEditingItemTimeLimit(parseInt(e.target.value) || undefined)} + className="w-16 h-6 text-xs" + placeholder="min" + min={1} + /> + min +
+
+ + {/* Content textarea */} +