Directory Search (#82)

This adds a frontend directory search by filtering out files that don't
match. It also allows navigation of the directory using the arrow keys
while maintaining focus on the search box.
This commit is contained in:
Sylvie Crowe 2024-06-26 16:59:45 -07:00 committed by GitHub
parent f9236fc18b
commit 5da3257031
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 149 additions and 14 deletions

View File

@ -50,11 +50,15 @@
display: flex; display: flex;
border-radius: 3px; border-radius: 3px;
&.focused {
background-color: var(--accent-color);
}
&:focus { &:focus {
background-color: var(--accent-color); background-color: var(--accent-color);
} }
&:hover:not(:focus) { &:hover:not(:focus):not(.focused) {
background-color: var(--highlight-bg-color); background-color: var(--highlight-bg-color);
} }
@ -78,3 +82,21 @@
} }
} }
} }
.dir-table-search-line {
display: flex;
gap: 0.7rem;
.dir-table-search-box {
background-color: var(--panel-bg-color);
margin-bottom: 0.5rem;
border: none;
width: 15rem;
color: var(--main-text-color);
border-radius: 4px;
&:focus {
outline-color: var(--accent-color);
}
}
}

View File

@ -22,6 +22,9 @@ import "./directorypreview.less";
interface DirectoryTableProps { interface DirectoryTableProps {
data: FileInfo[]; data: FileInfo[];
cwd: string; cwd: string;
focusIndex: number;
enter: boolean;
setFocusIndex: (_: number) => void;
setFileName: (_: string) => void; setFileName: (_: string) => void;
} }
@ -151,7 +154,7 @@ function cleanMimetype(input: string): string {
return truncated.trim(); return truncated.trim();
} }
function DirectoryTable({ data, cwd, setFileName }: DirectoryTableProps) { function DirectoryTable({ data, cwd, focusIndex, enter, setFocusIndex, setFileName }: DirectoryTableProps) {
let settings = jotai.useAtomValue(atoms.settingsConfigAtom); let settings = jotai.useAtomValue(atoms.settingsConfigAtom);
const getIconFromMimeType = React.useCallback( const getIconFromMimeType = React.useCallback(
(mimeType: string): string => { (mimeType: string): string => {
@ -279,9 +282,23 @@ function DirectoryTable({ data, cwd, setFileName }: DirectoryTableProps) {
))} ))}
</div> </div>
{table.getState().columnSizingInfo.isResizingColumn ? ( {table.getState().columnSizingInfo.isResizingColumn ? (
<MemoizedTableBody table={table} cwd={cwd} setFileName={setFileName} /> <MemoizedTableBody
table={table}
cwd={cwd}
focusIndex={focusIndex}
enter={enter}
setFileName={setFileName}
setFocusIndex={setFocusIndex}
/>
) : ( ) : (
<TableBody table={table} cwd={cwd} setFileName={setFileName} /> <TableBody
table={table}
cwd={cwd}
focusIndex={focusIndex}
enter={enter}
setFileName={setFileName}
setFocusIndex={setFocusIndex}
/>
)} )}
</div> </div>
); );
@ -290,24 +307,37 @@ function DirectoryTable({ data, cwd, setFileName }: DirectoryTableProps) {
interface TableBodyProps { interface TableBodyProps {
table: Table<FileInfo>; table: Table<FileInfo>;
cwd: string; cwd: string;
focusIndex: number;
enter: boolean;
setFocusIndex: (_: number) => void;
setFileName: (_: string) => void; setFileName: (_: string) => void;
} }
function TableBody({ table, cwd, setFileName }: TableBodyProps) { function TableBody({ table, cwd, focusIndex, enter, setFocusIndex, setFileName }: TableBodyProps) {
let [refresh, setRefresh] = React.useState(false); let [refresh, setRefresh] = React.useState(false);
React.useEffect(() => {
const selected = (table.getSortedRowModel()?.flatRows[focusIndex]?.getValue("path") as string) ?? null;
if (selected != null) {
console.log("yipee");
const fullPath = cwd.concat("/", selected);
setFileName(fullPath);
}
}, [enter]);
table.getRow;
return ( return (
<div className="dir-table-body"> <div className="dir-table-body">
{table.getRowModel().rows.map((row) => ( {table.getRowModel().rows.map((row, idx) => (
<div <div
className="dir-table-body-row" className={clsx("dir-table-body-row", { focused: focusIndex === idx })}
key={row.id} key={row.id}
tabIndex={0}
onDoubleClick={() => { onDoubleClick={() => {
const newFileName = row.getValue("path") as string; const newFileName = row.getValue("path") as string;
const fullPath = cwd.concat("/", newFileName); const fullPath = cwd.concat("/", newFileName);
setFileName(fullPath); setFileName(fullPath);
}} }}
onClick={() => setFocusIndex(idx)}
onContextMenu={(e) => handleFileContextMenu(e, cwd.concat("/", row.getValue("path") as string))} onContextMenu={(e) => handleFileContextMenu(e, cwd.concat("/", row.getValue("path") as string))}
> >
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
@ -333,15 +363,98 @@ const MemoizedTableBody = React.memo(
) as typeof TableBody; ) as typeof TableBody;
interface DirectoryPreviewProps { interface DirectoryPreviewProps {
contentAtom: jotai.Atom<Promise<string>>;
fileNameAtom: jotai.WritableAtom<string, [string], void>; fileNameAtom: jotai.WritableAtom<string, [string], void>;
} }
function DirectoryPreview({ contentAtom, fileNameAtom }: DirectoryPreviewProps) { function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
const contentText = jotai.useAtomValue(contentAtom); const [searchText, setSearchText] = React.useState("");
let content: FileInfo[] = JSON.parse(contentText); let [focusIndex, setFocusIndex] = React.useState(0);
const [content, setContent] = React.useState<FileInfo[]>([]);
let [fileName, setFileName] = jotai.useAtom(fileNameAtom); let [fileName, setFileName] = jotai.useAtom(fileNameAtom);
return <DirectoryTable data={content} cwd={fileName} setFileName={setFileName} />; const [enter, setEnter] = React.useState(false);
React.useEffect(() => {
const getContent = async () => {
const file = await services.FileService.ReadFile(fileName);
const serializedContent = util.base64ToString(file?.data64);
let content: FileInfo[] = JSON.parse(serializedContent);
let filtered = content.filter((fileInfo) => {
return fileInfo.path.toLowerCase().includes(searchText);
});
setContent(filtered);
};
getContent();
}, [fileName, searchText]);
const handleKeyDown = React.useCallback(
(e) => {
switch (e.key) {
case "Escape":
//todo: escape block focus
break;
case "ArrowUp":
e.preventDefault();
setFocusIndex((idx) => Math.max(idx - 1, 0));
break;
case "ArrowDown":
e.preventDefault();
setFocusIndex((idx) => Math.min(idx + 1, content.length - 1));
break;
case "Enter":
e.preventDefault();
console.log("enter thinks focus Index is ", focusIndex);
let newFileName = content[focusIndex].path;
console.log(
"enter thinks contents are",
content.slice(0, focusIndex + 1).map((fi) => fi.path)
);
setEnter((current) => !current);
/*
const fullPath = fileName.concat("/", newFileName);
setFileName(fullPath);
*/
break;
default:
}
},
[content, focusIndex, setEnter]
);
React.useEffect(() => {
console.log(focusIndex);
}, [focusIndex]);
React.useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [handleKeyDown]);
return (
<>
<div className="dir-table-search-line">
<label>Search:</label>
<input
type="text"
className="dir-table-search-box"
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
maxLength={400}
autoFocus={true}
/>
</div>
<DirectoryTable
data={content}
cwd={fileName}
focusIndex={focusIndex}
enter={enter}
setFileName={setFileName}
setFocusIndex={setFocusIndex}
/>
</>
);
} }
export { DirectoryPreview }; export { DirectoryPreview };

View File

@ -251,7 +251,7 @@ function PreviewView({ blockId }: { blockId: string }) {
) { ) {
specializedView = <CodeEditPreview readonly={true} contentAtom={fileContentAtom} filename={fileName} />; specializedView = <CodeEditPreview readonly={true} contentAtom={fileContentAtom} filename={fileName} />;
} else if (mimeType === "directory") { } else if (mimeType === "directory") {
specializedView = <DirectoryPreview contentAtom={fileContentAtom} fileNameAtom={fileNameAtom} />; specializedView = <DirectoryPreview fileNameAtom={fileNameAtom} />;
} else { } else {
specializedView = ( specializedView = (
<div className="view-preview"> <div className="view-preview">