Lets an Admin/Super-Admin review pending catalog submissions and approve (in place, same id) or reject (with a required reason) them, per organized-ideas.md §3/§9. Backend (supabase/migrations/20260910010000_admin_review_queue.sql): - Per-user pending-submission cap (10), enforced in catalog_submissions' insert policy rather than trusted to the client. - catalog_entity_usage_impact(entity_type, entity_id): a SECURITY DEFINER, admin-gated aggregate function answering "how many diagrams reference this, and a short sample" by scanning diagrams.data JSONB — never raw diagram content, and available to regular Admins even though they don't otherwise have diagram visibility (only Super Admins do, per §6). - catalog_submission_submitters(ids[]): same admin-gated pattern, batched, so the queue can show who submitted something without opening general profile browsing to regular Admins. - Follow-up migration: a rejected submission had no way out (the delete policy only allowed withdrawing 'pending') — extended to allow 'rejected' too, so a submitter can dismiss one they don't intend to revise. - 12 new pgTAP tests (38/38 total) covering the cap, both privileged functions (including the non-admin-gets-rejected case), and withdrawing pending vs. rejected submissions. Frontend: - authStore: minimal role awareness, replacing TopBar's local username fetch, used to gate the Review Queue UI. - AdminSubmissionRepository/SupabaseAdminSubmissionRepository + adminReviewStore: list all submissions, approve/reject, usage impact, submitter usernames. - AdminReviewModal: per-submission diff view (current vs. proposed, both row-shaped via the existing catalog<->row mappers), a duplicate-detection nudge (Levenshtein distance against existing public device names) for new device submissions, and an inline impact-check for edits to already-public entries before approving. - TopBar: role-gated "Review Queue" button with a pending-count badge; "My Submissions" gets an unseen-outcome badge (localStorage-tracked, like the existing hidden-template preference) so a submitter notices a decision without having to keep reopening the modal. - Deliberately deferred: the site-wide announcement banner (its own follow-up, per discussion) and the Admin/Super-Admin role-assignment UI (§9's later phase — becoming an Admin locally still means setting profiles.role via SQL/Studio). Verified: tsc -b and oxlint clean; supabase db reset + 38/38 pgTAP tests pass; confirmed the two new RPC functions are actually reachable through PostgREST (not just raw SQL) via a live curl call; manually tested submit -> review -> approve/reject -> (for rejected) dismiss end to end.
55 lines
2.0 KiB
TypeScript
55 lines
2.0 KiB
TypeScript
import { useEffect } from 'react'
|
|
import { useAdminReviewStore } from '../../state/adminReviewStore'
|
|
import { useAuthStore } from '../../state/authStore'
|
|
import { useCatalogStore } from '../../state/catalogStore'
|
|
import { useProjectStore } from '../../state/projectStore'
|
|
import { useSubmissionStore } from '../../state/submissionStore'
|
|
import FlowCanvas from '../canvas/FlowCanvas'
|
|
import DevicePalette from '../palette/DevicePalette'
|
|
import ConnectionErrorBanner from './ConnectionErrorBanner'
|
|
import RightPanel from './RightPanel'
|
|
import TopBar from './TopBar'
|
|
|
|
export default function AppShell() {
|
|
const isLoaded = useProjectStore((s) => s.isLoaded)
|
|
const loadInitialDiagram = useProjectStore((s) => s.loadInitialDiagram)
|
|
const isCatalogLoaded = useCatalogStore((s) => s.isLoaded)
|
|
const loadCatalog = useCatalogStore((s) => s.loadCatalog)
|
|
const loadMySubmissions = useSubmissionStore((s) => s.loadMySubmissions)
|
|
const loadAuth = useAuthStore((s) => s.load)
|
|
const role = useAuthStore((s) => s.role)
|
|
const loadAdminQueue = useAdminReviewStore((s) => s.loadAll)
|
|
|
|
useEffect(() => {
|
|
loadInitialDiagram()
|
|
loadCatalog()
|
|
loadMySubmissions()
|
|
loadAuth()
|
|
}, [loadInitialDiagram, loadCatalog, loadMySubmissions, loadAuth])
|
|
|
|
// Only Admins/Super-Admins need the review queue at all — and role isn't
|
|
// known until loadAuth() above resolves, so this is a separate effect
|
|
// keyed on it rather than bundled into the one above.
|
|
useEffect(() => {
|
|
if (role === 'admin' || role === 'super_admin') loadAdminQueue()
|
|
}, [role, loadAdminQueue])
|
|
|
|
if (!isLoaded || !isCatalogLoaded) {
|
|
return <div className="flex h-screen items-center justify-center text-sm text-slate-400">Loading…</div>
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-screen flex-col">
|
|
<TopBar />
|
|
<div className="flex min-h-0 flex-1">
|
|
<DevicePalette />
|
|
<main className="relative min-w-0 flex-1">
|
|
<ConnectionErrorBanner />
|
|
<FlowCanvas />
|
|
</main>
|
|
<RightPanel />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|