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:
38 files changed
+2303
-1700
No files matched your search
@@ -19,16 +19,17 @@ export const ManageCollectionPage = ({
|
||||
...layoutProps
|
||||
}: ManageCollectionPageProps) => (
|
||||
<ManagePageLayout {...layoutProps}>
|
||||
{itemCount === 0 ? (
|
||||
emptyState
|
||||
) : (
|
||||
<Stack component="ul" aria-label={ariaLabel} gap="sm" m={0} p={0} style={{ listStyle: "none" }}>
|
||||
{children}
|
||||
</Stack>
|
||||
)}
|
||||
{itemCount === 0 ? emptyState : <ManageCollectionList ariaLabel={ariaLabel}>{children}</ManageCollectionList>}
|
||||
</ManagePageLayout>
|
||||
);
|
||||
|
||||
/** The `ul` wrapper used by every manage list, so collections rendered outside `ManageCollectionPage` still match. */
|
||||
export const ManageCollectionList = ({ ariaLabel, children }: { ariaLabel: string; children: ReactNode }) => (
|
||||
<Stack component="ul" aria-label={ariaLabel} gap="sm" m={0} p={0} style={{ listStyle: "none" }}>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
export interface ManageCollectionItemProps {
|
||||
leading?: ReactNode;
|
||||
title: ReactNode;
|
||||
|
||||
@@ -8,6 +8,8 @@ import { MANAGE_FLOATING_ACTION_BOTTOM_OFFSET } from "./manage-page.constants";
|
||||
|
||||
export interface ManagePageLayoutProps {
|
||||
title: ReactNode;
|
||||
/** Overrides the default breadcrumb, e.g. to map a dynamic route segment onto a readable name. */
|
||||
breadcrumb?: ReactNode;
|
||||
primaryAction?: ReactNode;
|
||||
toolbar?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
@@ -18,6 +20,7 @@ export interface ManagePageLayoutProps {
|
||||
|
||||
export const ManagePageLayout = ({
|
||||
title,
|
||||
breadcrumb,
|
||||
primaryAction,
|
||||
toolbar,
|
||||
footer,
|
||||
@@ -29,7 +32,7 @@ export const ManagePageLayout = ({
|
||||
|
||||
return (
|
||||
<ManageContainer size={size}>
|
||||
<DynamicBreadcrumb />
|
||||
{breadcrumb ?? <DynamicBreadcrumb />}
|
||||
<Stack pb={floatingPrimaryAction ? { base: MANAGE_FLOATING_ACTION_BOTTOM_OFFSET, md: 0 } : undefined}>
|
||||
<Group justify="space-between" align="center">
|
||||
{titleNode}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Group, Paper } from "@mantine/core";
|
||||
|
||||
export interface ManageStickyFooterProps {
|
||||
/** Secondary controls, pinned to the start of the bar. */
|
||||
secondary?: ReactNode;
|
||||
/** The primary action(s) of the page, pinned to the end of the bar. */
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bar that stays visible at the bottom of the viewport while the page scrolls,
|
||||
* so the primary action of a long page is always one click away.
|
||||
*/
|
||||
export const ManageStickyFooter = ({ secondary, children }: ManageStickyFooterProps) => (
|
||||
<Paper
|
||||
withBorder
|
||||
shadow="md"
|
||||
radius="md"
|
||||
p="sm"
|
||||
pos="sticky"
|
||||
bottom="var(--mantine-spacing-md)"
|
||||
bg="var(--mantine-color-body)"
|
||||
style={{ zIndex: 2 }}
|
||||
>
|
||||
<Group justify="space-between" align="center" gap="sm" wrap="wrap">
|
||||
<Group gap="xs" wrap="wrap">
|
||||
{secondary}
|
||||
</Group>
|
||||
<Group gap="sm" wrap="nowrap" ml="auto">
|
||||
{children}
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
Reference in new issue
Block a user