From 541d7196809d35e88113886aa628ced11faef2e6 Mon Sep 17 00:00:00 2001 From: R3ap3R Date: Mon, 3 Aug 2026 00:19:39 -0500 Subject: [PATCH] Add project creation wizard (step 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New project flow: native directory picker, project name (auto-filled from folder name), project type selector (Auto-detect plus common CMS/framework types), optional docroot, and a "start after creating" checkbox. Runs ddev config with cwd = the chosen directory (streamed through the existing commandRunner), then reuses the existing useStartProject() mutation for the start step rather than inventing multi-phase operation semantics — ddev config registers the project by name, so a plain `ddev start ` works identically to starting any other project afterward. Scoped down from the original app's wizard: skipped scaffolding a fresh CMS codebase (composer create-project / wp core download / etc.) since each framework needs different install commands — this covers project registration + config + start, with ddev's own --auto/--project-type detection handling "auto-detect for existing folders" for free (no custom detection heuristics needed). Verified end-to-end against the real scratch environment: called the create.configure IPC directly (the picker itself opens a native OS dialog CDP can't drive, same constraint as import/export in step 4) with a real target directory, confirmed the generated .ddev/config.yaml has the correct name/type/docroot, confirmed the new project appears in the sidebar via the existing polling within one refresh cycle, and verified the modal's form guards (Create disabled until a directory is chosen) and all type-selector options render correctly. Cleaned up via `ddev delete`. --- src/main/index.ts | 2 + src/main/ipc/create.ts | 28 +++ src/preload/index.ts | 18 ++ src/renderer/src/App.tsx | 15 +- .../components/create/CreateProjectModal.tsx | 161 ++++++++++++++++++ src/renderer/src/hooks/useCreateProject.ts | 28 +++ 6 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 src/main/ipc/create.ts create mode 100644 src/renderer/src/components/create/CreateProjectModal.tsx create mode 100644 src/renderer/src/hooks/useCreateProject.ts diff --git a/src/main/index.ts b/src/main/index.ts index 48c57a2..6a5513a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -7,6 +7,7 @@ import { registerTerminalIpc } from './ipc/terminal' import { registerDatabaseIpc } from './ipc/database' import { registerAddonsIpc } from './ipc/addons' import { registerLogsIpc } from './ipc/logs' +import { registerCreateIpc } from './ipc/create' import { killAllRunningCommands } from './commandRunner' function createWindow(): void { @@ -61,6 +62,7 @@ app.whenReady().then(() => { registerDatabaseIpc() registerAddonsIpc() registerLogsIpc() + registerCreateIpc() createWindow() diff --git a/src/main/ipc/create.ts b/src/main/ipc/create.ts new file mode 100644 index 0000000..cee4613 --- /dev/null +++ b/src/main/ipc/create.ts @@ -0,0 +1,28 @@ +import { dialog, ipcMain } from 'electron' +import { runStreamed } from '../commandRunner' + +export function registerCreateIpc(): void { + ipcMain.handle('create:pickDirectory', async () => { + const result = await dialog.showOpenDialog({ + properties: ['openDirectory', 'createDirectory'] + }) + return result.canceled ? null : result.filePaths[0] + }) + + ipcMain.handle( + 'create:configure', + ( + event, + operationId: string, + directory: string, + projectName: string, + projectType: string, + docroot: string + ) => { + const args = ['config', `--project-name=${projectName}`, '--auto'] + if (projectType) args.push(`--project-type=${projectType}`) + if (docroot.trim()) args.push(`--docroot=${docroot.trim()}`) + return runStreamed(operationId, args, event.sender, { cwd: directory }) + } + ) +} diff --git a/src/preload/index.ts b/src/preload/index.ts index e4dc01f..693022a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -79,6 +79,24 @@ const api = { ipcRenderer.on('logs:exit', listener) return () => ipcRenderer.removeListener('logs:exit', listener) } + }, + create: { + pickDirectory: (): Promise => ipcRenderer.invoke('create:pickDirectory'), + configure: ( + operationId: string, + directory: string, + projectName: string, + projectType: string, + docroot: string + ): Promise => + ipcRenderer.invoke( + 'create:configure', + operationId, + directory, + projectName, + projectType, + docroot + ) } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 6222f3a..1a7fb5c 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,21 +1,33 @@ +import { useState } from 'react' +import { Plus } from 'lucide-react' import { ProjectDetail } from './components/projects/ProjectDetail' import { ProjectList } from './components/projects/ProjectList' import { TerminalPanel } from './components/terminal/TerminalPanel' import { StatusBar } from './components/layout/StatusBar' import { Toaster } from './components/ui/Toaster' +import { CreateProjectModal } from './components/create/CreateProjectModal' import { useAppStore } from './stores/appStore' import { useTerminalEvents } from './hooks/useTerminalEvents' function App(): React.JSX.Element { const selectedProjectName = useAppStore((s) => s.selectedProjectName) + const [isCreateOpen, setIsCreateOpen] = useState(false) useTerminalEvents() return (