feat: add Update all downloaded scripts button

- Add bulk update button on Downloaded Scripts tab
- Use existing loadMultipleScripts API for all downloaded script slugs
- Confirmation modal before running (may take several minutes)
- Inline result: success/fail counts, hover for failed slugs
- Invalidate getAllDownloadedScripts and getScriptCardsWithCategories on success
This commit is contained in:
Michel Rögl-Brunner
2026-01-29 11:17:36 +01:00
parent 9b77fc7ddb
commit 34eade3971

View File

@@ -8,7 +8,9 @@ import { ScriptDetailModal } from "./ScriptDetailModal";
import { CategorySidebar } from "./CategorySidebar"; import { CategorySidebar } from "./CategorySidebar";
import { FilterBar, type FilterState } from "./FilterBar"; import { FilterBar, type FilterState } from "./FilterBar";
import { ViewToggle } from "./ViewToggle"; import { ViewToggle } from "./ViewToggle";
import { ConfirmationModal } from "./ConfirmationModal";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { RefreshCw } from "lucide-react";
import type { ScriptCard as ScriptCardType } from "~/types/script"; import type { ScriptCard as ScriptCardType } from "~/types/script";
import type { Server } from "~/types/server"; import type { Server } from "~/types/server";
import { getDefaultFilters, mergeFiltersWithDefaults } from "./filterUtils"; import { getDefaultFilters, mergeFiltersWithDefaults } from "./filterUtils";
@@ -32,8 +34,15 @@ export function DownloadedScriptsTab({
const [filters, setFilters] = useState<FilterState>(getDefaultFilters()); const [filters, setFilters] = useState<FilterState>(getDefaultFilters());
const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false); const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false);
const [isLoadingFilters, setIsLoadingFilters] = useState(true); const [isLoadingFilters, setIsLoadingFilters] = useState(true);
const [updateAllConfirmOpen, setUpdateAllConfirmOpen] = useState(false);
const [updateResult, setUpdateResult] = useState<{
successCount: number;
failCount: number;
failed: { slug: string; error: string }[];
} | null>(null);
const gridRef = useRef<HTMLDivElement>(null); const gridRef = useRef<HTMLDivElement>(null);
const utils = api.useUtils();
const { const {
data: scriptCardsData, data: scriptCardsData,
isLoading: githubLoading, isLoading: githubLoading,
@@ -50,6 +59,27 @@ export function DownloadedScriptsTab({
{ enabled: !!selectedSlug }, { enabled: !!selectedSlug },
); );
const loadMultipleScriptsMutation = api.scripts.loadMultipleScripts.useMutation({
onSuccess: (data) => {
void utils.scripts.getAllDownloadedScripts.invalidate();
void utils.scripts.getScriptCardsWithCategories.invalidate();
setUpdateResult({
successCount: data.successful?.length ?? 0,
failCount: data.failed?.length ?? 0,
failed: data.failed ?? [],
});
setTimeout(() => setUpdateResult(null), 8000);
},
onError: (error) => {
setUpdateResult({
successCount: 0,
failCount: 1,
failed: [{ slug: "Request failed", error: error.message }],
});
setTimeout(() => setUpdateResult(null), 8000);
},
});
// Load SAVE_FILTER setting, saved filters, and view mode on component mount // Load SAVE_FILTER setting, saved filters, and view mode on component mount
useEffect(() => { useEffect(() => {
const loadSettings = async () => { const loadSettings = async () => {
@@ -416,6 +446,21 @@ export function DownloadedScriptsTab({
setSelectedSlug(null); setSelectedSlug(null);
}; };
const handleUpdateAllClick = () => {
setUpdateResult(null);
setUpdateAllConfirmOpen(true);
};
const handleUpdateAllConfirm = () => {
setUpdateAllConfirmOpen(false);
const slugs = downloadedScripts
.map((s) => s.slug)
.filter((slug): slug is string => Boolean(slug));
if (slugs.length > 0) {
loadMultipleScriptsMutation.mutate({ slugs });
}
};
if (githubLoading || localLoading) { if (githubLoading || localLoading) {
return ( return (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
@@ -508,6 +553,43 @@ export function DownloadedScriptsTab({
{/* Main Content */} {/* Main Content */}
<div className="order-1 min-w-0 flex-1 lg:order-2" ref={gridRef}> <div className="order-1 min-w-0 flex-1 lg:order-2" ref={gridRef}>
{/* Update all downloaded scripts */}
<div className="mb-4 flex flex-wrap items-center gap-3">
<Button
onClick={handleUpdateAllClick}
disabled={loadMultipleScriptsMutation.isPending}
variant="secondary"
size="default"
className="flex items-center gap-2"
>
{loadMultipleScriptsMutation.isPending ? (
<>
<RefreshCw className="h-4 w-4 animate-spin" />
<span>Updating...</span>
</>
) : (
<>
<RefreshCw className="h-4 w-4" />
<span>Update all downloaded scripts</span>
</>
)}
</Button>
{updateResult && (
<span className="text-muted-foreground text-sm">
Updated {updateResult.successCount} successfully
{updateResult.failCount > 0
? `, ${updateResult.failCount} failed`
: ""}
.
{updateResult.failCount > 0 && updateResult.failed.length > 0 && (
<span className="ml-1" title={updateResult.failed.map((f) => `${f.slug}: ${f.error}`).join("\n")}>
(hover for details)
</span>
)}
</span>
)}
</div>
{/* Enhanced Filter Bar */} {/* Enhanced Filter Bar */}
<FilterBar <FilterBar
filters={filters} filters={filters}
@@ -621,6 +703,17 @@ export function DownloadedScriptsTab({
onClose={handleCloseModal} onClose={handleCloseModal}
onInstallScript={onInstallScript} onInstallScript={onInstallScript}
/> />
<ConfirmationModal
isOpen={updateAllConfirmOpen}
onClose={() => setUpdateAllConfirmOpen(false)}
onConfirm={handleUpdateAllConfirm}
title="Update all downloaded scripts"
message={`Update all ${downloadedScripts.length} downloaded scripts? This may take several minutes.`}
variant="simple"
confirmButtonText="Update all"
cancelButtonText="Cancel"
/>
</div> </div>
</div> </div>
</div> </div>