From 29158d32bade9f4cffe2bd8cf1183af43faf9078 Mon Sep 17 00:00:00 2001 From: reaper Date: Sat, 8 Aug 2026 20:38:11 -0500 Subject: [PATCH] Fix Drupal 403 (missing core) and add opt-in file deletion on project delete Same class of gap as the earlier WordPress fix: `ddev config --project-type=drupal*` only scaffolds DDEV's settings.php bridge, not Drupal core itself, so a freshly created Drupal project 404/403'd with nothing for the webserver to serve. The wizard's Drupal path now chains configure -> start -> composer create-project drupal/recommended-project -> composer require drush/drush -> drush site:install, with site name/admin fields shown only for that project type. Docroot defaults to `web` when Drupal is selected, matching what recommended-project's installer-paths expect. Also: Delete previously always left the project folder on disk with no way to remove it from the app, which surprised a user expecting it gone. Replaced the window.confirm with a proper modal that has an opt-in "also delete project files from disk" checkbox, defaulting to the previous (safe) behavior. Co-Authored-By: Claude Sonnet 5 --- package.json | 2 +- src/main/ipc/create.ts | 53 ++++++ src/main/ipc/projects.ts | 18 +- src/preload/index.ts | 32 +++- .../components/create/CreateProjectModal.tsx | 17 +- .../components/create/types/DrupalSetup.tsx | 170 ++++++++++++++++++ .../projects/DeleteProjectModal.tsx | 70 ++++++++ .../src/components/projects/ProjectDetail.tsx | 25 ++- src/renderer/src/hooks/useCreateProject.ts | 48 +++++ src/renderer/src/hooks/useDdev.ts | 23 ++- 10 files changed, 437 insertions(+), 21 deletions(-) create mode 100644 src/renderer/src/components/create/types/DrupalSetup.tsx create mode 100644 src/renderer/src/components/projects/DeleteProjectModal.tsx diff --git a/package.json b/package.json index 27900c7..1f6eabc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aurora-dockside", - "version": "1.0.1", + "version": "1.0.2", "description": "A desktop GUI for managing DDEV local development environments", "main": "./out/main/index.js", "homepage": "http://localhost:3000/reaper/aurora-dockside", diff --git a/src/main/ipc/create.ts b/src/main/ipc/create.ts index ddf6868..13a07f1 100644 --- a/src/main/ipc/create.ts +++ b/src/main/ipc/create.ts @@ -21,6 +21,9 @@ export function registerCreateIpc(): void { ) => { const args = ['config', `--project-name=${projectName}`, '--auto'] if (projectType) args.push(`--project-type=${projectType}`) + // ddev creates a missing docroot automatically (--create-docroot is a + // deprecated no-op as of DDEV v1.25) — Drupal's docroot ('web') gets + // created here, ahead of the composer install that populates it. if (docroot.trim()) args.push(`--docroot=${docroot.trim()}`) return runStreamed(operationId, args, event.sender, { cwd: directory }) } @@ -71,4 +74,54 @@ export function registerCreateIpc(): void { return runStreamed(operationId, args, event.sender, { cwd: directory }) } ) + + // Same class of gap as WordPress: `ddev config --project-type=drupal*` + // only writes DDEV's settings.php bridge, not Drupal core — that's what + // was producing the 403 (empty docroot, nothing for the webserver to + // serve). `drupal/recommended-project`'s own composer.json places core + // under web/ via installer-paths, so this runs from the project root + // (matching the --docroot=web set by create:configure), not inside web/. + ipcMain.handle('create:downloadDrupal', (event, operationId: string, directory: string) => + runStreamed( + operationId, + ['composer', 'create-project', 'drupal/recommended-project'], + event.sender, + { cwd: directory } + ) + ) + + // drush was dropped from drupal/recommended-project's default + // dependencies in recent Drupal releases, so `drush site:install` has + // nothing to run without adding it explicitly first. + ipcMain.handle('create:requireDrush', (event, operationId: string, directory: string) => + runStreamed(operationId, ['composer', 'require', 'drush/drush'], event.sender, { + cwd: directory + }) + ) + + ipcMain.handle( + 'create:setupDrupal', + ( + event, + operationId: string, + directory: string, + siteName: string, + adminUser: string, + adminPassword: string, + adminEmail: string, + profile: string + ) => { + const args = [ + 'drush', + 'site:install', + profile || 'standard', + `--site-name=${siteName}`, + `--account-name=${adminUser}`, + `--account-pass=${adminPassword}`, + `--account-mail=${adminEmail}`, + '-y' + ] + return runStreamed(operationId, args, event.sender, { cwd: directory }) + } + ) } diff --git a/src/main/ipc/projects.ts b/src/main/ipc/projects.ts index ff53e4c..c24c8c5 100644 --- a/src/main/ipc/projects.ts +++ b/src/main/ipc/projects.ts @@ -1,4 +1,5 @@ import { ipcMain } from 'electron' +import { rm } from 'fs/promises' import { describeProject, listProjects } from '../ddev' import { runStreamed } from '../commandRunner' @@ -19,10 +20,19 @@ export function registerProjectsIpc(): void { runStreamed(operationId, ['restart', name, '-y'], event.sender) ) // Removes DDEV's project registration + containers + database (auto- - // snapshotted first, unless omitted) — does not touch the project's files - // on disk. - ipcMain.handle('projects:delete', (event, operationId: string, name: string) => - runStreamed(operationId, ['delete', name, '--yes'], event.sender) + // snapshotted first, unless omitted). Project files on disk are only + // touched if the caller opts in via deleteFiles — approot comes from the + // renderer's already-fetched project data (not user-typed), and is only + // used once `ddev delete` has actually succeeded, so a failed delete never + // touches the filesystem. + ipcMain.handle( + 'projects:delete', + async (event, operationId: string, name: string, approot: string, deleteFiles: boolean) => { + await runStreamed(operationId, ['delete', name, '--yes'], event.sender) + if (deleteFiles) { + await rm(approot, { recursive: true, force: true }) + } + } ) // Reconfigures a project's PHP version, web server, database, or Xdebug diff --git a/src/preload/index.ts b/src/preload/index.ts index d6b2a73..6f739b6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -25,8 +25,13 @@ const api = { ipcRenderer.invoke('projects:stop', operationId, name), restart: (operationId: string, name: string): Promise => ipcRenderer.invoke('projects:restart', operationId, name), - delete: (operationId: string, name: string): Promise => - ipcRenderer.invoke('projects:delete', operationId, name), + delete: ( + operationId: string, + name: string, + approot: string, + deleteFiles: boolean + ): Promise => + ipcRenderer.invoke('projects:delete', operationId, name, approot, deleteFiles), updateEnvironment: ( operationId: string, name: string, @@ -129,6 +134,29 @@ const api = { adminPassword, adminEmail, multisite + ), + downloadDrupal: (operationId: string, directory: string): Promise => + ipcRenderer.invoke('create:downloadDrupal', operationId, directory), + requireDrush: (operationId: string, directory: string): Promise => + ipcRenderer.invoke('create:requireDrush', operationId, directory), + setupDrupal: ( + operationId: string, + directory: string, + siteName: string, + adminUser: string, + adminPassword: string, + adminEmail: string, + profile: string + ): Promise => + ipcRenderer.invoke( + 'create:setupDrupal', + operationId, + directory, + siteName, + adminUser, + adminPassword, + adminEmail, + profile ) }, zoom: { diff --git a/src/renderer/src/components/create/CreateProjectModal.tsx b/src/renderer/src/components/create/CreateProjectModal.tsx index 1bae70d..d8b14a1 100644 --- a/src/renderer/src/components/create/CreateProjectModal.tsx +++ b/src/renderer/src/components/create/CreateProjectModal.tsx @@ -16,6 +16,7 @@ import { import { useCreateProject } from '../../hooks/useCreateProject' import { useAppStore } from '../../stores/appStore' import { getTypeLabel, PROJECT_TYPES } from './types/registry' +import { DrupalSetup } from './types/DrupalSetup' import { GenericSetup } from './types/GenericSetup' import { WordpressSetup } from './types/WordpressSetup' import type { TypeSetupHandle } from './types/shared' @@ -228,7 +229,15 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React. + + {showAdvanced && ( +
+
+ + +
+
+ )} + + ) +}) diff --git a/src/renderer/src/components/projects/DeleteProjectModal.tsx b/src/renderer/src/components/projects/DeleteProjectModal.tsx new file mode 100644 index 0000000..033d366 --- /dev/null +++ b/src/renderer/src/components/projects/DeleteProjectModal.tsx @@ -0,0 +1,70 @@ +import { useState } from 'react' +import { AlertTriangle } from 'lucide-react' + +export function DeleteProjectModal({ + projectName, + onCancel, + onConfirm +}: { + projectName: string + onCancel: () => void + onConfirm: (deleteFiles: boolean) => void +}): React.JSX.Element { + const [deleteFiles, setDeleteFiles] = useState(false) + + return ( +
+
+
+
+ +
+
+

+ Delete "{projectName}"? +

+

+ DDEV will take a database snapshot first, then remove the project's containers + and DDEV registration. +

+
+
+ + + +
+ + +
+
+
+ ) +} diff --git a/src/renderer/src/components/projects/ProjectDetail.tsx b/src/renderer/src/components/projects/ProjectDetail.tsx index b0a7523..d04771a 100644 --- a/src/renderer/src/components/projects/ProjectDetail.tsx +++ b/src/renderer/src/components/projects/ProjectDetail.tsx @@ -27,6 +27,7 @@ import { import { StatusBadge } from './StatusBadge' import { DatabaseSection } from './DatabaseSection' import { AddonsSection } from './AddonsSection' +import { DeleteProjectModal } from './DeleteProjectModal' import { LogViewer } from '../logs/LogViewer' import { useAppStore } from '../../stores/appStore' @@ -75,6 +76,7 @@ export function ProjectDetail({ name }: { name: string }): React.JSX.Element { const updateEnvironment = useUpdateEnvironment() const selectProject = useAppStore((s) => s.selectProject) const [isLogsOpen, setIsLogsOpen] = useState(false) + const [isDeleteOpen, setIsDeleteOpen] = useState(false) const isBusy = startProject.isPending || @@ -84,16 +86,13 @@ export function ProjectDetail({ name }: { name: string }): React.JSX.Element { const isEnvUpdating = updateEnvironment.isPending || restartProject.isPending - function handleDelete(): void { + function handleDeleteConfirm(deleteFiles: boolean): void { if (!project) return - const confirmed = window.confirm( - `Delete "${project.name}"? DDEV will take a database snapshot first, then remove the ` + - `project's containers and DDEV registration. Your project files on disk are not touched.` + setIsDeleteOpen(false) + deleteProject.mutate( + { name: project.name, approot: project.approot, deleteFiles }, + { onSuccess: () => selectProject(null) } ) - if (!confirmed) return - deleteProject.mutate(project.name, { - onSuccess: () => selectProject(null) - }) } async function applyEnvironmentChange(updates: EnvironmentUpdate): Promise { @@ -210,7 +209,7 @@ export function ProjectDetail({ name }: { name: string }): React.JSX.Element {