From 43b189e3a6bf24dc7cbd4fd2b33e77a417d132eb Mon Sep 17 00:00:00 2001 From: Maksym Yezhov Date: Mon, 15 Dec 2025 16:41:58 -0800 Subject: [PATCH] feat: lazy load of the yaml library components --- .../shared/FavoriteComponentToggle.tsx | 65 +++++++++++++++---- .../ComponentLibraryProvider.tsx | 62 ++++++++++++++---- .../componentLibrary.ts | 15 ++++- src/services/componentService.ts | 28 -------- 4 files changed, 114 insertions(+), 56 deletions(-) diff --git a/src/components/shared/FavoriteComponentToggle.tsx b/src/components/shared/FavoriteComponentToggle.tsx index b1bacc4d7..b9edd3709 100644 --- a/src/components/shared/FavoriteComponentToggle.tsx +++ b/src/components/shared/FavoriteComponentToggle.tsx @@ -1,22 +1,32 @@ +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from "@tanstack/react-query"; import { PackagePlus, Star } from "lucide-react"; -import type { MouseEvent, PropsWithChildren } from "react"; +import type { ComponentProps, MouseEvent, PropsWithChildren } from "react"; import { useCallback, useMemo, useState } from "react"; import { ConfirmationDialog } from "@/components/shared/Dialogs"; import { Button } from "@/components/ui/button"; import { Icon } from "@/components/ui/icon"; +import { Spinner } from "@/components/ui/spinner"; +import { useGuaranteedHydrateComponentReference } from "@/hooks/useHydrateComponentReference"; import { cn } from "@/lib/utils"; import { useComponentLibrary } from "@/providers/ComponentLibraryProvider"; +import { isFavoriteComponent } from "@/providers/ComponentLibraryProvider/componentLibrary"; import { hydrateComponentReference } from "@/services/componentService"; import type { ComponentReference } from "@/utils/componentSpec"; import { getComponentName } from "@/utils/getComponentName"; +import { withSuspenseWrapper } from "./SuspenseWrapper"; + interface ComponentFavoriteToggleProps { component: ComponentReference; hideDelete?: boolean; } -interface StateButtonProps { +interface StateButtonProps extends ComponentProps { active?: boolean; isDanger?: boolean; onClick?: () => void; @@ -27,6 +37,7 @@ const IconStateButton = ({ isDanger = false, onClick, children, + ...props }: PropsWithChildren) => { const handleFavorite = useCallback( (e: MouseEvent) => { @@ -51,6 +62,7 @@ const IconStateButton = ({ )} variant="ghost" size="icon" + {...props} > {children} @@ -84,6 +96,42 @@ const DeleteFromLibraryButton = ({ active, onClick }: StateButtonProps) => { ); }; +const favoriteComponentKey = (component: ComponentReference) => { + return ["component", "is-favorite", component.digest]; +}; + +const FavoriteToggleButton = withSuspenseWrapper( + ({ component }: { component: ComponentReference }) => { + const queryClient = useQueryClient(); + + const { setComponentFavorite } = useComponentLibrary(); + const hydratedComponent = useGuaranteedHydrateComponentReference(component); + + const { data: isFavorited } = useSuspenseQuery({ + queryKey: favoriteComponentKey(hydratedComponent), + queryFn: async () => isFavoriteComponent(hydratedComponent), + }); + + const { mutate: setFavorite } = useMutation({ + mutationFn: async () => + setComponentFavorite(hydratedComponent, !isFavorited), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: favoriteComponentKey(hydratedComponent), + }); + }, + }); + + return ; + }, + () => , + () => ( + + + + ), +); + export const ComponentFavoriteToggle = ({ component, hideDelete = false, @@ -91,21 +139,14 @@ export const ComponentFavoriteToggle = ({ const { addToComponentLibrary, removeFromComponentLibrary, - checkIfFavorited, checkIfUserComponent, checkLibraryContainsComponent, - setComponentFavorite, } = useComponentLibrary(); const [isOpen, setIsOpen] = useState(false); const { spec, url } = component; - const isFavorited = useMemo( - () => checkIfFavorited(component), - [component, checkIfFavorited], - ); - const isUserComponent = useMemo( () => checkIfUserComponent(component), [component, checkIfUserComponent], @@ -121,10 +162,6 @@ export const ComponentFavoriteToggle = ({ [spec, url], ); - const onFavorite = useCallback(() => { - setComponentFavorite(component, !isFavorited); - }, [isFavorited, setComponentFavorite]); - // Delete User Components const handleDelete = useCallback(async () => { removeFromComponentLibrary(component); @@ -166,7 +203,7 @@ export const ComponentFavoriteToggle = ({ {!isInLibrary && } {isInLibrary && !isUserComponent && ( - + )} {showDeleteButton && ( diff --git a/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx b/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx index ecfc01c6e..3cb2168b0 100644 --- a/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx +++ b/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx @@ -31,7 +31,7 @@ import { updateComponentInListByText, updateComponentRefInList, } from "@/utils/componentStore"; -import { USER_COMPONENTS_LIST_NAME } from "@/utils/constants"; +import { MINUTES, USER_COMPONENTS_LIST_NAME } from "@/utils/constants"; import { createPromiseFromDomEvent } from "@/utils/dom"; import { getComponentName } from "@/utils/getComponentName"; import { @@ -48,11 +48,11 @@ import { } from "../../hooks/useRequiredContext"; import { useComponentSpec } from "../ComponentSpecProvider"; import { - fetchFavoriteComponents, fetchUsedComponents, fetchUserComponents, filterToUniqueByDigest, flattenFolders, + isFavoriteComponent, populateComponentRefs, } from "./componentLibrary"; import { useForcedSearchContext } from "./ForcedSearchProvider"; @@ -70,7 +70,7 @@ type ComponentLibraryContextType = { componentLibrary: ComponentLibrary | undefined; userComponentsFolder: ComponentFolder | undefined; usedComponentsFolder: ComponentFolder; - favoritesFolder: ComponentFolder; + favoritesFolder: ComponentFolder | undefined; isLoading: boolean; error: Error | null; existingComponentLibraries: StoredLibrary[] | undefined; @@ -93,6 +93,9 @@ type ComponentLibraryContextType = { component: ComponentReference, favorited: boolean, ) => void; + /** + * @deprecated + */ checkIfFavorited: (component: ComponentReference) => boolean; checkIfUserComponent: (component: ComponentReference) => boolean; checkLibraryContainsComponent: (component: ComponentReference) => boolean; @@ -220,9 +223,45 @@ export const ComponentLibraryProvider = ({ ); // Fetch "Starred" components - const favoritesFolder: ComponentFolder = useMemo( - () => fetchFavoriteComponents(componentLibrary), - [componentLibrary], + const { data: favoritesFolderData, refetch: refetchFavorites } = useQuery({ + queryKey: ["favorites"], + queryFn: async () => { + const favoritesFolder: ComponentFolder = { + name: "Favorite Components", + components: [], + folders: [], + isUserFolder: false, + }; + + if (!componentLibrary || !componentLibrary.folders) { + return favoritesFolder; + } + + const uniqueLibraryComponents = filterToUniqueByDigest( + flattenFolders(componentLibrary), + ); + + for (const component of uniqueLibraryComponents) { + if (await isFavoriteComponent(component)) { + favoritesFolder.components?.push(component); + } + } + + return favoritesFolder; + }, + enabled: Boolean(componentLibrary), + staleTime: 10 * MINUTES, + }); + + const favoritesFolder = useMemo( + () => + favoritesFolderData ?? { + name: "Favorite Components", + components: [], + folders: [], + isUserFolder: false, + }, + [favoritesFolderData], ); // Methods @@ -230,11 +269,10 @@ export const ComponentLibraryProvider = ({ const { data: updatedLibrary } = await refetchLibrary(); if (updatedLibrary) { - populateComponentRefs(updatedLibrary).then((result) => { - setComponentLibrary(result); - }); + setComponentLibrary(updatedLibrary); + await refetchFavorites(); } - }, [refetchLibrary]); + }, [refetchLibrary, refetchFavorites]); const refreshUserComponents = useCallback(async () => { const { data: updatedUserComponents } = await refetchUserComponents(); @@ -571,9 +609,7 @@ export const ComponentLibraryProvider = ({ setComponentLibrary(undefined); return; } - populateComponentRefs(rawComponentLibrary).then((result) => { - setComponentLibrary(result); - }); + setComponentLibrary(rawComponentLibrary); }, [rawComponentLibrary]); useEffect(() => { diff --git a/src/providers/ComponentLibraryProvider/componentLibrary.ts b/src/providers/ComponentLibraryProvider/componentLibrary.ts index 7c3fe2359..72da76704 100644 --- a/src/providers/ComponentLibraryProvider/componentLibrary.ts +++ b/src/providers/ComponentLibraryProvider/componentLibrary.ts @@ -13,7 +13,7 @@ import { getAllComponentFilesFromList, } from "@/utils/componentStore"; import { USER_COMPONENTS_LIST_NAME } from "@/utils/constants"; -import { getComponentByUrl } from "@/utils/localforage"; +import { getComponentById, getComponentByUrl } from "@/utils/localforage"; import { componentSpecToYaml } from "@/utils/yaml"; export const fetchUserComponents = async (): Promise => { @@ -80,6 +80,19 @@ export const fetchUsedComponents = (graphSpec: GraphSpec): ComponentFolder => { }; }; +export async function isFavoriteComponent(component: ComponentReference) { + if (!component.digest) return false; + + const storedComponent = await getComponentById( + `component-${component.digest}`, + ); + + return storedComponent?.favorited ?? false; +} + +/** + * @deprecated + */ export const fetchFavoriteComponents = ( componentLibrary: ComponentLibrary | undefined, ): ComponentFolder => { diff --git a/src/services/componentService.ts b/src/services/componentService.ts index f822b2b1a..4604c4faa 100644 --- a/src/services/componentService.ts +++ b/src/services/componentService.ts @@ -1,6 +1,5 @@ import { getAppSettings } from "@/appSettings"; import { - type ComponentFolder, type ComponentLibrary, isValidComponentLibrary, } from "@/types/componentLibrary"; @@ -112,36 +111,9 @@ export const fetchAndStoreComponentLibrary = updatedAt: Date.now(), }); - // Also store individual components for future reference - await storeComponentsFromLibrary(obj); - return obj; }; -/** - * Store all components from the library in local storage - */ -const storeComponentsFromLibrary = async ( - library: ComponentLibrary, -): Promise => { - const processFolder = async (folder: ComponentFolder) => { - // Store each component in the folder - for (const component of folder.components || []) { - await fetchAndStoreComponent(component); - } - - // Process subfolders recursively - for (const subfolder of folder.folders || []) { - await processFolder(subfolder); - } - }; - - // Process all top-level folders - for (const folder of library.folders) { - await processFolder(folder); - } -}; - /** * Fetch and store a single component by URL */