# 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 (
);
}
```
## 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 (
);
}
// 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