Compare commits

...

1 Commits

Author SHA1 Message Date
Michel Roegl-Brunner
ff26c54c44 Fix selectedRepositories undefined error with generic filter validation
- Create filterUtils.ts with getDefaultFilters() and mergeFiltersWithDefaults()
- Update ScriptsGrid, DownloadedScriptsTab, and FilterBar to use utility functions
- Prevents crashes when loading old saved filters missing new properties
- Future-proof: new filter properties automatically get defaults
- Fixes TypeError: can't access property 'length', selectedRepositories is undefined
2025-11-14 09:33:47 +01:00
4 changed files with 52 additions and 26 deletions

View File

@@ -10,6 +10,7 @@ import { FilterBar, type FilterState } from './FilterBar';
import { ViewToggle } from './ViewToggle'; import { ViewToggle } from './ViewToggle';
import { Button } from './ui/button'; import { Button } from './ui/button';
import type { ScriptCard as ScriptCardType } from '~/types/script'; import type { ScriptCard as ScriptCardType } from '~/types/script';
import { getDefaultFilters, mergeFiltersWithDefaults } from './filterUtils';
interface DownloadedScriptsTabProps { interface DownloadedScriptsTabProps {
onInstallScript?: ( onInstallScript?: (
@@ -25,14 +26,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null); const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'card' | 'list'>('card'); const [viewMode, setViewMode] = useState<'card' | 'list'>('card');
const [filters, setFilters] = useState<FilterState>({ const [filters, setFilters] = useState<FilterState>(getDefaultFilters());
searchQuery: '',
showUpdatable: null,
selectedTypes: [],
selectedRepositories: [],
sortBy: 'name',
sortOrder: 'asc',
});
const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false); const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false);
const [isLoadingFilters, setIsLoadingFilters] = useState(true); const [isLoadingFilters, setIsLoadingFilters] = useState(true);
const gridRef = useRef<HTMLDivElement>(null); const gridRef = useRef<HTMLDivElement>(null);
@@ -63,7 +57,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr
if (filtersResponse.ok) { if (filtersResponse.ok) {
const filtersData = await filtersResponse.json(); const filtersData = await filtersResponse.json();
if (filtersData.filters) { if (filtersData.filters) {
setFilters(filtersData.filters as FilterState); setFilters(mergeFiltersWithDefaults(filtersData.filters));
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import { Button } from "./ui/button";
import { ContextualHelpIcon } from "./ContextualHelpIcon"; import { ContextualHelpIcon } from "./ContextualHelpIcon";
import { Package, Monitor, Wrench, Server, FileText, Calendar, RefreshCw, Filter, GitBranch } from "lucide-react"; import { Package, Monitor, Wrench, Server, FileText, Calendar, RefreshCw, Filter, GitBranch } from "lucide-react";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
import { getDefaultFilters } from "./filterUtils";
export interface FilterState { export interface FilterState {
searchQuery: string; searchQuery: string;
@@ -67,14 +68,7 @@ export function FilterBar({
}; };
const clearAllFilters = () => { const clearAllFilters = () => {
onFiltersChange({ onFiltersChange(getDefaultFilters());
searchQuery: "",
showUpdatable: null,
selectedTypes: [],
selectedRepositories: [],
sortBy: "name",
sortOrder: "asc",
});
}; };
const hasActiveFilters = const hasActiveFilters =

View File

@@ -11,6 +11,7 @@ import { ViewToggle } from './ViewToggle';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { Clock } from 'lucide-react'; import { Clock } from 'lucide-react';
import type { ScriptCard as ScriptCardType } from '~/types/script'; import type { ScriptCard as ScriptCardType } from '~/types/script';
import { getDefaultFilters, mergeFiltersWithDefaults } from './filterUtils';
interface ScriptsGridProps { interface ScriptsGridProps {
@@ -25,14 +26,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
const [viewMode, setViewMode] = useState<'card' | 'list'>('card'); const [viewMode, setViewMode] = useState<'card' | 'list'>('card');
const [selectedSlugs, setSelectedSlugs] = useState<Set<string>>(new Set()); const [selectedSlugs, setSelectedSlugs] = useState<Set<string>>(new Set());
const [downloadProgress, setDownloadProgress] = useState<{ current: number; total: number; currentScript: string; failed: Array<{ slug: string; error: string }> } | null>(null); const [downloadProgress, setDownloadProgress] = useState<{ current: number; total: number; currentScript: string; failed: Array<{ slug: string; error: string }> } | null>(null);
const [filters, setFilters] = useState<FilterState>({ const [filters, setFilters] = useState<FilterState>(getDefaultFilters());
searchQuery: '',
showUpdatable: null,
selectedTypes: [],
selectedRepositories: [],
sortBy: 'name',
sortOrder: 'asc',
});
const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false); const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false);
const [isLoadingFilters, setIsLoadingFilters] = useState(true); const [isLoadingFilters, setIsLoadingFilters] = useState(true);
const [isNewestMinimized, setIsNewestMinimized] = useState(false); const [isNewestMinimized, setIsNewestMinimized] = useState(false);
@@ -67,7 +61,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
if (filtersResponse.ok) { if (filtersResponse.ok) {
const filtersData = await filtersResponse.json(); const filtersData = await filtersResponse.json();
if (filtersData.filters) { if (filtersData.filters) {
setFilters(filtersData.filters as FilterState); setFilters(mergeFiltersWithDefaults(filtersData.filters));
} }
} }
} }

View File

@@ -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<FilterState> | 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,
};
}