fix: NavigationService initialization timing issue and add tab drag reordering (#9700)

* fix: NavigationService initialization timing issue and add tab drag reordering

- Fix NavigationService timing issue in TabsService by adding fallback navigation with setTimeout
- Add drag and drop functionality for tab reordering with visual feedback
- Remove unused MessageSquareDiff icon from Navbar
- Add reorderTabs action to tabs store

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update tabs.ts

* Update TabContainer.tsx

* Update TabContainer.tsx

* fix(dnd): horizontal sortable (#9827)

* refactor(CodeViewer): improve props, aligned to CodeEditor (#9786)

* refactor(CodeViewer): improve props, aligned to CodeEditor

* refactor: simplify internal variables

* refactor: remove default lineNumbers

* fix: shiki theme container style

* revert: use ReactMarkdown for prompt editing

* fix: draggable list id type (#9809)

* refactor(dnd): rename idKey to itemKey for clarity

* refactor: key and id type for draggable lists

* chore: update yarn lock

* fix: type error

* refactor: improve getId fallbacks

* feat: integrate file selection and upload functionality in KnowledgeFiles component (#9815)

* feat: integrate file selection and upload functionality in KnowledgeFiles component

- Added useFiles hook to manage file selection.
- Updated handleAddFile to utilize the new file selection logic, allowing multiple file uploads.
- Improved user experience by handling file uploads asynchronously and logging the results.

* feat: enhance file upload interaction in KnowledgeFiles component

- Wrapped Dragger component in a div to allow for custom click handling.
- Prevented default click behavior to improve user experience when adding files.
- Maintained existing file upload functionality while enhancing the UI interaction.

* refactor(KnowledgeFiles): 提取文件处理逻辑到独立函数

将重复的文件上传和处理逻辑提取到独立的processFiles函数中,提高代码复用性和可维护性

---------

Co-authored-by: icarus <eurfelux@gmail.com>

* fix(Sortable): correct gap and horizontal style

* feat: make tabs sortable (example)

* refactor: improve sortable direction and gap

* refactor: update example

* fix: remove useless states

---------

Co-authored-by: beyondkmp <beyondkmp@gmail.com>
Co-authored-by: icarus <eurfelux@gmail.com>
Co-authored-by: Pleasure1234 <3196812536@qq.com>

* fix: syntax error

* refactor: remove useless styles

* fix: tabs overflow, add scrollbar

* fix: button gap

* fix: app region drag

* refactor: remove scrollbar, add space for app dragging

* Revert "refactor: remove scrollbar, add space for app dragging"

This reverts commit f6ebeb143e.

* refactor: update style

* refactor: add a scroll-to-right button

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: one <wangan.cs@gmail.com>
Co-authored-by: beyondkmp <beyondkmp@gmail.com>
Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
Pleasure1234 2025-09-04 02:42:42 +08:00 committed by GitHub
parent a9a38f88bb
commit 0a36869b3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 177 additions and 45 deletions

View File

@ -1,4 +1,7 @@
import { PlusOutlined } from '@ant-design/icons'
import { TopNavbarOpenedMinappTabs } from '@renderer/components/app/PinnedMinapps'
import { Sortable, useDndReorder } from '@renderer/components/dnd'
import Scrollbar from '@renderer/components/Scrollbar'
import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
@ -7,11 +10,12 @@ import { getThemeModeLabel, getTitleLabel } from '@renderer/i18n/label'
import tabsService from '@renderer/services/TabsService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import type { Tab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
import { ThemeMode } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { Tooltip } from 'antd'
import { Button, Tooltip } from 'antd'
import {
ChevronRight,
FileSearch,
Folder,
Hammer,
@ -28,13 +32,11 @@ import {
Terminal,
X
} from 'lucide-react'
import { useCallback, useEffect } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { TopNavbarOpenedMinappTabs } from '../app/PinnedMinapps'
interface TabsContainerProps {
children: React.ReactNode
}
@ -81,6 +83,8 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const { settedTheme, toggleTheme } = useTheme()
const { hideMinappPopup } = useMinappPopup()
const { t } = useTranslation()
const scrollRef = useRef<HTMLDivElement>(null)
const [canScroll, setCanScroll] = useState(false)
const getTabId = (path: string): string => {
if (path === '/') return 'home'
@ -142,34 +146,83 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
navigate(tab.path)
}
const handleScrollRight = () => {
scrollRef.current?.scrollBy({ left: 200, behavior: 'smooth' })
}
useEffect(() => {
const scrollElement = scrollRef.current
if (!scrollElement) return
const checkScrollability = () => {
setCanScroll(scrollElement.scrollWidth > scrollElement.clientWidth)
}
checkScrollability()
const resizeObserver = new ResizeObserver(checkScrollability)
resizeObserver.observe(scrollElement)
window.addEventListener('resize', checkScrollability)
return () => {
resizeObserver.disconnect()
window.removeEventListener('resize', checkScrollability)
}
}, [tabs])
const visibleTabs = useMemo(() => tabs.filter((tab) => !specialTabs.includes(tab.id)), [tabs])
const { onSortEnd } = useDndReorder<Tab>({
originalList: tabs,
filteredList: visibleTabs,
onUpdate: (newTabs) => dispatch(setTabs(newTabs)),
itemKey: 'id'
})
return (
<Container>
<TabsBar $isFullscreen={isFullscreen}>
{tabs
.filter((tab) => !specialTabs.includes(tab.id))
.map((tab) => {
return (
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
<TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
<TabTitle>{getTitleLabel(tab.id)}</TabTitle>
</TabHeader>
{tab.id !== 'home' && (
<CloseButton
className="close-button"
onClick={(e) => {
e.stopPropagation()
closeTab(tab.id)
}}>
<X size={12} />
</CloseButton>
)}
</Tab>
)
})}
<AddTabButton onClick={handleAddTab} className={classNames({ active: activeTabId === 'launchpad' })}>
<PlusOutlined />
</AddTabButton>
<TabsArea>
<TabsScroll ref={scrollRef}>
<Sortable
items={visibleTabs}
itemKey="id"
layout="list"
horizontal
gap={'6px'}
onSortEnd={onSortEnd}
className="tabs-sortable"
renderItem={(tab) => (
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
<TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
<TabTitle>{getTitleLabel(tab.id)}</TabTitle>
</TabHeader>
{tab.id !== 'home' && (
<CloseButton
className="close-button"
data-no-dnd
onClick={(e) => {
e.stopPropagation()
closeTab(tab.id)
}}>
<X size={12} />
</CloseButton>
)}
</Tab>
)}
/>
</TabsScroll>
{canScroll && (
<ScrollButton onClick={handleScrollRight} className="scroll-right-button" shape="circle" size="small">
<ChevronRight size={16} />
</ScrollButton>
)}
<AddTabButton onClick={handleAddTab} className={classNames({ active: activeTabId === 'launchpad' })}>
<PlusOutlined />
</AddTabButton>
</TabsArea>
<RightButtonsContainer>
<TopNavbarOpenedMinappTabs />
<Tooltip
@ -200,6 +253,7 @@ const Container = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
`
const TabsBar = styled.div<{ $isFullscreen: boolean }>`
@ -221,6 +275,34 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>`
}
`
const TabsArea = styled.div`
display: flex;
align-items: center;
flex: 1 1 auto;
min-width: 0;
gap: 6px;
padding-right: 2rem;
position: relative;
-webkit-app-region: drag;
> * {
-webkit-app-region: no-drag;
}
&:hover {
.scroll-right-button {
opacity: 1;
}
}
`
const TabsScroll = styled(Scrollbar)`
&::-webkit-scrollbar {
display: none;
}
`
const Tab = styled.div<{ active?: boolean }>`
display: flex;
align-items: center;
@ -228,12 +310,12 @@ const Tab = styled.div<{ active?: boolean }>`
padding: 4px 10px;
padding-right: 8px;
background: ${(props) => (props.active ? 'var(--color-list-item)' : 'transparent')};
transition: background 0.2s;
border-radius: var(--list-item-border-radius);
cursor: pointer;
user-select: none;
height: 30px;
min-width: 90px;
transition: background 0.2s;
.close-button {
opacity: 0;
transition: opacity 0.2s;
@ -251,12 +333,15 @@ const TabHeader = styled.div`
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
flex: 1;
`
const TabIcon = styled.span`
display: flex;
align-items: center;
color: var(--color-text-2);
flex-shrink: 0;
`
const TabTitle = styled.span`
@ -265,6 +350,8 @@ const TabTitle = styled.span`
display: flex;
align-items: center;
margin-right: 4px;
overflow: hidden;
white-space: nowrap;
`
const CloseButton = styled.span`
@ -284,6 +371,7 @@ const AddTabButton = styled.div`
cursor: pointer;
color: var(--color-text-2);
border-radius: var(--list-item-border-radius);
flex-shrink: 0;
&.active {
background: var(--color-list-item);
}
@ -292,11 +380,28 @@ const AddTabButton = styled.div`
}
`
const ScrollButton = styled(Button)`
position: absolute;
right: 4rem;
top: 50%;
transform: translateY(-50%);
z-index: 1;
opacity: 0;
transition: opacity 0.2s ease-in-out;
border: none;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
`
const RightButtonsContainer = styled.div`
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
flex-shrink: 0;
`
const ThemeButton = styled.div`

View File

@ -56,6 +56,8 @@ interface SortableProps<T> {
listStyle?: React.CSSProperties
/** Ghost item style */
ghostItemStyle?: React.CSSProperties
/** Item gap */
gap?: number | string
}
function Sortable<T>({
@ -70,7 +72,8 @@ function Sortable<T>({
useDragOverlay = true,
showGhost = false,
className,
listStyle
listStyle,
gap
}: SortableProps<T>) {
const sensors = useSensors(
useSensor(PortalSafePointerSensor, {
@ -150,7 +153,12 @@ function Sortable<T>({
onDragCancel={handleDragCancel}
modifiers={modifiers}>
<SortableContext items={itemIds} strategy={strategy}>
<ListWrapper className={className} data-layout={layout} style={listStyle}>
<ListWrapper
className={className}
data-layout={layout}
data-direction={horizontal ? 'horizontal' : 'vertical'}
$gap={gap}
style={listStyle}>
{items.map((item, index) => (
<SortableItem
key={itemIds[index]}
@ -176,17 +184,31 @@ function Sortable<T>({
)
}
const ListWrapper = styled.div`
const ListWrapper = styled.div<{ $gap?: number | string }>`
gap: ${({ $gap }) => $gap};
&[data-layout='grid'] {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
width: 100%;
gap: 12px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
&[data-layout='list'] {
display: flex;
align-items: center;
[data-direction='horizontal'] {
flex-direction: row;
}
[data-direction='vertical'] {
flex-direction: column;
}
}
`
export default Sortable

View File

@ -14,7 +14,7 @@ import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { Menu, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { FC } from 'react'
import styled from 'styled-components'
@ -83,11 +83,6 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</NavbarLeft>
</motion.div>
)}

View File

@ -251,6 +251,7 @@ const McpServersList: FC = () => {
itemKey="id"
onSortEnd={onSortEnd}
layout="grid"
gap={'12px'}
useDragOverlay
showGhost
renderItem={(server) => (

View File

@ -34,12 +34,18 @@ class TabsService {
const remainingTabs = tabs.filter((tab) => tab.id !== tabId)
const lastTab = remainingTabs[remainingTabs.length - 1]
store.dispatch(setActiveTab(lastTab.id))
// 使用 NavigationService 导航到新的标签页
if (NavigationService.navigate) {
NavigationService.navigate(lastTab.path)
} else {
logger.error('Navigation service is not initialized')
return false
logger.warn('Navigation service not ready, will navigate on next render')
setTimeout(() => {
if (NavigationService.navigate) {
NavigationService.navigate(lastTab.path)
}
}, 100)
}
}

View File

@ -24,6 +24,9 @@ const tabsSlice = createSlice({
name: 'tabs',
initialState,
reducers: {
setTabs: (state, action: PayloadAction<Tab[]>) => {
state.tabs = action.payload
},
addTab: (state, action: PayloadAction<Tab>) => {
const existingTab = state.tabs.find((tab) => tab.path === action.payload.path)
if (!existingTab) {
@ -53,5 +56,5 @@ const tabsSlice = createSlice({
}
})
export const { addTab, removeTab, setActiveTab, updateTab } = tabsSlice.actions
export const { setTabs, addTab, removeTab, setActiveTab, updateTab } = tabsSlice.actions
export default tabsSlice.reducer