import { useMemo, useRef, useState, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { pluralize } from '@/react/common/string-utils'; import { Button } from '@@/buttons/Button'; import { Directory, File, FileNode } from '@@/form-components/FilePicker/types'; import { CommandPalette } from '@@/CommandPalette/CommandPalette'; import { globToRegex } from '@@/CommandPalette/utils'; import { Widget } from '@@/Widget'; import { isDirectory, getAllFilePaths, getFolderState } from './utils'; import { TreeNode } from './TreeNode'; import { SelectedPanel } from './SelectedPanel'; interface Props { files: FileNode[]; /* array of file paths that have been selected */ value: string[]; onChange: (filePaths: string[]) => void; exampleExpressions?: string[]; } export function FilePicker({ files, value, onChange, exampleExpressions = ['*.yml', 'src/**', '**/dist/*'], }: Props) { const [expanded, setExpanded] = useState>(() => new Set([])); const selected = useMemo( () => new Set(value.map((p) => (p.startsWith('/') ? p.slice(1) : p))), [value] ); const [filter, setFilter] = useState(''); const treeAreaRef = useRef(null); const commandPaletteRef = useRef(null); const allFilePaths = useMemo( () => files.flatMap((file) => getAllFilePaths(file, file.name)), [files] ); return (

Browse files, select individually, or use wildcard expressions. Contains {allFilePaths.length}{' '} {pluralize(allFilePaths.length, 'file')}. Try patterns like:

{exampleExpressions.map((pattern, index) => { return ( ); })}
{allFilePaths.length === 0 && (

No files available

)} {!filter.trim() && ( <>
{files.map((file) => ( ))}
)}
{selected.size} selected
); function renderDropdown(paths: string[]): ReactNode { if (!treeAreaRef.current) return null; return createPortal( paths.length === 0 ? (

No matching files

) : (
    {paths.map((path) => (
  • /{path}
  • ))}
), treeAreaRef.current ); } function handleChange(next: Set) { onChange( Array.from(next) .map((f) => `/${f}`) .sort() ); } function toggleDirectory(path: string) { setExpanded((prev) => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); } function toggleSelect(path: string, item: Directory | File) { if (!isDirectory(item)) { const next = new Set(selected); if (next.has(path)) { next.delete(path); } else { next.add(path); } handleChange(next); return; } const filePaths = getAllFilePaths(item, path); const state = getFolderState(item, selected, path); const next = new Set(selected); if (state === 'checked') { filePaths.forEach((p) => next.delete(p)); } else { filePaths.forEach((p) => next.add(p)); } handleChange(next); } function removeFile(path: string) { const next = new Set(selected); next.delete(path); handleChange(next); } function addExpression(pattern: string) { const re = globToRegex(pattern); const matchedPaths = allFilePaths.filter((p) => re.test(p)); handleChange(new Set([...selected, ...matchedPaths])); } }