Files
av-planner/src/components/layout/AppShell.tsx
T
aarbit 1f8d49345e Add the Admin review queue
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.
2026-09-08 13:01:56 -05:00

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>
)
}