feat(workshop): rebuild install and publish as pages

Replace the stacked install modals (browse -> details -> import review)
with page-based flows so the primary action is always reachable:

- /manage/custom-widgets/workshop browses submissions as a card grid
  with large previews, sitting in a tab next to the installed widgets
- /manage/custom-widgets/workshop/[id] merges the listing, security
  review and source setup onto one page with a sticky install footer
- /manage/custom-widgets/publish/[id] replaces the publish modal
- /manage/workshop redirects; its navbar entry is gone
- the Custom CSS picker collapses from three modals to one

Extract useCustomWidgetImport and ImportReviewContent so the Workshop
page and the file/clipboard dialog share one import pipeline.

Votes now show the viewer's own choice filled Reddit-style, backed by a
new user-votes query, and screenshots get a zoomable lightbox.

Rework the installed list onto ManageCollectionItem: show the widget
description, replace the cryptic "JSX - 2" badge with plain request and
origin facts, and swap the toggle and bare pencil for a status badge,
a labelled Edit button and a menu entry for enable/disable.

Fixes found while reviewing:

- importing could run twice and duplicate a widget; guard on pending and
  succeeded, and return to the installed list instead of the heavy edit
  form
- a background refetch of the submission (window focus, or the
  invalidation a vote triggers) reset the source setup and silently wiped
  part-typed credentials; key the reset on the sources, not the object
- breadcrumbs raised MISSING_MESSAGE for unmapped dynamic segments; fall
  back to the raw segment via t.has
- publishing had no in-flight guard and could post duplicate listings
- tooltips on disabled vote and report controls never fired, making the
  sign-in hint unreachable
This commit is contained in:
Thomas Camlong committed 2026-08-17 01:14:38 +02:00
1 parent 50537d077c
commit 206905ea2d
38 files changed
+2303 -1700

No files matched your search

+16 -1
View File
@@ -657,6 +657,8 @@ export const workshopQueryKeys = {
] as const,
comments: (backend: WorkshopBackend, submission: string) =>
[...workshopQueryKeys.all, "comments", backend.baseUrl, submission] as const,
userVotes: (backend: WorkshopBackend) =>
[...workshopQueryKeys.all, "user-votes", backend.baseUrl, backend.currentUser?.id ?? "anonymous"] as const,
};
export const workshopListQueryOptions = (backend: WorkshopBackend, options: WorkshopListOptions = {}) =>
@@ -683,6 +685,14 @@ export const workshopReportSummariesQueryOptions = (backend: WorkshopBackend, su
enabled: submission.length > 0,
});
/** The votes the signed-in user already cast, so controls can render their own choice. */
export const workshopUserVotesQueryOptions = (backend: WorkshopBackend) =>
queryOptions({
queryKey: workshopQueryKeys.userVotes(backend),
queryFn: () => backend.listVotesForCurrentUser(),
enabled: backend.currentUser !== null,
});
export const workshopCommentsQueryOptions = (backend: WorkshopBackend, submission: string) =>
queryOptions({
queryKey: workshopQueryKeys.comments(backend, submission),
@@ -771,12 +781,17 @@ export function useWorkshopCommentsQuery(backend: WorkshopBackend, submission: s
return useQuery(workshopCommentsQueryOptions(backend, submission));
}
export function useWorkshopUserVotesQuery(backend: WorkshopBackend) {
return useQuery(workshopUserVotesQueryOptions(backend));
}
function useWorkshopInvalidation(backend: WorkshopBackend) {
const queryClient = useQueryClient();
return {
lists: () => queryClient.invalidateQueries({ queryKey: workshopQueryKeys.lists(backend.baseUrl) }),
details: () => queryClient.invalidateQueries({ queryKey: workshopQueryKeys.details(backend.baseUrl) }),
detail: (id: string) => queryClient.invalidateQueries({ queryKey: workshopQueryKeys.detail(backend, id) }),
userVotes: () => queryClient.invalidateQueries({ queryKey: workshopQueryKeys.userVotes(backend) }),
removeDetail: (id: string) => queryClient.removeQueries({ queryKey: workshopQueryKeys.detail(backend, id) }),
reports: () => queryClient.invalidateQueries({ queryKey: workshopQueryKeys.reports(backend) }),
reportSummaries: (submission: string) =>
@@ -814,7 +829,7 @@ export function useWorkshopVoteMutation(backend: WorkshopBackend) {
const invalidation = useWorkshopInvalidation(backend);
return useMutation({
...workshopVoteMutationOptions(backend),
onSuccess: async () => Promise.all([invalidation.lists(), invalidation.details()]),
onSuccess: async () => Promise.all([invalidation.lists(), invalidation.details(), invalidation.userVotes()]),
});
}