diff --git a/client/src/CommandBar/steps/Initial.tsx b/client/src/CommandBar/steps/Initial.tsx index 8722dbf617..5ae2105694 100644 --- a/client/src/CommandBar/steps/Initial.tsx +++ b/client/src/CommandBar/steps/Initial.tsx @@ -93,12 +93,11 @@ const InitialCommandBar = ({}: Props) => { ]; const projectItems: CommandBarItemGeneralType[] = projects .map( - (p, i): CommandBarItemGeneralType => ({ + (p): CommandBarItemGeneralType => ({ label: p.name, Icon: MagazineIcon, id: `project-${p.id}`, key: `project-${p.id}`, - shortcut: i < 9 ? ['cmd', (i + 1).toString()] : undefined, onClick: () => switchProject(p.id), footerHint: project?.id === p.id diff --git a/client/src/Project/CurrentTabContent/ChatTab/index.tsx b/client/src/Project/CurrentTabContent/ChatTab/index.tsx index 3a5eae4aa7..d223c000d1 100644 --- a/client/src/Project/CurrentTabContent/ChatTab/index.tsx +++ b/client/src/Project/CurrentTabContent/ChatTab/index.tsx @@ -20,6 +20,7 @@ import { ChatTabType } from '../../../types/general'; import { ProjectContext } from '../../../context/projectContext'; import { CommandBarContext } from '../../../context/commandBarContext'; import { openInSplitViewShortcut } from '../../../consts/commandBar'; +import { UIContext } from '../../../context/uiContext'; import Conversation from './Conversation'; import ActionsDropdown from './ActionsDropdown'; @@ -41,6 +42,7 @@ const ChatTab = ({ const { t } = useTranslation(); const { focusedPanel } = useContext(TabsContext.All); const { closeTab } = useContext(TabsContext.Handlers); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); const { setFocusedTabItems } = useContext(CommandBarContext.Handlers); const { project, refreshCurrentProjectConversations } = useContext( ProjectContext.Current, @@ -74,7 +76,10 @@ const ChatTab = ({ }, [handleMoveToAnotherSide], ); - useKeyboardNavigation(handleKeyEvent, focusedPanel !== side); + useKeyboardNavigation( + handleKeyEvent, + focusedPanel !== side || isLeftSidebarFocused, + ); useEffect(() => { if (focusedPanel === side) { @@ -108,21 +113,23 @@ const ChatTab = ({ /> {title || t('New chat')} - - - + + + )}
diff --git a/client/src/Project/CurrentTabContent/FileTab/index.tsx b/client/src/Project/CurrentTabContent/FileTab/index.tsx index bf7ee4312a..5ab6dd929e 100644 --- a/client/src/Project/CurrentTabContent/FileTab/index.tsx +++ b/client/src/Project/CurrentTabContent/FileTab/index.tsx @@ -38,6 +38,7 @@ import { CommandBarContext } from '../../../context/commandBarContext'; import { openInSplitViewShortcut } from '../../../consts/commandBar'; import BreadcrumbsPathContainer from '../../../components/Breadcrumbs/PathContainer'; import { RepositoriesContext } from '../../../context/repositoriesContext'; +import { UIContext } from '../../../context/uiContext'; import ActionsDropdown from './ActionsDropdown'; type Props = { @@ -76,6 +77,7 @@ const FileTab = ({ const [isPending, startTransition] = useTransition(); const { openNewTab, updateTabProperty } = useContext(TabsContext.Handlers); const { focusedPanel } = useContext(TabsContext.All); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); const { fileHighlights, hoveredLines } = useContext( FileHighlightsContext.Values, ); @@ -166,7 +168,7 @@ const FileTab = ({ ); useKeyboardNavigation( handleKeyEvent, - !file?.contents || focusedPanel !== side, + !file?.contents || focusedPanel !== side || isLeftSidebarFocused, ); useEffect(() => { @@ -227,21 +229,23 @@ const FileTab = ({ nonInteractive />
- - - + + + )}
{file?.lang === 'jupyter notebook' ? ( diff --git a/client/src/Project/CurrentTabContent/index.tsx b/client/src/Project/CurrentTabContent/index.tsx index c118a12910..88ba20755e 100644 --- a/client/src/Project/CurrentTabContent/index.tsx +++ b/client/src/Project/CurrentTabContent/index.tsx @@ -4,6 +4,7 @@ import { Trans } from 'react-i18next'; import { TabsContext } from '../../context/tabsContext'; import { DraggableTabItem, TabType, TabTypesEnum } from '../../types/general'; import { SplitViewIcon } from '../../icons'; +import { UIContext } from '../../context/uiContext'; import EmptyTab from './EmptyTab'; import FileTab from './FileTab'; import Header from './Header'; @@ -26,6 +27,7 @@ const CurrentTabContent = ({ TabsContext[side === 'left' ? 'CurrentLeft' : 'CurrentRight'], ); const { setFocusedPanel } = useContext(TabsContext.Handlers); + const { setIsLeftSidebarFocused } = useContext(UIContext.Focus); const [{ isOver, canDrop }, drop] = useDrop( () => ({ @@ -44,6 +46,7 @@ const CurrentTabContent = ({ const focusPanel = useCallback(() => { setFocusedPanel(side); + setIsLeftSidebarFocused(false); }, [side]); const handleMoveToAnotherSide = useCallback(() => { diff --git a/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx b/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx index 175a1f9dcd..bf55777d39 100644 --- a/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx +++ b/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx @@ -23,11 +23,18 @@ import ConversationEntry from './CoversationEntry'; type Props = { setExpanded: Dispatch>; isExpanded: boolean; + focusedIndex: string; + index: number; }; const reactRoot = document.getElementById('root')!; -const ConversationsNav = ({ isExpanded, setExpanded }: Props) => { +const ConversationsNav = ({ + isExpanded, + setExpanded, + focusedIndex, + index, +}: Props) => { const { t } = useTranslation(); const { project } = useContext(ProjectContext.Current); const containerRef = useRef(null); @@ -46,18 +53,25 @@ const ConversationsNav = ({ isExpanded, setExpanded }: Props) => { e?.stopPropagation(); }, []); + useEffect(() => { + if (focusedIndex === index.toString() && containerRef.current) { + containerRef.current.scrollIntoView({ block: 'nearest' }); + } + }, [focusedIndex, index]); + return ( -
+
{
)} -
- {project?.conversations.map((c) => ( - - ))} -
+ {isExpanded && ( +
+ {project?.conversations.map((c, ci) => ( + + ))} +
+ )}
); }; diff --git a/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx b/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx index 6056e9b3c3..1586f043bd 100644 --- a/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx +++ b/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx @@ -3,9 +3,12 @@ import { ConversationShortType } from '../../../types/api'; import { TabsContext } from '../../../context/tabsContext'; import { TabTypesEnum } from '../../../types/general'; -type Props = ConversationShortType & {}; +type Props = ConversationShortType & { + index: string; + focusedIndex: string; +}; -const ConversationEntry = ({ title, id }: Props) => { +const ConversationEntry = ({ title, id, index, focusedIndex }: Props) => { const { openNewTab } = useContext(TabsContext.Handlers); const handleClick = useCallback(() => { @@ -16,9 +19,14 @@ const ConversationEntry = ({ title, id }: Props) => { {title} diff --git a/client/src/Project/LeftSidebar/NavPanel/Repo.tsx b/client/src/Project/LeftSidebar/NavPanel/Repo.tsx index 6d777fe19a..ce3c61bb95 100644 --- a/client/src/Project/LeftSidebar/NavPanel/Repo.tsx +++ b/client/src/Project/LeftSidebar/NavPanel/Repo.tsx @@ -40,6 +40,8 @@ type Props = { allBranches: { name: string; last_commit_unix_secs: number }[]; indexedBranches: string[]; indexingData?: RepoIndexingStatusType; + focusedIndex: string; + index: number; }; const reactRoot = document.getElementById('root')!; @@ -56,6 +58,8 @@ const RepoNav = ({ lastIndex, currentPath, indexingData, + focusedIndex, + index, }: Props) => { const { t } = useTranslation(); const [files, setFiles] = useState([]); @@ -122,15 +126,25 @@ const RepoNav = ({ ].includes(indexingData.status); }, [indexingData]); + useEffect(() => { + if (focusedIndex === index.toString() && containerRef.current) { + containerRef.current.scrollIntoView({ block: 'nearest' }); + } + }, [focusedIndex, index]); + return ( -
+
{isIndexing && indexingData ? ( )} -
- {files.map((f) => ( - - ))} -
+ {isExpanded && ( +
+ {files.map((f, fi) => ( + + ))} +
+ )}
); }; diff --git a/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx b/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx index ad2bd30915..9b4415e921 100644 --- a/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx +++ b/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx @@ -16,6 +16,8 @@ import { TabTypesEnum, } from '../../../types/general'; import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; +import { UIContext } from '../../../context/uiContext'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; type Props = { name: string; @@ -30,6 +32,8 @@ type Props = { currentPath?: string; branch?: string | null; indexingData?: RepoIndexingStatusType; + focusedIndex: string; + index: string; }; const RepoEntry = ({ @@ -45,8 +49,11 @@ const RepoEntry = ({ lastIndex, branch, indexingData, + focusedIndex, + index, }: Props) => { const { openNewTab } = useContext(TabsContext.Handlers); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); const [isOpen, setOpen] = useState( defaultOpen || (currentPath && currentPath.startsWith(fullPath)), ); @@ -103,6 +110,22 @@ const RepoEntry = ({ } }, [isDirectory, fullPath, openNewTab, repoRef, branch]); + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleClick(); + } + }, + [handleClick], + ); + useKeyboardNavigation(handleKeyEvent, focusedIndex !== index); + + useEffect(() => { + if (focusedIndex === index && ref.current) { + ref.current.scrollIntoView({ block: 'nearest' }); + } + }, [focusedIndex, index]); + return (
{isDirectory ? (
@@ -167,13 +195,13 @@ const RepoEntry = ({ {/*
*/} {/*)}*/} - {subItems?.length ? ( + {isOpen && subItems?.length ? (
- {subItems.map((si) => ( + {subItems.map((si, sii) => ( ))}
diff --git a/client/src/Project/LeftSidebar/NavPanel/index.tsx b/client/src/Project/LeftSidebar/NavPanel/index.tsx index 69aca529de..e774bfd5d7 100644 --- a/client/src/Project/LeftSidebar/NavPanel/index.tsx +++ b/client/src/Project/LeftSidebar/NavPanel/index.tsx @@ -1,8 +1,18 @@ -import { memo, useContext, useEffect, useMemo, useState } from 'react'; +import { + memo, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { ProjectContext } from '../../../context/projectContext'; import { TabTypesEnum } from '../../../types/general'; import { TabsContext } from '../../../context/tabsContext'; import { RepositoriesContext } from '../../../context/repositoriesContext'; +import { UIContext } from '../../../context/uiContext'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; import RepoNav from './Repo'; import ConversationsNav from './Conversations'; @@ -15,6 +25,10 @@ const NavPanel = ({}: Props) => { const { tab: leftTab } = useContext(TabsContext.CurrentLeft); const { tab: rightTab } = useContext(TabsContext.CurrentRight); const { indexingStatus } = useContext(RepositoriesContext); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); + const ref = useRef(null); + const [focusedIndex, setFocusedIndex] = useState(-1); + const [focusedIndexFull, setFocusedIndexFull] = useState(''); const currentlyFocusedTab = useMemo(() => { const focusedTab = focusedPanel === 'left' ? leftTab : rightTab; @@ -41,12 +55,39 @@ const NavPanel = ({}: Props) => { } }, [currentlyFocusedTab]); + const handleKeyEvent = useCallback((e: KeyboardEvent) => { + if (ref.current) { + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault(); + e.stopPropagation(); + const nodes = ref.current.querySelectorAll('[data-node-index]'); + setFocusedIndex((prev) => { + const newInd = + e.key === 'ArrowDown' + ? prev < nodes.length - 1 + ? prev + 1 + : 0 + : prev > 0 + ? prev - 1 + : nodes.length - 1; + setFocusedIndexFull( + (nodes[newInd] as HTMLElement)?.dataset?.nodeIndex || '', + ); + return newInd; + }); + } + } + }, []); + useKeyboardNavigation(handleKeyEvent, !isLeftSidebarFocused); + return ( -
+
{!!project?.conversations.length && ( )} {project?.repos.map((r, i) => ( @@ -69,6 +110,15 @@ const NavPanel = ({}: Props) => { ? currentlyFocusedTab?.path : undefined } + focusedIndex={focusedIndexFull} + index={ + i + + (project?.conversations.length + ? expanded === 0 + ? project?.conversations.length + 1 + : 1 + : 0) + } /> ))}
diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx index 62c9fc41ec..ac6de34ec2 100644 --- a/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx @@ -26,6 +26,7 @@ import { splitPath } from '../../../utils'; import { ProjectContext } from '../../../context/projectContext'; import { CommandBarContext } from '../../../context/commandBarContext'; import { regexToggleShortcut } from '../../../consts/shortcuts'; +import { UIContext } from '../../../context/uiContext'; import CodeResult from './Results/CodeResult'; import RepoResult from './Results/RepoResult'; import FileResult from './Results/FileResult'; @@ -58,6 +59,7 @@ const RegexSearchPanel = ({ projectId, isRegexEnabled }: Props) => { const inputRef = useRef(null); const { setIsRegexSearchEnabled } = useContext(ProjectContext.RegexSearch); const { isVisible } = useContext(CommandBarContext.General); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); const [focusedIndex, setFocusedIndex] = useState(-1); const onChange = useCallback((e: ChangeEvent) => { @@ -180,7 +182,10 @@ const RegexSearchPanel = ({ projectId, isRegexEnabled }: Props) => { }, [resultsRaw], ); - useKeyboardNavigation(handleKeyEvent, isVisible || !isRegexEnabled); + useKeyboardNavigation( + handleKeyEvent, + isVisible || !isRegexEnabled || !isLeftSidebarFocused, + ); return !isRegexEnabled ? null : (
diff --git a/client/src/Project/LeftSidebar/index.tsx b/client/src/Project/LeftSidebar/index.tsx index c1057df6ea..fae480ead0 100644 --- a/client/src/Project/LeftSidebar/index.tsx +++ b/client/src/Project/LeftSidebar/index.tsx @@ -1,4 +1,4 @@ -import React, { memo, useContext } from 'react'; +import React, { memo, useCallback, useContext, MouseEvent } from 'react'; import useResizeableWidth from '../../hooks/useResizeableWidth'; import { LEFT_SIDEBAR_WIDTH_KEY } from '../../services/storage'; import ProjectsDropdown from '../../components/Header/ProjectsDropdown'; @@ -6,8 +6,11 @@ import { ChevronDownIcon } from '../../icons'; import Dropdown from '../../components/Dropdown'; import { DeviceContext } from '../../context/deviceContext'; import { ProjectContext } from '../../context/projectContext'; -import NavPanel from './NavPanel'; +import { UIContext } from '../../context/uiContext'; +import { checkEventKeys } from '../../utils/keyboardUtils'; +import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; import RegexSearchPanel from './RegexSearchPanel'; +import NavPanel from './NavPanel'; type Props = {}; @@ -15,12 +18,29 @@ const LeftSidebar = ({}: Props) => { const { os } = useContext(DeviceContext); const { project } = useContext(ProjectContext.Current); const { isRegexSearchEnabled } = useContext(ProjectContext.RegexSearch); + const { setIsLeftSidebarFocused } = useContext(UIContext.Focus); + const { panelRef, dividerRef } = useResizeableWidth( true, LEFT_SIDEBAR_WIDTH_KEY, 20, 40, ); + + const handleKeyEvent = useCallback((e: KeyboardEvent) => { + if (checkEventKeys(e, ['cmd', '0'])) { + e.preventDefault(); + e.stopPropagation(); + setIsLeftSidebarFocused(true); + } + }, []); + useKeyboardNavigation(handleKeyEvent); + + const handleClick = useCallback((e: MouseEvent) => { + e.stopPropagation(); + setIsLeftSidebarFocused(true); + }, []); + return (
{
- - {!isRegexSearchEnabled && } +
+ + {!isRegexSearchEnabled && } +
{ useTranslation(); const { project } = useContext(ProjectContext.Current); const { rightTabs, leftTabs } = useContext(TabsContext.All); + const { setIsLeftSidebarFocused } = useContext(UIContext.Focus); const { setActiveRightTab, setActiveLeftTab, @@ -69,6 +73,24 @@ const Project = ({}: Props) => { setFocusedPanel('left'); }, []); + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (checkEventKeys(e, ['cmd', '1'])) { + e.preventDefault(); + e.stopPropagation(); + setFocusedPanel('left'); + setIsLeftSidebarFocused(false); + } else if (checkEventKeys(e, ['cmd', '2']) && rightTabs.length) { + e.preventDefault(); + e.stopPropagation(); + setFocusedPanel('right'); + setIsLeftSidebarFocused(false); + } + }, + [rightTabs.length], + ); + useKeyboardNavigation(handleKeyEvent); + return !project?.repos?.length ? ( ) : ( diff --git a/client/src/context/providers/UIContextProvider.tsx b/client/src/context/providers/UIContextProvider.tsx index 3c1260de37..0d5693b6f5 100644 --- a/client/src/context/providers/UIContextProvider.tsx +++ b/client/src/context/providers/UIContextProvider.tsx @@ -53,6 +53,7 @@ export const UIContextProvider = memo( const [theme, setTheme] = useState( (getPlainFromStorage(THEME) as 'system' | null) || 'system', ); + const [isLeftSidebarFocused, setIsLeftSidebarFocused] = useState(false); const refreshToken = useCallback(async (refToken: string) => { if (refToken) { @@ -163,6 +164,14 @@ export const UIContextProvider = memo( [theme], ); + const focusContextValue = useMemo( + () => ({ + isLeftSidebarFocused, + setIsLeftSidebarFocused, + }), + [isLeftSidebarFocused], + ); + return ( @@ -170,7 +179,9 @@ export const UIContextProvider = memo( - {children} + + {children} + diff --git a/client/src/context/uiContext.ts b/client/src/context/uiContext.ts index 2c3e0f133d..2cd0afb867 100644 --- a/client/src/context/uiContext.ts +++ b/client/src/context/uiContext.ts @@ -35,4 +35,8 @@ export const UIContext = { theme: 'system' as Theme, setTheme: (t: Theme) => {}, }), + Focus: createContext({ + isLeftSidebarFocused: false, + setIsLeftSidebarFocused: (b: boolean) => {}, + }), };