33 * Refactored to use catalogue-based history system via useUserInteractions hook
44 */
55
6+ import type { EntityType } from "@bibgraph/types" ;
67import { logError , logger } from "@bibgraph/utils/logger" ;
78import { type CatalogueEntity , catalogueService } from "@bibgraph/utils/storage/catalogue-db" ;
89import {
@@ -11,6 +12,7 @@ import {
1112 Card ,
1213 Divider ,
1314 Group ,
15+ Skeleton ,
1416 Stack ,
1517 Text ,
1618 TextInput ,
@@ -27,9 +29,133 @@ import {
2729import { useNavigate } from "@tanstack/react-router" ;
2830import { useState } from "react" ;
2931
32+ import { useEntityDisplayName } from "@/hooks/use-entity-display-name" ;
33+
3034import { BORDER_STYLE_GRAY_3 , ICON_SIZE } from "@/config/style-constants" ;
3135import { 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 ( / U R L : ( [ ^ \n ] + ) / ) ?. [ 1 ] ;
56+ const titleFromNotes = entry . notes ?. match ( / T i t l e : ( [ ^ \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+
33159interface 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 ( / T i t l e : ( [ ^ \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