diff --git a/src/App.tsx b/src/App.tsx index 7d5fee1..43856ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,6 +40,7 @@ function App() { } /> } /> + } /> } /> } /> } /> @@ -53,6 +54,7 @@ function App() { } /> } /> } /> + } /> } /> } /> () const ensureBuildFromConfig = useMutation(api.builds.ensureBuildFromConfig) const pluginFlashCounts = useQuery(api.plugins.getAll) ?? {} + const sharedBuild = useQuery( + api.builds.getByHash, + buildHashParam ? { buildHash: buildHashParam } : 'skip' + ) const STORAGE_KEY = 'quick_build_target' const persistTargetSelection = (targetId: string) => { @@ -59,6 +64,7 @@ export default function BuildNew() { const [errorMessage, setErrorMessage] = useState(null) const [showModuleOverrides, setShowModuleOverrides] = useState(false) const [showPlugins, setShowPlugins] = useState(true) + const [isLoadingSharedBuild, setIsLoadingSharedBuild] = useState(false) useEffect(() => { if (!activeCategory && TARGET_CATEGORIES.length > 0) { @@ -111,6 +117,67 @@ export default function BuildNew() { } }, [selectedTarget]) + // Pre-populate form from shared build + useEffect(() => { + if (!buildHashParam) return + if (sharedBuild === undefined) { + setIsLoadingSharedBuild(true) + return + } + setIsLoadingSharedBuild(false) + + if (!sharedBuild) { + setErrorMessage( + 'Build not found. The shared build may have been deleted.' + ) + toast.error('Build not found', { + description: 'The shared build could not be loaded.', + }) + return + } + + const config = sharedBuild.config + + // Set target and category + if (config.target && TARGETS[config.target]) { + setSelectedTarget(config.target) + const category = TARGETS[config.target].category || 'Other' + if (TARGET_CATEGORIES.includes(category)) { + setActiveCategory(category) + } + } + + // Set version + if ( + config.version && + (VERSIONS as readonly string[]).includes(config.version) + ) { + setSelectedVersion(config.version as (typeof VERSIONS)[number]) + } + + // Set module config (already in the correct format) + if (config.modulesExcluded) { + setModuleConfig(config.modulesExcluded) + if (Object.keys(config.modulesExcluded).length > 0) { + setShowModuleOverrides(true) + } + } + + // Set plugin config (convert array to object format) + if (config.pluginsEnabled && config.pluginsEnabled.length > 0) { + const pluginObj: Record = {} + config.pluginsEnabled.forEach((pluginId) => { + // Extract slug from "slug@version" format if present + const slug = pluginId.includes('@') ? pluginId.split('@')[0] : pluginId + if (slug in registryData) { + pluginObj[slug] = true + } + }) + setPluginConfig(pluginObj) + setShowPlugins(true) + } + }, [buildHashParam, sharedBuild]) + const moduleCount = Object.keys(moduleConfig).length const pluginCount = Object.keys(pluginConfig).filter( (id) => pluginConfig[id] === true @@ -176,6 +243,19 @@ export default function BuildNew() { const isFlashDisabled = !selectedTarget || isFlashing + if (isLoadingSharedBuild) { + return ( +
+
+ +

+ Loading shared build configuration... +

+
+
+ ) + } + return (
diff --git a/src/pages/BuildProgress.tsx b/src/pages/BuildProgress.tsx index 06aebca..57cfec4 100644 --- a/src/pages/BuildProgress.tsx +++ b/src/pages/BuildProgress.tsx @@ -1,8 +1,9 @@ import { useMutation, useQuery } from 'convex/react' import { pick } from 'convex-helpers' -import { ArrowLeft, CheckCircle, Loader2, XCircle } from 'lucide-react' +import { ArrowLeft, CheckCircle, Loader2, Share2, XCircle } from 'lucide-react' import { useState } from 'react' import { Link, useParams } from 'react-router-dom' +import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { humanizeStatus } from '@/lib/utils' import { api } from '../../convex/_generated/api' @@ -25,6 +26,7 @@ export default function BuildProgress() { const [sourceDownloadError, setSourceDownloadError] = useState( null ) + const [shareUrlCopied, setShareUrlCopied] = useState(false) if (!buildHash) { return ( @@ -129,6 +131,21 @@ export default function BuildProgress() { ? `https://github.com/MeshEnvy/configurable-web-flasher/actions/runs/${build.githubRunId}` : null + const shareUrl = `${window.location.origin}/builds/new/${build.buildHash}` + + const handleShare = async () => { + try { + await navigator.clipboard.writeText(shareUrl) + setShareUrlCopied(true) + toast.success('Share link copied to clipboard') + setTimeout(() => setShareUrlCopied(false), 2000) + } catch { + toast.error('Failed to copy link', { + description: 'Please copy the URL manually', + }) + } + } + return (
@@ -140,40 +157,46 @@ export default function BuildProgress() { Back to Quick Build -

- Hash: {build.buildHash} -

-
- {getStatusIcon()} -
-

- Target -

-

{targetLabel}

-
- - {humanizeStatus(status)} - - - {new Date(build.updatedAt).toLocaleString()} - {githubActionUrl && ( - <> - - - View run - - - )} +
+
+ {getStatusIcon()} +
+

+ Target +

+

{targetLabel}

+
+ + {humanizeStatus(status)} + + + {new Date(build.updatedAt).toLocaleString()} + {githubActionUrl && ( + <> + + + View run + + + )} +
+
{status !== 'success' && status !== 'failure' && (