mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 08:29:07 +08:00
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:
parent
a9a38f88bb
commit
0a36869b3c
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -251,6 +251,7 @@ const McpServersList: FC = () => {
|
||||
itemKey="id"
|
||||
onSortEnd={onSortEnd}
|
||||
layout="grid"
|
||||
gap={'12px'}
|
||||
useDragOverlay
|
||||
showGhost
|
||||
renderItem={(server) => (
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user