Skip to content

Commit b66d4ce

Browse files
committed
fix(web): show display names instead of entity IDs in history page
Extract HistoryEntryCard sub-component with useEntityDisplayName hook to fetch proper titles from OpenAlex API. Matches behavior of sidebar HistoryCard component. - Add NON_ENTITY_URL_PATTERNS for pages like /about, /settings - Show formatted entity type + shortened ID as fallback - Skip display name fetch for non-entity URLs - Show loading skeleton while fetching
1 parent 0c7db9a commit b66d4ce

File tree

1 file changed

+135
-67
lines changed

1 file changed

+135
-67
lines changed

apps/web/src/components/HistoryManager.tsx

Lines changed: 135 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Refactored to use catalogue-based history system via useUserInteractions hook
44
*/
55

6+
import type { EntityType } from "@bibgraph/types";
67
import { logError, logger } from "@bibgraph/utils/logger";
78
import { type CatalogueEntity,catalogueService } from "@bibgraph/utils/storage/catalogue-db";
89
import {
@@ -11,6 +12,7 @@ import {
1112
Card,
1213
Divider,
1314
Group,
15+
Skeleton,
1416
Stack,
1517
Text,
1618
TextInput,
@@ -27,9 +29,133 @@ import {
2729
import { useNavigate } from "@tanstack/react-router";
2830
import { useState } from "react";
2931

32+
import { useEntityDisplayName } from "@/hooks/use-entity-display-name";
33+
3034
import { BORDER_STYLE_GRAY_3, ICON_SIZE } from "@/config/style-constants";
3135
import { useUserInteractions } from "@/hooks/use-user-interactions";
3236

37+
/** Non-entity pages that shouldn't trigger display name fetches */
38+
const NON_ENTITY_URL_PATTERNS = ["/about", "/settings", "/history", "/bookmarks", "/catalogue"];
39+
40+
/**
41+
* Sub-component for rendering a single history entry with display name resolution
42+
*/
43+
interface HistoryEntryCardProps {
44+
entry: CatalogueEntity;
45+
onNavigate: (entry: CatalogueEntity) => void;
46+
onDelete: (entityRecordId: string, title?: string) => void;
47+
formatDate: (date: Date) => string;
48+
}
49+
50+
const HistoryEntryCard = ({ entry, onNavigate, onDelete, formatDate }: HistoryEntryCardProps) => {
51+
// Check if this is a special ID (search or list)
52+
const isSpecialId = entry.entityId.startsWith("search-") || entry.entityId.startsWith("list-");
53+
54+
// Try to extract URL and title from notes
55+
const urlFromNotes = entry.notes?.match(/URL: ([^\n]+)/)?.[1];
56+
const titleFromNotes = entry.notes?.match(/Title: ([^\n]+)/)?.[1];
57+
58+
// Check if URL points to a non-entity page
59+
const isNonEntityUrl = urlFromNotes && NON_ENTITY_URL_PATTERNS.some(pattern => urlFromNotes.includes(pattern));
60+
61+
// Only fetch display name for valid entity URLs
62+
const { displayName, isLoading } = useEntityDisplayName({
63+
entityId: entry.entityId,
64+
entityType: entry.entityType as EntityType,
65+
enabled: !isSpecialId && !isNonEntityUrl,
66+
});
67+
68+
// Format entity type for display (e.g., "works" -> "Work")
69+
const formatEntityType = (entityType: string): string => {
70+
const singular = entityType.endsWith("s") ? entityType.slice(0, -1) : entityType;
71+
return singular.charAt(0).toUpperCase() + singular.slice(1);
72+
};
73+
74+
// Format entity ID for display (shortened if long)
75+
const formatEntityId = (entityId: string): string => {
76+
const idMatch = entityId.match(/([A-Z]\d+)$/);
77+
if (idMatch) {
78+
const id = idMatch[1];
79+
return id.length > 8 ? `${id.slice(0, 8)}...` : id;
80+
}
81+
return entityId.length > 15 ? `${entityId.slice(0, 15)}...` : entityId;
82+
};
83+
84+
// Determine the title to display with proper priority
85+
let title: string;
86+
if (isSpecialId) {
87+
title = entry.entityId.startsWith("search-")
88+
? `Search: ${entry.entityId.replace("search-", "").split("-")[0]}`
89+
: `List: ${entry.entityId.replace("list-", "")}`;
90+
} else if (isNonEntityUrl && urlFromNotes) {
91+
// For non-entity pages, show the page name
92+
const pageName = urlFromNotes.replace(/.*[#/]/, "").split("/")[0];
93+
title = pageName.charAt(0).toUpperCase() + pageName.slice(1);
94+
} else if (displayName) {
95+
// Prefer freshly fetched display name
96+
title = displayName;
97+
} else if (titleFromNotes) {
98+
// Fall back to stored title from notes
99+
title = titleFromNotes;
100+
} else {
101+
// Improved fallback: "Work W1234567..." instead of "works: W123456789012345"
102+
title = `${formatEntityType(entry.entityType)} ${formatEntityId(entry.entityId)}`;
103+
}
104+
105+
return (
106+
<Card
107+
key={`${entry.entityId}-${entry.addedAt.getTime()}`}
108+
style={{ border: BORDER_STYLE_GRAY_3 }}
109+
padding="md"
110+
shadow="sm"
111+
>
112+
<Group justify="space-between" align="flex-start">
113+
<Stack gap="xs" style={{ flex: 1 }}>
114+
{isLoading && !titleFromNotes ? (
115+
<Skeleton height={16} width="60%" />
116+
) : (
117+
<Text size="sm" fw={500}>
118+
{title}
119+
</Text>
120+
)}
121+
{entry.notes && (
122+
<Text size="xs" c="dimmed" lineClamp={2}>
123+
{entry.notes.split('\n').filter(line => !line.startsWith('URL:') && !line.startsWith('Title:')).join('\n')}
124+
</Text>
125+
)}
126+
<Text size="xs" c="dimmed">
127+
{formatDate(new Date(entry.addedAt))}
128+
</Text>
129+
</Stack>
130+
<Group gap="xs">
131+
<Tooltip label="Navigate to this entry">
132+
<ActionIcon
133+
variant="light"
134+
color="blue"
135+
onClick={() => onNavigate(entry)}
136+
aria-label={`Navigate to ${title}`}
137+
>
138+
<IconExternalLink size={ICON_SIZE.MD} />
139+
</ActionIcon>
140+
</Tooltip>
141+
{entry.id && (
142+
<Tooltip label="Delete history entry">
143+
<ActionIcon
144+
variant="light"
145+
color="red"
146+
aria-label={`Delete ${title} from history`}
147+
onClick={() => onDelete(entry.id!, title)}
148+
>
149+
<IconTrash size={ICON_SIZE.MD} />
150+
</ActionIcon>
151+
</Tooltip>
152+
)}
153+
</Group>
154+
</Group>
155+
</Card>
156+
);
157+
};
158+
33159
interface HistoryManagerProps {
34160
onNavigate?: (url: string) => void;
35161
}
@@ -238,73 +364,15 @@ export const HistoryManager = ({ onNavigate }: HistoryManagerProps) => {
238364
{entries.length} {entries.length === 1 ? 'item' : 'items'}
239365
</Text>
240366
</Group>
241-
{entries.map((entry) => {
242-
// Extract title from entry notes or use entity ID
243-
let title = entry.entityId;
244-
const titleMatch = entry.notes?.match(/Title: ([^\n]+)/);
245-
if (titleMatch) {
246-
title = titleMatch[1];
247-
} else if (entry.entityId.startsWith("search-")) {
248-
title = `Search: ${entry.entityId.replace("search-", "").split("-")[0]}`;
249-
} else if (entry.entityId.startsWith("list-")) {
250-
title = `List: ${entry.entityId.replace("list-", "")}`;
251-
} else {
252-
title = `${entry.entityType}: ${entry.entityId}`;
253-
}
254-
255-
return (
256-
<Card
257-
key={`${entry.entityId}-${entry.addedAt.getTime()}`}
258-
style={{ border: BORDER_STYLE_GRAY_3 }}
259-
padding="md"
260-
shadow="sm"
261-
>
262-
<Group justify="space-between" align="flex-start">
263-
<Stack gap="xs" style={{ flex: 1 }}>
264-
<Text size="sm" fw={500}>
265-
{title}
266-
</Text>
267-
{entry.notes && (
268-
<Text size="xs" c="dimmed" lineClamp={2}>
269-
{entry.notes.split('\n').filter(line => !line.startsWith('URL:') && !line.startsWith('Title:')).join('\n')}
270-
</Text>
271-
)}
272-
<Text size="xs" c="dimmed">
273-
{formatDate(new Date(entry.addedAt))}
274-
</Text>
275-
</Stack>
276-
<Group gap="xs">
277-
<Tooltip label="Navigate to this entry">
278-
<ActionIcon
279-
variant="light"
280-
color="blue"
281-
onClick={() => handleNavigate(entry)}
282-
aria-label={`Navigate to ${title}`}
283-
>
284-
<IconExternalLink size={ICON_SIZE.MD} />
285-
</ActionIcon>
286-
</Tooltip>
287-
{entry.id && (
288-
<Tooltip label="Delete history entry">
289-
<ActionIcon
290-
variant="light"
291-
color="red"
292-
aria-label={`Delete ${title} from history`}
293-
onClick={() => {
294-
if (entry.id) {
295-
handleDeleteHistoryEntry(entry.id, title);
296-
}
297-
}}
298-
>
299-
<IconTrash size={ICON_SIZE.MD} />
300-
</ActionIcon>
301-
</Tooltip>
302-
)}
303-
</Group>
304-
</Group>
305-
</Card>
306-
);
307-
})}
367+
{entries.map((entry) => (
368+
<HistoryEntryCard
369+
key={`${entry.entityId}-${entry.addedAt.getTime()}`}
370+
entry={entry}
371+
onNavigate={handleNavigate}
372+
onDelete={handleDeleteHistoryEntry}
373+
formatDate={formatDate}
374+
/>
375+
))}
308376
{groupKey !== Object.keys(groupedEntries)[Object.keys(groupedEntries).length - 1] && (
309377
<Divider size="xs" my="xs" />
310378
)}

0 commit comments

Comments
 (0)