diff --git a/src/app/_components/DownloadedScriptsTab.tsx b/src/app/_components/DownloadedScriptsTab.tsx index 1f31f6b..279906c 100644 --- a/src/app/_components/DownloadedScriptsTab.tsx +++ b/src/app/_components/DownloadedScriptsTab.tsx @@ -10,6 +10,7 @@ import { FilterBar, type FilterState } from './FilterBar'; import { ViewToggle } from './ViewToggle'; import { Button } from './ui/button'; import type { ScriptCard as ScriptCardType } from '~/types/script'; +import { getDefaultFilters, mergeFiltersWithDefaults } from './filterUtils'; interface DownloadedScriptsTabProps { onInstallScript?: ( @@ -25,14 +26,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr const [isModalOpen, setIsModalOpen] = useState(false); const [selectedCategory, setSelectedCategory] = useState(null); const [viewMode, setViewMode] = useState<'card' | 'list'>('card'); - const [filters, setFilters] = useState({ - searchQuery: '', - showUpdatable: null, - selectedTypes: [], - selectedRepositories: [], - sortBy: 'name', - sortOrder: 'asc', - }); + const [filters, setFilters] = useState(getDefaultFilters()); const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false); const [isLoadingFilters, setIsLoadingFilters] = useState(true); const gridRef = useRef(null); @@ -63,7 +57,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr if (filtersResponse.ok) { const filtersData = await filtersResponse.json(); if (filtersData.filters) { - setFilters(filtersData.filters as FilterState); + setFilters(mergeFiltersWithDefaults(filtersData.filters)); } } } diff --git a/src/app/_components/FilterBar.tsx b/src/app/_components/FilterBar.tsx index 1d94346..2f72ca7 100644 --- a/src/app/_components/FilterBar.tsx +++ b/src/app/_components/FilterBar.tsx @@ -5,6 +5,7 @@ import { Button } from "./ui/button"; import { ContextualHelpIcon } from "./ContextualHelpIcon"; import { Package, Monitor, Wrench, Server, FileText, Calendar, RefreshCw, Filter, GitBranch } from "lucide-react"; import { api } from "~/trpc/react"; +import { getDefaultFilters } from "./filterUtils"; export interface FilterState { searchQuery: string; @@ -67,14 +68,7 @@ export function FilterBar({ }; const clearAllFilters = () => { - onFiltersChange({ - searchQuery: "", - showUpdatable: null, - selectedTypes: [], - selectedRepositories: [], - sortBy: "name", - sortOrder: "asc", - }); + onFiltersChange(getDefaultFilters()); }; const hasActiveFilters = diff --git a/src/app/_components/ScriptsGrid.tsx b/src/app/_components/ScriptsGrid.tsx index 8096d7c..32837f4 100644 --- a/src/app/_components/ScriptsGrid.tsx +++ b/src/app/_components/ScriptsGrid.tsx @@ -11,6 +11,7 @@ import { ViewToggle } from './ViewToggle'; import { Button } from './ui/button'; import { Clock } from 'lucide-react'; import type { ScriptCard as ScriptCardType } from '~/types/script'; +import { getDefaultFilters, mergeFiltersWithDefaults } from './filterUtils'; interface ScriptsGridProps { @@ -25,14 +26,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { const [viewMode, setViewMode] = useState<'card' | 'list'>('card'); const [selectedSlugs, setSelectedSlugs] = useState>(new Set()); const [downloadProgress, setDownloadProgress] = useState<{ current: number; total: number; currentScript: string; failed: Array<{ slug: string; error: string }> } | null>(null); - const [filters, setFilters] = useState({ - searchQuery: '', - showUpdatable: null, - selectedTypes: [], - selectedRepositories: [], - sortBy: 'name', - sortOrder: 'asc', - }); + const [filters, setFilters] = useState(getDefaultFilters()); const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false); const [isLoadingFilters, setIsLoadingFilters] = useState(true); const [isNewestMinimized, setIsNewestMinimized] = useState(false); @@ -67,7 +61,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { if (filtersResponse.ok) { const filtersData = await filtersResponse.json(); if (filtersData.filters) { - setFilters(filtersData.filters as FilterState); + setFilters(mergeFiltersWithDefaults(filtersData.filters)); } } } diff --git a/src/app/_components/filterUtils.ts b/src/app/_components/filterUtils.ts new file mode 100644 index 0000000..43a8423 --- /dev/null +++ b/src/app/_components/filterUtils.ts @@ -0,0 +1,44 @@ +import type { FilterState } from "./FilterBar"; + +/** + * Returns the default FilterState with all properties initialized. + * This serves as the single source of truth for default filter values. + */ +export function getDefaultFilters(): FilterState { + return { + searchQuery: "", + showUpdatable: null, + selectedTypes: [], + selectedRepositories: [], + sortBy: "name", + sortOrder: "asc", + }; +} + +/** + * Merges saved filters with defaults, ensuring all FilterState properties exist. + * This prevents crashes when loading old saved filters that are missing new properties. + * + * @param savedFilters - Partial or undefined saved filters from storage + * @returns Complete FilterState with all properties guaranteed to exist + */ +export function mergeFiltersWithDefaults( + savedFilters: Partial | undefined +): FilterState { + const defaults = getDefaultFilters(); + + if (!savedFilters) { + return defaults; + } + + // Merge saved filters with defaults, ensuring all properties exist + return { + searchQuery: savedFilters.searchQuery ?? defaults.searchQuery, + showUpdatable: savedFilters.showUpdatable ?? defaults.showUpdatable, + selectedTypes: savedFilters.selectedTypes ?? defaults.selectedTypes, + selectedRepositories: savedFilters.selectedRepositories ?? defaults.selectedRepositories, + sortBy: savedFilters.sortBy ?? defaults.sortBy, + sortOrder: savedFilters.sortOrder ?? defaults.sortOrder, + }; +} +