Fix WordPress scaffolding gap; add delete project + WP admin link
Found via real usage: creating a WordPress project only produced
wp-content/ + the wp-config.php bridge — ddev config never downloads
WordPress core, and even with core present the site has no database
until `wp core install` runs (confirmed live: 403/302 until fixed).
The wizard's WordPress path now chains configure -> start ->
`wp core download` -> `wp core install` as separate tracked operations
(each needs its own terminal/status lifecycle, same reasoning as the
configure->start split from the original wizard work), with new site
title/admin username/password/email fields shown only for the
WordPress project type. wp-cli requires the containers running, so
"start after creating" is bypassed (always start) for this path rather
than leaving a checkbox that could produce the exact half-built state
this fixes.
Also added, per direct request:
- Delete project: `ddev delete <name> --yes` (keeps ddev's default
database snapshot as a safety net), confirmed via window.confirm
with copy clarifying it only removes DDEV's registration/containers/
database, not the project's files on disk. Clears the app's selection
on success.
- WP Admin quick-link: for running wordpress-type projects, a header
button opening {primary_url}/wp-admin/ directly.
Verified end-to-end against a fresh throwaway project via CDP: full
configure->start->download->install chain produced a real working
site (200 on the homepage, proper login redirect on wp-admin, WP Admin
button href matches), and delete genuinely removes the project from
`ddev list` with the confirmation copy rendering correctly. Two
apparent bugs surfaced during this testing turned out to be the test
script reading DOM state before React's re-render or before ddev's
multi-step delete (build+start+snapshot+teardown) had actually
finished — not real defects; re-verified with proper waits.
This commit is contained in:
@@ -25,4 +25,46 @@ export function registerCreateIpc(): void {
|
|||||||
return runStreamed(operationId, args, event.sender, { cwd: directory })
|
return runStreamed(operationId, args, event.sender, { cwd: directory })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// `ddev config --project-type=wordpress` only scaffolds the DDEV-managed
|
||||||
|
// wp-config.php bridge and wp-content/uploads — it doesn't download
|
||||||
|
// WordPress core (wp-admin, wp-includes, index.php, etc.), and even with
|
||||||
|
// core downloaded the site has no database tables until `wp core install`
|
||||||
|
// runs. Both require the project to be started, since wp-cli runs inside
|
||||||
|
// the web container. Kept as two separate tracked operations rather than
|
||||||
|
// one combined command so each phase's terminal:exit event correctly owns
|
||||||
|
// its own status-bar/toast lifecycle (see useCreateProject.ts).
|
||||||
|
ipcMain.handle('create:downloadWordpress', (event, operationId: string, directory: string) =>
|
||||||
|
runStreamed(operationId, ['wp', 'core', 'download'], event.sender, { cwd: directory })
|
||||||
|
)
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
'create:setupWordpress',
|
||||||
|
(
|
||||||
|
event,
|
||||||
|
operationId: string,
|
||||||
|
directory: string,
|
||||||
|
siteUrl: string,
|
||||||
|
title: string,
|
||||||
|
adminUser: string,
|
||||||
|
adminPassword: string,
|
||||||
|
adminEmail: string
|
||||||
|
) =>
|
||||||
|
runStreamed(
|
||||||
|
operationId,
|
||||||
|
[
|
||||||
|
'wp',
|
||||||
|
'core',
|
||||||
|
'install',
|
||||||
|
`--url=${siteUrl}`,
|
||||||
|
`--title=${title}`,
|
||||||
|
`--admin_user=${adminUser}`,
|
||||||
|
`--admin_password=${adminPassword}`,
|
||||||
|
`--admin_email=${adminEmail}`,
|
||||||
|
'--skip-email'
|
||||||
|
],
|
||||||
|
event.sender,
|
||||||
|
{ cwd: directory }
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,10 @@ export function registerProjectsIpc(): void {
|
|||||||
ipcMain.handle('projects:restart', (event, operationId: string, name: string) =>
|
ipcMain.handle('projects:restart', (event, operationId: string, name: string) =>
|
||||||
runStreamed(operationId, ['restart', name], event.sender)
|
runStreamed(operationId, ['restart', name], 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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -23,7 +23,9 @@ const api = {
|
|||||||
stop: (operationId: string, name: string): Promise<void> =>
|
stop: (operationId: string, name: string): Promise<void> =>
|
||||||
ipcRenderer.invoke('projects:stop', operationId, name),
|
ipcRenderer.invoke('projects:stop', operationId, name),
|
||||||
restart: (operationId: string, name: string): Promise<void> =>
|
restart: (operationId: string, name: string): Promise<void> =>
|
||||||
ipcRenderer.invoke('projects:restart', operationId, name)
|
ipcRenderer.invoke('projects:restart', operationId, name),
|
||||||
|
delete: (operationId: string, name: string): Promise<void> =>
|
||||||
|
ipcRenderer.invoke('projects:delete', operationId, name)
|
||||||
},
|
},
|
||||||
terminal: {
|
terminal: {
|
||||||
cancel: (operationId: string): Promise<boolean> =>
|
cancel: (operationId: string): Promise<boolean> =>
|
||||||
@@ -96,6 +98,27 @@ const api = {
|
|||||||
projectName,
|
projectName,
|
||||||
projectType,
|
projectType,
|
||||||
docroot
|
docroot
|
||||||
|
),
|
||||||
|
downloadWordpress: (operationId: string, directory: string): Promise<void> =>
|
||||||
|
ipcRenderer.invoke('create:downloadWordpress', operationId, directory),
|
||||||
|
setupWordpress: (
|
||||||
|
operationId: string,
|
||||||
|
directory: string,
|
||||||
|
siteUrl: string,
|
||||||
|
title: string,
|
||||||
|
adminUser: string,
|
||||||
|
adminPassword: string,
|
||||||
|
adminEmail: string
|
||||||
|
): Promise<void> =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
'create:setupWordpress',
|
||||||
|
operationId,
|
||||||
|
directory,
|
||||||
|
siteUrl,
|
||||||
|
title,
|
||||||
|
adminUser,
|
||||||
|
adminPassword,
|
||||||
|
adminEmail
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { FolderOpen, X } from 'lucide-react'
|
import { FolderOpen, X } from 'lucide-react'
|
||||||
import { useCreateProject } from '../../hooks/useCreateProject'
|
import {
|
||||||
|
useCreateProject,
|
||||||
|
useDownloadWordpress,
|
||||||
|
useSetupWordpress
|
||||||
|
} from '../../hooks/useCreateProject'
|
||||||
import { useStartProject } from '../../hooks/useDdev'
|
import { useStartProject } from '../../hooks/useDdev'
|
||||||
import { useAppStore } from '../../stores/appStore'
|
import { useAppStore } from '../../stores/appStore'
|
||||||
|
|
||||||
@@ -25,34 +29,65 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
|
|||||||
const [docroot, setDocroot] = useState('')
|
const [docroot, setDocroot] = useState('')
|
||||||
const [startAfterCreate, setStartAfterCreate] = useState(true)
|
const [startAfterCreate, setStartAfterCreate] = useState(true)
|
||||||
|
|
||||||
|
const [siteTitle, setSiteTitle] = useState('')
|
||||||
|
const [adminUser, setAdminUser] = useState('admin')
|
||||||
|
const [adminPassword, setAdminPassword] = useState('')
|
||||||
|
const [adminEmail, setAdminEmail] = useState('')
|
||||||
|
|
||||||
const createProject = useCreateProject()
|
const createProject = useCreateProject()
|
||||||
const startProject = useStartProject()
|
const startProject = useStartProject()
|
||||||
|
const downloadWordpress = useDownloadWordpress()
|
||||||
|
const setupWordpress = useSetupWordpress()
|
||||||
const selectProject = useAppStore((s) => s.selectProject)
|
const selectProject = useAppStore((s) => s.selectProject)
|
||||||
|
|
||||||
const isSubmitting = createProject.isPending || startProject.isPending
|
const isWordpress = projectType === 'wordpress'
|
||||||
const canSubmit = directory !== null && projectName.trim().length > 0 && !isSubmitting
|
const isSubmitting =
|
||||||
|
createProject.isPending ||
|
||||||
|
startProject.isPending ||
|
||||||
|
downloadWordpress.isPending ||
|
||||||
|
setupWordpress.isPending
|
||||||
|
const canSubmit =
|
||||||
|
directory !== null &&
|
||||||
|
projectName.trim().length > 0 &&
|
||||||
|
!isSubmitting &&
|
||||||
|
(!isWordpress || (adminUser.trim() && adminPassword.trim() && adminEmail.trim()))
|
||||||
|
|
||||||
async function handlePickDirectory(): Promise<void> {
|
async function handlePickDirectory(): Promise<void> {
|
||||||
const picked = await window.api.create.pickDirectory()
|
const picked = await window.api.create.pickDirectory()
|
||||||
if (!picked) return
|
if (!picked) return
|
||||||
setDirectory(picked)
|
setDirectory(picked)
|
||||||
if (!projectName) {
|
if (!projectName) {
|
||||||
setProjectName(picked.split('/').filter(Boolean).pop() ?? '')
|
const name = picked.split('/').filter(Boolean).pop() ?? ''
|
||||||
|
setProjectName(name)
|
||||||
|
if (!siteTitle) setSiteTitle(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(): Promise<void> {
|
async function handleSubmit(): Promise<void> {
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
await createProject.mutateAsync({
|
const name = projectName.trim()
|
||||||
directory,
|
|
||||||
projectName: projectName.trim(),
|
await createProject.mutateAsync({ directory, projectName: name, projectType, docroot })
|
||||||
projectType,
|
|
||||||
docroot
|
if (isWordpress) {
|
||||||
})
|
// wp-cli needs the containers running, so this ignores the "start
|
||||||
if (startAfterCreate) {
|
// after creating" checkbox — an unstarted WordPress project would
|
||||||
startProject.mutate(projectName.trim())
|
// just be the same half-built state this whole flow exists to avoid.
|
||||||
|
await startProject.mutateAsync(name)
|
||||||
|
await downloadWordpress.mutateAsync({ directory })
|
||||||
|
await setupWordpress.mutateAsync({
|
||||||
|
directory,
|
||||||
|
siteUrl: `https://${name}.ddev.site`,
|
||||||
|
title: siteTitle.trim() || name,
|
||||||
|
adminUser: adminUser.trim(),
|
||||||
|
adminPassword: adminPassword.trim(),
|
||||||
|
adminEmail: adminEmail.trim()
|
||||||
|
})
|
||||||
|
} else if (startAfterCreate) {
|
||||||
|
startProject.mutate(name)
|
||||||
}
|
}
|
||||||
selectProject(projectName.trim())
|
|
||||||
|
selectProject(name)
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +105,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 p-4">
|
<div className="flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
<label className="mb-1 block text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
||||||
Project folder
|
Project folder
|
||||||
@@ -128,14 +163,69 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
{isWordpress ? (
|
||||||
<input
|
<div className="flex flex-col gap-3 rounded-md border border-neutral-200 p-3 dark:border-neutral-800">
|
||||||
type="checkbox"
|
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
checked={startAfterCreate}
|
WordPress core will be downloaded and installed automatically — the project is
|
||||||
onChange={(e) => setStartAfterCreate(e.target.checked)}
|
started as part of this.
|
||||||
/>
|
</p>
|
||||||
Start project after creating
|
<div>
|
||||||
</label>
|
<label className="mb-1 block text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
||||||
|
Site title
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={siteTitle}
|
||||||
|
onChange={(e) => setSiteTitle(e.target.value)}
|
||||||
|
placeholder={projectName || 'My WordPress Site'}
|
||||||
|
className="w-full rounded-md border border-neutral-300 px-3 py-1.5 text-sm dark:border-neutral-700 dark:bg-neutral-950"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
||||||
|
Admin username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={adminUser}
|
||||||
|
onChange={(e) => setAdminUser(e.target.value)}
|
||||||
|
className="w-full rounded-md border border-neutral-300 px-3 py-1.5 text-sm dark:border-neutral-700 dark:bg-neutral-950"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
||||||
|
Admin password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={adminPassword}
|
||||||
|
onChange={(e) => setAdminPassword(e.target.value)}
|
||||||
|
className="w-full rounded-md border border-neutral-300 px-3 py-1.5 text-sm dark:border-neutral-700 dark:bg-neutral-950"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
||||||
|
Admin email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={adminEmail}
|
||||||
|
onChange={(e) => setAdminEmail(e.target.value)}
|
||||||
|
placeholder="[email protected]"
|
||||||
|
className="w-full rounded-md border border-neutral-300 px-3 py-1.5 text-sm dark:border-neutral-700 dark:bg-neutral-950"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={startAfterCreate}
|
||||||
|
onChange={(e) => setStartAfterCreate(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Start project after creating
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 border-t border-neutral-200 p-3 dark:border-neutral-800">
|
<div className="flex justify-end gap-2 border-t border-neutral-200 p-3 dark:border-neutral-800">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { FileText, Play, RotateCw, Square } from 'lucide-react'
|
import { FileText, KeyRound, Play, RotateCw, Square, Trash2 } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
|
useDeleteProject,
|
||||||
useProjectDetail,
|
useProjectDetail,
|
||||||
useRestartProject,
|
useRestartProject,
|
||||||
useStartProject,
|
useStartProject,
|
||||||
@@ -10,15 +11,34 @@ import { StatusBadge } from './StatusBadge'
|
|||||||
import { DatabaseSection } from './DatabaseSection'
|
import { DatabaseSection } from './DatabaseSection'
|
||||||
import { AddonsSection } from './AddonsSection'
|
import { AddonsSection } from './AddonsSection'
|
||||||
import { LogViewer } from '../logs/LogViewer'
|
import { LogViewer } from '../logs/LogViewer'
|
||||||
|
import { useAppStore } from '../../stores/appStore'
|
||||||
|
|
||||||
export function ProjectDetail({ name }: { name: string }): React.JSX.Element {
|
export function ProjectDetail({ name }: { name: string }): React.JSX.Element {
|
||||||
const { data: project, isLoading, isError, error } = useProjectDetail(name)
|
const { data: project, isLoading, isError, error } = useProjectDetail(name)
|
||||||
const startProject = useStartProject()
|
const startProject = useStartProject()
|
||||||
const stopProject = useStopProject()
|
const stopProject = useStopProject()
|
||||||
const restartProject = useRestartProject()
|
const restartProject = useRestartProject()
|
||||||
|
const deleteProject = useDeleteProject()
|
||||||
|
const selectProject = useAppStore((s) => s.selectProject)
|
||||||
const [isLogsOpen, setIsLogsOpen] = useState(false)
|
const [isLogsOpen, setIsLogsOpen] = useState(false)
|
||||||
|
|
||||||
const isBusy = startProject.isPending || stopProject.isPending || restartProject.isPending
|
const isBusy =
|
||||||
|
startProject.isPending ||
|
||||||
|
stopProject.isPending ||
|
||||||
|
restartProject.isPending ||
|
||||||
|
deleteProject.isPending
|
||||||
|
|
||||||
|
function handleDelete(): 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.`
|
||||||
|
)
|
||||||
|
if (!confirmed) return
|
||||||
|
deleteProject.mutate(project.name, {
|
||||||
|
onSuccess: () => selectProject(null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="p-6 text-sm text-neutral-500">Loading {name}…</div>
|
return <div className="p-6 text-sm text-neutral-500">Loading {name}…</div>
|
||||||
@@ -77,6 +97,24 @@ export function ProjectDetail({ name }: { name: string }): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
<FileText size={14} /> Logs
|
<FileText size={14} /> Logs
|
||||||
</button>
|
</button>
|
||||||
|
{project.type === 'wordpress' && isRunning && (
|
||||||
|
<a
|
||||||
|
href={`${project.primary_url}/wp-admin/`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-md border border-neutral-300 px-3 py-1.5 text-sm font-medium hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-900"
|
||||||
|
>
|
||||||
|
<KeyRound size={14} /> WP Admin
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-md border border-red-300 px-3 py-1.5 text-sm font-medium text-red-600 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} /> Delete
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,22 @@ export interface CreateProjectInput {
|
|||||||
docroot: string
|
docroot: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WordpressSetupInput {
|
||||||
|
directory: string
|
||||||
|
siteUrl: string
|
||||||
|
title: string
|
||||||
|
adminUser: string
|
||||||
|
adminPassword: string
|
||||||
|
adminEmail: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginOperation(label: string): string {
|
||||||
|
const operationId = crypto.randomUUID()
|
||||||
|
useTerminalStore.getState().startOperation(operationId, label)
|
||||||
|
useStatusStore.getState().begin(operationId, label)
|
||||||
|
return operationId
|
||||||
|
}
|
||||||
|
|
||||||
// Only runs `ddev config`, not `ddev start` — the caller is expected to
|
// Only runs `ddev config`, not `ddev start` — the caller is expected to
|
||||||
// follow a successful creation with the existing useStartProject() mutation,
|
// follow a successful creation with the existing useStartProject() mutation,
|
||||||
// reusing its own tracked operation/toast lifecycle rather than needing this
|
// reusing its own tracked operation/toast lifecycle rather than needing this
|
||||||
@@ -17,12 +33,39 @@ export function useCreateProject(): UseMutationResult<void, Error, CreateProject
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ directory, projectName, projectType, docroot }: CreateProjectInput) => {
|
mutationFn: async ({ directory, projectName, projectType, docroot }: CreateProjectInput) => {
|
||||||
const operationId = crypto.randomUUID()
|
const operationId = beginOperation(`Create project ${projectName}`)
|
||||||
const label = `Create project ${projectName}`
|
|
||||||
useTerminalStore.getState().startOperation(operationId, label)
|
|
||||||
useStatusStore.getState().begin(operationId, label)
|
|
||||||
await window.api.create.configure(operationId, directory, projectName, projectType, docroot)
|
await window.api.create.configure(operationId, directory, projectName, projectType, docroot)
|
||||||
},
|
},
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['projects'] })
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['projects'] })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `ddev config --project-type=wordpress` only scaffolds the wp-config.php
|
||||||
|
// bridge, not WordPress core itself — see create.ts in the main process for
|
||||||
|
// why this and useSetupWordpress are separate tracked operations run after
|
||||||
|
// the project has been started.
|
||||||
|
export function useDownloadWordpress(): UseMutationResult<void, Error, { directory: string }> {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ directory }) => {
|
||||||
|
const operationId = beginOperation('Download WordPress core')
|
||||||
|
await window.api.create.downloadWordpress(operationId, directory)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSetupWordpress(): UseMutationResult<void, Error, WordpressSetupInput> {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ directory, siteUrl, title, adminUser, adminPassword, adminEmail }) => {
|
||||||
|
const operationId = beginOperation('Install WordPress')
|
||||||
|
await window.api.create.setupWordpress(
|
||||||
|
operationId,
|
||||||
|
directory,
|
||||||
|
siteUrl,
|
||||||
|
title,
|
||||||
|
adminUser,
|
||||||
|
adminPassword,
|
||||||
|
adminEmail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,3 +69,9 @@ export function useRestartProject(): UseMutationResult<void, Error, string> {
|
|||||||
window.api.projects.restart(operationId, name)
|
window.api.projects.restart(operationId, name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDeleteProject(): UseMutationResult<void, Error, string> {
|
||||||
|
return useProjectAction('Delete', (operationId, name) =>
|
||||||
|
window.api.projects.delete(operationId, name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user