1+ import { View } from '@/application/types' ;
2+ import { findAncestors } from '@/components/_shared/outline/utils' ;
3+ import { RichTooltip } from '@/components/_shared/popover' ;
4+ import PageIcon from '@/components/_shared/view-icon/PageIcon' ;
5+ import { useAppHandlers , useAppOutline } from '@/components/app/app.hooks' ;
6+ import { IconButton , Paper , Tooltip } from '@mui/material' ;
7+ import React , { useCallback , useMemo } from 'react' ;
8+ import { useTranslation } from 'react-i18next' ;
9+ import { ReactComponent as MoreIcon } from '@/assets/more.svg' ;
10+ import { ReactComponent as PrivateIcon } from '@/assets/lock.svg' ;
11+
12+ function ListItem ( {
13+ selectedView,
14+ view,
15+ onClick,
16+ onClose,
17+ } : {
18+ selectedView : string ;
19+ view : View ;
20+ onClick : ( ) => void ;
21+ onClose : ( ) => void ;
22+ } ) {
23+ const { t } = useTranslation ( ) ;
24+ const outline = useAppOutline ( ) ;
25+ const [ open , setOpen ] = React . useState < boolean > ( false ) ;
26+ const toView = useAppHandlers ( ) . toView ;
27+
28+ const ancestors = useMemo ( ( ) => {
29+ if ( ! outline ) return [ ] ;
30+ return findAncestors ( outline , view . view_id ) ?. slice ( 0 , - 1 ) || [ ] ;
31+ } , [ outline , view . view_id ] ) ;
32+
33+ const renderBreadcrumb = useCallback ( ( view : View ) => {
34+ const isPrivate = view . is_private && view . extra ?. is_space ;
35+
36+ return < Tooltip
37+ disableInteractive = { true }
38+ title = { view . name }
39+ >
40+ < div
41+ style = { {
42+ cursor : view . extra ?. is_space ? 'default' : 'pointer' ,
43+ } }
44+ onClick = { e => {
45+ e . stopPropagation ( ) ;
46+ if ( view . extra ?. is_space ) return ;
47+ void toView ( view . view_id ) ;
48+ onClose ( ) ;
49+ } }
50+ className = { `text-text-caption max-w-[250px] overflow-hidden ${ view . extra ?. is_space ? '' : 'hover:underline' } flex items-center gap-2` }
51+ >
52+ < span className = { 'truncate' } > { view . name || t ( 'menuAppHeader.defaultNewPageName' ) } </ span >
53+ { isPrivate &&
54+ < div className = { 'h-4 w-4 text-base min-w-4 text-text-title opacity-80' } >
55+ < PrivateIcon />
56+ </ div >
57+ }
58+ </ div >
59+ </ Tooltip > ;
60+ } , [ onClose , t , toView ] ) ;
61+
62+ const breadcrumbs = useMemo ( ( ) => {
63+ if ( ! ancestors ) return null ;
64+ if ( ancestors . length <= 3 ) {
65+ return ancestors . map ( ( ancestor , index ) => {
66+ return < div
67+ key = { ancestor . view_id }
68+ className = { 'flex items-center gap-2' }
69+ >
70+ { renderBreadcrumb ( ancestor ) }
71+ { index !== ancestors . length - 1 && < span > { '/' } </ span > }
72+ </ div > ;
73+ } ) ;
74+ }
75+
76+ const first = renderBreadcrumb ( ancestors [ 0 ] ) ;
77+ const last = renderBreadcrumb ( ancestors [ ancestors . length - 1 ] ) ;
78+
79+ return < >
80+ { first }
81+ < div className = { 'flex items-center gap-2' } >
82+ < span > { '/' } </ span >
83+ < RichTooltip
84+ open = { open }
85+ placement = "bottom"
86+ onClose = { ( ) => setOpen ( false ) }
87+ content = {
88+ < Paper className = { 'p-1' } >
89+ { ancestors . slice ( 1 , - 1 ) . map ( ( ancestor ) => {
90+ return < div
91+ key = { ancestor . view_id }
92+ className = { 'flex items-center w-full gap-2 p-1.5' }
93+ >
94+ { renderBreadcrumb ( ancestor ) }
95+ </ div > ;
96+ } ) }
97+ </ Paper >
98+ }
99+ >
100+ < IconButton
101+ onClick = { ( e ) => {
102+ e . stopPropagation ( ) ;
103+ setOpen ( ( prev ) => ! prev ) ;
104+ } }
105+ size = { 'small' }
106+ >
107+ < MoreIcon />
108+ </ IconButton >
109+ </ RichTooltip >
110+ < span > { '/' } </ span >
111+ { last }
112+ </ div >
113+ </ > ;
114+
115+ } , [ ancestors , open , renderBreadcrumb ] ) ;
116+
117+ return (
118+ < div
119+ data-item-id = { view . view_id }
120+ style = { {
121+ backgroundColor : selectedView === view . view_id ? 'var(--fill-list-active)' : undefined ,
122+ } }
123+ onClick = { onClick }
124+ className = { 'flex border-t border-line-default w-full p-4 cursor-pointer hover:bg-fill-list-active gap-2' }
125+ >
126+ < div className = { 'w-7 h-7 border flex items-center justify-center rounded border-line-border' } >
127+ < PageIcon
128+ view = { view }
129+ className = { 'w-4 h-4 flex items-center justify-center' }
130+ />
131+ </ div >
132+ < div className = { 'flex flex-col py-[3px] gap-2 w-full' } >
133+ < div className = { 'text-base font-medium flex-1 truncate' } >
134+ { view . name . trim ( ) || t ( 'menuAppHeader.defaultNewPageName' ) }
135+ </ div >
136+ { ancestors ?. length ?
137+ < div className = { 'text-sm text-text-caption overflow-hidden w-full gap-2 flex items-center' } >
138+ { breadcrumbs }
139+ </ div > : null }
140+
141+ </ div >
142+
143+ </ div >
144+ ) ;
145+ }
146+
147+ export default ListItem ;
0 commit comments