feat: Phase 5 BYOB foundation + Phase 3 GitHub integration

Phase 5 - BYOB Backend (TASK-007A/B):
- LocalSQL Adapter with full CloudStore API compatibility
- QueryBuilder translates Parse-style queries to SQL
- SchemaManager with PostgreSQL/Supabase export
- LocalBackendServer with REST endpoints
- BackendManager with IPC handlers for Electron
- In-memory fallback when better-sqlite3 unavailable

Phase 3 - GitHub Panel (GIT-004):
- Issues tab with list/detail views
- Pull Requests tab with list/detail views
- GitHub API client with OAuth support
- Repository info hook integration

Phase 3 - Editor UX Bugfixes (TASK-013):
- Legacy runtime detection banners
- Read-only enforcement for legacy projects
- Code editor modal close improvements
- Property panel stuck state fix
- Blockly node deletion and UI polish

Phase 11 - Cloud Functions Planning:
- Architecture documentation for workflow automation
- Execution history storage schema design
- Canvas overlay concept for debugging

Docs: Updated LEARNINGS.md and COMMON-ISSUES.md
This commit is contained in:
Richard Osborne
2026-01-15 17:37:15 +01:00
parent dd73b1339b
commit ddcb9cd02e
86 changed files with 17408 additions and 1873 deletions

View File

@@ -109,6 +109,7 @@
.InputWrapper {
overflow-x: hidden;
overflow-y: hidden; // Prevent tiny vertical scrollbar on single-line inputs
flex-grow: 1;
padding-top: 1px;
}

View File

@@ -41,6 +41,8 @@ export interface LauncherProps {
onLaunchProject?: (projectId: string) => void;
onOpenProjectFolder?: (projectId: string) => void;
onDeleteProject?: (projectId: string) => void;
onMigrateProject?: (projectId: string) => void;
onOpenReadOnly?: (projectId: string) => void;
// Project organization service (optional - for Storybook compatibility)
projectOrganizationService?: any;
@@ -178,6 +180,8 @@ export function Launcher({
onLaunchProject,
onOpenProjectFolder,
onDeleteProject,
onMigrateProject,
onOpenReadOnly,
projectOrganizationService,
githubUser,
githubIsAuthenticated,
@@ -285,6 +289,8 @@ export function Launcher({
onLaunchProject,
onOpenProjectFolder,
onDeleteProject,
onMigrateProject,
onOpenReadOnly,
githubUser,
githubIsAuthenticated,
githubIsConnecting,

View File

@@ -43,6 +43,8 @@ export interface LauncherContextValue {
onLaunchProject?: (projectId: string) => void;
onOpenProjectFolder?: (projectId: string) => void;
onDeleteProject?: (projectId: string) => void;
onMigrateProject?: (projectId: string) => void;
onOpenReadOnly?: (projectId: string) => void;
// GitHub OAuth integration (optional - for Storybook compatibility)
githubUser?: GitHubUser | null;

View File

@@ -17,3 +17,32 @@
.VersionControlTooltip {
cursor: default;
}
// Legacy project styles
.LegacyCard {
border-color: var(--theme-color-border-danger) !important;
&:hover {
border-color: var(--theme-color-border-danger) !important;
}
}
.LegacyBanner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-2);
margin-top: var(--spacing-3);
padding: var(--spacing-2) var(--spacing-3);
background-color: var(--theme-color-bg-danger-subtle);
border: 1px solid var(--theme-color-border-danger);
border-radius: var(--border-radius-medium);
}
.LegacyDetails {
margin-top: var(--spacing-2);
padding: var(--spacing-3);
background-color: var(--theme-color-bg-2);
border-radius: var(--border-radius-medium);
border: 1px solid var(--theme-color-border-default);
}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { FeedbackType } from '@noodl-constants/FeedbackType';
@@ -23,6 +23,13 @@ import { useProjectOrganization } from '../../hooks/useProjectOrganization';
import { TagPill, TagPillSize } from '../TagPill';
import css from './LauncherProjectCard.module.scss';
// Runtime version detection types
export interface RuntimeVersionInfo {
version: 'react17' | 'react19' | 'unknown';
confidence: 'high' | 'medium' | 'low';
indicators: string[];
}
// FIXME: Use the timeSince function from the editor package when this is moved there
function timeSince(date: Date | number) {
const date_unix = typeof date === 'number' ? date : date.getTime();
@@ -71,11 +78,15 @@ export interface LauncherProjectData {
uncommittedChangesAmount?: number;
imageSrc: string;
contributors?: UserBadgeProps[];
runtimeInfo?: RuntimeVersionInfo;
}
export interface LauncherProjectCardProps extends LauncherProjectData {
contextMenuItems: ContextMenuProps[];
onClick?: () => void;
runtimeInfo?: RuntimeVersionInfo;
onMigrateProject?: () => void;
onOpenReadOnly?: () => void;
}
export function LauncherProjectCard({
@@ -90,25 +101,54 @@ export function LauncherProjectCard({
imageSrc,
contextMenuItems,
contributors,
onClick
onClick,
runtimeInfo,
onMigrateProject,
onOpenReadOnly
}: LauncherProjectCardProps) {
const { tags, getProjectMeta } = useProjectOrganization();
const [showLegacyDetails, setShowLegacyDetails] = useState(false);
// Get project tags
const projectMeta = getProjectMeta(localPath);
const projectTags = projectMeta ? tags.filter((tag) => projectMeta.tagIds.includes(tag.id)) : [];
// Determine if this is a legacy project
const isLegacy = runtimeInfo?.version === 'react17';
const isDetecting = runtimeInfo === undefined;
return (
<Card background={CardBackground.Bg2} hoverBackground={CardBackground.Bg3} onClick={onClick}>
<Card
background={CardBackground.Bg2}
hoverBackground={CardBackground.Bg3}
onClick={
isLegacy
? () => {
// Auto-expand details when user clicks legacy project
setShowLegacyDetails(true);
}
: onClick
}
UNSAFE_className={isLegacy ? css.LegacyCard : undefined}
>
<Stack direction="row">
<div className={css.Image} style={{ backgroundImage: `url(${imageSrc})` }} />
<div className={css.Details}>
<Columns layoutString="1 1 1" hasXGap={4}>
<div>
<Title hasBottomSpacing size={TitleSize.Medium}>
{title}
</Title>
<HStack hasSpacing={2} UNSAFE_style={{ alignItems: 'center' }}>
<Title hasBottomSpacing size={TitleSize.Medium}>
{title}
</Title>
{/* Legacy warning icon */}
{isLegacy && (
<Tooltip content="This project uses React 17 and needs migration">
<Icon icon={IconName.WarningCircle} variant={FeedbackType.Danger} size={IconSize.Default} />
</Tooltip>
)}
</HStack>
{/* Tags */}
{projectTags.length > 0 && (
@@ -219,6 +259,66 @@ export function LauncherProjectCard({
)}
</HStack>
</Columns>
{/* Legacy warning banner */}
{isLegacy && (
<div className={css.LegacyBanner}>
<HStack hasSpacing={2} UNSAFE_style={{ alignItems: 'center', flex: 1 }}>
<Icon icon={IconName.WarningCircle} variant={FeedbackType.Danger} size={IconSize.Small} />
<Text size={TextSize.Small}>React 17 (Legacy Runtime)</Text>
</HStack>
<TextButton
label={showLegacyDetails ? 'Less' : 'Options'}
size={TextButtonSize.Small}
onClick={(e) => {
e.stopPropagation();
setShowLegacyDetails(!showLegacyDetails);
}}
/>
</div>
)}
{/* Expanded legacy details */}
{isLegacy && showLegacyDetails && (
<div className={css.LegacyDetails}>
<Label variant={TextType.Shy} size={LabelSize.Default}>
This project needs migration to work with OpenNoodl 1.2+. Your original project will remain untouched.
</Label>
<HStack hasSpacing={2} UNSAFE_style={{ marginTop: 'var(--spacing-3)' }}>
<PrimaryButton
label="Migrate Project"
size={PrimaryButtonSize.Small}
onClick={(e) => {
e.stopPropagation();
onMigrateProject?.();
}}
/>
<PrimaryButton
label="View Read-Only"
size={PrimaryButtonSize.Small}
variant={PrimaryButtonVariant.Muted}
onClick={(e) => {
e.stopPropagation();
onOpenReadOnly?.();
}}
/>
<TextButton
label="Learn More"
size={TextButtonSize.Small}
icon={IconName.ExternalLink}
onClick={(e) => {
e.stopPropagation();
// TODO: Open documentation
window.open('https://docs.opennoodl.com/migration', '_blank');
}}
/>
</HStack>
</div>
)}
</div>
</Stack>
</Card>

View File

@@ -36,7 +36,9 @@ export function Projects({}: ProjectsViewProps) {
onOpenProject,
onLaunchProject,
onOpenProjectFolder,
onDeleteProject
onDeleteProject,
onMigrateProject,
onOpenReadOnly
} = useLauncherContext();
const { getProjectMeta, getProjectsInFolder, folders, moveProjectToFolder } = useProjectOrganization();
@@ -189,6 +191,8 @@ export function Projects({}: ProjectsViewProps) {
key={project.id}
{...project}
onClick={() => onLaunchProject?.(project.id)}
onMigrateProject={() => onMigrateProject?.(project.id)}
onOpenReadOnly={() => onOpenReadOnly?.(project.id)}
contextMenuItems={[
{
label: 'Launch project',