Skip to content

Commit 1d63324

Browse files
committed
feat: lazy load of the yaml library components
1 parent d437913 commit 1d63324

File tree

4 files changed

+114
-56
lines changed

4 files changed

+114
-56
lines changed

src/components/shared/FavoriteComponentToggle.tsx

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
1+
import {
2+
useMutation,
3+
useQueryClient,
4+
useSuspenseQuery,
5+
} from "@tanstack/react-query";
16
import { PackagePlus, Star } from "lucide-react";
2-
import type { MouseEvent, PropsWithChildren } from "react";
7+
import type { ComponentProps, MouseEvent, PropsWithChildren } from "react";
38
import { useCallback, useMemo, useState } from "react";
49

510
import { ConfirmationDialog } from "@/components/shared/Dialogs";
611
import { Button } from "@/components/ui/button";
712
import { Icon } from "@/components/ui/icon";
13+
import { Spinner } from "@/components/ui/spinner";
14+
import { useGuaranteedHydrateComponentReference } from "@/hooks/useHydrateComponentReference";
815
import { cn } from "@/lib/utils";
916
import { useComponentLibrary } from "@/providers/ComponentLibraryProvider";
17+
import { isFavoriteComponent } from "@/providers/ComponentLibraryProvider/componentLibrary";
1018
import { hydrateComponentReference } from "@/services/componentService";
1119
import type { ComponentReference } from "@/utils/componentSpec";
1220
import { getComponentName } from "@/utils/getComponentName";
1321

22+
import { withSuspenseWrapper } from "./SuspenseWrapper";
23+
1424
interface ComponentFavoriteToggleProps {
1525
component: ComponentReference;
1626
hideDelete?: boolean;
1727
}
1828

19-
interface StateButtonProps {
29+
interface StateButtonProps extends ComponentProps<typeof Button> {
2030
active?: boolean;
2131
isDanger?: boolean;
2232
onClick?: () => void;
@@ -27,6 +37,7 @@ const IconStateButton = ({
2737
isDanger = false,
2838
onClick,
2939
children,
40+
...props
3041
}: PropsWithChildren<StateButtonProps>) => {
3142
const handleFavorite = useCallback(
3243
(e: MouseEvent) => {
@@ -51,6 +62,7 @@ const IconStateButton = ({
5162
)}
5263
variant="ghost"
5364
size="icon"
65+
{...props}
5466
>
5567
{children}
5668
</Button>
@@ -84,28 +96,57 @@ const DeleteFromLibraryButton = ({ active, onClick }: StateButtonProps) => {
8496
);
8597
};
8698

99+
const favoriteComponentKey = (component: ComponentReference) => {
100+
return ["component", "is-favorite", component.digest];
101+
};
102+
103+
const FavoriteToggleButton = withSuspenseWrapper(
104+
({ component }: { component: ComponentReference }) => {
105+
const queryClient = useQueryClient();
106+
107+
const { setComponentFavorite } = useComponentLibrary();
108+
const hydratedComponent = useGuaranteedHydrateComponentReference(component);
109+
110+
const { data: isFavorited } = useSuspenseQuery({
111+
queryKey: favoriteComponentKey(hydratedComponent),
112+
queryFn: async () => isFavoriteComponent(hydratedComponent),
113+
});
114+
115+
const { mutate: setFavorite } = useMutation({
116+
mutationFn: async () =>
117+
setComponentFavorite(hydratedComponent, !isFavorited),
118+
onSuccess: () => {
119+
queryClient.invalidateQueries({
120+
queryKey: favoriteComponentKey(hydratedComponent),
121+
});
122+
},
123+
});
124+
125+
return <FavoriteStarButton active={isFavorited} onClick={setFavorite} />;
126+
},
127+
() => <Spinner size={10} />,
128+
() => (
129+
<IconStateButton disabled>
130+
<Icon name="Star" />
131+
</IconStateButton>
132+
),
133+
);
134+
87135
export const ComponentFavoriteToggle = ({
88136
component,
89137
hideDelete = false,
90138
}: ComponentFavoriteToggleProps) => {
91139
const {
92140
addToComponentLibrary,
93141
removeFromComponentLibrary,
94-
checkIfFavorited,
95142
checkIfUserComponent,
96143
checkLibraryContainsComponent,
97-
setComponentFavorite,
98144
} = useComponentLibrary();
99145

100146
const [isOpen, setIsOpen] = useState(false);
101147

102148
const { spec, url } = component;
103149

104-
const isFavorited = useMemo(
105-
() => checkIfFavorited(component),
106-
[component, checkIfFavorited],
107-
);
108-
109150
const isUserComponent = useMemo(
110151
() => checkIfUserComponent(component),
111152
[component, checkIfUserComponent],
@@ -121,10 +162,6 @@ export const ComponentFavoriteToggle = ({
121162
[spec, url],
122163
);
123164

124-
const onFavorite = useCallback(() => {
125-
setComponentFavorite(component, !isFavorited);
126-
}, [isFavorited, setComponentFavorite]);
127-
128165
// Delete User Components
129166
const handleDelete = useCallback(async () => {
130167
removeFromComponentLibrary(component);
@@ -166,7 +203,7 @@ export const ComponentFavoriteToggle = ({
166203
{!isInLibrary && <AddToLibraryButton onClick={openConfirmationDialog} />}
167204

168205
{isInLibrary && !isUserComponent && (
169-
<FavoriteStarButton active={isFavorited} onClick={onFavorite} />
206+
<FavoriteToggleButton component={component} />
170207
)}
171208

172209
{showDeleteButton && (

src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
updateComponentInListByText,
3232
updateComponentRefInList,
3333
} from "@/utils/componentStore";
34-
import { USER_COMPONENTS_LIST_NAME } from "@/utils/constants";
34+
import { MINUTES, USER_COMPONENTS_LIST_NAME } from "@/utils/constants";
3535
import { createPromiseFromDomEvent } from "@/utils/dom";
3636
import { getComponentName } from "@/utils/getComponentName";
3737
import {
@@ -48,11 +48,11 @@ import {
4848
} from "../../hooks/useRequiredContext";
4949
import { useComponentSpec } from "../ComponentSpecProvider";
5050
import {
51-
fetchFavoriteComponents,
5251
fetchUsedComponents,
5352
fetchUserComponents,
5453
filterToUniqueByDigest,
5554
flattenFolders,
55+
isFavoriteComponent,
5656
populateComponentRefs,
5757
} from "./componentLibrary";
5858
import { useForcedSearchContext } from "./ForcedSearchProvider";
@@ -70,7 +70,7 @@ type ComponentLibraryContextType = {
7070
componentLibrary: ComponentLibrary | undefined;
7171
userComponentsFolder: ComponentFolder | undefined;
7272
usedComponentsFolder: ComponentFolder;
73-
favoritesFolder: ComponentFolder;
73+
favoritesFolder: ComponentFolder | undefined;
7474
isLoading: boolean;
7575
error: Error | null;
7676
existingComponentLibraries: StoredLibrary[] | undefined;
@@ -93,6 +93,9 @@ type ComponentLibraryContextType = {
9393
component: ComponentReference,
9494
favorited: boolean,
9595
) => void;
96+
/**
97+
* @deprecated
98+
*/
9699
checkIfFavorited: (component: ComponentReference) => boolean;
97100
checkIfUserComponent: (component: ComponentReference) => boolean;
98101
checkLibraryContainsComponent: (component: ComponentReference) => boolean;
@@ -220,21 +223,56 @@ export const ComponentLibraryProvider = ({
220223
);
221224

222225
// Fetch "Starred" components
223-
const favoritesFolder: ComponentFolder = useMemo(
224-
() => fetchFavoriteComponents(componentLibrary),
225-
[componentLibrary],
226+
const { data: favoritesFolderData, refetch: refetchFavorites } = useQuery({
227+
queryKey: ["favorites"],
228+
queryFn: async () => {
229+
const favoritesFolder: ComponentFolder = {
230+
name: "Favorite Components",
231+
components: [],
232+
folders: [],
233+
isUserFolder: false,
234+
};
235+
236+
if (!componentLibrary || !componentLibrary.folders) {
237+
return favoritesFolder;
238+
}
239+
240+
const uniqueLibraryComponents = filterToUniqueByDigest(
241+
flattenFolders(componentLibrary),
242+
);
243+
244+
for (const component of uniqueLibraryComponents) {
245+
if (await isFavoriteComponent(component)) {
246+
favoritesFolder.components?.push(component);
247+
}
248+
}
249+
250+
return favoritesFolder;
251+
},
252+
enabled: Boolean(componentLibrary),
253+
staleTime: 10 * MINUTES,
254+
});
255+
256+
const favoritesFolder = useMemo(
257+
() =>
258+
favoritesFolderData ?? {
259+
name: "Favorite Components",
260+
components: [],
261+
folders: [],
262+
isUserFolder: false,
263+
},
264+
[favoritesFolderData],
226265
);
227266

228267
// Methods
229268
const refreshComponentLibrary = useCallback(async () => {
230269
const { data: updatedLibrary } = await refetchLibrary();
231270

232271
if (updatedLibrary) {
233-
populateComponentRefs(updatedLibrary).then((result) => {
234-
setComponentLibrary(result);
235-
});
272+
setComponentLibrary(updatedLibrary);
273+
await refetchFavorites();
236274
}
237-
}, [refetchLibrary]);
275+
}, [refetchLibrary, refetchFavorites]);
238276

239277
const refreshUserComponents = useCallback(async () => {
240278
const { data: updatedUserComponents } = await refetchUserComponents();
@@ -571,9 +609,7 @@ export const ComponentLibraryProvider = ({
571609
setComponentLibrary(undefined);
572610
return;
573611
}
574-
populateComponentRefs(rawComponentLibrary).then((result) => {
575-
setComponentLibrary(result);
576-
});
612+
setComponentLibrary(rawComponentLibrary);
577613
}, [rawComponentLibrary]);
578614

579615
useEffect(() => {

src/providers/ComponentLibraryProvider/componentLibrary.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
getAllComponentFilesFromList,
1414
} from "@/utils/componentStore";
1515
import { USER_COMPONENTS_LIST_NAME } from "@/utils/constants";
16-
import { getComponentByUrl } from "@/utils/localforage";
16+
import { getComponentById, getComponentByUrl } from "@/utils/localforage";
1717
import { componentSpecToYaml } from "@/utils/yaml";
1818

1919
export const fetchUserComponents = async (): Promise<ComponentFolder> => {
@@ -80,6 +80,19 @@ export const fetchUsedComponents = (graphSpec: GraphSpec): ComponentFolder => {
8080
};
8181
};
8282

83+
export async function isFavoriteComponent(component: ComponentReference) {
84+
if (!component.digest) return false;
85+
86+
const storedComponent = await getComponentById(
87+
`component-${component.digest}`,
88+
);
89+
90+
return storedComponent?.favorited ?? false;
91+
}
92+
93+
/**
94+
* @deprecated
95+
*/
8396
export const fetchFavoriteComponents = (
8497
componentLibrary: ComponentLibrary | undefined,
8598
): ComponentFolder => {

src/services/componentService.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getAppSettings } from "@/appSettings";
22
import {
3-
type ComponentFolder,
43
type ComponentLibrary,
54
isValidComponentLibrary,
65
} from "@/types/componentLibrary";
@@ -112,36 +111,9 @@ export const fetchAndStoreComponentLibrary =
112111
updatedAt: Date.now(),
113112
});
114113

115-
// Also store individual components for future reference
116-
await storeComponentsFromLibrary(obj);
117-
118114
return obj;
119115
};
120116

121-
/**
122-
* Store all components from the library in local storage
123-
*/
124-
const storeComponentsFromLibrary = async (
125-
library: ComponentLibrary,
126-
): Promise<void> => {
127-
const processFolder = async (folder: ComponentFolder) => {
128-
// Store each component in the folder
129-
for (const component of folder.components || []) {
130-
await fetchAndStoreComponent(component);
131-
}
132-
133-
// Process subfolders recursively
134-
for (const subfolder of folder.folders || []) {
135-
await processFolder(subfolder);
136-
}
137-
};
138-
139-
// Process all top-level folders
140-
for (const folder of library.folders) {
141-
await processFolder(folder);
142-
}
143-
};
144-
145117
/**
146118
* Fetch and store a single component by URL
147119
*/

0 commit comments

Comments
 (0)