feat: product drafts, moderator edits with reasons and locks, info history, unlisted products and tags
Pull request / title (pull_request) Successful in 1s
CI / test (pull_request) Successful in 1m4s
CI / image (apps/api/Dockerfile, t5de-hub-api) (pull_request) Skipped
CI / image (apps/web/Dockerfile, t5de-hub-web) (pull_request) Skipped

New uploads are drafts: only their creator and moderators see them, nobody can get or wear them, and they're
deleted if not published within 30 days (their catalogs get delete changes). The creator reviews the info and
publishes it.

Editing a product's info is a permission check now (product:edit): moderators can edit anyone's with a reason the
creator sees, lock fields (product:lock; locked fields need product:edit-locked) and lower ratings
(product:rate-lower, not below its files'). Every change is kept in the product's info history, with who and why,
and can be undone by the same rules.

Products can be unlisted (out of search and lists, the link still works) and tagged; lists filter by tag and
catalog. The console has a Products section (everyone's for moderators, by status) and a drafts to-do.

A product's own category is left for later: the hub has no list of IMVU's categories to pick from.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-11-05 05:40:43 -08:00
1 parent 2cbedd26fc
commit 8793b5afd8
24 files changed
+4106 -96

No files matched your search

+2 -1
View File
@@ -53,7 +53,8 @@ export function UploadForm({ catalogs, product }: { catalogs: Option[]; product?
const answer = await send(start.url, start.token, body, setProgress);
const value = answer.body as { product?: unknown; message?: unknown } | null;
if (answer.status === 201 && typeof value?.product === "string") {
router.push(`/products/${value.product}`);
// A new product is a draft: on to reviewing its info and publishing it.
router.push(product ? `/products/${value.product}` : `/products/${value.product}/edit`);
router.refresh();
return;
}