Skip to content

Commit 36296b8

Browse files
committed
feat(web): add 'Add to Graph' button on entity detail pages
Adds a grape-colored action button next to Bookmark and Catalogue buttons on entity pages. Clicking adds the entity to the graph list with 'user' provenance for analysis in the Graph visualization. Button shows filled variant when entity is in graph, light when not. Includes mobile support in both action panel and sticky bottom bar.
1 parent dbea2fe commit 36296b8

File tree

1 file changed

+86
-1
lines changed

1 file changed

+86
-1
lines changed

apps/web/src/components/entity-detail/EntityDetailLayout.tsx

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type { EntityType } from "@bibgraph/types";
22
import { logger } from "@bibgraph/utils";
33
import { ActionIcon, Affix, Badge, Box, Code, Group, Modal, Paper, SegmentedControl, Stack, Text, Title, Tooltip } from "@mantine/core";
4-
import { IconBookmark, IconBookmarkFilled, IconBookmarkOff, IconCode, IconListCheck, IconMenu2, IconX } from "@tabler/icons-react";
4+
import { notifications } from "@mantine/notifications";
5+
import { IconBookmark, IconBookmarkFilled, IconBookmarkOff, IconCode, IconGraph, IconListCheck, IconMenu2, IconX } from "@tabler/icons-react";
56
import React, { ReactNode, useState } from "react";
67

78
import { BORDER_STYLE_GRAY_3, ICON_SIZE } from "@/config/style-constants";
89
import { useQueryBookmarking } from "@/hooks/use-query-bookmarking";
910
import { useResponsiveDesign } from "@/hooks/use-sprinkles";
1011
import { useThemeColors } from "@/hooks/use-theme-colors";
1112
import { useUserInteractions } from "@/hooks/use-user-interactions";
13+
import { useGraphList } from "@/hooks/useGraphList";
1214

1315
import { AddToListModal } from "../catalogue/AddToListModal";
1416
import { EntityDataDisplay } from "../EntityDataDisplay";
@@ -68,6 +70,13 @@ export const EntityDetailLayout = ({
6870
// Mobile navigation state
6971
const [mobileActionsOpen, setMobileActionsOpen] = useState(false);
7072

73+
// Graph list hook for adding entities to the graph
74+
const graphList = useGraphList();
75+
const [isAddingToGraph, setIsAddingToGraph] = useState(false);
76+
77+
// Check if entity is already in graph
78+
const isInGraph = graphList.nodes.some(node => node.entityId === entityId);
79+
7180
const handleBookmarkToggle = async () => {
7281
try {
7382
await (userInteractions.isBookmarked ? userInteractions.unbookmarkEntity() : userInteractions.bookmarkEntity({
@@ -91,6 +100,42 @@ export const EntityDetailLayout = ({
91100
logger.error("ui", "Failed to toggle query bookmark", { error, entityType, entityId });
92101
}
93102
};
103+
104+
const handleAddToGraphToggle = async () => {
105+
setIsAddingToGraph(true);
106+
try {
107+
if (isInGraph) {
108+
await graphList.removeNode(entityId);
109+
notifications.show({
110+
title: "Removed from Graph",
111+
message: `${displayName} removed from graph`,
112+
color: "gray",
113+
});
114+
} else {
115+
await graphList.addNode({
116+
entityId,
117+
entityType,
118+
label: displayName,
119+
provenance: "user",
120+
});
121+
notifications.show({
122+
title: "Added to Graph",
123+
message: `${displayName} added to graph for analysis`,
124+
color: "green",
125+
});
126+
}
127+
} catch (error) {
128+
logger.error("ui", "Failed to toggle graph list", { error, entityType, entityId });
129+
notifications.show({
130+
title: "Error",
131+
message: isInGraph ? "Failed to remove from graph" : "Failed to add to graph",
132+
color: "red",
133+
});
134+
} finally {
135+
setIsAddingToGraph(false);
136+
}
137+
};
138+
94139
return (
95140
<Box
96141
p={isMobile() ? "sm" : "xl"}
@@ -155,6 +200,19 @@ export const EntityDetailLayout = ({
155200

156201
<Group gap="sm">
157202
{/* Desktop Action Buttons */}
203+
<Tooltip label={isInGraph ? "Remove from graph" : "Add to graph for analysis"} position="bottom">
204+
<ActionIcon
205+
size="lg"
206+
variant={isInGraph ? "filled" : "light"}
207+
color="grape"
208+
onClick={handleAddToGraphToggle}
209+
loading={isAddingToGraph || graphList.loading}
210+
data-testid="add-to-graph-button"
211+
>
212+
<IconGraph size={ICON_SIZE.XL} />
213+
</ActionIcon>
214+
</Tooltip>
215+
158216
<Tooltip label="Add to catalogue list" position="bottom">
159217
<ActionIcon
160218
size="lg"
@@ -236,6 +294,20 @@ export const EntityDetailLayout = ({
236294
<Stack gap="sm">
237295
<Text size="sm" fw={600} c="dimmed">Actions</Text>
238296
<Group gap="sm" justify="space-around">
297+
<ActionIcon
298+
size="lg"
299+
variant={isInGraph ? "filled" : "light"}
300+
color="grape"
301+
onClick={() => {
302+
handleAddToGraphToggle();
303+
setMobileActionsOpen(false);
304+
}}
305+
loading={isAddingToGraph || graphList.loading}
306+
data-testid="mobile-add-to-graph-button"
307+
>
308+
<IconGraph size={ICON_SIZE.LG} />
309+
</ActionIcon>
310+
239311
<ActionIcon
240312
size="lg"
241313
variant="light"
@@ -403,6 +475,19 @@ export const EntityDetailLayout = ({
403475
}}
404476
>
405477
<Group justify="space-around" gap="xs">
478+
<Tooltip label={isInGraph ? "Remove from graph" : "Add to graph"} position="top">
479+
<ActionIcon
480+
size="lg"
481+
variant={isInGraph ? "filled" : "light"}
482+
color="grape"
483+
onClick={handleAddToGraphToggle}
484+
loading={isAddingToGraph || graphList.loading}
485+
aria-label={isInGraph ? "Remove from graph" : "Add to graph"}
486+
>
487+
<IconGraph size={ICON_SIZE.LG} />
488+
</ActionIcon>
489+
</Tooltip>
490+
406491
<Tooltip label="Add to list" position="top">
407492
<ActionIcon
408493
size="lg"

0 commit comments

Comments
 (0)