@@ -186,8 +186,8 @@ const NoteEditor: React.FC<{ issueId: number, onAddNote: (id: number, text: stri
186186 } ;
187187
188188 return (
189- < div style = { { position : 'absolute' , bottom : 0 , left : 0 , right : 0 , padding : '20px 30px' , background : 'var(-- editor-bg)' , backdropFilter : 'blur(20px)' , borderTop : '1px solid var(--editor-border)' , zIndex : 10 } } >
190- < div style = { { position : 'relative' } } >
189+ < div className = "note- editor-bar pane-footer" >
190+ < div style = { { position : 'relative' , flex : 1 } } >
191191 < textarea
192192 className = "note-input"
193193 value = { noteText }
@@ -199,9 +199,9 @@ const NoteEditor: React.FC<{ issueId: number, onAddNote: (id: number, text: stri
199199 }
200200 } }
201201 placeholder = "Add a note... (Shift + Enter to send)"
202- style = { { width : '100%' , height : 44 , background : 'var(--input-bg )' , border : '1px solid var(--input-border) ' , borderRadius : 12 , padding : '12px 50px 12px 15px' , color : 'var(--text-primary)' , resize : 'none' , fontSize : 13 , transition : 'all 0.2s' } }
202+ style = { { width : '100%' , height : 36 , display : 'block' , background : 'rgba(255,255,255,0.05 )' , border : 'none ' , borderRadius : 8 , padding : '9px 50px 9px 15px' , color : 'var(--text-primary)' , resize : 'none' , fontSize : 13 , transition : 'all 0.2s' , outline : 'none ' } }
203203 onFocus = { e => ( e . target as any ) . style . height = '100px' }
204- onBlur = { e => { if ( ! noteText ) ( e . target as any ) . style . height = '44px ' } }
204+ onBlur = { e => { if ( ! noteText ) ( e . target as any ) . style . height = '36px ' } }
205205 />
206206 < button
207207 onClick = { handleSend }
@@ -1470,22 +1470,22 @@ const App: React.FC = () => {
14701470 </ div >
14711471
14721472 { /* Sidebar Footer with Settings */ }
1473- < div style = { { padding : '15px' , borderTop : '1px solid #222' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center ' } } >
1473+ < div className = "pane-footer" style = { { borderTop : '1px solid var(--border-color) ' } } >
14741474 < div
14751475 className = "sidebar-item"
14761476 onClick = { ( ) => setShowSettings ( true ) }
1477- style = { { margin : 0 , padding : '8px 12px' , flex : 1 , textAlign : 'center' , background : 'rgba(255,255,255,0.05)' , borderRadius : 8 , fontSize : 13 } }
1477+ style = { { margin : 0 , padding : 0 , flex : 1 , height : 36 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , background : 'rgba(255,255,255,0.05)' , borderRadius : 8 , fontSize : 13 } }
14781478 >
14791479 ⚙️ Settings
14801480 </ div >
14811481 </ div >
14821482 </ aside >
14831483
1484- { /* Sidebar Resize Handle */ }
14851484 < div
1485+ className = "no-drag"
14861486 onMouseDown = { ( ) => setResizingPane ( 'sidebar' ) }
1487- style = { { width : 4 , cursor : 'col-resize' , background : resizingPane === 'sidebar' ? '#0c66ff' : 'transparent' , flexShrink : 0 } }
1488- onMouseEnter = { e => ( e . target as HTMLDivElement ) . style . background = '#333 ' }
1487+ style = { { width : 4 , cursor : 'col-resize' , background : resizingPane === 'sidebar' ? '#0c66ff' : 'transparent' , flexShrink : 0 , margin : '0 -2px' , zIndex : 100 , transition : 'background 0.2s' } }
1488+ onMouseEnter = { e => ( e . target as HTMLDivElement ) . style . background = '#0c66ff ' }
14891489 onMouseLeave = { e => ( e . target as HTMLDivElement ) . style . background = resizingPane === 'sidebar' ? '#0c66ff' : 'transparent' }
14901490 />
14911491
@@ -1705,7 +1705,7 @@ const App: React.FC = () => {
17051705
17061706 < div
17071707 ref = { issueListRef }
1708- style = { { overflowY : 'auto' , flex : 1 , paddingBottom : 60 , position : 'relative' } }
1708+ style = { { overflowY : 'auto' , flex : 1 , paddingBottom : 20 , position : 'relative' } }
17091709 >
17101710 < div className = "issue-list-content" ref = { listRef } >
17111711 { /* Sliding Selection Indicator */ }
@@ -1778,64 +1778,66 @@ const App: React.FC = () => {
17781778 </ div >
17791779 </ div >
17801780
1781- < div className = "add-task-bar" style = { { position : 'absolute' , bottom : 0 , left : 0 , right : 0 } } >
1782- < span style = { { color : '#0c66ff' , fontSize : 20 , cursor : 'pointer' } } > +</ span >
1783- < input type = "text" placeholder = "快速添加任务..." value = { newTaskSubject } onChange = { e => setNewTaskSubject ( e . target . value ) } onKeyDown = { e => {
1784- if ( e . key === 'Enter' && newTaskSubject . trim ( ) && vm . selectedProjectId !== - 1 ) {
1785- vm . createIssue ( newTaskSubject , vm . selectedProjectId ! , quickAddVersionId || undefined , quickAddAssigneeId || undefined ) ;
1786- setNewTaskSubject ( '' ) ;
1787- }
1788- } } style = { { flex : 1 , background : 'transparent' , border : 'none' , color : 'var(--text-primary)' , padding : 8 } } />
1789- < div style = { { display : 'flex' , gap : 10 , alignItems : 'center' } } >
1790- { vm . selectedProjectId !== - 1 && vm . selectedProjectId !== null && (
1781+ < div className = "add-task-bar pane-footer" >
1782+ < div style = { { display : 'flex' , alignItems : 'center' , gap : 10 , background : 'rgba(255,255,255,0.05)' , borderRadius : 8 , padding : '0 12px' , flex : 1 , height : 36 } } >
1783+ < span style = { { color : '#0c66ff' , fontSize : 18 , cursor : 'pointer' , display : 'flex' , alignItems : 'center' } } > +</ span >
1784+ < input type = "text" placeholder = "快速添加任务..." value = { newTaskSubject } onChange = { e => setNewTaskSubject ( e . target . value ) } onKeyDown = { e => {
1785+ if ( e . key === 'Enter' && newTaskSubject . trim ( ) && vm . selectedProjectId !== - 1 ) {
1786+ vm . createIssue ( newTaskSubject , vm . selectedProjectId ! , quickAddVersionId || undefined , quickAddAssigneeId || undefined ) ;
1787+ setNewTaskSubject ( '' ) ;
1788+ }
1789+ } } style = { { flex : 1 , background : 'transparent' , border : 'none' , color : 'var(--text-primary)' , padding : '4px 0' , fontSize : 13 , outline : 'none' } } />
1790+ < div style = { { display : 'flex' , gap : 10 , alignItems : 'center' } } >
1791+ { vm . selectedProjectId !== - 1 && vm . selectedProjectId !== null && (
1792+ < span style = { { fontSize : 11 , color : '#888' , position : 'relative' } } >
1793+ { ( vm . projectVersionsMap [ vm . selectedProjectId ] || [ ] ) . find ( ( v : { id : number } ) => v . id === quickAddVersionId ) ?. name || '无版本' }
1794+ < select
1795+ value = { quickAddVersionId || '' }
1796+ onChange = { e => setQuickAddVersionId ( e . target . value ? parseInt ( e . target . value ) : null ) }
1797+ style = { { position : 'absolute' , left : 0 , top : 0 , width : '100%' , height : '100%' , opacity : 0 , cursor : 'pointer' } }
1798+ >
1799+ < option value = "" > 无版本</ option >
1800+ { ( vm . projectVersionsMap [ vm . selectedProjectId ] || [ ] ) . map ( ( v : { id : number ; name : string } ) => (
1801+ < option key = { v . id } value = { v . id } > { v . name } </ option >
1802+ ) ) }
1803+ </ select >
1804+ < span style = { { marginLeft : 3 , fontSize : 10 , color : '#666' } } > ⌄</ span >
1805+ </ span >
1806+ ) }
17911807 < span style = { { fontSize : 11 , color : '#888' , position : 'relative' } } >
1792- { ( vm . projectVersionsMap [ vm . selectedProjectId ] || [ ] ) . find ( ( v : { id : number } ) => v . id === quickAddVersionId ) ?. name || '无版本' }
1808+ { quickAddAssigneeId === null
1809+ ? '👤 暂未指派'
1810+ : currentProjectMembers . find ( m => m . id === quickAddAssigneeId ) ?. name || '👤 暂未指派' }
17931811 < select
1794- value = { quickAddVersionId || '' }
1795- onChange = { e => setQuickAddVersionId ( e . target . value ? parseInt ( e . target . value ) : null ) }
1812+ value = { quickAddAssigneeId || '' }
1813+ onChange = { e => setQuickAddAssigneeId ( e . target . value ? parseInt ( e . target . value ) : null ) }
17961814 style = { { position : 'absolute' , left : 0 , top : 0 , width : '100%' , height : '100%' , opacity : 0 , cursor : 'pointer' } }
17971815 >
1798- < option value = "" > 无版本</ option >
1799- { ( vm . projectVersionsMap [ vm . selectedProjectId ] || [ ] ) . map ( ( v : { id : number ; name : string } ) => (
1800- < option key = { v . id } value = { v . id } > { v . name } </ option >
1801- ) ) }
1816+ < option value = "" > 👤 暂未指派</ option >
1817+ { renderGroupedMemberOptions ( currentProjectMembers ) }
18021818 </ select >
18031819 < span style = { { marginLeft : 3 , fontSize : 10 , color : '#666' } } > ⌄</ span >
18041820 </ span >
1821+ </ div >
1822+ { vm . selectedProjectId === - 1 && (
1823+ < div style = { { fontSize : 11 , color : '#444' } } > 请选择项目</ div >
18051824 ) }
1806- < span style = { { fontSize : 11 , color : '#888' , position : 'relative' } } >
1807- { quickAddAssigneeId === null
1808- ? '👤 暂未指派'
1809- : currentProjectMembers . find ( m => m . id === quickAddAssigneeId ) ?. name || '👤 暂未指派' }
1810- < select
1811- value = { quickAddAssigneeId || '' }
1812- onChange = { e => setQuickAddAssigneeId ( e . target . value ? parseInt ( e . target . value ) : null ) }
1813- style = { { position : 'absolute' , left : 0 , top : 0 , width : '100%' , height : '100%' , opacity : 0 , cursor : 'pointer' } }
1814- >
1815- < option value = "" > 👤 暂未指派</ option >
1816- { renderGroupedMemberOptions ( currentProjectMembers ) }
1817- </ select >
1818- < span style = { { marginLeft : 3 , fontSize : 10 , color : '#666' } } > ⌄</ span >
1819- </ span >
18201825 </ div >
1821- { vm . selectedProjectId === - 1 && (
1822- < div style = { { fontSize : 11 , color : '#444' } } > 请选择项目</ div >
1823- ) }
18241826 </ div >
18251827 </ section >
18261828
1827- { /* List Resize Handle */ }
18281829 < div
1830+ className = "no-drag"
18291831 onMouseDown = { ( ) => setResizingPane ( 'list' ) }
1830- style = { { width : 4 , cursor : 'col-resize' , background : resizingPane === 'list' ? '#0c66ff' : 'transparent' , flexShrink : 0 } }
1831- onMouseEnter = { e => ( e . target as HTMLDivElement ) . style . background = '#333 ' }
1832+ style = { { width : 4 , cursor : 'col-resize' , background : resizingPane === 'list' ? '#0c66ff' : 'transparent' , flexShrink : 0 , margin : '0 -2px' , zIndex : 100 , transition : 'background 0.2s' } }
1833+ onMouseEnter = { e => ( e . target as HTMLDivElement ) . style . background = '#0c66ff ' }
18321834 onMouseLeave = { e => ( e . target as HTMLDivElement ) . style . background = resizingPane === 'list' ? '#0c66ff' : 'transparent' }
18331835 />
18341836
18351837 { /* Detail */ }
18361838 < main className = "issue-detail-pane" ref = { detailPaneRef } >
18371839 { selectedIssue ? (
1838- < div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' , position : 'relative ' } } >
1840+ < div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' , alignItems : 'stretch ' } } >
18391841 < div style = { { padding : '40px 30px 20px' } } >
18401842 { /* ID and actions row */ }
18411843 < div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 10 } } >
@@ -2032,7 +2034,15 @@ const App: React.FC = () => {
20322034 < div style = { { fontSize : 12 , color : 'var(--text-secondary)' , marginTop : 15 } } > 创建人:{ selectedIssue . author . name } • 时间:{ format ( new Date ( selectedIssue . created_on ) , 'yyyy-MM-dd HH:mm' ) } </ div >
20332035 </ div >
20342036
2035- < div style = { { flex : 1 , overflowY : 'auto' , paddingBottom : 100 } } >
2037+ < div
2038+ style = { { flex : 1 , overflowY : 'auto' , overflowX : 'auto' , paddingBottom : 20 } }
2039+ onWheel = { ( e ) => {
2040+ if ( e . shiftKey && e . deltaY !== 0 ) {
2041+ e . currentTarget . scrollLeft += e . deltaY ;
2042+ e . preventDefault ( ) ;
2043+ }
2044+ } }
2045+ >
20362046 < div style = { { padding : '0 30px' } } >
20372047 < div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 15 } } >
20382048 < h3 style = { { fontSize : 13 , fontWeight : 500 , color : '#666' , textTransform : 'uppercase' } } > Description</ h3 >
@@ -2111,28 +2121,37 @@ const App: React.FC = () => {
21112121 { /* Attachments Section */ }
21122122 { ( ( ) => {
21132123 const currentDescription = editingDescription ? editDescriptionValue : ( selectedIssue . description || '' ) ;
2124+ const journals = selectedIssue . journals || [ ] ;
2125+ const allNotes = journals . map ( ( j : IssueJournal ) => j . notes || '' ) . join ( '\n' ) ;
2126+ const allText = currentDescription + '\n' + allNotes ;
21142127
2115- const isImageReferenced = ( filename : string , text : string ) => {
2128+ const isImageReferenced = ( filename : string , contentUrl : string | undefined , text : string ) => {
21162129 if ( ! text ) return false ;
2130+
2131+ // Check if full content_url appears in text (most reliable)
2132+ if ( contentUrl && text . includes ( contentUrl ) ) return true ;
2133+
21172134 const encoded = encodeURIComponent ( filename ) ;
2135+ const escapedFilename = filename . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
2136+ const escapedEncoded = encoded . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
21182137
2119- // Check for Textile image syntax: !filename! or !filename(alt)!
2120- const textilePattern = new RegExp ( `!${ filename . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g , '\\$&' ) } (?:\\([^)]*\\))? !` , 'g ' ) ;
2121- const textileEncodedPattern = new RegExp ( `!${ encoded . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g , '\\$&' ) } (?:\\([^)]*\\))? !` , 'g ' ) ;
2138+ // Check for Textile image syntax: !filename!
2139+ const textilePattern = new RegExp ( `!${ escapedFilename } !` , 'i ' ) ;
2140+ const textileEncodedPattern = new RegExp ( `!${ escapedEncoded } !` , 'i ' ) ;
21222141
2123- // Check for Markdown image syntax:  or 
2124- const markdownPattern = new RegExp ( `!\\[[^\\]]*\\]\\((?:attachment:)?${ filename . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } \\)` , 'g' ) ;
2125- const markdownEncodedPattern = new RegExp ( `!\\[[^\\]]*\\]\\((?:attachment:)?${ encoded . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } \\)` , 'g' ) ;
2142+ // Check for Markdown image syntax:  or 
2143+ const hasMarkdown = text . includes ( `(${ filename } )` ) ||
2144+ text . includes ( `(${ encoded } )` ) ||
2145+ text . includes ( `(attachment:${ filename } )` ) ||
2146+ text . includes ( `(attachment:${ encoded } )` ) ;
21262147
2127- return textilePattern . test ( text ) ||
2128- textileEncodedPattern . test ( text ) ||
2129- markdownPattern . test ( text ) ||
2130- markdownEncodedPattern . test ( text ) ;
2148+ if ( hasMarkdown ) return true ;
2149+ return textilePattern . test ( text ) || textileEncodedPattern . test ( text ) ;
21312150 } ;
21322151
21332152 const filteredAttachments = selectedIssue . attachments ?. filter ( a => {
21342153 if ( a . content_type ?. startsWith ( 'image/' ) ) {
2135- if ( isImageReferenced ( a . filename , currentDescription ) ) return false ;
2154+ if ( isImageReferenced ( a . filename , a . content_url , allText ) ) return false ;
21362155 }
21372156 return true ;
21382157 } ) || [ ] ;
@@ -2141,12 +2160,12 @@ const App: React.FC = () => {
21412160 if ( u . content_type ?. startsWith ( 'image/' ) ) {
21422161 // Check if tempUrl is used in markdown image syntax:  or textile !data:...!
21432162 const isTempInImage = u . tempUrl && (
2144- currentDescription . includes ( `](${ u . tempUrl } )` ) ||
2145- currentDescription . includes ( `!${ u . tempUrl } !` )
2163+ allText . includes ( `](${ u . tempUrl } )` ) ||
2164+ allText . includes ( `!${ u . tempUrl } !` )
21462165 ) ;
21472166
21482167 if ( isTempInImage ) return false ;
2149- if ( isImageReferenced ( u . filename , currentDescription ) ) return false ;
2168+ if ( isImageReferenced ( u . filename , undefined , allText ) ) return false ;
21502169 }
21512170 return true ;
21522171 } ) ;
0 commit comments