From 625a6a642820019e8fd3448b2529df88f5146738 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 */} +