# 04 - Post-Migration Editor Experience ## Overview After migration, the editor needs to clearly communicate which components were successfully migrated, which need review, and provide easy access to migration notes and AI suggestions. ## Component Panel Indicators ### Visual Status Badges ```tsx // packages/noodl-editor/src/editor/src/views/panels/ComponentsPanel/ComponentItem.tsx interface ComponentItemProps { component: ComponentModel; migrationNote?: ComponentMigrationNote; onClick: () => void; onContextMenu: (e: React.MouseEvent) => void; } function ComponentItem({ component, migrationNote, onClick, onContextMenu }: ComponentItemProps) { const status = migrationNote?.status; const statusConfig = { 'auto': null, // No badge for auto-migrated 'ai-migrated': { icon: , tooltip: 'AI migrated - click to see changes', className: 'status-ai' }, 'needs-review': { icon: , tooltip: 'Needs manual review', className: 'status-warning' }, 'manually-fixed': { icon: , tooltip: 'Manually fixed', className: 'status-success' } }; const badge = status ? statusConfig[status] : null; return (
{component.localName} {badge && ( {badge.icon} )}
); } ``` ### CSS for Status Indicators ```scss // packages/noodl-editor/src/editor/src/styles/components-panel.scss .component-item { display: flex; align-items: center; padding: 6px 12px; cursor: pointer; border-radius: 4px; gap: 8px; &:hover { background: var(--color-bg-hover); } &.status-warning { .component-item__badge { color: var(--color-warning); } &::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--color-warning); border-radius: 2px 0 0 2px; } } &.status-ai { .component-item__badge { color: var(--color-info); } } &.status-success { .component-item__badge { color: var(--color-success); } } } .component-item__badge { display: flex; align-items: center; justify-content: center; margin-left: auto; svg { width: 12px; height: 12px; } } ``` ## Migration Notes Panel ### Accessing Migration Notes When a user clicks on a component with a migration status, show a panel with details: ```tsx // packages/noodl-editor/src/editor/src/views/panels/MigrationNotesPanel.tsx interface MigrationNotesPanelProps { component: ComponentModel; note: ComponentMigrationNote; onDismiss: () => void; onViewOriginal: () => void; onViewMigrated: () => void; } function MigrationNotesPanel({ component, note, onDismiss, onViewOriginal, onViewMigrated }: MigrationNotesPanelProps) { const statusLabels = { 'auto': 'Automatically Migrated', 'ai-migrated': 'AI Migrated', 'needs-review': 'Needs Manual Review', 'manually-fixed': 'Manually Fixed' }; const statusIcons = { 'auto': , 'ai-migrated': , 'needs-review': , 'manually-fixed': }; return (
{/* Status Header */}
{statusIcons[note.status]} {statusLabels[note.status]}
{/* Component Name */}
{component.name}
{/* Issues List */} {note.issues && note.issues.length > 0 && (

Issues Detected

    {note.issues.map((issue, i) => (
  • {issue.type || 'Issue'} {issue}
  • ))}
)} {/* AI Suggestion */} {note.aiSuggestion && (

Claude's Suggestion

{note.aiSuggestion}
)} {/* Actions */}
{note.status === 'needs-review' && ( <> )}
{/* Help Link */}
); } ``` ## Migration Summary in Project Info ### Project Info Panel Addition ```tsx // packages/noodl-editor/src/editor/src/views/panels/ProjectInfoPanel.tsx function ProjectInfoPanel({ project }: { project: ProjectModel }) { const migrationInfo = project.migratedFrom; const migrationNotes = project.migrationNotes; const notesCounts = migrationNotes ? { total: Object.keys(migrationNotes).length, needsReview: Object.values(migrationNotes) .filter(n => n.status === 'needs-review').length, aiMigrated: Object.values(migrationNotes) .filter(n => n.status === 'ai-migrated').length } : null; return ( {/* Existing project info... */} {migrationInfo && (

Migration Info

Migrated from: React 17
Migration date: {formatDate(migrationInfo.date)}
Original location: {migrationInfo.originalPath}
{migrationInfo.aiAssisted && (
AI assisted: Yes
)}
{notesCounts && notesCounts.needsReview > 0 && (
{notesCounts.needsReview} component{notesCounts.needsReview > 1 ? 's' : ''} need review
)}
)}
); } ``` ## Component Filter for Migration Status ### Filter in Components Panel ```tsx // packages/noodl-editor/src/editor/src/views/panels/ComponentsPanel/ComponentFilter.tsx interface ComponentFilterProps { activeFilter: ComponentFilter; onFilterChange: (filter: ComponentFilter) => void; migrationCounts?: { needsReview: number; aiMigrated: number; }; } type ComponentFilter = 'all' | 'needs-review' | 'ai-migrated' | 'pages' | 'components'; function ComponentFilterBar({ activeFilter, onFilterChange, migrationCounts }: ComponentFilterProps) { const hasMigrationFilters = migrationCounts && (migrationCounts.needsReview > 0 || migrationCounts.aiMigrated > 0); return (
onFilterChange('all')} > All onFilterChange('pages')} > Pages onFilterChange('components')} > Components {hasMigrationFilters && ( <>
{migrationCounts.needsReview > 0 && ( onFilterChange('needs-review')} badge={migrationCounts.needsReview} variant="warning" > Needs Review )} {migrationCounts.aiMigrated > 0 && ( onFilterChange('ai-migrated')} badge={migrationCounts.aiMigrated} variant="info" > AI Migrated )} )}
); } ``` ## Dismissing Migration Warnings ### Dismiss Functionality ```typescript // packages/noodl-editor/src/editor/src/models/migration/MigrationNotes.ts export function dismissMigrationNote( project: ProjectModel, componentId: string ): void { if (!project.migrationNotes?.[componentId]) { return; } // Mark as dismissed with timestamp project.migrationNotes[componentId] = { ...project.migrationNotes[componentId], dismissedAt: new Date().toISOString() }; // Save project project.save(); } export function getMigrationNotesForDisplay( project: ProjectModel, showDismissed: boolean = false ): Record { if (!project.migrationNotes) { return {}; } if (showDismissed) { return project.migrationNotes; } // Filter out dismissed notes return Object.fromEntries( Object.entries(project.migrationNotes) .filter(([_, note]) => !note.dismissedAt) ); } ``` ### Restore Dismissed Warnings ```tsx // packages/noodl-editor/src/editor/src/views/panels/ComponentsPanel/DismissedWarnings.tsx function DismissedWarningsSection({ project }: { project: ProjectModel }) { const [showDismissed, setShowDismissed] = useState(false); const dismissedNotes = Object.entries(project.migrationNotes || {}) .filter(([_, note]) => note.dismissedAt); if (dismissedNotes.length === 0) { return null; } return (
{showDismissed && (
{dismissedNotes.map(([componentId, note]) => (
{getComponentName(project, componentId)}
))}
)}
); } ``` ## Migration Log Viewer ### Full Log Dialog ```tsx // packages/noodl-editor/src/editor/src/views/migration/MigrationLogViewer.tsx interface MigrationLogViewerProps { session: MigrationSession; onClose: () => void; } function MigrationLogViewer({ session, onClose }: MigrationLogViewerProps) { const [filter, setFilter] = useState<'all' | 'success' | 'warning' | 'error'>('all'); const [search, setSearch] = useState(''); const filteredLog = session.progress?.log.filter(entry => { if (filter !== 'all' && entry.level !== filter) { return false; } if (search && !entry.message.toLowerCase().includes(search.toLowerCase())) { return false; } return true; }) || []; const exportLog = () => { const content = session.progress?.log .map(e => `[${e.timestamp}] [${e.level.toUpperCase()}] ${e.component || ''}: ${e.message}`) .join('\n'); downloadFile('migration-log.txt', content); }; return (
{/* Summary Stats */}
e.level === 'success').length || 0} variant="success" /> e.level === 'warning').length || 0} variant="warning" /> e.level === 'error').length || 0} variant="error" /> {session.ai?.enabled && ( )}
{/* Filters */}
setSearch(e.target.value)} />
{/* Log Entries */}
{filteredLog.map((entry, i) => (
{formatTime(entry.timestamp)} {entry.level.toUpperCase()} {entry.component && ( {entry.component} )} {entry.message} {entry.cost && ( ${entry.cost.toFixed(3)} )} {entry.details && (
Details
{entry.details}
)}
))} {filteredLog.length === 0 && (
No log entries match your filters
)}
); } ``` ## Code Diff Viewer ### View Changes in Components ```tsx // packages/noodl-editor/src/editor/src/views/migration/CodeDiffViewer.tsx interface CodeDiffViewerProps { componentName: string; originalCode: string; migratedCode: string; changes: string[]; onClose: () => void; } function CodeDiffViewer({ componentName, originalCode, migratedCode, changes, onClose }: CodeDiffViewerProps) { const [viewMode, setViewMode] = useState<'split' | 'unified'>('split'); return (
{/* Change Summary */}

Changes Made

    {changes.map((change, i) => (
  • {change}
  • ))}
{/* View Mode Toggle */}
{/* Diff Display */}
{viewMode === 'split' ? ( ) : ( )}
); } // Using Monaco Editor for diff view function SplitDiff({ original, modified }: { original: string; modified: string }) { const containerRef = useRef(null); useEffect(() => { if (!containerRef.current) return; const editor = monaco.editor.createDiffEditor(containerRef.current, { renderSideBySide: true, readOnly: true, theme: 'vs-dark' }); editor.setModel({ original: monaco.editor.createModel(original, 'javascript'), modified: monaco.editor.createModel(modified, 'javascript') }); return () => editor.dispose(); }, [original, modified]); return
; } ``` ## Testing Checklist - [ ] Status badges appear on components - [ ] Clicking badge opens migration notes panel - [ ] AI suggestions display with markdown formatting - [ ] Dismiss functionality works - [ ] Dismissed warnings can be restored - [ ] Filter shows only matching components - [ ] Migration info appears in project info - [ ] Log viewer shows all entries - [ ] Log can be filtered and searched - [ ] Log can be exported - [ ] Code diff viewer shows changes - [ ] Diff supports split and unified modes