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
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ const createMockComponentLibraryContext = (
removeFromComponentLibrary: vi.fn(),
setComponentFavorite: vi.fn(),
checkIfUserComponent: vi.fn().mockReturnValue(false),
checkLibraryContainsComponent: vi.fn().mockReturnValue(false),
getComponentLibrary: vi.fn(),
};
};
Expand Down
115 changes: 78 additions & 37 deletions src/components/shared/FavoriteComponentToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ 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 {
flattenFolders,
isFavoriteComponent,
} from "@/providers/ComponentLibraryProvider/componentLibrary";
import { hydrateComponentReference } from "@/services/componentService";
import type { ComponentReference } from "@/utils/componentSpec";
import { type ComponentReference } from "@/utils/componentSpec";
import { MINUTES } from "@/utils/constants";
import { getComponentName } from "@/utils/getComponentName";

import { withSuspenseWrapper } from "./SuspenseWrapper";
Expand Down Expand Up @@ -132,41 +136,64 @@ const FavoriteToggleButton = withSuspenseWrapper(
),
);

export const ComponentFavoriteToggle = ({
const useComponentFlags = (component: ComponentReference) => {
const { checkIfUserComponent, componentLibrary } = useComponentLibrary();

const isUserComponent = useMemo(
() => checkIfUserComponent(component),
[checkIfUserComponent, component],
);

const flatComponentList = useMemo(
() => (componentLibrary ? flattenFolders(componentLibrary) : []),
[componentLibrary],
);

const { data: isInLibrary } = useSuspenseQuery({
queryKey: ["component", "flags", component.digest],
queryFn: async () => {
if (!componentLibrary) return false;

for (const c of flatComponentList) {
if (c.name && c.name !== component.name) {
// micro optimization to skip components with different names
continue;
}

const hydratedComponent = await hydrateComponentReference(c);
const digest = c.digest ?? hydratedComponent?.digest;

if (digest === component.digest) {
return true;
}
}

return false;
},
staleTime: 10 * MINUTES,
});

return { isInLibrary, isUserComponent };
};

const ComponentFavoriteToggleInternal = ({
component,
hideDelete = false,
}: ComponentFavoriteToggleProps) => {
const {
addToComponentLibrary,
removeFromComponentLibrary,
checkIfUserComponent,
checkLibraryContainsComponent,
} = useComponentLibrary();
const { addToComponentLibrary, removeFromComponentLibrary } =
useComponentLibrary();

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

const { spec, url } = component;

const isUserComponent = useMemo(
() => checkIfUserComponent(component),
[component, checkIfUserComponent],
);

const isInLibrary = useMemo(
() => checkLibraryContainsComponent(component),
[component, checkLibraryContainsComponent],
);
const { isInLibrary, isUserComponent } = useComponentFlags(component);

const displayName = useMemo(
() => getComponentName({ spec, url }),
[spec, url],
);

// Delete User Components
const handleDelete = useCallback(async () => {
removeFromComponentLibrary(component);
}, [removeFromComponentLibrary]);

/* Confirmation Dialog handlers */
const openConfirmationDialog = useCallback(() => {
setIsOpen(true);
Expand All @@ -180,7 +207,7 @@ export const ComponentFavoriteToggle = ({
const handleConfirm = useCallback(async () => {
setIsOpen(false);

if (!isInLibrary) {
if (!isUserComponent) {
const hydratedComponent = await hydrateComponentReference(component);

if (!hydratedComponent) {
Expand All @@ -189,42 +216,56 @@ export const ComponentFavoriteToggle = ({
return;
}

addToComponentLibrary(hydratedComponent);
return;
await addToComponentLibrary(hydratedComponent);
} else {
await removeFromComponentLibrary(component);
}
}, [
component,
isUserComponent,
addToComponentLibrary,
removeFromComponentLibrary,
]);

handleDelete();
}, [component, isInLibrary, addToComponentLibrary, handleDelete]);

const showDeleteButton = isInLibrary && isUserComponent && !hideDelete;
const showDeleteButton = !isInLibrary && isUserComponent && !hideDelete;

return (
<>
{!isInLibrary && <AddToLibraryButton onClick={openConfirmationDialog} />}

{isInLibrary && !isUserComponent && (
<FavoriteToggleButton component={component} />
{!isInLibrary && !isUserComponent && (
<AddToLibraryButton onClick={openConfirmationDialog} />
)}

{isInLibrary && <FavoriteToggleButton component={component} />}

{showDeleteButton && (
<DeleteFromLibraryButton onClick={openConfirmationDialog} />
)}

<ConfirmationDialog
isOpen={isOpen}
title={
!isInLibrary
!isUserComponent
? "Add to Component Library?"
: "Delete custom component?"
}
description={
!isInLibrary
!isUserComponent
? `This will add "${displayName}" to your Component Library for use in your pipelines.`
: `"${displayName}" is a custom user component. Unstarring it will remove it from your library. This action cannot be undone.`
: `"${displayName}" is a custom user component. This will remove it from your library. This action cannot be undone.`
}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
</>
);
};

export const ComponentFavoriteToggle = withSuspenseWrapper(
ComponentFavoriteToggleInternal,
() => <Spinner size={10} />,
() => (
<IconStateButton disabled>
<Icon name="Star" />
</IconStateButton>
),
);
Original file line number Diff line number Diff line change
Expand Up @@ -536,29 +536,6 @@ describe("ComponentLibraryProvider - Component Management", () => {
result.current.checkIfUserComponent(standardComponent);
expect(isUserComponent).toBe(false);
});

it("should correctly check if library contains component", async () => {
const libraryComponent: ComponentReference = {
name: "library-component",
digest: "library-digest",
spec: mockComponentSpec,
};

mockFlattenFolders.mockReturnValue([libraryComponent]);
mockFilterToUniqueByDigest.mockReturnValue([libraryComponent]);

const { result } = renderHook(() => useComponentLibrary(), {
wrapper: createWrapper,
});

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

const containsComponent =
result.current.checkLibraryContainsComponent(libraryComponent);
expect(containsComponent).toBe(true);
});
});

describe("Component Favoriting", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ type ComponentLibraryContextType = {
favorited: boolean,
) => void;
checkIfUserComponent: (component: ComponentReference) => boolean;
checkLibraryContainsComponent: (component: ComponentReference) => boolean;

getComponentLibrary: (libraryName: AvailableComponentLibraries) => Library;
};
Expand Down Expand Up @@ -347,21 +346,6 @@ export const ComponentLibraryProvider = ({
[userComponentsFolder],
);

const checkLibraryContainsComponent = useCallback(
(component: ComponentReference) => {
if (!componentLibrary) return false;

if (checkIfUserComponent(component)) return true;

const uniqueComponents = filterToUniqueByDigest(
flattenFolders(componentLibrary),
);

return uniqueComponents.some((c) => c.digest === component.digest);
},
[componentLibrary, checkIfUserComponent],
);

/**
* Local component library search
*/
Expand Down Expand Up @@ -639,7 +623,6 @@ export const ComponentLibraryProvider = ({
removeFromComponentLibrary,
setComponentFavorite,
checkIfUserComponent,
checkLibraryContainsComponent,
}),
[
componentLibrary,
Expand All @@ -656,7 +639,6 @@ export const ComponentLibraryProvider = ({
removeFromComponentLibrary,
setComponentFavorite,
checkIfUserComponent,
checkLibraryContainsComponent,
],
);

Expand Down