mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
Wire cross-project file refs into viewer UI
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
4f42079974
commit
1019e011c0
15 files changed
+536
-43
No files matched your search
@@ -10,10 +10,14 @@ import { api } from "./client";
|
||||
export interface FileResourceQuery {
|
||||
path: string;
|
||||
workspace?: WorkspaceFileSelector;
|
||||
projectId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
}
|
||||
|
||||
export interface FileResourceListQuery {
|
||||
workspace?: WorkspaceFileSelector;
|
||||
projectId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
mode?: WorkspaceFileListMode;
|
||||
q?: string | null;
|
||||
limit?: number;
|
||||
@@ -21,6 +25,10 @@ export interface FileResourceListQuery {
|
||||
|
||||
function buildQuery(query: FileResourceQuery | FileResourceListQuery): string {
|
||||
const params = new URLSearchParams();
|
||||
if (query.projectId && query.workspaceId) {
|
||||
params.set("projectId", query.projectId);
|
||||
params.set("workspaceId", query.workspaceId);
|
||||
}
|
||||
if ("path" in query) params.set("path", query.path);
|
||||
if (query.workspace && query.workspace !== "auto") {
|
||||
params.set("workspace", query.workspace);
|
||||
|
||||
@@ -15,6 +15,12 @@ export interface ArtifactFileChipProps {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
function artifactFileDisplay(ref: WorkspaceFileRef) {
|
||||
if (!ref.projectName) return ref.displayPath;
|
||||
const prefix = `${ref.projectName} / `;
|
||||
return ref.displayPath.startsWith(prefix) ? ref.displayPath : `${prefix}${ref.displayPath}`;
|
||||
}
|
||||
|
||||
export function ArtifactFileChip({
|
||||
workspaceFileRef,
|
||||
label,
|
||||
@@ -24,7 +30,7 @@ export function ArtifactFileChip({
|
||||
title,
|
||||
}: ArtifactFileChipProps) {
|
||||
const viewer = useFileViewer();
|
||||
const display = typeof label !== "undefined" ? label : workspaceFileRef.displayPath;
|
||||
const display = typeof label !== "undefined" ? label : artifactFileDisplay(workspaceFileRef);
|
||||
const canOpen = !!(onOpen || viewer);
|
||||
const lineSuffix = workspaceFileRef.line
|
||||
? ` line ${workspaceFileRef.line}${workspaceFileRef.column ? ` column ${workspaceFileRef.column}` : ""}`
|
||||
@@ -61,6 +67,8 @@ export function ArtifactFileChip({
|
||||
line: workspaceFileRef.line ?? null,
|
||||
column: workspaceFileRef.column ?? null,
|
||||
workspace,
|
||||
projectId: workspaceFileRef.projectId ?? null,
|
||||
workspaceId: workspaceFileRef.projectId ? workspaceFileRef.workspaceId : null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -342,6 +342,7 @@ function LoadingView({ elapsedMs }: { elapsedMs: number }) {
|
||||
|
||||
interface FileViewerSheetProps {
|
||||
issueId: string;
|
||||
companyId?: string | null;
|
||||
/** When not provided, the sheet defaults to the context state. */
|
||||
state?: FileViewerUrlState | null;
|
||||
/** When true, renders the "Open file" prompt when no file is selected but sheet is open. */
|
||||
@@ -353,6 +354,7 @@ interface FileViewerSheetProps {
|
||||
|
||||
export function FileViewerSheet({
|
||||
issueId,
|
||||
companyId,
|
||||
state: stateProp,
|
||||
showPromptWhenEmpty = false,
|
||||
open: openProp,
|
||||
@@ -383,13 +385,13 @@ export function FileViewerSheet({
|
||||
}, 75);
|
||||
return () => window.clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state?.path, state?.workspace]);
|
||||
}, [state?.path, state?.workspace, state?.projectId, state?.workspaceId]);
|
||||
|
||||
const resolveQuery = useQuery({
|
||||
queryKey: state
|
||||
? queryKeys.issues.fileResource(issueId, state.path, state.workspace)
|
||||
? queryKeys.issues.fileResource(issueId, state)
|
||||
: ["issues", "file-resources", issueId, "resolve", "__closed__"],
|
||||
queryFn: () => fileResourcesApi.resolve(issueId, { path: state!.path, workspace: state!.workspace }),
|
||||
queryFn: () => fileResourcesApi.resolve(issueId, state!),
|
||||
enabled: !!state && computedOpen,
|
||||
retry: false,
|
||||
staleTime: 30_000,
|
||||
@@ -400,9 +402,9 @@ export function FileViewerSheet({
|
||||
|
||||
const contentQuery = useQuery({
|
||||
queryKey: state
|
||||
? queryKeys.issues.fileResourceContent(issueId, state.path, state.workspace)
|
||||
? queryKeys.issues.fileResourceContent(issueId, state)
|
||||
: ["issues", "file-resources", issueId, "content", "__closed__"],
|
||||
queryFn: () => fileResourcesApi.content(issueId, { path: state!.path, workspace: state!.workspace }),
|
||||
queryFn: () => fileResourcesApi.content(issueId, state!),
|
||||
enabled: !!state && computedOpen && canPreview,
|
||||
retry: false,
|
||||
staleTime: 30_000,
|
||||
@@ -434,9 +436,23 @@ export function FileViewerSheet({
|
||||
);
|
||||
|
||||
const handleBrowseOpen = useCallback(
|
||||
(ref: { path: string; workspace: WorkspaceFileSelector; line?: number | null; column?: number | null }) => {
|
||||
(ref: {
|
||||
path: string;
|
||||
workspace: WorkspaceFileSelector;
|
||||
line?: number | null;
|
||||
column?: number | null;
|
||||
projectId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
}) => {
|
||||
viewer.open(
|
||||
{ path: ref.path, line: ref.line ?? null, column: ref.column ?? null, workspace: ref.workspace },
|
||||
{
|
||||
path: ref.path,
|
||||
line: ref.line ?? null,
|
||||
column: ref.column ?? null,
|
||||
workspace: ref.workspace,
|
||||
projectId: ref.projectId ?? null,
|
||||
workspaceId: ref.workspaceId ?? null,
|
||||
},
|
||||
{ fromBrowse: true },
|
||||
);
|
||||
},
|
||||
@@ -619,13 +635,15 @@ export function FileViewerSheet({
|
||||
onRetry={handleRetry}
|
||||
onSetAnnouncement={setAnnouncement}
|
||||
onFallbackToProject={
|
||||
state.workspace !== "project"
|
||||
state.workspace !== "project" && !state.projectId && !state.workspaceId
|
||||
? () =>
|
||||
viewer.open({
|
||||
path: state.path,
|
||||
line: state.line,
|
||||
column: state.column,
|
||||
workspace: "project",
|
||||
projectId: null,
|
||||
workspaceId: null,
|
||||
})
|
||||
: null
|
||||
}
|
||||
@@ -633,8 +651,11 @@ export function FileViewerSheet({
|
||||
) : browseMode ? (
|
||||
<WorkspaceFileBrowser
|
||||
issueId={issueId}
|
||||
companyId={companyId}
|
||||
onOpen={handleBrowseOpen}
|
||||
initialQuery={viewer.query}
|
||||
initialProjectId={viewer.browseProjectId}
|
||||
initialWorkspaceId={viewer.browseWorkspaceId}
|
||||
className="min-h-0 flex-1 p-4"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import type { Root } from "react-dom/client";
|
||||
import type { WorkspaceFileListItem, WorkspaceFileListResponse } from "@paperclipai/shared";
|
||||
import type { Project, ProjectWorkspace, WorkspaceFileListItem, WorkspaceFileListResponse } from "@paperclipai/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { WorkspaceFileBrowser, describeUnavailable } from "./WorkspaceFileBrowser";
|
||||
|
||||
@@ -64,6 +65,72 @@ function availableResponse(items: WorkspaceFileListItem[], truncated = false): W
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkspace(overrides: Partial<ProjectWorkspace> = {}): ProjectWorkspace {
|
||||
return {
|
||||
id: "workspace-content",
|
||||
companyId: "company-1",
|
||||
projectId: "project-content",
|
||||
name: "Paperclip Content",
|
||||
sourceType: "local_path",
|
||||
cwd: "/srv/paperclip/home/paperclipai/paperclip-content",
|
||||
repoUrl: null,
|
||||
repoRef: null,
|
||||
defaultRef: null,
|
||||
visibility: "default",
|
||||
setupCommand: null,
|
||||
cleanupCommand: null,
|
||||
remoteProvider: null,
|
||||
remoteWorkspaceRef: null,
|
||||
sharedWorkspaceKey: null,
|
||||
metadata: null,
|
||||
runtimeConfig: null,
|
||||
isPrimary: true,
|
||||
createdAt: new Date("2026-06-08T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-06-08T00:00:00.000Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createProject(overrides: Partial<Project> = {}): Project {
|
||||
const workspace = createWorkspace();
|
||||
return {
|
||||
id: "project-content",
|
||||
companyId: "company-1",
|
||||
urlKey: "paperclip-content",
|
||||
goalId: null,
|
||||
goalIds: [],
|
||||
goals: [],
|
||||
name: "Paperclip Content",
|
||||
description: null,
|
||||
status: "in_progress",
|
||||
leadAgentId: null,
|
||||
targetDate: null,
|
||||
color: null,
|
||||
icon: null,
|
||||
env: null,
|
||||
pauseReason: null,
|
||||
pausedAt: null,
|
||||
executionWorkspacePolicy: null,
|
||||
codebase: {
|
||||
workspaceId: workspace.id,
|
||||
repoUrl: null,
|
||||
repoRef: null,
|
||||
defaultRef: null,
|
||||
repoName: null,
|
||||
localFolder: workspace.cwd,
|
||||
managedFolder: "",
|
||||
effectiveLocalFolder: workspace.cwd ?? "",
|
||||
origin: "local_folder",
|
||||
},
|
||||
workspaces: [workspace],
|
||||
primaryWorkspace: workspace,
|
||||
archivedAt: null,
|
||||
createdAt: new Date("2026-06-08T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-06-08T00:00:00.000Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function unavailableResponse(reason: string): WorkspaceFileListResponse {
|
||||
return {
|
||||
kind: "workspace_file_list",
|
||||
@@ -77,7 +144,7 @@ function unavailableResponse(reason: string): WorkspaceFileListResponse {
|
||||
};
|
||||
}
|
||||
|
||||
function ok(data: WorkspaceFileListResponse) {
|
||||
function ok<T>(data: T) {
|
||||
return { data, isFetching: false, isError: false, error: null, refetch: vi.fn() };
|
||||
}
|
||||
|
||||
@@ -100,11 +167,11 @@ describe("WorkspaceFileBrowser", () => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
function renderBrowser(onOpen = vi.fn()) {
|
||||
function renderBrowser(onOpen = vi.fn(), props: Partial<ComponentProps<typeof WorkspaceFileBrowser>> = {}) {
|
||||
const root = createRoot(container);
|
||||
roots.push(root);
|
||||
act(() => {
|
||||
root.render(<WorkspaceFileBrowser issueId="issue-1" onOpen={onOpen} />);
|
||||
root.render(<WorkspaceFileBrowser issueId="issue-1" onOpen={onOpen} {...props} />);
|
||||
});
|
||||
return { root, onOpen };
|
||||
}
|
||||
@@ -169,6 +236,48 @@ describe("WorkspaceFileBrowser", () => {
|
||||
renderBrowser();
|
||||
expect(container.textContent).toContain("No workspace yet");
|
||||
});
|
||||
|
||||
it("opens a result from a selected other project workspace", () => {
|
||||
const contentItem = createItem({
|
||||
relativePath: "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/README.md",
|
||||
displayPath: "Paperclip Content / content-os/cases/active/2026-06-06-pap-10199-bundled-skills/README.md",
|
||||
workspaceLabel: "Paperclip Content",
|
||||
workspaceKind: "project_workspace",
|
||||
workspaceId: "workspace-content",
|
||||
projectId: "project-content",
|
||||
projectName: "Paperclip Content",
|
||||
});
|
||||
useQueryMock.mockImplementation((options: { queryKey: readonly unknown[] }) => {
|
||||
if (options.queryKey[0] === "projects") return ok([createProject()]);
|
||||
return ok(availableResponse([contentItem]));
|
||||
});
|
||||
|
||||
const { onOpen } = renderBrowser(vi.fn(), {
|
||||
companyId: "company-1",
|
||||
initialProjectId: "project-content",
|
||||
initialWorkspaceId: "workspace-content",
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("Other project");
|
||||
expect(container.textContent).toContain("Browsing Paperclip Content / Paperclip Content");
|
||||
const listCall = useQueryMock.mock.calls.find(([options]) => options.queryKey?.[3] === "list");
|
||||
expect(listCall?.[0].queryKey[4]).toMatchObject({
|
||||
workspace: "project",
|
||||
projectId: "project-content",
|
||||
workspaceId: "workspace-content",
|
||||
});
|
||||
|
||||
const option = container.querySelector('[role="option"]')!;
|
||||
act(() => {
|
||||
option.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
||||
});
|
||||
expect(onOpen).toHaveBeenCalledWith({
|
||||
path: "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/README.md",
|
||||
workspace: "project",
|
||||
projectId: "project-content",
|
||||
workspaceId: "workspace-content",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("describeUnavailable", () => {
|
||||
|
||||
@@ -10,13 +10,16 @@ import {
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AlertTriangle, Cloud, FileCode2, FolderOpen, Loader2, Search } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { timeAgo } from "@/lib/timeAgo";
|
||||
import { fileResourcesApi } from "@/api/file-resources";
|
||||
import { projectsApi } from "@/api/projects";
|
||||
import { ApiError } from "@/api/client";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { parseWorkspaceFileRef } from "@/lib/workspace-file-parser";
|
||||
import type {
|
||||
Project,
|
||||
WorkspaceFileListItem,
|
||||
WorkspaceFileListMode,
|
||||
WorkspaceFileSelector,
|
||||
@@ -28,6 +31,8 @@ const WORKSPACE_OPTIONS: ReadonlyArray<{ value: WorkspaceFileSelector; label: st
|
||||
{ value: "project", label: "Project" },
|
||||
];
|
||||
|
||||
type BrowserSource = "current" | "other";
|
||||
|
||||
// Hard list cap. The spec called out ~50 to keep reads cheap; 100 trades a bit
|
||||
// more scan for fewer "refine to narrow" dead-ends on large trees. Footer always
|
||||
// discloses truncation so the cap is never silent.
|
||||
@@ -118,6 +123,36 @@ function WorkspaceSelector({
|
||||
);
|
||||
}
|
||||
|
||||
function SourceSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: BrowserSource;
|
||||
onChange: (next: BrowserSource) => void;
|
||||
}) {
|
||||
return (
|
||||
<div role="group" aria-label="File source" className="inline-flex shrink-0 rounded-md border border-border p-0.5">
|
||||
{[
|
||||
{ value: "current" as const, label: "Current workspace" },
|
||||
{ value: "other" as const, label: "Other project" },
|
||||
].map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
aria-pressed={value === option.value}
|
||||
onClick={() => onChange(option.value)}
|
||||
className={cn(
|
||||
"rounded px-2 py-1 text-xs transition-colors",
|
||||
value === option.value ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface WorkspaceFileRowProps {
|
||||
item: WorkspaceFileListItem;
|
||||
optionId: string;
|
||||
@@ -170,19 +205,37 @@ function WorkspaceFileRow({ item, optionId, selected, showTimestamp, onOpen, onH
|
||||
|
||||
export interface WorkspaceFileBrowserProps {
|
||||
issueId: string;
|
||||
companyId?: string | null;
|
||||
onOpen: (ref: {
|
||||
path: string;
|
||||
workspace: WorkspaceFileSelector;
|
||||
line?: number | null;
|
||||
column?: number | null;
|
||||
projectId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
}) => void;
|
||||
/** Seed the search field (e.g. from a URL-backed deep link). */
|
||||
initialQuery?: string | null;
|
||||
initialProjectId?: string | null;
|
||||
initialWorkspaceId?: string | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function WorkspaceFileBrowser({ issueId, onOpen, initialQuery, className }: WorkspaceFileBrowserProps) {
|
||||
export function WorkspaceFileBrowser({
|
||||
issueId,
|
||||
companyId,
|
||||
onOpen,
|
||||
initialQuery,
|
||||
initialProjectId,
|
||||
initialWorkspaceId,
|
||||
className,
|
||||
}: WorkspaceFileBrowserProps) {
|
||||
const [source, setSource] = useState<BrowserSource>(
|
||||
initialProjectId && initialWorkspaceId ? "other" : "current",
|
||||
);
|
||||
const [workspace, setWorkspace] = useState<WorkspaceFileSelector>("auto");
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(initialProjectId ?? null);
|
||||
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(initialWorkspaceId ?? null);
|
||||
const [searchInput, setSearchInput] = useState(initialQuery ?? "");
|
||||
const [debouncedQuery, setDebouncedQuery] = useState(initialQuery?.trim() ?? "");
|
||||
// When the workspace has no git change-tracking we silently fall back to a full
|
||||
@@ -193,6 +246,49 @@ export function WorkspaceFileBrowser({ issueId, onOpen, initialQuery, className
|
||||
const listboxId = useId();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const projectsQuery = useQuery({
|
||||
queryKey: companyId ? queryKeys.projects.list(companyId) : ["projects", "__none__"],
|
||||
queryFn: () => projectsApi.list(companyId!),
|
||||
enabled: source === "other" && !!companyId,
|
||||
retry: false,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
const projectRows = useMemo(
|
||||
() => Array.isArray(projectsQuery.data) ? projectsQuery.data : [],
|
||||
[projectsQuery.data],
|
||||
);
|
||||
const projectsWithWorkspaces = useMemo(
|
||||
() => projectRows.filter((project: Project) => project.workspaces.length > 0),
|
||||
[projectRows],
|
||||
);
|
||||
const selectedProject = useMemo(
|
||||
() => projectsWithWorkspaces.find((project) => project.id === selectedProjectId) ?? null,
|
||||
[projectsWithWorkspaces, selectedProjectId],
|
||||
);
|
||||
const selectedWorkspace = useMemo(
|
||||
() => selectedProject?.workspaces.find((item) => item.id === selectedWorkspaceId) ?? null,
|
||||
[selectedProject, selectedWorkspaceId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (source !== "other" || !projectsQuery.data) return;
|
||||
const nextProject = selectedProject ?? projectsWithWorkspaces[0] ?? null;
|
||||
if (!nextProject) {
|
||||
setSelectedProjectId(null);
|
||||
setSelectedWorkspaceId(null);
|
||||
return;
|
||||
}
|
||||
if (selectedProjectId !== nextProject.id) {
|
||||
setSelectedProjectId(nextProject.id);
|
||||
}
|
||||
const nextWorkspace = nextProject.workspaces.find((item) => item.id === selectedWorkspaceId)
|
||||
?? nextProject.primaryWorkspace
|
||||
?? nextProject.workspaces[0]
|
||||
?? null;
|
||||
setSelectedWorkspaceId(nextWorkspace?.id ?? null);
|
||||
}, [projectsQuery.data, projectsWithWorkspaces, selectedProject, selectedProjectId, selectedWorkspaceId, source]);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = window.setTimeout(() => setDebouncedQuery(searchInput.trim()), 150);
|
||||
return () => window.clearTimeout(handle);
|
||||
@@ -201,15 +297,37 @@ export function WorkspaceFileBrowser({ issueId, onOpen, initialQuery, className
|
||||
// A new search or workspace should re-attempt recent-change tracking.
|
||||
useEffect(() => {
|
||||
setRecentUnavailable(false);
|
||||
}, [workspace]);
|
||||
}, [workspace, source, selectedProjectId, selectedWorkspaceId]);
|
||||
|
||||
const q = debouncedQuery || null;
|
||||
const isSearch = q !== null;
|
||||
const mode: WorkspaceFileListMode = isSearch ? "all" : recentUnavailable ? "all" : "changed";
|
||||
const targetProjectId = source === "other" ? selectedProjectId : null;
|
||||
const targetWorkspaceId = source === "other" ? selectedWorkspaceId : null;
|
||||
const effectiveWorkspace: WorkspaceFileSelector = source === "other" ? "project" : workspace;
|
||||
const canListFiles = source === "current" || Boolean(targetProjectId && targetWorkspaceId);
|
||||
const targetRef = targetProjectId && targetWorkspaceId
|
||||
? { projectId: targetProjectId, workspaceId: targetWorkspaceId }
|
||||
: {};
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: queryKeys.issues.fileResources(issueId, { workspace, mode, q, limit: LIST_LIMIT }),
|
||||
queryFn: () => fileResourcesApi.list(issueId, { workspace, mode, q, limit: LIST_LIMIT }),
|
||||
queryKey: queryKeys.issues.fileResources(issueId, {
|
||||
workspace: effectiveWorkspace,
|
||||
projectId: targetProjectId,
|
||||
workspaceId: targetWorkspaceId,
|
||||
mode,
|
||||
q,
|
||||
limit: LIST_LIMIT,
|
||||
}),
|
||||
queryFn: () => fileResourcesApi.list(issueId, {
|
||||
workspace: effectiveWorkspace,
|
||||
projectId: targetProjectId,
|
||||
workspaceId: targetWorkspaceId,
|
||||
mode,
|
||||
q,
|
||||
limit: LIST_LIMIT,
|
||||
}),
|
||||
enabled: canListFiles,
|
||||
retry: false,
|
||||
staleTime: 15_000,
|
||||
});
|
||||
@@ -228,7 +346,7 @@ export function WorkspaceFileBrowser({ issueId, onOpen, initialQuery, className
|
||||
// Keep the highlighted option valid as results change.
|
||||
useEffect(() => {
|
||||
setHighlightedIndex(items.length > 0 ? 0 : -1);
|
||||
}, [items, q, workspace]);
|
||||
}, [items, q, workspace, source, selectedProjectId, selectedWorkspaceId]);
|
||||
|
||||
const announcement = useMemo(() => {
|
||||
if (listQuery.isFetching) return "Loading workspace files…";
|
||||
@@ -242,8 +360,9 @@ export function WorkspaceFileBrowser({ issueId, onOpen, initialQuery, className
|
||||
const value = searchInput.trim();
|
||||
if (!value) return;
|
||||
const parsed = parseWorkspaceFileRef(value);
|
||||
if (parsed) onOpen({ path: parsed.path, workspace, line: parsed.line, column: parsed.column });
|
||||
else onOpen({ path: value, workspace });
|
||||
const target = { workspace: effectiveWorkspace, ...targetRef };
|
||||
if (parsed) onOpen({ path: parsed.path, ...target, line: parsed.line, column: parsed.column });
|
||||
else onOpen({ path: value, ...target });
|
||||
}
|
||||
|
||||
function handleSearchKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||
@@ -256,15 +375,54 @@ export function WorkspaceFileBrowser({ issueId, onOpen, initialQuery, className
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
const item = highlightedIndex >= 0 ? items[highlightedIndex] : undefined;
|
||||
if (item) onOpen({ path: item.relativePath, workspace });
|
||||
if (item) {
|
||||
const itemTarget = item.projectId
|
||||
? { projectId: item.projectId, workspaceId: item.workspaceId }
|
||||
: targetRef;
|
||||
onOpen({
|
||||
path: item.relativePath,
|
||||
workspace: effectiveWorkspace,
|
||||
...itemTarget,
|
||||
});
|
||||
}
|
||||
else openTypedPath();
|
||||
}
|
||||
}
|
||||
|
||||
function handleProjectChange(projectId: string) {
|
||||
const project = projectsWithWorkspaces.find((item) => item.id === projectId) ?? null;
|
||||
setSelectedProjectId(project?.id ?? null);
|
||||
setSelectedWorkspaceId(project?.primaryWorkspace?.id ?? project?.workspaces[0]?.id ?? null);
|
||||
}
|
||||
|
||||
const activeOptionId = highlightedIndex >= 0 ? `${listboxId}-opt-${highlightedIndex}` : undefined;
|
||||
|
||||
let body: ReactNode;
|
||||
if (listQuery.isFetching && !data) {
|
||||
if (source === "other" && !companyId) {
|
||||
body = (
|
||||
<StateMessage
|
||||
icon={<FolderOpen aria-hidden="true" className="h-5 w-5 text-muted-foreground" />}
|
||||
title="No company selected"
|
||||
body="Choose a company before browsing another project workspace."
|
||||
/>
|
||||
);
|
||||
} else if (source === "other" && projectsQuery.isFetching && projectsWithWorkspaces.length === 0) {
|
||||
body = (
|
||||
<StateMessage
|
||||
icon={<Loader2 aria-hidden="true" className="h-5 w-5 animate-spin text-muted-foreground" />}
|
||||
title="Loading project workspaces"
|
||||
body="Registered workspaces will appear here."
|
||||
/>
|
||||
);
|
||||
} else if (source === "other" && !canListFiles) {
|
||||
body = (
|
||||
<StateMessage
|
||||
icon={<FolderOpen aria-hidden="true" className="h-5 w-5 text-muted-foreground" />}
|
||||
title="No project workspaces"
|
||||
body="No same-company project has a registered workspace to browse."
|
||||
/>
|
||||
);
|
||||
} else if (listQuery.isFetching && !data) {
|
||||
body = (
|
||||
<div className="space-y-1.5 py-2" aria-busy="true">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
@@ -309,7 +467,16 @@ export function WorkspaceFileBrowser({ issueId, onOpen, initialQuery, className
|
||||
optionId={`${listboxId}-opt-${index}`}
|
||||
selected={index === highlightedIndex}
|
||||
showTimestamp={!isSearch}
|
||||
onOpen={() => onOpen({ path: item.relativePath, workspace })}
|
||||
onOpen={() => {
|
||||
const itemTarget = item.projectId
|
||||
? { projectId: item.projectId, workspaceId: item.workspaceId }
|
||||
: targetRef;
|
||||
onOpen({
|
||||
path: item.relativePath,
|
||||
workspace: effectiveWorkspace,
|
||||
...itemTarget,
|
||||
});
|
||||
}}
|
||||
onHover={() => setHighlightedIndex(index)}
|
||||
/>
|
||||
))}
|
||||
@@ -344,10 +511,62 @@ export function WorkspaceFileBrowser({ issueId, onOpen, initialQuery, className
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Workspace</span>
|
||||
<WorkspaceSelector value={workspace} onChange={setWorkspace} />
|
||||
<span className="text-xs text-muted-foreground">Source</span>
|
||||
<SourceSelector value={source} onChange={setSource} />
|
||||
{source === "current" ? (
|
||||
<>
|
||||
<span className="text-xs text-muted-foreground">Workspace</span>
|
||||
<WorkspaceSelector value={workspace} onChange={setWorkspace} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{source === "other" ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Project</span>
|
||||
<Select
|
||||
value={selectedProjectId ?? undefined}
|
||||
onValueChange={handleProjectChange}
|
||||
disabled={!companyId || projectsQuery.isFetching || projectsWithWorkspaces.length === 0}
|
||||
>
|
||||
<SelectTrigger size="sm" className="h-8 max-w-[260px] text-xs">
|
||||
<SelectValue placeholder={projectsQuery.isFetching ? "Loading projects" : "Choose project"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projectsWithWorkspaces.map((project) => (
|
||||
<SelectItem key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<span className="text-xs text-muted-foreground">Workspace</span>
|
||||
<Select
|
||||
value={selectedWorkspaceId ?? undefined}
|
||||
onValueChange={setSelectedWorkspaceId}
|
||||
disabled={!selectedProject || selectedProject.workspaces.length === 0}
|
||||
>
|
||||
<SelectTrigger size="sm" className="h-8 max-w-[260px] text-xs">
|
||||
<SelectValue placeholder="Choose workspace" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(selectedProject?.workspaces ?? []).map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{source === "other" && selectedProject && selectedWorkspace ? (
|
||||
<div className="truncate text-[11px] text-muted-foreground" title={`${selectedProject.name} / ${selectedWorkspace.name}`}>
|
||||
Browsing {selectedProject.name} / {selectedWorkspace.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex items-baseline justify-between gap-2 pt-1">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{isSearch ? <>Files matching “{q}”</> : "Recently changed"}
|
||||
|
||||
@@ -47,6 +47,8 @@ export function WorkspaceFileLink({
|
||||
line: workspaceFileRef.line ?? null,
|
||||
column: workspaceFileRef.column ?? null,
|
||||
workspace: "auto",
|
||||
projectId: workspaceFileRef.projectId ?? null,
|
||||
workspaceId: workspaceFileRef.workspaceId ?? null,
|
||||
});
|
||||
const href = canOpen
|
||||
? `${location.pathname}${deepLinkSearch}${location.hash}`
|
||||
|
||||
@@ -27,6 +27,8 @@ describe("readFileViewerStateFromSearch", () => {
|
||||
line: 42,
|
||||
column: 3,
|
||||
workspace: "project",
|
||||
projectId: null,
|
||||
workspaceId: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +53,14 @@ describe("writeFileViewerStateToSearch", () => {
|
||||
it("sets all params when opening", () => {
|
||||
const next = writeFileViewerStateToSearch(
|
||||
"?existing=1",
|
||||
{ path: "ui/src/a.ts", line: 42, column: 3, workspace: "project" },
|
||||
{
|
||||
path: "ui/src/a.ts",
|
||||
line: 42,
|
||||
column: 3,
|
||||
workspace: "project",
|
||||
projectId: null,
|
||||
workspaceId: null,
|
||||
},
|
||||
);
|
||||
const params = new URLSearchParams(next);
|
||||
expect(params.get("file")).toBe("ui/src/a.ts");
|
||||
@@ -64,14 +73,37 @@ describe("writeFileViewerStateToSearch", () => {
|
||||
it("omits workspace when auto", () => {
|
||||
const next = writeFileViewerStateToSearch(
|
||||
"",
|
||||
{ path: "a.ts", line: null, column: null, workspace: "auto" },
|
||||
{ path: "a.ts", line: null, column: null, workspace: "auto", projectId: null, workspaceId: null },
|
||||
);
|
||||
expect(next.includes("workspace")).toBe(false);
|
||||
});
|
||||
|
||||
it("round-trips explicit target project workspace params", () => {
|
||||
const next = writeFileViewerStateToSearch(
|
||||
"?existing=1",
|
||||
{
|
||||
path: "content-os/cases/readme.md",
|
||||
line: 7,
|
||||
column: null,
|
||||
workspace: "auto",
|
||||
projectId: "17acae7d-9d0c-46bf-9c82-be9694ac3461",
|
||||
workspaceId: "0de5f74f-a7d4-4f73-a9a0-455a2b968cf2",
|
||||
},
|
||||
);
|
||||
const state = readFileViewerStateFromSearch(next);
|
||||
expect(state).toEqual({
|
||||
path: "content-os/cases/readme.md",
|
||||
line: 7,
|
||||
column: null,
|
||||
workspace: "auto",
|
||||
projectId: "17acae7d-9d0c-46bf-9c82-be9694ac3461",
|
||||
workspaceId: "0de5f74f-a7d4-4f73-a9a0-455a2b968cf2",
|
||||
});
|
||||
});
|
||||
|
||||
it("clears viewer params when closing", () => {
|
||||
const next = writeFileViewerStateToSearch(
|
||||
"?file=a.ts&line=1&column=2&workspace=project&keep=yes",
|
||||
"?file=a.ts&line=1&column=2&workspace=project&projectId=project-1&workspaceId=workspace-1&keep=yes",
|
||||
null,
|
||||
);
|
||||
const params = new URLSearchParams(next);
|
||||
@@ -79,6 +111,8 @@ describe("writeFileViewerStateToSearch", () => {
|
||||
expect(params.get("line")).toBeNull();
|
||||
expect(params.get("column")).toBeNull();
|
||||
expect(params.get("workspace")).toBeNull();
|
||||
expect(params.get("projectId")).toBeNull();
|
||||
expect(params.get("workspaceId")).toBeNull();
|
||||
expect(params.get("keep")).toBe("yes");
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface FileViewerUrlState {
|
||||
line: number | null;
|
||||
column: number | null;
|
||||
workspace: WorkspaceFileSelector;
|
||||
projectId: string | null;
|
||||
workspaceId: string | null;
|
||||
}
|
||||
|
||||
export interface FileViewerContextValue {
|
||||
@@ -18,8 +20,12 @@ export interface FileViewerContextValue {
|
||||
browse: boolean;
|
||||
/** The active browse search query (URL `q`), or null. */
|
||||
query: string | null;
|
||||
browseProjectId: string | null;
|
||||
browseWorkspaceId: string | null;
|
||||
open(
|
||||
ref: Pick<ParsedWorkspaceFileRef, "path" | "line" | "column"> & { workspace?: WorkspaceFileSelector },
|
||||
ref: Pick<ParsedWorkspaceFileRef, "path" | "line" | "column" | "projectId" | "workspaceId"> & {
|
||||
workspace?: WorkspaceFileSelector;
|
||||
},
|
||||
opts?: { fromBrowse?: boolean },
|
||||
): void;
|
||||
/** Open (or stay in) browse mode, optionally seeding the search query. */
|
||||
@@ -43,6 +49,9 @@ export function readFileViewerStateFromSearch(search: string): FileViewerUrlStat
|
||||
const lineRaw = params.get("line");
|
||||
const columnRaw = params.get("column");
|
||||
const workspaceRaw = params.get("workspace");
|
||||
const projectIdRaw = params.get("projectId");
|
||||
const workspaceIdRaw = params.get("workspaceId");
|
||||
const hasExplicitTarget = Boolean(projectIdRaw && workspaceIdRaw);
|
||||
const line = lineRaw ? Number.parseInt(lineRaw, 10) : NaN;
|
||||
const column = columnRaw ? Number.parseInt(columnRaw, 10) : NaN;
|
||||
const workspace = (workspaceRaw === "execution" || workspaceRaw === "project")
|
||||
@@ -53,6 +62,8 @@ export function readFileViewerStateFromSearch(search: string): FileViewerUrlStat
|
||||
line: Number.isFinite(line) && line > 0 ? line : null,
|
||||
column: Number.isFinite(column) && column > 0 ? column : null,
|
||||
workspace,
|
||||
projectId: hasExplicitTarget ? projectIdRaw : null,
|
||||
workspaceId: hasExplicitTarget ? workspaceIdRaw : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,6 +77,8 @@ export function writeFileViewerStateToSearch(current: string, next: FileViewerUr
|
||||
params.delete("line");
|
||||
params.delete("column");
|
||||
params.delete("workspace");
|
||||
params.delete("projectId");
|
||||
params.delete("workspaceId");
|
||||
} else {
|
||||
params.set("file", next.path);
|
||||
if (next.line !== null) params.set("line", String(next.line));
|
||||
@@ -74,6 +87,10 @@ export function writeFileViewerStateToSearch(current: string, next: FileViewerUr
|
||||
else params.delete("column");
|
||||
if (next.workspace && next.workspace !== "auto") params.set("workspace", next.workspace);
|
||||
else params.delete("workspace");
|
||||
if (next.projectId) params.set("projectId", next.projectId);
|
||||
else params.delete("projectId");
|
||||
if (next.workspaceId) params.set("workspaceId", next.workspaceId);
|
||||
else params.delete("workspaceId");
|
||||
}
|
||||
const str = params.toString();
|
||||
return str ? `?${str}` : "";
|
||||
@@ -81,13 +98,21 @@ export function writeFileViewerStateToSearch(current: string, next: FileViewerUr
|
||||
|
||||
export interface FileViewerBrowseState {
|
||||
q: string | null;
|
||||
projectId: string | null;
|
||||
workspaceId: string | null;
|
||||
}
|
||||
|
||||
export function readBrowseStateFromSearch(search: string): FileViewerBrowseState | null {
|
||||
const params = new URLSearchParams(search);
|
||||
if (params.get("browse") !== "1") return null;
|
||||
const q = params.get("q");
|
||||
return { q: q && q.length > 0 ? q : null };
|
||||
const projectId = params.get("projectId");
|
||||
const workspaceId = params.get("workspaceId");
|
||||
return {
|
||||
q: q && q.length > 0 ? q : null,
|
||||
projectId: projectId || null,
|
||||
workspaceId: workspaceId || null,
|
||||
};
|
||||
}
|
||||
|
||||
interface FileViewerProviderProps {
|
||||
@@ -124,6 +149,8 @@ function EnabledFileViewerProvider({ issueId, children }: Omit<FileViewerProvide
|
||||
line: ref.line ?? null,
|
||||
column: ref.column ?? null,
|
||||
workspace: ref.workspace ?? "auto",
|
||||
projectId: ref.projectId ?? null,
|
||||
workspaceId: ref.workspaceId ?? null,
|
||||
});
|
||||
if (opts?.fromBrowse) {
|
||||
const params = new URLSearchParams(nextSearch);
|
||||
@@ -174,6 +201,8 @@ function EnabledFileViewerProvider({ issueId, children }: Omit<FileViewerProvide
|
||||
state,
|
||||
browse: browseState !== null,
|
||||
query: browseState?.q ?? null,
|
||||
browseProjectId: browseState?.projectId ?? null,
|
||||
browseWorkspaceId: browseState?.workspaceId ?? null,
|
||||
open,
|
||||
openBrowse,
|
||||
backToFiles,
|
||||
|
||||
+21
-5
@@ -87,12 +87,28 @@ export const queryKeys = {
|
||||
liveRuns: (issueId: string) => ["issues", "live-runs", issueId] as const,
|
||||
activeRun: (issueId: string) => ["issues", "active-run", issueId] as const,
|
||||
workProducts: (issueId: string) => ["issues", "work-products", issueId] as const,
|
||||
fileResources: (issueId: string, options: { workspace?: string; mode?: string; q?: string | null; limit?: number } = {}) =>
|
||||
fileResources: (
|
||||
issueId: string,
|
||||
options: {
|
||||
workspace?: string;
|
||||
projectId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
mode?: string;
|
||||
q?: string | null;
|
||||
limit?: number;
|
||||
} = {},
|
||||
) =>
|
||||
["issues", "file-resources", issueId, "list", options] as const,
|
||||
fileResource: (issueId: string, path: string, workspace: string) =>
|
||||
["issues", "file-resources", issueId, "resolve", path, workspace] as const,
|
||||
fileResourceContent: (issueId: string, path: string, workspace: string) =>
|
||||
["issues", "file-resources", issueId, "content", path, workspace] as const,
|
||||
fileResource: (
|
||||
issueId: string,
|
||||
query: { path: string; workspace?: string; projectId?: string | null; workspaceId?: string | null },
|
||||
) =>
|
||||
["issues", "file-resources", issueId, "resolve", query] as const,
|
||||
fileResourceContent: (
|
||||
issueId: string,
|
||||
query: { path: string; workspace?: string; projectId?: string | null; workspaceId?: string | null },
|
||||
) =>
|
||||
["issues", "file-resources", issueId, "content", query] as const,
|
||||
},
|
||||
routines: {
|
||||
list: (companyId: string, filters?: { projectId?: string | null }) =>
|
||||
|
||||
@@ -68,6 +68,27 @@ describe("remarkWorkspaceFileRefs", () => {
|
||||
expect(parsed?.column).toBe(2);
|
||||
});
|
||||
|
||||
it("round-trips explicit project workspace identity", () => {
|
||||
const href = buildWorkspaceFileHref({
|
||||
path: "content-os/cases/readme.md",
|
||||
line: 5,
|
||||
column: null,
|
||||
raw: "content-os/cases/readme.md:5",
|
||||
projectId: "17acae7d-9d0c-46bf-9c82-be9694ac3461",
|
||||
workspaceId: "0de5f74f-a7d4-4f73-a9a0-455a2b968cf2",
|
||||
projectName: "Paperclip Content",
|
||||
});
|
||||
const parsed = parseWorkspaceFileHref(href);
|
||||
expect(parsed).toMatchObject({
|
||||
path: "content-os/cases/readme.md",
|
||||
line: 5,
|
||||
column: null,
|
||||
projectId: "17acae7d-9d0c-46bf-9c82-be9694ac3461",
|
||||
workspaceId: "0de5f74f-a7d4-4f73-a9a0-455a2b968cf2",
|
||||
projectName: "Paperclip Content",
|
||||
});
|
||||
});
|
||||
|
||||
it("converts linked inline-code file paths into workspace-file links", () => {
|
||||
const tree: MarkdownNode = {
|
||||
type: "paragraph",
|
||||
|
||||
@@ -11,9 +11,12 @@ type MarkdownNode = {
|
||||
|
||||
export function buildWorkspaceFileHref(ref: ParsedWorkspaceFileRef): string {
|
||||
const params = new URLSearchParams();
|
||||
if (ref.projectId) params.set("projectId", ref.projectId);
|
||||
if (ref.workspaceId) params.set("workspaceId", ref.workspaceId);
|
||||
params.set("path", ref.path);
|
||||
if (ref.line !== null) params.set("line", String(ref.line));
|
||||
if (ref.column !== null) params.set("column", String(ref.column));
|
||||
if (ref.projectName) params.set("projectName", ref.projectName);
|
||||
return `${WORKSPACE_FILE_HREF_SCHEME}?${params.toString()}`;
|
||||
}
|
||||
|
||||
@@ -25,6 +28,10 @@ export function parseWorkspaceFileHref(href: string | null | undefined): ParsedW
|
||||
const params = new URLSearchParams(withoutLeadingQuestion);
|
||||
const path = params.get("path");
|
||||
if (!path) return null;
|
||||
const projectIdRaw = params.get("projectId");
|
||||
const workspaceIdRaw = params.get("workspaceId");
|
||||
const hasExplicitTarget = Boolean(projectIdRaw && workspaceIdRaw);
|
||||
const projectName = params.get("projectName");
|
||||
const lineRaw = params.get("line");
|
||||
const columnRaw = params.get("column");
|
||||
const line = lineRaw ? Number.parseInt(lineRaw, 10) : NaN;
|
||||
@@ -33,6 +40,9 @@ export function parseWorkspaceFileHref(href: string | null | undefined): ParsedW
|
||||
path,
|
||||
line: Number.isFinite(line) && line > 0 ? line : null,
|
||||
column: Number.isFinite(column) && column > 0 ? column : null,
|
||||
projectId: hasExplicitTarget ? projectIdRaw : null,
|
||||
workspaceId: hasExplicitTarget ? workspaceIdRaw : null,
|
||||
projectName: projectName || null,
|
||||
raw: path,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ export interface ParsedWorkspaceFileRef {
|
||||
path: string;
|
||||
line: number | null;
|
||||
column: number | null;
|
||||
projectId?: string | null;
|
||||
projectName?: string | null;
|
||||
workspaceId?: string | null;
|
||||
/** The original matched text (useful for rendering) */
|
||||
raw: string;
|
||||
}
|
||||
@@ -77,7 +80,10 @@ export function parseWorkspaceFileRef(input: string): ParsedWorkspaceFileRef | n
|
||||
}
|
||||
|
||||
export function formatWorkspaceFileRefDisplay(ref: ParsedWorkspaceFileRef): string {
|
||||
if (ref.line && ref.column) return `${ref.path}:${ref.line}:${ref.column}`;
|
||||
if (ref.line) return `${ref.path}:${ref.line}`;
|
||||
return ref.path;
|
||||
const path = ref.line && ref.column
|
||||
? `${ref.path}:${ref.line}:${ref.column}`
|
||||
: ref.line
|
||||
? `${ref.path}:${ref.line}`
|
||||
: ref.path;
|
||||
return ref.projectName ? `${ref.projectName} / ${path}` : path;
|
||||
}
|
||||
@@ -4405,6 +4405,7 @@ export function IssueDetail() {
|
||||
{fileViewerEnabled ? (
|
||||
<IssueFileViewer
|
||||
issueId={issue.id}
|
||||
companyId={issue.companyId}
|
||||
promptOpen={fileViewerPromptOpen}
|
||||
onPromptOpenChange={setFileViewerPromptOpen}
|
||||
/>
|
||||
@@ -4417,10 +4418,12 @@ export function IssueDetail() {
|
||||
|
||||
function IssueFileViewer({
|
||||
issueId,
|
||||
companyId,
|
||||
promptOpen,
|
||||
onPromptOpenChange,
|
||||
}: {
|
||||
issueId: string;
|
||||
companyId: string;
|
||||
promptOpen: boolean;
|
||||
onPromptOpenChange: (next: boolean) => void;
|
||||
}) {
|
||||
@@ -4430,6 +4433,7 @@ function IssueFileViewer({
|
||||
return (
|
||||
<FileViewerSheet
|
||||
issueId={issueId}
|
||||
companyId={companyId}
|
||||
open={open}
|
||||
showPromptWhenEmpty={showPromptWhenEmpty}
|
||||
onOpenChange={(next) => {
|
||||
|
||||
@@ -157,7 +157,7 @@ function FileViewerShell({
|
||||
state={
|
||||
showPromptWhenEmpty
|
||||
? null
|
||||
: { path, line, column, workspace }
|
||||
: { path, line, column, workspace, projectId: null, workspaceId: null }
|
||||
}
|
||||
showPromptWhenEmpty={showPromptWhenEmpty}
|
||||
open
|
||||
@@ -176,12 +176,12 @@ function seedContent(
|
||||
content: WorkspaceFileContent | null,
|
||||
) {
|
||||
queryClient.setQueryData(
|
||||
queryKeys.issues.fileResource(ISSUE_ID, path, workspace),
|
||||
queryKeys.issues.fileResource(ISSUE_ID, { path, workspace }),
|
||||
resource,
|
||||
);
|
||||
if (content) {
|
||||
queryClient.setQueryData(
|
||||
queryKeys.issues.fileResourceContent(ISSUE_ID, path, workspace),
|
||||
queryKeys.issues.fileResourceContent(ISSUE_ID, { path, workspace }),
|
||||
content,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,11 +135,17 @@ function ViewFromBrowse() {
|
||||
const queryClient = useQueryClient();
|
||||
queryClient.setQueryData(listKey(ISSUE_ID), recentList);
|
||||
queryClient.setQueryData(
|
||||
queryKeys.issues.fileResource(ISSUE_ID, "ui/src/components/FileViewerSheet.tsx", "auto"),
|
||||
queryKeys.issues.fileResource(ISSUE_ID, {
|
||||
path: "ui/src/components/FileViewerSheet.tsx",
|
||||
workspace: "auto",
|
||||
}),
|
||||
viewedResource,
|
||||
);
|
||||
queryClient.setQueryData(
|
||||
queryKeys.issues.fileResourceContent(ISSUE_ID, "ui/src/components/FileViewerSheet.tsx", "auto"),
|
||||
queryKeys.issues.fileResourceContent(ISSUE_ID, {
|
||||
path: "ui/src/components/FileViewerSheet.tsx",
|
||||
workspace: "auto",
|
||||
}),
|
||||
viewedContent,
|
||||
);
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user