mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 23:32:55 +01:00
534 lines
15 KiB
Markdown
534 lines
15 KiB
Markdown
# 01 - Project Detection and Visual Indicators
|
|
|
|
## Overview
|
|
|
|
Detect legacy React 17 projects and display clear visual indicators throughout the UI so users understand which projects need migration.
|
|
|
|
## Detection Logic
|
|
|
|
### When to Check
|
|
|
|
1. **On app startup** - Scan recent projects list
|
|
2. **On "Open Project"** - Check selected folder
|
|
3. **On project list refresh** - Re-scan visible projects
|
|
|
|
### How to Detect Runtime Version
|
|
|
|
```typescript
|
|
// packages/noodl-editor/src/editor/src/models/migration/ProjectScanner.ts
|
|
|
|
interface RuntimeVersionInfo {
|
|
version: 'react17' | 'react19' | 'unknown';
|
|
confidence: 'high' | 'medium' | 'low';
|
|
indicators: string[];
|
|
}
|
|
|
|
async function detectRuntimeVersion(projectPath: string): Promise<RuntimeVersionInfo> {
|
|
const indicators: string[] = [];
|
|
|
|
// Check 1: Explicit version in project.json (most reliable)
|
|
const projectJson = await readProjectJson(projectPath);
|
|
if (projectJson.runtimeVersion) {
|
|
return {
|
|
version: projectJson.runtimeVersion,
|
|
confidence: 'high',
|
|
indicators: ['Explicit runtimeVersion field in project.json']
|
|
};
|
|
}
|
|
|
|
// Check 2: Look for migratedFrom field (indicates already migrated)
|
|
if (projectJson.migratedFrom) {
|
|
return {
|
|
version: 'react19',
|
|
confidence: 'high',
|
|
indicators: ['Project has migratedFrom metadata']
|
|
};
|
|
}
|
|
|
|
// Check 3: Check project version number
|
|
// OpenNoodl 1.2+ = React 19, earlier = React 17
|
|
const editorVersion = projectJson.editorVersion || projectJson.version;
|
|
if (editorVersion) {
|
|
const [major, minor] = editorVersion.split('.').map(Number);
|
|
if (major >= 1 && minor >= 2) {
|
|
indicators.push(`Editor version ${editorVersion} >= 1.2`);
|
|
return { version: 'react19', confidence: 'high', indicators };
|
|
} else {
|
|
indicators.push(`Editor version ${editorVersion} < 1.2`);
|
|
return { version: 'react17', confidence: 'high', indicators };
|
|
}
|
|
}
|
|
|
|
// Check 4: Heuristic - scan for React 17 specific patterns in custom code
|
|
const customCodePatterns = await scanForLegacyPatterns(projectPath);
|
|
if (customCodePatterns.found) {
|
|
indicators.push(...customCodePatterns.patterns);
|
|
return { version: 'react17', confidence: 'medium', indicators };
|
|
}
|
|
|
|
// Check 5: If project was created before OpenNoodl fork, assume React 17
|
|
const projectCreated = projectJson.createdAt || await getProjectCreationDate(projectPath);
|
|
if (projectCreated && new Date(projectCreated) < new Date('2024-01-01')) {
|
|
indicators.push('Project created before OpenNoodl fork');
|
|
return { version: 'react17', confidence: 'medium', indicators };
|
|
}
|
|
|
|
// Default: Assume React 19 for truly unknown projects
|
|
return {
|
|
version: 'unknown',
|
|
confidence: 'low',
|
|
indicators: ['No version indicators found']
|
|
};
|
|
}
|
|
```
|
|
|
|
### Legacy Pattern Scanner
|
|
|
|
```typescript
|
|
// Quick scan for legacy React patterns in JavaScript files
|
|
|
|
interface LegacyPatternScan {
|
|
found: boolean;
|
|
patterns: string[];
|
|
files: Array<{ path: string; line: number; pattern: string; }>;
|
|
}
|
|
|
|
async function scanForLegacyPatterns(projectPath: string): Promise<LegacyPatternScan> {
|
|
const jsFiles = await glob(`${projectPath}/**/*.{js,jsx,ts,tsx}`, {
|
|
ignore: ['**/node_modules/**']
|
|
});
|
|
|
|
const legacyPatterns = [
|
|
{ regex: /componentWillMount\s*\(/, name: 'componentWillMount' },
|
|
{ regex: /componentWillReceiveProps\s*\(/, name: 'componentWillReceiveProps' },
|
|
{ regex: /componentWillUpdate\s*\(/, name: 'componentWillUpdate' },
|
|
{ regex: /UNSAFE_componentWillMount/, name: 'UNSAFE_componentWillMount' },
|
|
{ regex: /UNSAFE_componentWillReceiveProps/, name: 'UNSAFE_componentWillReceiveProps' },
|
|
{ regex: /UNSAFE_componentWillUpdate/, name: 'UNSAFE_componentWillUpdate' },
|
|
{ regex: /ref\s*=\s*["'][^"']+["']/, name: 'String ref' },
|
|
{ regex: /contextTypes\s*=/, name: 'Legacy contextTypes' },
|
|
{ regex: /childContextTypes\s*=/, name: 'Legacy childContextTypes' },
|
|
{ regex: /getChildContext\s*\(/, name: 'getChildContext' },
|
|
{ regex: /React\.createFactory/, name: 'createFactory' },
|
|
];
|
|
|
|
const results: LegacyPatternScan = {
|
|
found: false,
|
|
patterns: [],
|
|
files: []
|
|
};
|
|
|
|
for (const file of jsFiles) {
|
|
const content = await fs.readFile(file, 'utf-8');
|
|
const lines = content.split('\n');
|
|
|
|
for (const pattern of legacyPatterns) {
|
|
lines.forEach((line, index) => {
|
|
if (pattern.regex.test(line)) {
|
|
results.found = true;
|
|
if (!results.patterns.includes(pattern.name)) {
|
|
results.patterns.push(pattern.name);
|
|
}
|
|
results.files.push({
|
|
path: file,
|
|
line: index + 1,
|
|
pattern: pattern.name
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
```
|
|
|
|
## Visual Indicators
|
|
|
|
### Projects Panel - Recent Projects List
|
|
|
|
```tsx
|
|
// packages/noodl-editor/src/editor/src/views/ProjectsPanel/ProjectCard.tsx
|
|
|
|
interface ProjectCardProps {
|
|
project: RecentProject;
|
|
runtimeInfo: RuntimeVersionInfo;
|
|
}
|
|
|
|
function ProjectCard({ project, runtimeInfo }: ProjectCardProps) {
|
|
const isLegacy = runtimeInfo.version === 'react17';
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
return (
|
|
<div className={css['project-card', isLegacy && 'project-card--legacy']}>
|
|
<div className={css['project-card__header']}>
|
|
<FolderIcon />
|
|
<div className={css['project-card__info']}>
|
|
<h3 className={css['project-card__name']}>
|
|
{project.name}
|
|
{isLegacy && (
|
|
<Tooltip content="This project uses React 17 and needs migration">
|
|
<WarningIcon className={css['project-card__warning-icon']} />
|
|
</Tooltip>
|
|
)}
|
|
</h3>
|
|
<span className={css['project-card__date']}>
|
|
Last opened: {formatDate(project.lastOpened)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{isLegacy && (
|
|
<div className={css['project-card__legacy-banner']}>
|
|
<div className={css['legacy-banner__content']}>
|
|
<WarningIcon size={16} />
|
|
<span>Legacy Runtime (React 17)</span>
|
|
</div>
|
|
<button
|
|
className={css['legacy-banner__expand']}
|
|
onClick={() => setExpanded(!expanded)}
|
|
>
|
|
{expanded ? 'Less' : 'More'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{isLegacy && expanded && (
|
|
<div className={css['project-card__legacy-details']}>
|
|
<p>
|
|
This project needs migration to work with OpenNoodl 1.2+.
|
|
Your original project will remain untouched.
|
|
</p>
|
|
<div className={css['legacy-details__actions']}>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => openMigrationWizard(project)}
|
|
>
|
|
Migrate Project
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => openProjectReadOnly(project)}
|
|
>
|
|
Open Read-Only
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => openDocs('migration-guide')}
|
|
>
|
|
Learn More
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!isLegacy && (
|
|
<div className={css['project-card__actions']}>
|
|
<Button onClick={() => openProject(project)}>Open</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### CSS Styles
|
|
|
|
```scss
|
|
// packages/noodl-editor/src/editor/src/styles/projects-panel.scss
|
|
|
|
.project-card {
|
|
background: var(--color-bg-secondary);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
border: 1px solid var(--color-border);
|
|
transition: border-color 0.2s;
|
|
|
|
&:hover {
|
|
border-color: var(--color-border-hover);
|
|
}
|
|
|
|
&--legacy {
|
|
border-color: var(--color-warning-border);
|
|
|
|
&:hover {
|
|
border-color: var(--color-warning-border-hover);
|
|
}
|
|
}
|
|
}
|
|
|
|
.project-card__warning-icon {
|
|
color: var(--color-warning);
|
|
margin-left: 8px;
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.project-card__legacy-banner {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: var(--color-warning-bg);
|
|
border-radius: 4px;
|
|
padding: 8px 12px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.legacy-banner__content {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--color-warning-text);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.project-card__legacy-details {
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid var(--color-border);
|
|
|
|
p {
|
|
color: var(--color-text-secondary);
|
|
font-size: 13px;
|
|
margin-bottom: 12px;
|
|
}
|
|
}
|
|
|
|
.legacy-details__actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
```
|
|
|
|
### Open Project Dialog - Legacy Detection
|
|
|
|
```tsx
|
|
// packages/noodl-editor/src/editor/src/views/dialogs/OpenProjectDialog.tsx
|
|
|
|
function OpenProjectDialog({ onClose }: { onClose: () => void }) {
|
|
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
|
const [runtimeInfo, setRuntimeInfo] = useState<RuntimeVersionInfo | null>(null);
|
|
const [checking, setChecking] = useState(false);
|
|
|
|
const handleFolderSelect = async (path: string) => {
|
|
setSelectedPath(path);
|
|
setChecking(true);
|
|
|
|
try {
|
|
const info = await detectRuntimeVersion(path);
|
|
setRuntimeInfo(info);
|
|
} finally {
|
|
setChecking(false);
|
|
}
|
|
};
|
|
|
|
const isLegacy = runtimeInfo?.version === 'react17';
|
|
|
|
return (
|
|
<Dialog title="Open Project" onClose={onClose}>
|
|
<FolderPicker
|
|
value={selectedPath}
|
|
onChange={handleFolderSelect}
|
|
/>
|
|
|
|
{checking && (
|
|
<div className={css['checking-indicator']}>
|
|
<Spinner size={16} />
|
|
<span>Checking project version...</span>
|
|
</div>
|
|
)}
|
|
|
|
{runtimeInfo && isLegacy && (
|
|
<LegacyProjectNotice
|
|
projectPath={selectedPath}
|
|
runtimeInfo={runtimeInfo}
|
|
/>
|
|
)}
|
|
|
|
<DialogActions>
|
|
<Button variant="secondary" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
|
|
{isLegacy ? (
|
|
<>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => openProjectReadOnly(selectedPath)}
|
|
>
|
|
Open Read-Only
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => openMigrationWizard(selectedPath)}
|
|
>
|
|
Migrate & Open
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<Button
|
|
variant="primary"
|
|
disabled={!selectedPath || checking}
|
|
onClick={() => openProject(selectedPath)}
|
|
>
|
|
Open
|
|
</Button>
|
|
)}
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function LegacyProjectNotice({
|
|
projectPath,
|
|
runtimeInfo
|
|
}: {
|
|
projectPath: string;
|
|
runtimeInfo: RuntimeVersionInfo;
|
|
}) {
|
|
const projectName = path.basename(projectPath);
|
|
const defaultTargetPath = `${projectPath}-r19`;
|
|
const [targetPath, setTargetPath] = useState(defaultTargetPath);
|
|
|
|
return (
|
|
<div className={css['legacy-notice']}>
|
|
<div className={css['legacy-notice__header']}>
|
|
<WarningIcon size={20} />
|
|
<h3>Legacy Project Detected</h3>
|
|
</div>
|
|
|
|
<p>
|
|
<strong>"{projectName}"</strong> was created with an older version of
|
|
Noodl using React 17. OpenNoodl 1.2+ uses React 19.
|
|
</p>
|
|
|
|
<p>
|
|
To open this project, we'll create a migrated copy.
|
|
Your original project will remain untouched.
|
|
</p>
|
|
|
|
<div className={css['legacy-notice__paths']}>
|
|
<div className={css['path-row']}>
|
|
<label>Original:</label>
|
|
<code>{projectPath}</code>
|
|
</div>
|
|
<div className={css['path-row']}>
|
|
<label>Copy:</label>
|
|
<input
|
|
type="text"
|
|
value={targetPath}
|
|
onChange={(e) => setTargetPath(e.target.value)}
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="small"
|
|
onClick={() => selectFolder().then(setTargetPath)}
|
|
>
|
|
Change...
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{runtimeInfo.confidence !== 'high' && (
|
|
<div className={css['legacy-notice__confidence']}>
|
|
<InfoIcon size={14} />
|
|
<span>
|
|
Detection confidence: {runtimeInfo.confidence}.
|
|
Indicators: {runtimeInfo.indicators.join(', ')}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Read-Only Mode
|
|
|
|
When opening a legacy project in read-only mode:
|
|
|
|
```typescript
|
|
// packages/noodl-editor/src/editor/src/models/projectmodel.ts
|
|
|
|
interface ProjectOpenOptions {
|
|
readOnly?: boolean;
|
|
legacyMode?: boolean;
|
|
}
|
|
|
|
async function openProject(path: string, options: ProjectOpenOptions = {}) {
|
|
const project = await ProjectModel.fromDirectory(path);
|
|
|
|
if (options.readOnly || options.legacyMode) {
|
|
project.setReadOnly(true);
|
|
|
|
// Show banner in editor
|
|
EditorBanner.show({
|
|
type: 'warning',
|
|
message: 'This project is open in read-only mode. Migrate to make changes.',
|
|
actions: [
|
|
{ label: 'Migrate Now', onClick: () => openMigrationWizard(path) },
|
|
{ label: 'Dismiss', onClick: () => EditorBanner.hide() }
|
|
]
|
|
});
|
|
}
|
|
|
|
return project;
|
|
}
|
|
```
|
|
|
|
### Read-Only Banner Component
|
|
|
|
```tsx
|
|
// packages/noodl-editor/src/editor/src/views/EditorBanner.tsx
|
|
|
|
interface EditorBannerProps {
|
|
type: 'info' | 'warning' | 'error';
|
|
message: string;
|
|
actions?: Array<{
|
|
label: string;
|
|
onClick: () => void;
|
|
}>;
|
|
}
|
|
|
|
function EditorBanner({ type, message, actions }: EditorBannerProps) {
|
|
return (
|
|
<div className={css['editor-banner', `editor-banner--${type}`]}>
|
|
<div className={css['editor-banner__content']}>
|
|
{type === 'warning' && <WarningIcon size={16} />}
|
|
{type === 'info' && <InfoIcon size={16} />}
|
|
{type === 'error' && <ErrorIcon size={16} />}
|
|
<span>{message}</span>
|
|
</div>
|
|
|
|
{actions && (
|
|
<div className={css['editor-banner__actions']}>
|
|
{actions.map((action, i) => (
|
|
<Button
|
|
key={i}
|
|
variant={i === 0 ? 'primary' : 'ghost'}
|
|
size="small"
|
|
onClick={action.onClick}
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Testing Checklist
|
|
|
|
- [ ] Legacy project shows warning icon in recent projects
|
|
- [ ] Clicking legacy project shows expanded details
|
|
- [ ] "Migrate Project" button opens migration wizard
|
|
- [ ] "Open Read-Only" opens project without changes
|
|
- [ ] Opening folder with legacy project shows detection dialog
|
|
- [ ] Target path can be customized
|
|
- [ ] Read-only mode shows banner
|
|
- [ ] Banner "Migrate Now" opens wizard
|
|
- [ ] New/modern projects open normally without warnings
|