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'
|
||||
|
||||
// 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 =
|
||||
'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 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
|
||||
|
||||
async function handlePickDirectory(): Promise<void> {
|
||||
@@ -65,7 +82,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
|
||||
setDirectory(picked)
|
||||
if (!projectName) {
|
||||
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"
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user