Files
rfc-app/frontend/src/components/CreateProjectModal.jsx
T
Ben Stull 33212c71e4 §22 S5: in-app create-project + global-directory empty states (@S5 C3.1–C3.2)
Adds the global-Owner create-project action and the role-aware deployment
directory, completing slice S5 of the three-tier refactor (release v0.44.0,
minor/non-breaking — no migration). Per
docs/design/2026-06-05-three-tier-projects-collections.md Part E (S5),
Part C.3 (C3.1–C3.2).

Backend:
- auth.can_create_project — global-Owner gate (deployment owner/admin or an
  explicit scope_type='global' Owner grant).
- bot.create_project — provision the Gitea content repo (seed README so main
  exists), read+append+commit projects.yaml in the registry repo, audit-log
  (create_project). The bot stays the only git writer (§1).
- POST /api/projects (api_deployment) — global-Owner gated; validates the id
  (slug, not 'default'), name, type, visibility, content_repo; commits via the
  bot, then re-mirrors the registry so projects + default collection rows flow
  from git (§22.2). GET /api/deployment gains viewer.can_create_project and
  default_project_readable. make_router now takes gitea + bot.

Frontend:
- api.createProject; DeploymentProvider surfaces viewer + defaultProjectReadable
  + refresh; DeploymentLanding redirects into the default only when readable
  (gated/absent default falls through to the directory, no 404 bounce).
- Directory.jsx role-aware empty states (C3.1 "Create your first project" CTA;
  C3.2 "Nothing has been shared with you yet") + Owner-only "New project"
  control + CreateProjectModal.

Tests: backend test_create_project_vertical.py (vertical + gates + the
deployment empty-state signals); frontend Directory.test.jsx empty-state cases.

Also: ignore the session-local .superpowers/ tooling dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 01:02:16 -07:00

114 lines
4.5 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// CreateProjectModal.jsx — §22 S5. The create-project form the deployment
// directory's "Create your first project" / "New project" control opens, gated
// on the viewer's `can_create_project` capability (a global Owner action).
//
// The form lets the Owner choose a project id (a slug, not "default"), a display
// name, the type of its initial (default) collection, a visibility, and an
// optional content-repo name (defaulting to `<id>-content`). The backend
// provisions the Gitea content repo, commits the project to projects.yaml,
// re-mirrors the registry, and returns the new project; on success the caller
// refreshes the directory (and the N=1 redirect then lands in the new project).
import { useState } from 'react'
import { createProject } from '../api'
const TYPE_OPTIONS = [
{ value: 'document', label: 'Document — prose RFCs' },
{ value: 'specification', label: 'Specification' },
{ value: 'bdd', label: 'BDD — behaviour scenarios' },
]
const VISIBILITY_OPTIONS = [
{ value: 'public', label: 'Public' },
{ value: 'unlisted', label: 'Unlisted (link-only)' },
{ value: 'gated', label: 'Gated (hidden from the public)' },
]
export default function CreateProjectModal({ onClose, onCreated }) {
const [projectId, setProjectId] = useState('')
const [name, setName] = useState('')
const [type, setType] = useState('document')
const [visibility, setVisibility] = useState('public')
const [contentRepo, setContentRepo] = useState('')
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState(null)
async function handleCreate(e) {
e.preventDefault()
const pid = projectId.trim().toLowerCase()
if (!pid || !name.trim()) return
setSubmitting(true)
setError(null)
try {
const project = await createProject({
projectId: pid,
name: name.trim(),
type,
visibility,
contentRepo: contentRepo.trim() || null,
})
onCreated?.(project)
} catch (err) {
setError(err.message || 'Failed to create the project.')
} finally {
setSubmitting(false)
}
}
return (
<div className="modal-overlay" onClick={e => { if (e.target === e.currentTarget) onClose() }}>
<div className="modal" style={{ maxWidth: 560 }}>
<div className="modal-header">
<h2>New project</h2>
<button className="modal-close" onClick={onClose}>×</button>
</div>
<div className="modal-body">
<form onSubmit={handleCreate} className="invitations-form">
<label htmlFor="proj-id">Project id</label>
<input
id="proj-id"
value={projectId}
onChange={e => setProjectId(e.target.value)}
placeholder="e.g. ohm, acme"
autoFocus
required
/>
<label htmlFor="proj-name" style={{ marginTop: 10 }}>Display name</label>
<input
id="proj-name"
value={name}
onChange={e => setName(e.target.value)}
placeholder="e.g. Open Human Model"
required
/>
<label htmlFor="proj-type" style={{ marginTop: 10 }}>Initial collection type</label>
<select id="proj-type" value={type} onChange={e => setType(e.target.value)}>
{TYPE_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
<label htmlFor="proj-vis" style={{ marginTop: 10 }}>Visibility</label>
<select id="proj-vis" value={visibility} onChange={e => setVisibility(e.target.value)}>
{VISIBILITY_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
<label htmlFor="proj-repo" style={{ marginTop: 10 }}>Content repo (optional)</label>
<input
id="proj-repo"
value={contentRepo}
onChange={e => setContentRepo(e.target.value)}
placeholder="defaults to <id>-content"
/>
<div style={{ marginTop: 12, display: 'flex', gap: 8, alignItems: 'center' }}>
<button type="submit" className="btn-primary" disabled={submitting}>
{submitting ? 'Creating…' : 'Create project'}
</button>
{error && <span style={{ color: '#c33' }}>{error}</span>}
</div>
</form>
</div>
<div className="modal-footer">
<button type="button" className="btn-link" onClick={onClose}>Close</button>
</div>
</div>
</div>
)
}