diff --git a/packages/desktop-client/src/components/settings/ThemeInstaller.test.tsx b/packages/desktop-client/src/components/settings/ThemeInstaller.test.tsx index dc14509319..96f07ddb43 100644 --- a/packages/desktop-client/src/components/settings/ThemeInstaller.test.tsx +++ b/packages/desktop-client/src/components/settings/ThemeInstaller.test.tsx @@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ThemeInstaller } from './ThemeInstaller'; +import { useThemeCatalog } from '@desktop-client/hooks/useThemeCatalog'; import { fetchThemeCss, validateThemeCss, @@ -24,8 +25,21 @@ vi.mock('@desktop-client/style/customThemes', async () => { ), }; }); -vi.mock('@desktop-client/data/customThemeCatalog.json', () => ({ - default: [ + +vi.mock('@desktop-client/hooks/useThemeCatalog', () => ({ + useThemeCatalog: vi.fn(), +})); + +describe('ThemeInstaller', () => { + const mockOnInstall = vi.fn(); + const mockOnClose = vi.fn(); + + const mockValidCss = `:root { + --color-primary: #007bff; + --color-secondary: #6c757d; + }`; + + const mockCatalog = [ { name: 'Demo Theme', repo: 'actualbudget/demo-theme', @@ -38,17 +52,7 @@ vi.mock('@desktop-client/data/customThemeCatalog.json', () => ({ name: 'Forest Green', repo: 'actualbudget/forest-theme', }, - ], -})); - -describe('ThemeInstaller', () => { - const mockOnInstall = vi.fn(); - const mockOnClose = vi.fn(); - - const mockValidCss = `:root { - --color-primary: #007bff; - --color-secondary: #6c757d; - }`; + ]; beforeEach(() => { vi.clearAllMocks(); @@ -56,6 +60,13 @@ describe('ThemeInstaller', () => { mockOnClose.mockClear(); vi.mocked(fetchThemeCss).mockResolvedValue(mockValidCss); vi.mocked(validateThemeCss).mockImplementation(css => css.trim()); + + // Mock useThemeCatalog to return catalog data immediately + vi.mocked(useThemeCatalog).mockReturnValue({ + data: mockCatalog, + isLoading: false, + error: null, + }); }); describe('rendering', () => { diff --git a/packages/desktop-client/src/components/settings/ThemeInstaller.tsx b/packages/desktop-client/src/components/settings/ThemeInstaller.tsx index c172f1586b..464e5df449 100644 --- a/packages/desktop-client/src/components/settings/ThemeInstaller.tsx +++ b/packages/desktop-client/src/components/settings/ThemeInstaller.tsx @@ -12,7 +12,7 @@ import { View } from '@actual-app/components/view'; import { Link } from '@desktop-client/components/common/Link'; import { FixedSizeList } from '@desktop-client/components/FixedSizeList'; -import customThemeCatalog from '@desktop-client/data/customThemeCatalog.json'; +import { useThemeCatalog } from '@desktop-client/hooks/useThemeCatalog'; import { extractRepoOwner, fetchThemeCss, @@ -48,6 +48,13 @@ export function ThemeInstaller({ const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + // Fetch catalog from GitHub using custom hook + const { + data: catalog, + isLoading: catalogLoading, + error: catalogError, + } = useThemeCatalog(); + // Initialize pastedCss with installed custom theme CSS if it exists useEffect(() => { // If there's an installed theme with empty repo (custom pasted CSS), restore it @@ -56,9 +63,6 @@ export function ThemeInstaller({ } }, [installedTheme]); - // TODO: inlined for now, but eventually we will fetch this from github directly - const catalog = customThemeCatalog as CatalogTheme[]; - // Calculate items per row based on container width const getItemsPerRow = useCallback((containerWidth: number) => { const padding = 8; // 4px on each side @@ -167,155 +171,191 @@ export function ThemeInstaller({ Choose from catalog: - - - {({ width, height }) => { - if (width === 0 || height === 0) { - return null; - } - - const itemsPerRow = getItemsPerRow(width); - const rows: CatalogTheme[][] = []; - for (let i = 0; i < catalog.length; i += itemsPerRow) { - rows.push(catalog.slice(i, i + itemsPerRow)); - } - - return ( - `row-${index}`} - renderRow={({ index, style }) => { - const rowThemes = rows[index]; - return ( -
- {rowThemes.map((theme, themeIndex) => { - const isSelected = - selectedCatalogTheme?.name === theme.name && - selectedCatalogTheme?.repo === theme.repo; - - const isLoadingSelected = isLoading && isSelected; - - return ( - - ); - })} -
- ); + {catalogError ? ( + + + Failed to load theme catalog. You can still paste custom CSS below. + + + ) : ( + + {catalogLoading ? ( + + - ); - }} -
-
+ + ) : ( + + {({ width, height }) => { + if (width === 0 || height === 0) { + return null; + } + + const catalogItems = catalog ?? []; + const itemsPerRow = getItemsPerRow(width); + const rows: CatalogTheme[][] = []; + for (let i = 0; i < catalogItems.length; i += itemsPerRow) { + rows.push(catalogItems.slice(i, i + itemsPerRow)); + } + + return ( + `row-${index}`} + renderRow={({ index, style }) => { + const rowThemes = rows[index]; + return ( +
+ {rowThemes.map((theme, themeIndex) => { + const isSelected = + selectedCatalogTheme?.name === theme.name && + selectedCatalogTheme?.repo === theme.repo; + + const isLoadingSelected = isLoading && isSelected; + + return ( + + ); + })} +
+ ); + }} + /> + ); + }} +
+ )} + + )} {/* Paste CSS Input */} (null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchCatalog = async () => { + setIsLoading(true); + setError(null); + + try { + const response = await fetch(CATALOG_URL); + + if (!response.ok) { + throw new Error(`Failed to fetch catalog: ${response.statusText}`); + } + + const data = await response.json(); + + // Validate that data is an array + if (!Array.isArray(data)) { + throw new Error('Invalid catalog format: expected an array'); + } + + setData(data); + } catch (err) { + setError( + err instanceof Error ? err.message : 'Failed to load theme catalog', + ); + } finally { + setIsLoading(false); + } + }; + + fetchCatalog(); + }, []); + + return { + data, + isLoading, + error, + }; +} diff --git a/upcoming-release-notes/6681.md b/upcoming-release-notes/6681.md new file mode 100644 index 0000000000..8c3c282021 --- /dev/null +++ b/upcoming-release-notes/6681.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [MatissJanis] +--- + +Themes: async fetch the theme catalog