Refactored dev-docs folder after multiple additions to organise correctly

This commit is contained in:
Richard Osborne
2026-01-07 20:28:40 +01:00
parent beff9f0886
commit 4a1080d547
125 changed files with 18456 additions and 957 deletions

View File

@@ -9,6 +9,7 @@ import { ipcRenderer, shell } from 'electron';
import React, { useCallback, useEffect, useState } from 'react';
import { filesystem } from '@noodl/platform';
import { CreateProjectModal } from '@noodl-core-ui/preview/launcher/Launcher/components/CreateProjectModal';
import {
CloudSyncType,
LauncherProjectData
@@ -19,6 +20,7 @@ import { GitHubUser } from '@noodl-core-ui/preview/launcher/Launcher/LauncherCon
import { useEventListener } from '../../hooks/useEventListener';
import { IRouteProps } from '../../pages/AppRoute';
import { GitHubOAuthService } from '../../services/GitHubOAuthService';
import { ProjectOrganizationService } from '../../services/ProjectOrganizationService';
import { LocalProjectsModel, ProjectItem } from '../../utils/LocalProjectsModel';
import { ToastLayer } from '../../views/ToastLayer/ToastLayer';
@@ -52,6 +54,9 @@ export function ProjectsPage(props: ProjectsPageProps) {
const [githubIsAuthenticated, setGithubIsAuthenticated] = useState<boolean>(false);
const [githubIsConnecting, setGithubIsConnecting] = useState<boolean>(false);
// Create project modal state
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
// Initialize and fetch projects on mount
useEffect(() => {
// Switch main window size to editor size
@@ -147,40 +152,55 @@ export function ProjectsPage(props: ProjectsPageProps) {
ToastLayer.showSuccess('Disconnected from GitHub');
});
const handleCreateProject = useCallback(async () => {
const handleCreateProject = useCallback(() => {
setIsCreateModalVisible(true);
}, []);
const handleChooseLocation = useCallback(async (): Promise<string | null> => {
try {
const direntry = await filesystem.openDialog({
allowCreateDirectory: true
});
if (!direntry) return;
// For now, use a simple prompt for project name
// TODO: Replace with a proper React dialog in future
const name = prompt('Project name:');
if (!name) return;
const path = filesystem.makeUniquePath(filesystem.join(direntry, name));
const activityId = 'creating-project';
ToastLayer.showActivity('Creating new project', activityId);
LocalProjectsModel.instance.newProject(
(project) => {
ToastLayer.hideActivity(activityId);
if (!project) {
ToastLayer.showError('Could not create project');
return;
}
// Navigate to editor with the newly created project
props.route.router.route({ to: 'editor', project });
},
{ name, path, projectTemplate: '' }
);
return direntry || null;
} catch (error) {
console.error('Failed to create project:', error);
ToastLayer.showError('Failed to create project');
console.error('Failed to choose location:', error);
return null;
}
}, [props.route]);
}, []);
const handleCreateProjectConfirm = useCallback(
async (name: string, location: string) => {
setIsCreateModalVisible(false);
try {
const path = filesystem.makeUniquePath(filesystem.join(location, name));
const activityId = 'creating-project';
ToastLayer.showActivity('Creating new project', activityId);
LocalProjectsModel.instance.newProject(
(project) => {
ToastLayer.hideActivity(activityId);
if (!project) {
ToastLayer.showError('Could not create project');
return;
}
// Navigate to editor with the newly created project
props.route.router.route({ to: 'editor', project });
},
{ name, path, projectTemplate: '' }
);
} catch (error) {
console.error('Failed to create project:', error);
ToastLayer.showError('Failed to create project');
}
},
[props.route]
);
const handleCreateModalClose = useCallback(() => {
setIsCreateModalVisible(false);
}, []);
const handleOpenProject = useCallback(async () => {
console.log('🔵 [handleOpenProject] Starting...');
@@ -328,18 +348,28 @@ export function ProjectsPage(props: ProjectsPageProps) {
}, []);
return (
<Launcher
projects={realProjects}
onCreateProject={handleCreateProject}
onOpenProject={handleOpenProject}
onLaunchProject={handleLaunchProject}
onOpenProjectFolder={handleOpenProjectFolder}
onDeleteProject={handleDeleteProject}
githubUser={githubUser}
githubIsAuthenticated={githubIsAuthenticated}
githubIsConnecting={githubIsConnecting}
onGitHubConnect={handleGitHubConnect}
onGitHubDisconnect={handleGitHubDisconnect}
/>
<>
<Launcher
projects={realProjects}
onCreateProject={handleCreateProject}
onOpenProject={handleOpenProject}
onLaunchProject={handleLaunchProject}
onOpenProjectFolder={handleOpenProjectFolder}
onDeleteProject={handleDeleteProject}
projectOrganizationService={ProjectOrganizationService.instance}
githubUser={githubUser}
githubIsAuthenticated={githubIsAuthenticated}
githubIsConnecting={githubIsConnecting}
onGitHubConnect={handleGitHubConnect}
onGitHubDisconnect={handleGitHubDisconnect}
/>
<CreateProjectModal
isVisible={isCreateModalVisible}
onClose={handleCreateModalClose}
onConfirm={handleCreateProjectConfirm}
onChooseLocation={handleChooseLocation}
/>
</>
);
}

View File

@@ -5,6 +5,8 @@
* Data is stored client-side in electron-store and keyed by project path.
*/
import Store from 'electron-store';
import { EventDispatcher } from '../../../shared/utils/EventDispatcher';
// ============================================================================
@@ -59,11 +61,23 @@ export const TAG_COLORS = [
export class ProjectOrganizationService extends EventDispatcher {
private static _instance: ProjectOrganizationService;
private store: Store<ProjectOrganizationData>;
private data: ProjectOrganizationData;
private storageKey = 'projectOrganization';
private constructor() {
super();
// Initialize electron-store
this.store = new Store<ProjectOrganizationData>({
name: 'project_organization',
defaults: {
version: 1,
folders: [],
tags: [],
projectMeta: {}
}
});
this.data = this.loadData();
}
@@ -80,26 +94,21 @@ export class ProjectOrganizationService extends EventDispatcher {
private loadData(): ProjectOrganizationData {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
return JSON.parse(stored);
}
return this.store.store; // Get all data from store
} catch (error) {
console.error('[ProjectOrganizationService] Failed to load data:', error);
return {
version: 1,
folders: [],
tags: [],
projectMeta: {}
};
}
// Return default empty structure
return {
version: 1,
folders: [],
tags: [],
projectMeta: {}
};
}
private saveData(): void {
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
this.store.store = this.data; // Save all data to store
this.notifyListeners('dataChanged', this.data);
} catch (error) {
console.error('[ProjectOrganizationService] Failed to save data:', error);

View File

@@ -2,6 +2,7 @@ import path from 'node:path';
import { GitStore } from '@noodl-store/GitStore';
import Store from 'electron-store';
import { isEqual } from 'underscore';
import { getTopLevelWorkingDirectory } from '@noodl/git/src/core/open';
import { setRequestGitAccount } from '@noodl/git/src/core/trampoline/trampoline-askpass-handler';
import { filesystem, platform } from '@noodl/platform';
@@ -9,13 +10,12 @@ import { ProjectModel } from '@noodl-models/projectmodel';
import { templateRegistry } from '@noodl-utils/forge';
import Model from '../../../shared/model';
import { projectFromDirectory, unzipIntoDirectory } from '../models/projectmodel.editor';
import { RuntimeVersionInfo } from '../models/migration/types';
import { detectRuntimeVersion } from '../models/migration/ProjectScanner';
import { RuntimeVersionInfo } from '../models/migration/types';
import { projectFromDirectory, unzipIntoDirectory } from '../models/projectmodel.editor';
import FileSystem from './filesystem';
import { tracker } from './tracker';
import { guid } from './utils';
import { getTopLevelWorkingDirectory } from '@noodl/git/src/core/open';
export interface ProjectItem {
id: string;
@@ -243,10 +243,6 @@ export class LocalProjectsModel extends Model {
projectFromDirectory(dirEntry, (project) => {
if (!project) {
fn();
// callback({
// result: 'failure',
// message: 'Failed to load project'
// });
return;
}
@@ -258,21 +254,42 @@ export class LocalProjectsModel extends Model {
project.toDirectory(project._retainedProjectDirectory, (res) => {
if (res.result === 'success') {
fn(project);
// callback({
// result: 'success',
// project: project
// });
} else {
fn();
// callback({
// result: 'failure',
// message: 'Failed to clone project'
// });
}
});
});
} else {
this._unzipAndLaunchProject('./external/projecttemplates/helloworld.zip', dirEntry, fn, options);
// Default template path
const defaultTemplatePath = './external/projecttemplates/helloworld.zip';
// Check if template exists, otherwise create an empty project
if (filesystem.exists(defaultTemplatePath)) {
this._unzipAndLaunchProject(defaultTemplatePath, dirEntry, fn, options);
} else {
console.warn('Default project template not found, creating empty project');
// Create minimal project.json for empty project
const minimalProject = {
name: name,
components: [],
settings: {}
};
await filesystem.writeFile(filesystem.join(dirEntry, 'project.json'), JSON.stringify(minimalProject, null, 2));
// Load the newly created empty project
projectFromDirectory(dirEntry, (project) => {
if (!project) {
fn();
return;
}
project.name = name;
this._addProject(project);
fn(project);
});
}
}
}
@@ -300,8 +317,8 @@ export class LocalProjectsModel extends Model {
/**
* Check if this project is in a git repository.
*
* @param project
* @returns
* @param project
* @returns
*/
async isGitProject(project: ProjectModel): Promise<boolean> {
const gitPath = await getTopLevelWorkingDirectory(project._retainedProjectDirectory);
@@ -452,7 +469,7 @@ export class LocalProjectsModel extends Model {
*/
async detectAllProjectRuntimes(): Promise<void> {
const projects = this.getProjects();
// Detect in parallel but don't wait for all to complete
// Instead, trigger detection and let events update the UI
for (const project of projects) {