33212c71e4
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>
72 lines
2.9 KiB
React
72 lines
2.9 KiB
React
// §22.10 (M3) — the deployment directory at `/`. Renders the caller-visible
|
|
// projects (from /api/deployment) as cards linking into each project's
|
|
// `/p/<id>/` home. App's DeploymentLanding only mounts this when the N=1
|
|
// land-in-corpus redirect does not apply (2+ visible projects, or no readable
|
|
// default) so OHM's "land in the corpus" UX is preserved.
|
|
//
|
|
// §22 S5 — role-aware empty states + the global-Owner create-project action.
|
|
// The deployment payload carries a `viewer` block; the directory reads it to
|
|
// render:
|
|
// * C3.1: a global Owner sees a "Create your first project" CTA (and a "New
|
|
// project" control when the directory is non-empty), opening the
|
|
// create-project modal (choose id, name, type, visibility).
|
|
// * C3.2: a non-owner (a granted account with no roles) sees the empty
|
|
// directory with no create action and a note that nothing is shared yet.
|
|
import { useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { useDeployment } from '../context/DeploymentProvider'
|
|
import { entryNoun } from './ProjectLayout.jsx'
|
|
import CreateProjectModal from './CreateProjectModal.jsx'
|
|
|
|
export default function Directory() {
|
|
const { name, tagline, projects, viewer, refresh } = useDeployment()
|
|
const [createOpen, setCreateOpen] = useState(false)
|
|
const canCreate = !!viewer?.can_create_project
|
|
|
|
return (
|
|
<main className="chrome-pane">
|
|
<div className="directory">
|
|
<div className="directory-head">
|
|
<h1>{name}</h1>
|
|
<div className="directory-actions">
|
|
{canCreate && projects.length > 0 && (
|
|
<button className="btn-primary" onClick={() => setCreateOpen(true)}>New project</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{tagline && <p className="directory-tagline">{tagline}</p>}
|
|
{projects.length === 0 ? (
|
|
// C3.1 / C3.2: role-keyed empty state.
|
|
canCreate ? (
|
|
<div className="directory-empty">
|
|
<p className="directory-tagline">No projects yet.</p>
|
|
<button className="btn-primary" onClick={() => setCreateOpen(true)}>
|
|
Create your first project
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<p className="directory-tagline">Nothing has been shared with you yet.</p>
|
|
)
|
|
) : (
|
|
<ul className="directory-list">
|
|
{projects.map(p => (
|
|
<li key={p.id} className="directory-card">
|
|
<Link to={`/p/${p.id}/`}>
|
|
<span className="directory-card-name">{p.name}</span>
|
|
<span className="directory-card-type">{entryNoun(p.type)}s</span>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
{createOpen && (
|
|
<CreateProjectModal
|
|
onClose={() => setCreateOpen(false)}
|
|
onCreated={() => { setCreateOpen(false); refresh?.() }}
|
|
/>
|
|
)}
|
|
</main>
|
|
)
|
|
}
|