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
@@ -1,24 +1,11 @@
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Code,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { Alert, Badge, Box, Button, Code, Group, Modal, Paper, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
import type { ModalProps } from "@mantine/core";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
|
||||
import type { ImportReview } from "../core/import";
|
||||
|
||||
export interface ImportReviewMessages {
|
||||
title: string;
|
||||
export interface ImportReviewContentMessages {
|
||||
description: string;
|
||||
name: string;
|
||||
origin: string;
|
||||
@@ -28,9 +15,13 @@ export interface ImportReviewMessages {
|
||||
permissions: string;
|
||||
actionWarningTitle: string;
|
||||
actionWarningDescription: string;
|
||||
permission(permission: string): string;
|
||||
}
|
||||
|
||||
export interface ImportReviewMessages extends ImportReviewContentMessages {
|
||||
title: string;
|
||||
cancel: string;
|
||||
confirm: string;
|
||||
permission(permission: string): string;
|
||||
}
|
||||
|
||||
export interface ImportReviewDialogProps {
|
||||
@@ -75,6 +66,71 @@ function ImportFact({ label, value }: { label: string; value: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export interface ImportReviewContentProps {
|
||||
review: ImportReview;
|
||||
messages: ImportReviewContentMessages;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* The security facts a user has to review before a custom widget is installed.
|
||||
* Shared by the import modal and the Workshop install page so both read identically.
|
||||
*/
|
||||
export function ImportReviewContent({ review, messages, children }: ImportReviewContentProps) {
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
{messages.description}
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xs">
|
||||
<ImportFact label={messages.name} value={review.name} />
|
||||
<ImportFact label={messages.origin} value={review.origins.join(", ")} />
|
||||
<ImportFact label={messages.authentication} value={review.authTypes.join(", ")} />
|
||||
<ImportFact label={messages.networkScope} value={review.networkScopes.join(", ")} />
|
||||
</SimpleGrid>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<div>
|
||||
<Text size="sm" fw={600} mb={6}>
|
||||
{messages.methods}
|
||||
</Text>
|
||||
<Group gap={6}>
|
||||
{review.methods.map((method) => (
|
||||
<Badge
|
||||
key={method}
|
||||
color={method === "DELETE" ? "red" : method === "GET" ? "blue" : "orange"}
|
||||
variant="light"
|
||||
>
|
||||
{method}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" fw={600} mb={6}>
|
||||
{messages.permissions}
|
||||
</Text>
|
||||
<Group gap={6}>
|
||||
{review.permissions.map((permission) => (
|
||||
<Badge key={permission} color="gray" variant="light">
|
||||
{messages.permission(permission)}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
{review.hasActions && (
|
||||
<Alert color="yellow" icon={<IconAlertTriangle size={16} />}>
|
||||
<Text size="sm" fw={600}>
|
||||
{messages.actionWarningTitle}
|
||||
</Text>
|
||||
<Text size="sm">{messages.actionWarningDescription}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function ImportReviewDialog({
|
||||
opened,
|
||||
review,
|
||||
@@ -103,56 +159,9 @@ export function ImportReviewDialog({
|
||||
{review && (
|
||||
<>
|
||||
<Box p="lg" style={{ flex: 1, minHeight: 0, overflowY: "auto" }}>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
{messages.description}
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xs">
|
||||
<ImportFact label={messages.name} value={review.name} />
|
||||
<ImportFact label={messages.origin} value={review.origins.join(", ")} />
|
||||
<ImportFact label={messages.authentication} value={review.authTypes.join(", ")} />
|
||||
<ImportFact label={messages.networkScope} value={review.networkScopes.join(", ")} />
|
||||
</SimpleGrid>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<div>
|
||||
<Text size="sm" fw={600} mb={6}>
|
||||
{messages.methods}
|
||||
</Text>
|
||||
<Group gap={6}>
|
||||
{review.methods.map((method) => (
|
||||
<Badge
|
||||
key={method}
|
||||
color={method === "DELETE" ? "red" : method === "GET" ? "blue" : "orange"}
|
||||
variant="light"
|
||||
>
|
||||
{method}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" fw={600} mb={6}>
|
||||
{messages.permissions}
|
||||
</Text>
|
||||
<Group gap={6}>
|
||||
{review.permissions.map((permission) => (
|
||||
<Badge key={permission} color="gray" variant="light">
|
||||
{messages.permission(permission)}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
{review.hasActions && (
|
||||
<Alert color="yellow" icon={<IconAlertTriangle size={16} />}>
|
||||
<Text size="sm" fw={600}>
|
||||
{messages.actionWarningTitle}
|
||||
</Text>
|
||||
<Text size="sm">{messages.actionWarningDescription}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<ImportReviewContent review={review} messages={messages}>
|
||||
{children}
|
||||
</Stack>
|
||||
</ImportReviewContent>
|
||||
</Box>
|
||||
<Box
|
||||
px="lg"
|
||||
|
||||
Reference in new issue
Block a user