feat: restore advanced module setup experience
This commit is contained in:
Binary file not shown.
@@ -12,6 +12,7 @@ function wpArgs(context, network) {
|
||||
|
||||
exports.projectCreate = async function projectCreate(context) {
|
||||
const s = context.settings
|
||||
const title = String(s.title || '').trim() || context.projectName
|
||||
const base = wpArgs(context, false)
|
||||
const networkBase = wpArgs(context, true)
|
||||
const siteUrl = context.urls.https
|
||||
@@ -19,10 +20,10 @@ exports.projectCreate = async function projectCreate(context) {
|
||||
await context.run('start', 'docker', ['compose', '-f', `${context.directory}/.aurora/compose.yaml`, 'up', '-d', '--build', '--remove-orphans'])
|
||||
await context.run('download', 'docker', [...base, 'core', 'download', `--locale=${s.locale || 'en_US'}`, '--force'])
|
||||
await context.run('config', 'docker', [...networkBase, 'config', 'create', '--dbname=db', '--dbuser=db', '--dbpass=db', '--dbhost=db:3306', '--skip-check', '--force'])
|
||||
await context.run('install', 'docker', [...networkBase, 'core', 'install', `--url=${siteUrl}`, `--title=${s.title}`, `--admin_user=${s.admin_user}`, `--admin_password=${s.admin_password}`, `--admin_email=${s.admin_email}`, '--skip-email'])
|
||||
await context.run('install', 'docker', [...networkBase, 'core', 'install', `--url=${siteUrl}`, `--title=${title}`, `--admin_user=${s.admin_user}`, `--admin_password=${s.admin_password}`, `--admin_email=${s.admin_email}`, '--skip-email'])
|
||||
await context.run('verify-install', 'docker', [...networkBase, 'core', 'is-installed'])
|
||||
if (s.multisite !== 'none') {
|
||||
await context.run('multisite-convert', 'docker', [...networkBase, 'core', 'multisite-convert', `--title=${s.title}`, ...(s.multisite === 'subdomain' ? ['--subdomains'] : [])])
|
||||
await context.run('multisite-convert', 'docker', [...networkBase, 'core', 'multisite-convert', `--title=${title}`, ...(s.multisite === 'subdomain' ? ['--subdomains'] : [])])
|
||||
await context.run('verify-network', 'docker', [...networkBase, 'core', 'is-installed', '--network'])
|
||||
await context.run('verify-network-db', 'docker', [...networkBase, 'db', 'query', "SHOW TABLES LIKE 'wp_blogs';", '--skip-column-names'])
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "wordpress",
|
||||
"name": "WordPress",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"category": "application",
|
||||
"description": "WordPress CMS provisioning and WP-CLI tooling.",
|
||||
"main": "main/index.cjs",
|
||||
@@ -12,14 +12,15 @@
|
||||
"settings": [],
|
||||
"creation": {
|
||||
"databases": ["mariadb"],
|
||||
"intro": { "title": "WordPress install", "description": "Core downloads automatically and the project starts for setup.", "icon": "globe" },
|
||||
"setup": [
|
||||
{ "id": "locale", "label": "Locale", "type": "select", "default": "en_US", "options": ["en_US", "en_GB", "de_DE", "fr_FR", "es_ES"] },
|
||||
{ "id": "title", "label": "Site title", "type": "text", "default": "Aurora Site" },
|
||||
{ "id": "admin_user", "label": "Admin username", "type": "text", "default": "admin" },
|
||||
{ "id": "admin_password", "label": "Admin password", "type": "text", "default": "" },
|
||||
{ "id": "admin_email", "label": "Admin email", "type": "text", "default": "[email protected]est" },
|
||||
{ "id": "multisite", "label": "Site mode", "type": "select", "default": "none", "options": ["none", "subdirectory", "subdomain"] },
|
||||
{ "id": "wp_debug", "label": "Enable WP_DEBUG", "type": "boolean", "default": true }
|
||||
{ "id": "title", "label": "Site title", "type": "text", "default": "", "placeholder": "My WordPress Site", "icon": "title", "span": 2 },
|
||||
{ "id": "admin_user", "label": "Admin username", "type": "text", "default": "admin", "required": true, "icon": "user" },
|
||||
{ "id": "admin_password", "label": "Admin password", "type": "text", "default": "", "required": true, "secret": true, "icon": "key" },
|
||||
{ "id": "admin_email", "label": "Admin email", "type": "text", "default": "", "required": true, "placeholder": "[email protected]", "icon": "mail", "span": 2 },
|
||||
{ "id": "locale", "label": "Select language", "type": "select", "default": "en_US", "advanced": true, "icon": "globe", "options": ["en_US", "en_GB", "de_DE", "es_ES", "fr_FR", "it_IT", "pt_BR", "nl_NL", "ja"], "optionLabels": { "en_US": "English (United States)", "en_GB": "English (UK)", "de_DE": "German", "es_ES": "Spanish (Spain)", "fr_FR": "French (France)", "it_IT": "Italian", "pt_BR": "Portuguese (Brazil)", "nl_NL": "Dutch", "ja": "Japanese" } },
|
||||
{ "id": "multisite", "label": "Is this a WordPress Multisite?", "type": "select", "default": "none", "advanced": true, "icon": "settings", "options": ["none", "subdirectory", "subdomain"], "optionLabels": { "none": "No", "subdirectory": "Yes – Subdirectory", "subdomain": "Yes – Subdomain" } },
|
||||
{ "id": "wp_debug", "label": "Enable WP_DEBUG", "type": "boolean", "default": true, "advanced": true }
|
||||
]
|
||||
},
|
||||
"project": { "adminPath": "/wp-admin/" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@aurora/module-wordpress",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"description": "WordPress application module for Aurora Dockside",
|
||||
"files": ["manifest.json", "main", "README.md"]
|
||||
|
||||
@@ -1,25 +1,46 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
import type { AuroraModuleManifest } from '@shared/types'
|
||||
import { ChevronDown, ChevronUp, Eye, EyeOff, Globe2, KeyRound, Mail, RefreshCw, Settings2, Type, UserRound } from 'lucide-react'
|
||||
import type { AuroraModuleManifest, AuroraModuleSetting } from '@shared/types'
|
||||
import type { TypeSetupHandle, TypeSetupProps } from './shared'
|
||||
|
||||
const inputClass = 'w-full rounded-lg border border-neutral-300 bg-white/80 px-3 py-2 text-sm shadow-sm transition placeholder:text-neutral-400 focus:border-cyan-400 focus:outline-none focus:ring-2 focus:ring-cyan-400/15 dark:border-white/10 dark:bg-neutral-950/70 dark:placeholder:text-neutral-600'
|
||||
const labelClass = 'mb-1.5 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400'
|
||||
const icons = { title: Type, user: UserRound, key: KeyRound, mail: Mail, globe: Globe2, settings: Settings2 }
|
||||
|
||||
function password(): string {
|
||||
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%'
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(20))
|
||||
return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('')
|
||||
}
|
||||
|
||||
export const ExternalModuleSetup = forwardRef<TypeSetupHandle, TypeSetupProps & { module: AuroraModuleManifest }>(
|
||||
function ExternalModuleSetup({ module, projectName, onValidityChange }, ref) {
|
||||
const fields = module.creation?.setup ?? []
|
||||
const [values, setValues] = useState<Record<string, string | number | boolean>>(() => Object.fromEntries(fields.map((field) => [field.id, field.default])))
|
||||
const valid = fields.every((field) => field.type === 'boolean' || field.type === 'number' || String(values[field.id] ?? '').trim().length > 0)
|
||||
const [advanced, setAdvanced] = useState(false)
|
||||
const [visibleSecrets, setVisibleSecrets] = useState<Set<string>>(new Set())
|
||||
const valid = fields.every((field) => !field.required || String(values[field.id] ?? '').trim().length > 0)
|
||||
useEffect(() => onValidityChange(valid), [valid, onValidityChange])
|
||||
useImperativeHandle(ref, () => ({
|
||||
runPostCreate: async ({ directory }) => {
|
||||
await window.api.create.runModuleProjectCreate(crypto.randomUUID(), module.id, directory, projectName, values)
|
||||
}
|
||||
}), [module.id, projectName, values])
|
||||
useImperativeHandle(ref, () => ({ runPostCreate: async ({ directory }) => window.api.create.runModuleProjectCreate(crypto.randomUUID(), module.id, directory, projectName, values) }), [module.id, projectName, values])
|
||||
|
||||
function fieldControl(field: AuroraModuleSetting): React.JSX.Element {
|
||||
if (field.type === 'boolean') return <label className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white/70 px-3 py-2 text-sm font-medium dark:border-white/10 dark:bg-neutral-950/50"><input type="checkbox" checked={Boolean(values[field.id])} onChange={(event) => setValues({ ...values, [field.id]: event.target.checked })}/>{field.label}</label>
|
||||
const Icon = field.icon ? icons[field.icon] : undefined
|
||||
if (field.type === 'select') return <select value={String(values[field.id] ?? '')} onChange={(event) => setValues({ ...values, [field.id]: event.target.value })} className={inputClass}>{field.options?.map((option) => <option key={option} value={option}>{field.optionLabels?.[option] ?? option}</option>)}</select>
|
||||
const visible = visibleSecrets.has(field.id)
|
||||
return <div className="relative">
|
||||
{Icon && <Icon size={15} className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400"/>}
|
||||
<input type={field.secret && !visible ? 'password' : field.type === 'number' ? 'number' : field.icon === 'mail' ? 'email' : 'text'} value={String(values[field.id] ?? '')} placeholder={field.placeholder || (field.id === 'title' ? projectName : undefined)} onChange={(event) => setValues({ ...values, [field.id]: field.type === 'number' ? Number(event.target.value) : event.target.value })} className={`${inputClass} ${Icon ? 'pl-9' : ''} ${field.secret ? 'pr-20' : ''}`}/>
|
||||
{field.secret && <div className="absolute right-2 top-1/2 flex -translate-y-1/2 gap-1"><button type="button" title="Generate password" onClick={() => setValues({ ...values, [field.id]: password() })} className="rounded p-1 text-neutral-400 hover:bg-neutral-100 hover:text-cyan-700 dark:hover:bg-white/10"><RefreshCw size={14}/></button><button type="button" title={visible ? 'Hide password' : 'Show password'} onClick={() => setVisibleSecrets((current) => { const next = new Set(current); visible ? next.delete(field.id) : next.add(field.id); return next })} className="rounded p-1 text-neutral-400 hover:bg-neutral-100 hover:text-cyan-700 dark:hover:bg-white/10">{visible ? <EyeOff size={14}/> : <Eye size={14}/>}</button></div>}
|
||||
</div>
|
||||
}
|
||||
|
||||
const standardFields = fields.filter((field) => !field.advanced)
|
||||
const advancedFields = fields.filter((field) => field.advanced)
|
||||
return <div className="grid gap-4">
|
||||
<div><h3 className="font-semibold">{module.name} setup</h3><p className="text-sm text-neutral-500">Provided by {module.name} {module.version}.</p></div>
|
||||
{fields.map((field) => <label key={field.id} className="grid gap-1.5 text-sm"><span className="font-medium">{field.label}</span>
|
||||
{field.type === 'boolean' ? <input type="checkbox" checked={Boolean(values[field.id])} onChange={(event) => setValues({ ...values, [field.id]: event.target.checked })}/>
|
||||
: field.type === 'select' ? <select value={String(values[field.id] ?? '')} onChange={(event) => setValues({ ...values, [field.id]: event.target.value })} className="rounded-lg border p-2 dark:bg-neutral-950">{field.options?.map((option) => <option key={option}>{option}</option>)}</select>
|
||||
: <input type={field.id.toLowerCase().includes('password') ? 'password' : field.type === 'number' ? 'number' : 'text'} value={String(values[field.id] ?? '')} onChange={(event) => setValues({ ...values, [field.id]: field.type === 'number' ? Number(event.target.value) : event.target.value })} className="rounded-lg border p-2 dark:bg-neutral-950"/>}
|
||||
</label>)}
|
||||
<div className="rounded-xl border border-cyan-200 bg-cyan-50 p-4 dark:border-cyan-400/20 dark:bg-cyan-400/10"><div className="flex items-start gap-3"><div className="grid size-10 flex-shrink-0 place-items-center rounded-lg bg-cyan-600 text-white dark:bg-cyan-300 dark:text-neutral-950"><Globe2 size={18}/></div><div><p className="text-sm font-semibold text-cyan-950 dark:text-cyan-100">{module.creation?.intro?.title ?? `${module.name} install`}</p><p className="mt-1 text-xs leading-5 text-cyan-800/80 dark:text-cyan-100/75">{module.creation?.intro?.description ?? `Setup is provided by ${module.name} ${module.version}.`}</p></div></div></div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">{standardFields.map((field) => <div key={field.id} className={field.span === 2 ? 'sm:col-span-2' : ''}><label className={labelClass}>{field.label}</label>{fieldControl(field)}</div>)}</div>
|
||||
{advancedFields.length > 0 && <><button type="button" onClick={() => setAdvanced((value) => !value)} className="flex items-center justify-between rounded-xl border border-neutral-200 bg-white/70 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-neutral-500 transition hover:border-cyan-200 hover:bg-cyan-50/50 hover:text-cyan-800 dark:border-white/10 dark:bg-white/[0.04] dark:hover:border-cyan-400/25 dark:hover:bg-cyan-400/10 dark:hover:text-cyan-200"><span>Advanced options</span>{advanced ? <ChevronUp size={12}/> : <ChevronDown size={12}/>}</button>{advanced && <div className="grid gap-3 rounded-xl border border-neutral-200 bg-neutral-50/70 p-4 dark:border-white/10 dark:bg-white/[0.03]">{advancedFields.map((field) => <div key={field.id}>{field.type !== 'boolean' && <label className={labelClass}>{field.label}</label>}{fieldControl(field)}</div>)}</div>}</>}
|
||||
</div>
|
||||
}
|
||||
)
|
||||
|
||||
@@ -119,6 +119,13 @@ export interface AuroraModuleSetting {
|
||||
type: AuroraModuleSettingType
|
||||
default: string | number | boolean
|
||||
options?: string[]
|
||||
optionLabels?: Record<string, string>
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
advanced?: boolean
|
||||
secret?: boolean
|
||||
icon?: 'title' | 'user' | 'key' | 'mail' | 'globe' | 'settings'
|
||||
span?: 1 | 2
|
||||
}
|
||||
|
||||
export interface AuroraModuleManifest {
|
||||
@@ -137,6 +144,7 @@ export interface AuroraModuleManifest {
|
||||
creation?: {
|
||||
databases?: Array<'mariadb' | 'postgres'>
|
||||
setup?: AuroraModuleSetting[]
|
||||
intro?: { title: string; description: string; icon?: string }
|
||||
}
|
||||
hooks?: Partial<Record<AuroraModuleLifecycleHook, AuroraModuleCommand[]>>
|
||||
project?: {
|
||||
|
||||
Reference in New Issue
Block a user