diff --git a/client/src/Project/LeftSidebar/NavPanel/index.tsx b/client/src/Project/LeftSidebar/NavPanel/index.tsx index e774bfd5d7..b9eb19b328 100644 --- a/client/src/Project/LeftSidebar/NavPanel/index.tsx +++ b/client/src/Project/LeftSidebar/NavPanel/index.tsx @@ -1,34 +1,22 @@ -import { - memo, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { memo, useContext, useEffect, useMemo, 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'; -type Props = {}; +type Props = { + focusedIndex: string; +}; -const NavPanel = ({}: Props) => { +const NavPanel = ({ focusedIndex }: Props) => { const [expanded, setExpanded] = useState(-1); const { project } = useContext(ProjectContext.Current); const { focusedPanel } = useContext(TabsContext.All); 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; @@ -55,38 +43,13 @@ 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 && ( )} @@ -110,7 +73,7 @@ const NavPanel = ({}: Props) => { ? currentlyFocusedTab?.path : undefined } - focusedIndex={focusedIndexFull} + focusedIndex={focusedIndex} index={ i + (project?.conversations.length diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenu.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenu.tsx index 4fd3ff464a..f795db35d6 100644 --- a/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenu.tsx +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenu.tsx @@ -41,13 +41,7 @@ const AutocompleteMenu = ({ ); return ( -
+
    {isOpen && ( <> diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenuItem.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenuItem.tsx index 45d35695ca..ae9f836b6e 100644 --- a/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenuItem.tsx +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenuItem.tsx @@ -2,6 +2,7 @@ import React, { memo, useEffect, useMemo, useRef } from 'react'; import { Trans } from 'react-i18next'; import { ResultItemType, SuggestionType } from '../../../types/results'; import CodeBlockSearch from '../../../components/Code/CodeBlockSearch'; +import CodeResult from './Results/CodeResult'; type Props = { item: SuggestionType; @@ -36,7 +37,13 @@ const AutocompleteMenuItem = ({ if (item.type === ResultItemType.CODE) { return item.snippets?.slice(0, 1).map((s) => ({ ...s, - code: s.code.split('\n').slice(0, 5).join('\n'), // don't render big snippets that have over 5 lines + line_range: { + start: s.lineStart || 0, + end: (s.lineStart || 0) + s.code.split('\n').length, + }, + highlights: s.highlights || [], + symbols: [], + data: s.code.split('\n').slice(0, 5).join('\n'), // don't render big snippets that have over 5 lines })); } return []; @@ -51,22 +58,26 @@ const AutocompleteMenuItem = ({ isFirst ? 'scroll-mt-8' : '' } outline-0 outline-none ${ item.type === ResultItemType.FLAG ? 'h-9' : '' - } px-1.5 py-2.5 hover:bg-bg-base-hover gap-1 border-transparent border-l-2 hover:border-bg-main group + } hover:bg-bg-base-hover gap-1group ${ isFocused ? 'bg-bg-shade-hover' : 'focus:bg-bg-shade-hover' } transition duration-150 ease-in-out`} > {item.type === ResultItemType.FLAG || item.type === ResultItemType.LANG ? ( - {item.data} + {item.data} ) : item.type === ResultItemType.CODE ? ( - + + + ) : item.type === ResultItemType.FILE ? ( <> {item.relativePath} diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeResult.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeResult.tsx index 154ec07d4b..8594b2c141 100644 --- a/client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeResult.tsx +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeResult.tsx @@ -12,7 +12,7 @@ import FileIcon from '../../../../components/FileIcon'; import { TabsContext } from '../../../../context/tabsContext'; import { TabTypesEnum } from '../../../../types/general'; import useKeyboardNavigation from '../../../../hooks/useKeyboardNavigation'; -import { isFocusInInput } from '../../../../utils/domUtils'; +import { UIContext } from '../../../../context/uiContext'; import CodeLine from './CodeLine'; type Props = { @@ -20,7 +20,8 @@ type Props = { repo_ref: string; lang: string; snippets: SnippetItem[]; - isFocused: boolean; + index: string; + focusedIndex: string; isFirst: boolean; }; @@ -29,10 +30,12 @@ const CodeResult = ({ repo_ref, lang, snippets, - isFocused, + index, + focusedIndex, isFirst, }: Props) => { const { openNewTab } = useContext(TabsContext.Handlers); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); const ref = useRef(null); const [isExpanded, setIsExpanded] = useState(true); const toggleExpanded = useCallback(() => { @@ -40,17 +43,14 @@ const CodeResult = ({ }, []); useEffect(() => { - if (isFocused) { + if (focusedIndex === index) { ref.current?.scrollIntoView({ block: 'nearest' }); } - }, [isFocused]); + }, [focusedIndex, index]); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { - if ( - e.key === 'Enter' && - (!isFocusInInput() || document.activeElement?.id === 'regex-search') - ) { + if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); openNewTab({ @@ -63,7 +63,10 @@ const CodeResult = ({ }, [repo_ref, relative_path, openNewTab], ); - useKeyboardNavigation(handleKeyEvent, !isFocused); + useKeyboardNavigation( + handleKeyEvent, + focusedIndex !== index || !isLeftSidebarFocused, + ); const handleClick = useCallback(() => { openNewTab({ @@ -79,9 +82,10 @@ const CodeResult = ({
    { const { openNewTab } = useContext(TabsContext.Handlers); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); const ref = useRef(null); + const [isExpanded, setIsExpanded] = useState(false); + const [files, setFiles] = useState([]); useEffect(() => { - if (isFocused) { + if (focusedIndex === index) { ref.current?.scrollIntoView({ block: 'nearest' }); } - }, [isFocused]); + }, [focusedIndex, index]); + + const fetchFiles = useCallback( + async (path?: string) => { + const resp = await getFolderContent(repo_ref, path); + if (!resp.entries) { + return []; + } + return resp?.entries.sort((a, b) => { + if ((a.entry_data === 'Directory') === (b.entry_data === 'Directory')) { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + } else { + return a.entry_data === 'Directory' ? -1 : 1; + } + }); + }, + [repo_ref], + ); + + useEffect(() => { + if (isExpanded && !files.length) { + fetchFiles(relative_path.text).then(setFiles); + } + }, [fetchFiles, files, isExpanded, relative_path.text]); + + const handleClick = useCallback(() => { + if (is_dir) { + setIsExpanded((prev) => !prev); + } else { + openNewTab({ + type: TabTypesEnum.FILE, + path: relative_path.text, + repoRef: repo_ref, + }); + } + }, [relative_path, repo_ref, is_dir, openNewTab]); const handleKeyEvent = useCallback( (e: KeyboardEvent) => { if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); - if (!is_dir) { - openNewTab({ - type: TabTypesEnum.FILE, - path: relative_path.text, - repoRef: repo_ref, - }); - } + handleClick(); } }, - [repo_ref, relative_path, is_dir, openNewTab], + [handleClick], + ); + useKeyboardNavigation( + handleKeyEvent, + focusedIndex !== index || !isLeftSidebarFocused, ); - useKeyboardNavigation(handleKeyEvent, !isFocused); - const handleClick = useCallback(() => { - if (is_dir) { - return; - } - openNewTab({ - type: TabTypesEnum.FILE, - path: relative_path.text, - repoRef: repo_ref, - }); - }, [relative_path, repo_ref, is_dir, openNewTab]); return ( -
    - {is_dir ? ( - - ) : ( - + + {is_dir ? ( + + ) : ( + + )} + {/**/} + + {relative_path.text} + + + {isExpanded && ( +
    + {files.map((f, fi) => ( + + ))} +
    )} - {/**/} -
    {relative_path.text}
    -
    + ); }; diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/Results/RepoResult.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/RepoResult.tsx index 63799d7f25..79bf033aae 100644 --- a/client/src/Project/LeftSidebar/RegexSearchPanel/Results/RepoResult.tsx +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/RepoResult.tsx @@ -1,9 +1,125 @@ -import { memo } from 'react'; +import React, { + memo, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; +import GitHubIcon from '../../../../icons/GitHubIcon'; +import { HardDriveIcon } from '../../../../icons'; +import { splitPath } from '../../../../utils'; +import { DirectoryEntry } from '../../../../types/api'; +import { getFolderContent } from '../../../../services/api'; +import RepoEntry from '../../NavPanel/RepoEntry'; +import useKeyboardNavigation from '../../../../hooks/useKeyboardNavigation'; +import { UIContext } from '../../../../context/uiContext'; -type Props = {}; +type Props = { + repoRef: string; + index: number; + isExpandable?: boolean; + focusedIndex: string; +}; + +const RepoResult = ({ repoRef, isExpandable, index, focusedIndex }: Props) => { + const { isLeftSidebarFocused } = useContext(UIContext.Focus); + const [isExpanded, setIsExpanded] = useState(false); + const [files, setFiles] = useState([]); + + const fetchFiles = useCallback( + async (path?: string) => { + const resp = await getFolderContent(repoRef, path); + if (!resp.entries) { + return []; + } + return resp?.entries.sort((a, b) => { + if ((a.entry_data === 'Directory') === (b.entry_data === 'Directory')) { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + } else { + return a.entry_data === 'Directory' ? -1 : 1; + } + }); + }, + [repoRef], + ); + + useEffect(() => { + if (isExpanded && !files.length) { + fetchFiles().then(setFiles); + } + }, [fetchFiles, files, isExpanded]); + + const onClick = useCallback(() => { + if (isExpandable) { + setIsExpanded((prev) => !prev); + } + }, [isExpandable]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + onClick(); + } + }, + [onClick], + ); + useKeyboardNavigation( + handleKeyEvent, + focusedIndex !== index.toString() || !isExpandable || !isLeftSidebarFocused, + ); -const RepoResult = ({}: Props) => { - return
    repo
    ; + return ( + + + {repoRef.startsWith('github.com/') ? ( + + ) : ( + + )} + {splitPath(repoRef) + .slice(repoRef.startsWith('github.com/') ? -2 : -1) + .join('/')} + + {isExpanded && ( +
    + {files.map((f, fi) => ( + + ))} +
    + )} +
    + ); }; export default memo(RepoResult); diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx index 6f2103f069..20f76524de 100644 --- a/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx @@ -1,4 +1,4 @@ -import { +import React, { ChangeEvent, FormEvent, memo, @@ -9,9 +9,7 @@ import { useState, } from 'react'; import { useTranslation } from 'react-i18next'; -// import throttle from 'lodash.throttle'; -// import { useCombobox } from 'downshift'; -import { CloseSignIcon, HardDriveIcon, RegexSearchIcon } from '../../../icons'; +import { CloseSignIcon, RegexSearchIcon } from '../../../icons'; import Button from '../../../components/Button'; import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; import { search } from '../../../services/api'; @@ -22,8 +20,6 @@ import { FileResItem, RepoItem, } from '../../../types/api'; -import GitHubIcon from '../../../icons/GitHubIcon'; -import { splitPath } from '../../../utils'; import { ProjectContext } from '../../../context/projectContext'; import { CommandBarContext } from '../../../context/commandBarContext'; import { regexToggleShortcut } from '../../../consts/shortcuts'; @@ -35,6 +31,8 @@ import FileResult from './Results/FileResult'; type Props = { projectId?: string; isRegexEnabled?: boolean; + focusedIndex: string; + setFocusedIndex: (i: number) => void; }; // const getAutocompleteThrottled = throttle( @@ -51,8 +49,14 @@ type Props = { type ResultType = CodeItem | RepoItem | FileResItem | DirectoryItem | FileItem; -const RegexSearchPanel = ({ projectId, isRegexEnabled }: Props) => { +const RegexSearchPanel = ({ + projectId, + isRegexEnabled, + focusedIndex, + setFocusedIndex, +}: Props) => { const { t } = useTranslation(); + // const { openNewTab } = useContext(TabsContext.Handlers); const [inputValue, setInputValue] = useState(''); // const [options, setOptions] = useState([]); const [results, setResults] = useState>({}); @@ -63,11 +67,10 @@ const RegexSearchPanel = ({ projectId, isRegexEnabled }: Props) => { const { isLeftSidebarFocused, setIsLeftSidebarFocused } = useContext( UIContext.Focus, ); - const [focusedIndex, setFocusedIndex] = useState(-1); const onChange = useCallback((e: ChangeEvent) => { setInputValue(e.target.value); - setFocusedIndex(-1); + setFocusedIndex(0); }, []); const onClear = useCallback(() => { @@ -142,25 +145,33 @@ const RegexSearchPanel = ({ projectId, isRegexEnabled }: Props) => { // }, // }); + useEffect(() => { + if (focusedIndex === 'input') { + inputRef.current?.focus(); + } + }, [focusedIndex]); + const onSubmit = useCallback( async (e: FormEvent) => { e.preventDefault(); - if (projectId) { - const data = await search(projectId, inputValue); - const newResults: Record = {}; - data.data.forEach((d) => { - if (!newResults[d.data.repo_ref]) { - newResults[d.data.repo_ref] = [d]; - } else { - newResults[d.data.repo_ref].push(d); - } - }); - setResults(newResults); - setResultsRaw(data.data); - // closeMenu(); + if (focusedIndex === 'input') { + if (projectId) { + const data = await search(projectId, inputValue); + const newResults: Record = {}; + data.data.forEach((d) => { + if (!newResults[d.data.repo_ref]) { + newResults[d.data.repo_ref] = [d]; + } else { + newResults[d.data.repo_ref].push(d); + } + }); + setResults(newResults); + setResultsRaw(data.data); + // closeMenu(); + } } }, - [inputValue], + [inputValue, focusedIndex], ); const handleKeyEvent = useCallback( @@ -168,22 +179,14 @@ const RegexSearchPanel = ({ projectId, isRegexEnabled }: Props) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); - inputRef.current?.blur(); - } else if (e.key === 'ArrowDown' && resultsRaw.length) { - e.preventDefault(); - e.stopPropagation(); - setFocusedIndex((prev) => - prev < resultsRaw.length - 1 ? prev + 1 : -1, - ); - } else if (e.key === 'ArrowUp' && resultsRaw.length) { - e.preventDefault(); - e.stopPropagation(); - setFocusedIndex((prev) => - prev > -1 ? prev - 1 : resultsRaw.length - 1, - ); + if (focusedIndex === 'input') { + onClear(); + } else { + setFocusedIndex(0); + } } }, - [resultsRaw], + [focusedIndex, onClear], ); useKeyboardNavigation( handleKeyEvent, @@ -217,6 +220,7 @@ const RegexSearchPanel = ({ projectId, isRegexEnabled }: Props) => { autoComplete="off" ref={inputRef} id="regex-search" + data-node-index="input" />
-
+
- {!isRegexSearchEnabled && } + {!isRegexSearchEnabled && }