Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions src/components/shared/FavoriteComponentToggle.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Button> {
active?: boolean;
isDanger?: boolean;
onClick?: () => void;
Expand All @@ -27,6 +37,7 @@ const IconStateButton = ({
isDanger = false,
onClick,
children,
...props
}: PropsWithChildren<StateButtonProps>) => {
const handleFavorite = useCallback(
(e: MouseEvent) => {
Expand All @@ -51,6 +62,7 @@ const IconStateButton = ({
)}
variant="ghost"
size="icon"
{...props}
>
{children}
</Button>
Expand Down Expand Up @@ -84,28 +96,57 @@ 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 <FavoriteStarButton active={isFavorited} onClick={setFavorite} />;
},
() => <Spinner size={10} />,
() => (
<IconStateButton disabled>
<Icon name="Star" />
</IconStateButton>
),
);

export const ComponentFavoriteToggle = ({
component,
hideDelete = false,
}: ComponentFavoriteToggleProps) => {
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],
Expand All @@ -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);
Expand Down Expand Up @@ -166,7 +203,7 @@ export const ComponentFavoriteToggle = ({
{!isInLibrary && <AddToLibraryButton onClick={openConfirmationDialog} />}

{isInLibrary && !isUserComponent && (
<FavoriteStarButton active={isFavorited} onClick={onFavorite} />
<FavoriteToggleButton component={component} />
)}

{showDeleteButton && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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";
Expand All @@ -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;
Expand All @@ -93,6 +93,9 @@ type ComponentLibraryContextType = {
component: ComponentReference,
favorited: boolean,
) => void;
/**
* @deprecated
*/
checkIfFavorited: (component: ComponentReference) => boolean;
checkIfUserComponent: (component: ComponentReference) => boolean;
checkLibraryContainsComponent: (component: ComponentReference) => boolean;
Expand Down Expand Up @@ -220,21 +223,56 @@ 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
const refreshComponentLibrary = useCallback(async () => {
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();
Expand Down Expand Up @@ -571,9 +609,7 @@ export const ComponentLibraryProvider = ({
setComponentLibrary(undefined);
return;
}
populateComponentRefs(rawComponentLibrary).then((result) => {
setComponentLibrary(result);
});
setComponentLibrary(rawComponentLibrary);
}, [rawComponentLibrary]);

useEffect(() => {
Expand Down
15 changes: 14 additions & 1 deletion src/providers/ComponentLibraryProvider/componentLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentFolder> => {
Expand Down Expand Up @@ -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 => {
Expand Down
28 changes: 0 additions & 28 deletions src/services/componentService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { getAppSettings } from "@/appSettings";
import {
type ComponentFolder,
type ComponentLibrary,
isValidComponentLibrary,
} from "@/types/componentLibrary";
Expand Down Expand Up @@ -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<void> => {
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
*/
Expand Down
Loading