Validate and slugify project names in the create wizard
DDEV project names must be valid hostname labels. The wizard auto-filled the name from the picked folder's basename verbatim, so a folder like "Aurora Admin" produced a name with a space that DDEV's `ddev config` rejected with an opaque CLI error the UI never surfaced. Slugify the auto-filled name and block manual entry of invalid names before they ever reach the ddev CLI. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
86be76132d
commit
b5085c1e8a
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { isValidProjectName, slugifyProjectName } from './CreateProjectModal'
|
||||||
|
|
||||||
|
describe('isValidProjectName', () => {
|
||||||
|
it('accepts hostname-safe names', () => {
|
||||||
|
expect(isValidProjectName('my-project')).toBe(true)
|
||||||
|
expect(isValidProjectName('project1')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects names with spaces', () => {
|
||||||
|
expect(isValidProjectName('Aurora Admin')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects names starting or ending with a hyphen', () => {
|
||||||
|
expect(isValidProjectName('-project')).toBe(false)
|
||||||
|
expect(isValidProjectName('project-')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects empty names', () => {
|
||||||
|
expect(isValidProjectName('')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('slugifyProjectName', () => {
|
||||||
|
it('lowercases and replaces spaces with hyphens', () => {
|
||||||
|
expect(slugifyProjectName('Aurora Admin')).toBe('aurora-admin')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('collapses runs of non-alphanumeric characters', () => {
|
||||||
|
expect(slugifyProjectName('My Cool!! Project')).toBe('my-cool-project')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('trims leading and trailing hyphens', () => {
|
||||||
|
expect(slugifyProjectName(' -Weird Name- ')).toBe('weird-name')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('produces a name that passes validation', () => {
|
||||||
|
const slug = slugifyProjectName('Aurora Admin')
|
||||||
|
expect(isValidProjectName(slug)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -23,6 +23,21 @@ import docksideIcon from '../../assets/dockside-icon.png'
|
|||||||
|
|
||||||
type Step = 'site' | 'setup'
|
type Step = 'site' | 'setup'
|
||||||
|
|
||||||
|
// DDEV project names must be valid hostname labels: alphanumeric and
|
||||||
|
// hyphens only, can't start/end with a hyphen.
|
||||||
|
const PROJECT_NAME_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/
|
||||||
|
|
||||||
|
export function isValidProjectName(name: string): boolean {
|
||||||
|
return PROJECT_NAME_PATTERN.test(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slugifyProjectName(raw: string): string {
|
||||||
|
return raw
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
const fieldClass =
|
const fieldClass =
|
||||||
'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 dark:border-white/10 dark:bg-neutral-950/70 dark:placeholder:text-neutral-600'
|
'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 dark:border-white/10 dark:bg-neutral-950/70 dark:placeholder:text-neutral-600'
|
||||||
|
|
||||||
@@ -56,7 +71,9 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
|
|||||||
const selectProject = useAppStore((s) => s.selectProject)
|
const selectProject = useAppStore((s) => s.selectProject)
|
||||||
const setupRef = useRef<TypeSetupHandle>(null)
|
const setupRef = useRef<TypeSetupHandle>(null)
|
||||||
|
|
||||||
const canContinue = directory !== null && projectName.trim().length > 0
|
const trimmedName = projectName.trim()
|
||||||
|
const nameValid = trimmedName.length > 0 && isValidProjectName(trimmedName)
|
||||||
|
const canContinue = directory !== null && nameValid
|
||||||
const canSubmit = canContinue && setupValid && !isSubmitting
|
const canSubmit = canContinue && setupValid && !isSubmitting
|
||||||
|
|
||||||
async function handlePickDirectory(): Promise<void> {
|
async function handlePickDirectory(): Promise<void> {
|
||||||
@@ -65,7 +82,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
|
|||||||
setDirectory(picked)
|
setDirectory(picked)
|
||||||
if (!projectName) {
|
if (!projectName) {
|
||||||
const name = picked.split('/').filter(Boolean).pop() ?? ''
|
const name = picked.split('/').filter(Boolean).pop() ?? ''
|
||||||
setProjectName(name)
|
setProjectName(slugifyProjectName(name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +215,12 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
|
|||||||
placeholder="my-project"
|
placeholder="my-project"
|
||||||
className={fieldClass}
|
className={fieldClass}
|
||||||
/>
|
/>
|
||||||
|
{trimmedName.length > 0 && !nameValid && (
|
||||||
|
<p className="mt-1.5 text-xs text-red-600 dark:text-red-400">
|
||||||
|
Use only letters, numbers, and hyphens — no spaces (e.g. "
|
||||||
|
{slugifyProjectName(trimmedName) || 'my-project'}").
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user