FN-7445: add Project Files creation and search controls

Improve the Project Files browser with first-class creation actions and recursive file search.

- Add visible create-file and create-folder buttons for Files — Project while keeping compact controls for embedded pickers.
- Add debounced recursive project file search with loading, retry, empty, and path-context result states.
- Cover project files creation and search behavior in dock, modal, and file browser tests.
- Document the new Project Files controls and add a release changeset.

Files changed:
 .changeset/fn-7445-project-files-create-search.md  |   7 +
 docs/dashboard-guide.md                            |   3 +-
 .../dashboard/app/components/DockFilesView.tsx     |   1 +
 packages/dashboard/app/components/FileBrowser.css  |  88 +++++++++++++
 packages/dashboard/app/components/FileBrowser.tsx  | 145 ++++++++++++++++++++-
 .../dashboard/app/components/FileBrowserModal.tsx  |   1 +
 .../components/__tests__/DockFilesView.test.tsx    |  34 +++--
 .../app/components/__tests__/FileBrowser.test.tsx  | 109 ++++++++++++++++
 .../components/__tests__/FileBrowserModal.test.tsx |  52 ++++++++
 9 files changed, 426 insertions(+), 14 deletions(-)

Fusion-Task-Id: FN-7445

Fusion-Task-Lineage: 41510def-4900-42ee-beab-b67f28dfeaee

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-02 13:44:41 -07:00
parent ec0256f7e5
commit 5984f327d3
9 changed files with 426 additions and 14 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Add visible create buttons and recursive search to Project Files.
category: feature
dev: Files — Project uses the existing workspace-safe create and /files/search APIs with settings pickers left compact.

View File

@@ -768,7 +768,8 @@ For the full research workflow, provider setup, CLI commands, API reference, and
The Files modal provides a workspace-aware file browser and editor.
- Use **New File** or **New Folder** in the browser header to create entries in the current folder; new files open in the editor after creation
- In **Files — Project**, use the visible **Create new file** and **Create new folder** buttons in the browser header to create entries in the current folder; new files open in the editor after creation
- In **Files — Project**, use **Search project files** to find project files recursively without navigating the tree; matching rows include path context so duplicate filenames can be distinguished
- Source/text editing supports a **Line #** header toggle to show or hide line numbers in the editor gutter
- The line-number preference is saved per project and restored automatically when you switch projects
- Known image, video/movie, audio, and PDF files render browser-native read-only previews inline with their real content type from the selected project or task workspace download URL; the explicit **Download** action still saves files as attachments, text files remain editable, and unknown binary files keep the read-only editor fallback

View File

@@ -168,6 +168,7 @@ export function DockFilesView({ projectId, openFile, layout = "auto" }: DockFile
workspace="project"
onRefresh={refresh}
projectId={projectId}
showProjectFileControls
/>
</div>

View File

@@ -193,6 +193,32 @@
gap: var(--space-xs);
}
.file-browser-search {
position: relative;
display: flex;
align-items: center;
flex: 1 1 calc(var(--space-xl) * 6);
min-width: calc(var(--space-xl) * 4.5);
max-width: calc(var(--space-xl) * 12);
}
.file-browser-search-icon {
position: absolute;
left: var(--space-sm);
color: var(--text-muted);
pointer-events: none;
}
.file-browser-search-input {
width: 100%;
min-width: 0;
padding-left: calc(var(--space-lg) + var(--space-md));
}
.file-browser-create-button {
white-space: nowrap;
}
.file-browser-new-menu {
position: relative;
flex-shrink: 0;
@@ -254,6 +280,50 @@
font-style: italic;
}
.file-browser-search-results {
display: flex;
flex-direction: column;
min-height: 100%;
}
.file-browser-search-result {
width: 100%;
border: none;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
}
.file-browser-search-result:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.file-node-path {
margin-left: auto;
min-width: 0;
max-width: 55%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-muted);
font-size: var(--font-size-xs);
}
.file-browser-search-status {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
padding: var(--space-xl);
color: var(--text-muted);
}
.file-browser-search-status--error {
color: var(--color-error);
}
.file-browser-loading,
.file-browser-error {
display: flex;
@@ -660,12 +730,30 @@ Narrow Files windows use the same single-pane list/editor behavior as mobile eve
max-width: none;
}
.file-browser-search {
order: 3;
flex-basis: 100%;
max-width: none;
min-width: 0;
}
.file-browser .file-browser-header-actions {
width: 100%;
margin-left: 0;
justify-content: flex-start;
}
.file-browser-create-button {
flex: 1 1 calc(var(--space-xl) * 4.5);
justify-content: center;
}
.file-node-path {
flex-basis: 100%;
max-width: none;
margin-left: calc(var(--space-lg) + var(--space-sm));
}
.file-editor-toolbar {
align-items: center;
flex-wrap: nowrap;

View File

@@ -1,9 +1,9 @@
import "./FileBrowser.css";
import { useState, useCallback, useEffect, useRef } from "react";
import { useState, useCallback, useEffect, useId, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Folder, File, ChevronRight, Loader2, Copy, Move, Trash2, Pencil, Download, Archive, FilePlus2, FolderPlus, Plus, ChevronDown } from "lucide-react";
import { Folder, File, ChevronRight, Loader2, Copy, Move, Trash2, Pencil, Download, Archive, FilePlus2, FolderPlus, Plus, ChevronDown, Search } from "lucide-react";
import type { FileNode } from "../api";
import { copyFile, createWorkspaceDirectory, createWorkspaceFile, moveFile, deleteFile, renameFile, downloadFileUrl, downloadZipUrl } from "../api";
import { copyFile, createWorkspaceDirectory, createWorkspaceFile, moveFile, deleteFile, renameFile, downloadFileUrl, downloadZipUrl, searchFiles } from "../api";
import { appendTokenQuery } from "../auth";
import { getErrorMessage } from "@fusion/core";
import { getParentDisplayPath, joinDisplayPath, normalizeDisplayPath } from "../utils/pathDisplay";
@@ -22,6 +22,8 @@ interface FileBrowserProps {
onRefresh?: () => void;
/** Optional project ID for multi-project scoping */
projectId?: string;
/** Show first-class Files — Project creation and recursive search controls instead of the compact picker chrome. */
showProjectFileControls?: boolean;
}
function formatBytes(bytes?: number): string {
@@ -339,8 +341,10 @@ export function FileBrowser({
workspace,
onRefresh,
projectId,
showProjectFileControls = false,
}: FileBrowserProps) {
const { t } = useTranslation("app");
const searchInputId = useId();
const [contextMenu, setContextMenu] = useState<ContextMenuState>(INITIAL_CONTEXT_MENU);
const [dialog, setDialog] = useState<DialogState>(INITIAL_DIALOG);
const [operationLoading, setOperationLoading] = useState(false);
@@ -348,12 +352,17 @@ export function FileBrowser({
const [isLongPressing, setIsLongPressing] = useState(false);
const [longPressTargetPath, setLongPressTargetPath] = useState<string | null>(null);
const [newMenuOpen, setNewMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<Array<{ path: string; name: string }>>([]);
const [searchLoading, setSearchLoading] = useState(false);
const [searchError, setSearchError] = useState<string | null>(null);
const longPressTimerRef = useRef<number | null>(null);
const longPressFeedbackTimerRef = useRef<number | null>(null);
const touchStartRef = useRef<TouchPoint | null>(null);
const touchOpenHandledRef = useRef(false);
const newMenuRef = useRef<HTMLDivElement>(null);
const searchRequestIdRef = useRef(0);
const clearLongPressTimers = useCallback(() => {
if (longPressTimerRef.current !== null) {
@@ -400,6 +409,51 @@ export function FileBrowser({
};
}, [newMenuOpen]);
const trimmedSearchQuery = searchQuery.trim();
const isSearching = showProjectFileControls && Boolean(workspace) && trimmedSearchQuery.length > 0;
const runSearch = useCallback((query: string) => {
if (!showProjectFileControls || !workspace) {
setSearchResults([]);
setSearchError(null);
setSearchLoading(false);
return;
}
const requestId = searchRequestIdRef.current + 1;
searchRequestIdRef.current = requestId;
setSearchLoading(true);
setSearchError(null);
searchFiles(query, workspace, projectId)
.then((result) => {
if (searchRequestIdRef.current !== requestId) return;
setSearchResults(result.files);
})
.catch((err) => {
if (searchRequestIdRef.current !== requestId) return;
setSearchResults([]);
setSearchError(getErrorMessage(err) || t("fileBrowser.searchFailed", "Search failed"));
})
.finally(() => {
if (searchRequestIdRef.current !== requestId) return;
setSearchLoading(false);
});
}, [projectId, showProjectFileControls, t, workspace]);
useEffect(() => {
if (!isSearching || !workspace) {
searchRequestIdRef.current += 1;
setSearchResults([]);
setSearchError(null);
setSearchLoading(false);
return;
}
const timer = window.setTimeout(() => runSearch(trimmedSearchQuery), 250);
return () => window.clearTimeout(timer);
}, [isSearching, runSearch, trimmedSearchQuery, workspace]);
const openContextMenuAt = useCallback((x: number, y: number, entry: FileNode, fullPath: string) => {
setContextMenu({
visible: true,
@@ -581,6 +635,11 @@ export function FileBrowser({
setOperationError(null);
}, []);
const handleSearchResultSelect = useCallback((path: string) => {
touchOpenHandledRef.current = false;
onSelectFile(path);
}, [onSelectFile]);
const handleFileNodeClick = useCallback((entry: FileNode, fullPath: string) => {
if (touchOpenHandledRef.current) {
touchOpenHandledRef.current = false;
@@ -633,7 +692,50 @@ export function FileBrowser({
</button>
)}
<span className="file-browser-path">{currentPath === "." ? t("fileBrowser.root", "Root") : normalizeDisplayPath(currentPath)}</span>
{showProjectFileControls && (
<div className="file-browser-search" role="search">
<Search size={16} aria-hidden="true" className="file-browser-search-icon" />
<input
id={searchInputId}
className="input file-browser-search-input"
type="search"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
aria-label={t("fileBrowser.searchProjectFiles", "Search project files")}
placeholder={t("fileBrowser.searchProjectFilesPlaceholder", "Search project files…")}
disabled={!workspace}
/>
</div>
)}
<div className="file-browser-header-actions">
{showProjectFileControls ? (
<>
{/**
* FNXC:FileBrowser 2026-07-02-00:00:
* Files — Project needs visible create-file and create-folder targets plus recursive search, while embedded settings pickers keep the compact New menu to avoid misleading picker chrome.
*/}
<button
type="button"
className="btn btn-sm file-browser-create-button"
onClick={() => openCreateDialog("create-file")}
disabled={!workspace}
title={t("fileBrowser.createNewFile", "Create new file")}
>
<FilePlus2 size={14} />
{t("fileBrowser.createNewFile", "Create new file")}
</button>
<button
type="button"
className="btn btn-sm file-browser-create-button"
onClick={() => openCreateDialog("create-folder")}
disabled={!workspace}
title={t("fileBrowser.createNewFolder", "Create new folder")}
>
<FolderPlus size={14} />
{t("fileBrowser.createNewFolder", "Create new folder")}
</button>
</>
) : (
<div className="file-browser-new-menu" ref={newMenuRef}>
{/*
* FNXC:FileBrowser 2026-06-22-15:24:
@@ -680,11 +782,46 @@ export function FileBrowser({
</div>
)}
</div>
)}
</div>
</div>
<div className="file-browser-list">
{entries.length === 0 ? (
{isSearching ? (
<div className="file-browser-search-results" aria-live="polite">
{searchLoading ? (
<div className="file-browser-search-status">
<Loader2 className="spin" size={18} />
<span>{t("fileBrowser.searchingFiles", "Searching files…")}</span>
</div>
) : searchError ? (
<div className="file-browser-search-status file-browser-search-status--error">
<span>{searchError}</span>
<button type="button" className="btn btn-sm" onClick={() => runSearch(trimmedSearchQuery)}>
{t("common.retry", "Retry")}
</button>
</div>
) : searchResults.length === 0 ? (
<div className="file-browser-empty">{t("fileBrowser.searchNoResults", "No files found")}</div>
) : (
searchResults.map((result) => (
<button
type="button"
key={result.path}
className="file-node file-node--file file-browser-search-result"
onClick={() => handleSearchResultSelect(result.path)}
title={result.path}
>
<div className="file-node-icon">
<File size={16} />
</div>
<div className="file-node-name">{result.name}</div>
<div className="file-node-path">{normalizeDisplayPath(result.path)}</div>
</button>
))
)}
</div>
) : entries.length === 0 ? (
<div className="file-browser-empty">{t("fileBrowser.emptyDirectory", "(empty directory)")}</div>
) : (
entries.map((entry) => {

View File

@@ -421,6 +421,7 @@ export function FileBrowserModal({
workspace={currentWorkspace}
onRefresh={refresh}
projectId={projectId}
showProjectFileControls={currentWorkspace === "project"}
/>
</div>

View File

@@ -110,17 +110,22 @@ vi.mock("../FileEditor", () => ({
},
}));
const capturedFileBrowserProps: Array<{ showProjectFileControls?: boolean; projectId?: string }> = [];
// Render the tree's files as buttons so we can click one.
vi.mock("../FileBrowser", () => ({
FileBrowser: ({ entries: e, onSelectFile }: { entries: FileNode[]; onSelectFile: (p: string) => void }) => (
<div data-testid="mock-file-browser">
{e.map((entry) => (
<button key={entry.name} type="button" onClick={() => onSelectFile(entry.name)}>
{entry.name}
</button>
))}
</div>
),
FileBrowser: ({ entries: e, onSelectFile, showProjectFileControls, projectId }: { entries: FileNode[]; onSelectFile: (p: string) => void; showProjectFileControls?: boolean; projectId?: string }) => {
capturedFileBrowserProps.push({ showProjectFileControls, projectId });
return (
<div data-testid="mock-file-browser" data-project-controls={showProjectFileControls ? "true" : "false"}>
{e.map((entry) => (
<button key={entry.name} type="button" onClick={() => onSelectFile(entry.name)}>
{entry.name}
</button>
))}
</div>
);
},
}));
const PROJECT_ID = "proj-1";
@@ -137,6 +142,7 @@ describe("DockFilesView shared current-file state", () => {
mockSave.mockClear();
capturedFileEditorProps.length = 0;
capturedEditorHookCalls.length = 0;
capturedFileBrowserProps.length = 0;
});
afterEach(() => cleanup());
@@ -151,6 +157,16 @@ describe("DockFilesView shared current-file state", () => {
expect(dockFilesCss).not.toContain("border-right: 1px solid var(--border);");
});
it("enables Files — Project controls in both compact and two-pane dock layouts", () => {
const dock = render(<DockFilesView projectId={PROJECT_ID} layout="auto" />);
expect(screen.getByTestId("mock-file-browser")).toHaveAttribute("data-project-controls", "true");
dock.unmount();
render(<DockFilesView projectId={PROJECT_ID} layout="two-pane" />);
expect(screen.getByTestId("mock-file-browser")).toHaveAttribute("data-project-controls", "true");
expect(capturedFileBrowserProps.every((props) => props.showProjectFileControls === true && props.projectId === PROJECT_ID)).toBe(true);
});
it("persists the selected file to scoped storage and a fresh expand instance reads it on mount", async () => {
// 1. Dock instance: select a file.
const dock = render(<DockFilesView projectId={PROJECT_ID} layout="auto" />);

View File

@@ -23,6 +23,7 @@ vi.mock("lucide-react", async () => {
Archive: (props: any) => <span data-testid="icon-archive" {...props} />,
FilePlus2: (props: any) => <span data-testid="icon-file-plus" {...props} />,
FolderPlus: (props: any) => <span data-testid="icon-folder-plus" {...props} />,
Search: (props: any) => <span data-testid="icon-search" {...props} />,
};
});
@@ -38,6 +39,7 @@ const mockDownloadFileUrl = vi.fn((_workspace: string, filePath: string) =>
const mockDownloadZipUrl = vi.fn((_workspace: string, filePath: string) =>
`/api/files/${encodeURIComponent(filePath)}/download-zip?workspace=test-ws`,
);
const mockSearchFiles = vi.fn();
vi.mock("../../api", () => ({
copyFile: (...args: any[]) => mockCopyFile(...args),
@@ -48,6 +50,7 @@ vi.mock("../../api", () => ({
renameFile: (...args: any[]) => mockRenameFile(...args),
downloadFileUrl: (workspace: string, filePath: string) => mockDownloadFileUrl(workspace, filePath),
downloadZipUrl: (workspace: string, filePath: string) => mockDownloadZipUrl(workspace, filePath),
searchFiles: (...args: any[]) => mockSearchFiles(...args),
}));
// ── Test Data ───────────────────────────────────────────────────────────
@@ -84,6 +87,7 @@ type FileBrowserTestOverrides = Partial<typeof defaultProps> & {
loading?: boolean;
error?: string | null;
onRetry?: () => void;
showProjectFileControls?: boolean;
};
function renderFileBrowser(overrides: FileBrowserTestOverrides = {}) {
@@ -118,6 +122,11 @@ function getNewFolderAction() {
return screen.getByRole("menuitem", { name: /New Folder/i });
}
async function typeProjectSearch(query: string) {
fireEvent.change(screen.getByRole("searchbox", { name: "Search project files" }), { target: { value: query } });
await waitFor(() => expect(mockSearchFiles).toHaveBeenCalled());
}
// ── Tests ───────────────────────────────────────────────────────────────
describe("FileBrowser", () => {
@@ -131,6 +140,7 @@ describe("FileBrowser", () => {
mockDownloadZipUrl.mockImplementation((_workspace: string, filePath: string) =>
`/api/files/${encodeURIComponent(filePath)}/download-zip?workspace=test-ws`,
);
mockSearchFiles.mockResolvedValue({ files: [] });
vi.useRealTimers();
Object.defineProperty(window, "innerWidth", {
configurable: true,
@@ -191,6 +201,42 @@ describe("FileBrowser", () => {
expect(screen.getByRole("button", { name: /^New$/i })).toBeDisabled();
});
it("shows visible Files — Project create buttons and search when enabled", () => {
renderFileBrowser({ showProjectFileControls: true });
expect(screen.getByRole("button", { name: "Create new file" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Create new folder" })).toBeInTheDocument();
expect(screen.getByRole("searchbox", { name: "Search project files" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^New$/i })).toBeNull();
});
it("keeps settings-style picker chrome compact unless project controls are enabled", () => {
renderFileBrowser();
expect(screen.getByRole("button", { name: /^New$/i })).toBeInTheDocument();
expect(screen.queryByRole("searchbox", { name: "Search project files" })).toBeNull();
expect(screen.queryByRole("button", { name: "Create new file" })).toBeNull();
});
it("disables visible create and search controls when no workspace is provided", () => {
renderFileBrowser({ showProjectFileControls: true, workspace: undefined });
expect(screen.getByRole("button", { name: "Create new file" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Create new folder" })).toBeDisabled();
expect(screen.getByRole("searchbox", { name: "Search project files" })).toBeDisabled();
});
it("clicking visible Create new file opens the existing dialog", () => {
renderFileBrowser({ showProjectFileControls: true });
fireEvent.click(screen.getByRole("button", { name: "Create new file" }));
expect(document.querySelector(".file-browser-dialog-title")?.textContent).toBe("New File");
expect(screen.getByPlaceholderText("File name")).toBeDefined();
});
it("clicking visible Create new folder opens the existing dialog", () => {
renderFileBrowser({ showProjectFileControls: true });
fireEvent.click(screen.getByRole("button", { name: "Create new folder" }));
expect(document.querySelector(".file-browser-dialog-title")?.textContent).toBe("New Folder");
expect(screen.getByPlaceholderText("Folder name")).toBeDefined();
});
it("clicking New File opens a dialog with name input", () => {
renderFileBrowser();
openNewMenu();
@@ -260,6 +306,69 @@ describe("FileBrowser", () => {
expect(screen.queryByPlaceholderText("File name")).toBeNull();
});
it("searches project files recursively and selects a result with path context", async () => {
mockSearchFiles.mockResolvedValue({
files: [
{ name: "config.json", path: "packages/app/config.json" },
{ name: "config.json", path: "packages/core/config.json" },
],
});
const onSelectFile = vi.fn();
renderFileBrowser({ showProjectFileControls: true, onSelectFile });
await typeProjectSearch("config");
await waitFor(() => {
expect(mockSearchFiles).toHaveBeenCalledWith("config", "test-ws", "project-1");
expect(screen.getByText("packages/app/config.json")).toBeInTheDocument();
expect(screen.getByText("packages/core/config.json")).toBeInTheDocument();
});
fireEvent.click(screen.getAllByRole("button", { name: /config.json/i })[1]);
expect(onSelectFile).toHaveBeenCalledWith("packages/core/config.json");
});
it("does not search without a workspace and preserves normal browsing", async () => {
renderFileBrowser({ showProjectFileControls: true, workspace: undefined });
fireEvent.change(screen.getByRole("searchbox", { name: "Search project files" }), { target: { value: "readme" } });
await waitFor(() => expect(screen.getByDisplayValue("readme")).toBeInTheDocument());
expect(mockSearchFiles).not.toHaveBeenCalled();
expect(screen.getByText("readme.md")).toBeInTheDocument();
});
it("shows search loading, no-results, and restores the current directory when cleared", async () => {
let resolveSearch: (value: { files: Array<{ path: string; name: string }> }) => void = () => {};
mockSearchFiles.mockReturnValue(new Promise((resolve) => {
resolveSearch = resolve;
}));
renderFileBrowser({ showProjectFileControls: true, entries: [] });
await typeProjectSearch("missing");
expect(screen.getByText("Searching files…")).toBeInTheDocument();
await act(async () => {
resolveSearch({ files: [] });
});
await waitFor(() => expect(screen.getByText("No files found")).toBeInTheDocument());
fireEvent.change(screen.getByRole("searchbox", { name: "Search project files" }), { target: { value: "" } });
expect(screen.getByText("(empty directory)")).toBeInTheDocument();
});
it("shows search errors with retry", async () => {
mockSearchFiles.mockRejectedValueOnce(new Error("Index unavailable"));
mockSearchFiles.mockResolvedValueOnce({ files: [{ name: "readme.md", path: "readme.md" }] });
renderFileBrowser({ showProjectFileControls: true });
await typeProjectSearch("readme");
await waitFor(() => expect(screen.getByText("Index unavailable")).toBeInTheDocument());
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
await waitFor(() => expect(screen.getByRole("button", { name: /readme\.md/ })).toBeInTheDocument());
expect(mockSearchFiles).toHaveBeenCalledTimes(2);
});
it("closes create dialog on Escape", () => {
renderFileBrowser();
openNewMenu();

View File

@@ -119,6 +119,58 @@ describe("FileBrowserModal", () => {
expect(mockUseWorkspaceFileBrowser).toHaveBeenCalledWith("project", true, undefined);
});
it("shows Files — Project create and search controls only for the project workspace", async () => {
const { rerender } = render(
<FileBrowserModal
initialWorkspace="project"
isOpen={true}
onClose={mockOnClose}
/>,
);
expect(screen.getByRole("button", { name: "Create new file" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Create new folder" })).toBeInTheDocument();
expect(screen.getByRole("searchbox", { name: "Search project files" })).toBeInTheDocument();
rerender(
<FileBrowserModal
initialWorkspace="FN-001"
isOpen={true}
onClose={mockOnClose}
/>,
);
await waitFor(() => {
expect(screen.queryByRole("searchbox", { name: "Search project files" })).toBeNull();
expect(screen.queryByRole("button", { name: "Create new file" })).toBeNull();
expect(screen.getByRole("button", { name: /^New$/i })).toBeInTheDocument();
});
});
it("keeps Files — Project controls available in the narrow mobile list pane", async () => {
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 375,
});
window.dispatchEvent(new Event("resize"));
render(
<FileBrowserModal
initialWorkspace="project"
isOpen={true}
onClose={mockOnClose}
/>,
);
await waitFor(() => {
expect(document.querySelector(".file-browser-modal--narrow")).toBeInTheDocument();
expect(screen.getByRole("searchbox", { name: "Search project files" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Create new file" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Create new folder" })).toBeInTheDocument();
});
});
it("opens a file in the editor when selected", async () => {
render(
<FileBrowserModal