diff --git a/packages/graph-explorer/src/modules/SearchSidebar/BundleSearchResult.tsx b/packages/graph-explorer/src/modules/SearchSidebar/BundleSearchResult.tsx
index 16a72415e..67d25ebc7 100644
--- a/packages/graph-explorer/src/modules/SearchSidebar/BundleSearchResult.tsx
+++ b/packages/graph-explorer/src/modules/SearchSidebar/BundleSearchResult.tsx
@@ -1,18 +1,29 @@
-import { BracketsIcon } from "lucide-react";
+import { useAtomValue } from "jotai";
+import { BracketsIcon, MinusCircleIcon, PlusCircleIcon } from "lucide-react";
import {
+ Button,
+ type ButtonProps,
CollapsibleContent,
SearchResultCollapsible,
SearchResultCollapsibleTrigger,
SearchResultSubtitle,
SearchResultSymbol,
SearchResultTitle,
+ Spinner,
+ stopPropagation,
} from "@/components";
import {
+ getAllGraphableEntities,
getDisplayValueForBundle,
type PatchedResultBundle,
} from "@/connector/entities";
-import { useTextTransform } from "@/hooks";
+import { edgesAtom, nodesAtom } from "@/core";
+import {
+ useAddToGraphMutation,
+ useRemoveFromGraph,
+ useTextTransform,
+} from "@/hooks";
import { createEntityKey, EntitySearchResult } from "./EntitySearchResult";
@@ -33,10 +44,11 @@ export function BundleSearchResult({
-
+
{title && {title}}
{subtitle}
+
@@ -50,3 +62,72 @@ export function BundleSearchResult({
);
}
+
+/**
+ * Adds or removes all the graphable entities (vertices and edges) contained
+ * within a bundle, including nested bundles.
+ *
+ * - Hidden entirely when the bundle contains no graphable entities.
+ * - Shows an add button when any entity in the bundle is missing from the
+ * graph. Clicking it adds the remaining entities (already-added ones are
+ * left untouched).
+ * - Shows a remove button once every entity in the bundle has been added.
+ * Clicking it removes all of them from the graph.
+ */
+function AddOrRemoveAllButton({
+ bundle,
+ ...props
+}: ButtonProps & { bundle: PatchedResultBundle }) {
+ const graphableEntities = getAllGraphableEntities(bundle.values);
+ const vertexIds = graphableEntities.vertices.map(vertex => vertex.id);
+ const edgeIds = graphableEntities.edges.map(edge => edge.id);
+
+ const nodesInGraph = useAtomValue(nodesAtom);
+ const edgesInGraph = useAtomValue(edgesAtom);
+
+ const mutation = useAddToGraphMutation();
+ const removeFromGraph = useRemoveFromGraph();
+
+ const hasGraphableEntities = vertexIds.length + edgeIds.length > 0;
+ if (!hasGraphableEntities) {
+ return null;
+ }
+
+ const allAdded =
+ vertexIds.every(id => nodesInGraph.has(id)) &&
+ edgeIds.every(id => edgesInGraph.has(id));
+
+ if (allAdded) {
+ const removeAllFromGraph = () =>
+ removeFromGraph({ vertices: vertexIds, edges: edgeIds });
+
+ return (
+
+ );
+ }
+
+ const addAllToGraph = () => mutation.mutate(graphableEntities);
+
+ return (
+
+ );
+}