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.
40 lines
1.9 KiB
TypeScript
40 lines
1.9 KiB
TypeScript
import type { CatalogSubmission } from './SubmissionRepository'
|
|
|
|
/** One entry in a usage-impact sample — never raw diagram content, just
|
|
* enough to identify it (see organized-ideas.md §3's impact-check). */
|
|
export interface UsageImpactSample {
|
|
id: string
|
|
name: string
|
|
ownerUsername: string
|
|
}
|
|
|
|
export interface UsageImpact {
|
|
/** Total diagrams referencing this entity — not capped, unlike `sample`. */
|
|
diagramCount: number
|
|
/** Up to 5 of the most recently updated referencing diagrams. */
|
|
sample: UsageImpactSample[]
|
|
}
|
|
|
|
/** Storage abstraction for the Admin's-eye view of the catalog submission
|
|
* queue — mirrors SubmissionRepository's role for the submitter's-eye
|
|
* view. Every method here relies on the caller actually being an Admin;
|
|
* RLS (and, for the impact function, an explicit is_admin() check) is the
|
|
* real enforcement, not this interface. */
|
|
export interface AdminSubmissionRepository {
|
|
/** Every submission across all users, most recent first. */
|
|
listAll(): Promise<CatalogSubmission[]>
|
|
/** Approves a submission: writes its proposed_data onto the live row at
|
|
* entity_id — promoting it to public in place if it wasn't already —
|
|
* and marks the submission approved. Same id throughout; never creates
|
|
* a duplicate public row (organized-ideas.md §3). */
|
|
approve(submission: CatalogSubmission): Promise<void>
|
|
/** Rejects a submission with a reason the submitter will see. */
|
|
reject(id: string, reason: string): Promise<void>
|
|
/** How many diagrams reference this entity, and a small sample — see
|
|
* organized-ideas.md §3's impact-check-before-editing. */
|
|
getUsageImpact(entityType: CatalogSubmission['entityType'], entityId: string): Promise<UsageImpact>
|
|
/** Submitter username per submission id, for the ones a username could
|
|
* be resolved for (batched — one round trip for a whole queue listing). */
|
|
getSubmitterUsernames(submissionIds: string[]): Promise<Record<string, string>>
|
|
}
|