Add field-level inline editing for discussion guide items
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 <noreply@anthropic.com>
This commit is contained in:
parent
28c7bbc055
commit
240a1c09f5
1 changed files with 262 additions and 7 deletions
|
|
@ -342,6 +342,19 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = React.memo((
|
|||
const [editingSubsectionId, setEditingSubsectionId] = useState<string | null>(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<EditingItemState | null>(null);
|
||||
const [editingItemContent, setEditingItemContent] = useState<string>('');
|
||||
const [editingItemProbes, setEditingItemProbes] = useState<string[]>([]);
|
||||
const [editingItemTimeLimit, setEditingItemTimeLimit] = useState<number | undefined>(undefined);
|
||||
const [isSavingItem, setIsSavingItem] = useState(false);
|
||||
|
||||
// Drag and drop state
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -351,12 +364,12 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = 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<DiscussionGuideViewerProps> = 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<DiscussionGuideViewerProps> = 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<DiscussionGuideSection>) => {
|
||||
setEditingSection(prevSection => {
|
||||
if (!prevSection) return prevSection;
|
||||
|
|
@ -1040,12 +1147,136 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = 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 (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-start gap-3 p-3 rounded-lg border bg-white border-blue-300 ring-2 ring-blue-200"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<Edit3 className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-3">
|
||||
{/* Badge row with time limit editor */}
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<Badge variant="outline" className="text-xs whitespace-nowrap">
|
||||
{itemType === 'activity' ? (
|
||||
<>
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
{typeof item.type === 'string' ? item.type.replace('_', ' ') : String(item.type || 'unknown')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MessageCircle className="h-3 w-3 mr-1" />
|
||||
{typeof item.type === 'string' ? item.type.replace('_', ' ') : String(item.type || 'unknown')}
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
|
||||
<div className="flex items-center gap-1 text-xs text-slate-500">
|
||||
<Clock className="h-3 w-3" />
|
||||
<Input
|
||||
type="number"
|
||||
value={editingItemTimeLimit || ''}
|
||||
onChange={(e) => setEditingItemTimeLimit(parseInt(e.target.value) || undefined)}
|
||||
className="w-16 h-6 text-xs"
|
||||
placeholder="min"
|
||||
min={1}
|
||||
/>
|
||||
<span>min</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content textarea */}
|
||||
<Textarea
|
||||
value={editingItemContent}
|
||||
onChange={(e) => setEditingItemContent(e.target.value)}
|
||||
placeholder="Enter content..."
|
||||
className="min-h-[60px]"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
cancelEditingItem();
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
saveEditingItem();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Probes for questions */}
|
||||
{itemType === 'question' && (
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-700 mb-1 block">
|
||||
Probe Questions (one per line)
|
||||
</label>
|
||||
<Textarea
|
||||
value={editingItemProbes.join('\n')}
|
||||
onChange={(e) => {
|
||||
const probes = e.target.value.trim()
|
||||
? e.target.value.split('\n').filter(p => p.trim())
|
||||
: [];
|
||||
setEditingItemProbes(probes);
|
||||
}}
|
||||
placeholder="Enter probe questions, one per line..."
|
||||
className="min-h-[40px] text-sm"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
cancelEditingItem();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Save/Cancel buttons */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={saveEditingItem}
|
||||
disabled={isSavingItem}
|
||||
>
|
||||
{isSavingItem ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin mr-1" />
|
||||
) : (
|
||||
<Save className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={cancelEditingItem}
|
||||
disabled={isSavingItem}
|
||||
>
|
||||
<X className="h-3 w-3 mr-1" />
|
||||
Cancel
|
||||
</Button>
|
||||
<span className="text-xs text-slate-400 ml-auto">
|
||||
Esc to cancel, {navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}+Enter to save
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render read-only version
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"flex items-start gap-3 p-3 rounded-lg border transition-colors",
|
||||
"group flex items-start gap-3 p-3 rounded-lg border transition-colors",
|
||||
isCurrent && "bg-blue-50 border-blue-200",
|
||||
isCompleted && "bg-green-50 border-green-200",
|
||||
!isCurrent && !isCompleted && "bg-slate-50 border-slate-200",
|
||||
|
|
@ -1102,8 +1333,32 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = React.memo((
|
|||
Set Position
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onSave && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startEditingItem(
|
||||
structuredGuide!.sections[sectionIndex].id,
|
||||
item.id,
|
||||
itemType,
|
||||
item,
|
||||
subsectionId
|
||||
);
|
||||
}}
|
||||
className={cn(
|
||||
"h-6 px-2 opacity-0 group-hover:opacity-100 transition-opacity",
|
||||
!onSetPosition && "ml-auto"
|
||||
)}
|
||||
>
|
||||
<Edit3 className="h-3 w-3 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<p className="text-sm text-slate-700 whitespace-pre-wrap">{item.content}</p>
|
||||
|
||||
{item.probes && item.probes.length > 0 && (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue