mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Gate file viewer UI behind experimental flag
This commit is contained in:
1 parent
8f6f8ce205
commit
f67c02f894
12 files changed
+199
-26
No files matched your search
@@ -31,6 +31,7 @@ export interface InstanceExperimentalSettings {
|
||||
enableIsolatedWorkspaces: boolean;
|
||||
enableStreamlinedLeftNavigation: boolean;
|
||||
enableIssuePlanDecompositions: boolean;
|
||||
enableExperimentalFileViewer: boolean;
|
||||
enableCloudSync: boolean;
|
||||
autoRestartDevServerWhenIdle: boolean;
|
||||
enableIssueGraphLivenessAutoRecovery: boolean;
|
||||
|
||||
@@ -40,6 +40,7 @@ export const instanceExperimentalSettingsSchema = z.object({
|
||||
enableIsolatedWorkspaces: z.boolean().default(false),
|
||||
enableStreamlinedLeftNavigation: z.boolean().default(false),
|
||||
enableIssuePlanDecompositions: z.boolean().default(false),
|
||||
enableExperimentalFileViewer: z.boolean().default(false),
|
||||
enableCloudSync: z.boolean().default(false),
|
||||
autoRestartDevServerWhenIdle: z.boolean().default(false),
|
||||
enableIssueGraphLivenessAutoRecovery: z.boolean().default(false),
|
||||
|
||||
@@ -65,6 +65,7 @@ describe("instance settings routes", () => {
|
||||
enableEnvironments: false,
|
||||
enableIsolatedWorkspaces: false,
|
||||
enableIssuePlanDecompositions: false,
|
||||
enableExperimentalFileViewer: false,
|
||||
enableCloudSync: false,
|
||||
autoRestartDevServerWhenIdle: false,
|
||||
enableIssueGraphLivenessAutoRecovery: true,
|
||||
@@ -84,6 +85,7 @@ describe("instance settings routes", () => {
|
||||
enableEnvironments: true,
|
||||
enableIsolatedWorkspaces: true,
|
||||
enableIssuePlanDecompositions: true,
|
||||
enableExperimentalFileViewer: true,
|
||||
enableCloudSync: true,
|
||||
autoRestartDevServerWhenIdle: false,
|
||||
enableIssueGraphLivenessAutoRecovery: true,
|
||||
@@ -128,6 +130,7 @@ describe("instance settings routes", () => {
|
||||
enableEnvironments: false,
|
||||
enableIsolatedWorkspaces: false,
|
||||
enableIssuePlanDecompositions: false,
|
||||
enableExperimentalFileViewer: false,
|
||||
enableCloudSync: false,
|
||||
autoRestartDevServerWhenIdle: false,
|
||||
enableIssueGraphLivenessAutoRecovery: true,
|
||||
|
||||
@@ -7,6 +7,7 @@ describe("instance settings service", () => {
|
||||
enableEnvironments: true,
|
||||
enableIsolatedWorkspaces: true,
|
||||
enableIssuePlanDecompositions: true,
|
||||
enableExperimentalFileViewer: true,
|
||||
enableCloudSync: true,
|
||||
autoRestartDevServerWhenIdle: true,
|
||||
enableIssueGraphLivenessAutoRecovery: true,
|
||||
@@ -17,6 +18,7 @@ describe("instance settings service", () => {
|
||||
enableIsolatedWorkspaces: true,
|
||||
enableStreamlinedLeftNavigation: false,
|
||||
enableIssuePlanDecompositions: true,
|
||||
enableExperimentalFileViewer: true,
|
||||
enableCloudSync: true,
|
||||
autoRestartDevServerWhenIdle: true,
|
||||
enableIssueGraphLivenessAutoRecovery: true,
|
||||
|
||||
@@ -45,6 +45,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
|
||||
enableIsolatedWorkspaces: parsed.data.enableIsolatedWorkspaces ?? false,
|
||||
enableStreamlinedLeftNavigation: parsed.data.enableStreamlinedLeftNavigation ?? false,
|
||||
enableIssuePlanDecompositions: parsed.data.enableIssuePlanDecompositions ?? false,
|
||||
enableExperimentalFileViewer: parsed.data.enableExperimentalFileViewer ?? false,
|
||||
enableCloudSync: parsed.data.enableCloudSync ?? false,
|
||||
autoRestartDevServerWhenIdle: parsed.data.autoRestartDevServerWhenIdle ?? false,
|
||||
enableIssueGraphLivenessAutoRecovery: parsed.data.enableIssueGraphLivenessAutoRecovery ?? false,
|
||||
@@ -58,6 +59,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
|
||||
enableIsolatedWorkspaces: false,
|
||||
enableStreamlinedLeftNavigation: false,
|
||||
enableIssuePlanDecompositions: false,
|
||||
enableExperimentalFileViewer: false,
|
||||
enableCloudSync: false,
|
||||
autoRestartDevServerWhenIdle: false,
|
||||
enableIssueGraphLivenessAutoRecovery: false,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createRoot } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { CommandPalette } from "./CommandPalette";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
|
||||
function act(callback: () => void | Promise<void>) {
|
||||
let result: void | Promise<void> | undefined;
|
||||
@@ -41,6 +42,10 @@ const mockProjectsApi = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockInstanceSettingsApi = vi.hoisted(() => ({
|
||||
getExperimental: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../context/CompanyContext", () => ({
|
||||
useCompany: () => companyState,
|
||||
}));
|
||||
@@ -57,10 +62,13 @@ vi.mock("../context/SidebarContext", () => ({
|
||||
const navigateState = vi.hoisted(() => ({
|
||||
navigate: vi.fn(),
|
||||
}));
|
||||
const locationState = vi.hoisted(() => ({
|
||||
location: { pathname: "/", search: "", hash: "" },
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/router", () => ({
|
||||
useNavigate: () => navigateState.navigate,
|
||||
useLocation: () => ({ pathname: "/", search: "", hash: "" }),
|
||||
useLocation: () => locationState.location,
|
||||
}));
|
||||
|
||||
vi.mock("../api/issues", () => ({
|
||||
@@ -75,6 +83,10 @@ vi.mock("../api/projects", () => ({
|
||||
projectsApi: mockProjectsApi,
|
||||
}));
|
||||
|
||||
vi.mock("../api/instanceSettings", () => ({
|
||||
instanceSettingsApi: mockInstanceSettingsApi,
|
||||
}));
|
||||
|
||||
vi.mock("./Identity", () => ({
|
||||
Identity: ({ name }: { name: string }) => <span>{name}</span>,
|
||||
}));
|
||||
@@ -142,7 +154,11 @@ async function waitForAssertion(assertion: () => void, attempts = 20) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
function renderWithQueryClient(node: ReactNode, container: HTMLDivElement) {
|
||||
function renderWithQueryClient(
|
||||
node: ReactNode,
|
||||
container: HTMLDivElement,
|
||||
seedQueryClient?: (queryClient: QueryClient) => void,
|
||||
) {
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -151,6 +167,7 @@ function renderWithQueryClient(node: ReactNode, container: HTMLDivElement) {
|
||||
},
|
||||
},
|
||||
});
|
||||
seedQueryClient?.(queryClient);
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
@@ -175,10 +192,17 @@ describe("CommandPalette", () => {
|
||||
mockIssuesApi.list.mockReset();
|
||||
mockAgentsApi.list.mockReset();
|
||||
mockProjectsApi.list.mockReset();
|
||||
mockInstanceSettingsApi.getExperimental.mockReset();
|
||||
navigateState.navigate.mockReset();
|
||||
locationState.location.pathname = "/";
|
||||
locationState.location.search = "";
|
||||
locationState.location.hash = "";
|
||||
mockIssuesApi.list.mockResolvedValue([]);
|
||||
mockAgentsApi.list.mockResolvedValue([]);
|
||||
mockProjectsApi.list.mockResolvedValue([]);
|
||||
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
|
||||
enableExperimentalFileViewer: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -212,6 +236,52 @@ describe("CommandPalette", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("hides the issue file viewer command by default", async () => {
|
||||
locationState.location.pathname = "/issues/PAP-1";
|
||||
const { root } = renderWithQueryClient(<CommandPalette />, container);
|
||||
|
||||
act(() => {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true, bubbles: true }));
|
||||
});
|
||||
|
||||
await waitForAssertion(() => {
|
||||
expect(container.textContent).toContain("Create new task");
|
||||
});
|
||||
expect(container.textContent).not.toContain("Open file in this issue");
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the issue file viewer command when the experimental flag is enabled", async () => {
|
||||
locationState.location.pathname = "/issues/PAP-1";
|
||||
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
|
||||
enableExperimentalFileViewer: true,
|
||||
});
|
||||
const { root } = renderWithQueryClient(
|
||||
<CommandPalette />,
|
||||
container,
|
||||
(queryClient) => {
|
||||
queryClient.setQueryData(queryKeys.instance.experimentalSettings, {
|
||||
enableExperimentalFileViewer: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true, bubbles: true }));
|
||||
});
|
||||
|
||||
await waitForAssertion(() => {
|
||||
expect(container.textContent).toContain("Open file in this issue");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("offers a Search-all command when the query is non-empty and routes Enter to /search when no issues match", async () => {
|
||||
mockIssuesApi.list.mockResolvedValue([]);
|
||||
const { root } = renderWithQueryClient(<CommandPalette />, container);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useSidebar } from "../context/SidebarContext";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { instanceSettingsApi } from "../api/instanceSettings";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import {
|
||||
CommandDialog,
|
||||
@@ -57,6 +58,12 @@ export function CommandPalette() {
|
||||
const { isMobile, setSidebarOpen } = useSidebar();
|
||||
const searchQuery = query.trim();
|
||||
const onIssueDetail = isOnIssueDetail(location.pathname);
|
||||
const { data: experimentalSettings } = useQuery({
|
||||
queryKey: queryKeys.instance.experimentalSettings,
|
||||
queryFn: () => instanceSettingsApi.getExperimental(),
|
||||
retry: false,
|
||||
});
|
||||
const fileViewerEnabled = experimentalSettings?.enableExperimentalFileViewer === true;
|
||||
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
@@ -186,7 +193,7 @@ export function CommandPalette() {
|
||||
Create new task
|
||||
<span className="ml-auto text-xs text-muted-foreground">C</span>
|
||||
</CommandItem>
|
||||
{onIssueDetail && (
|
||||
{onIssueDetail && fileViewerEnabled && (
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
|
||||
@@ -43,7 +43,7 @@ export function WorkspaceFileMarkdownBody({
|
||||
|
||||
return (
|
||||
<div onClick={handleClick}>
|
||||
<MarkdownBody {...props} linkWorkspaceFileRefs>{children}</MarkdownBody>
|
||||
<MarkdownBody {...props} linkWorkspaceFileRefs={!!viewer}>{children}</MarkdownBody>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -93,9 +93,15 @@ export function readBrowseStateFromSearch(search: string): FileViewerBrowseState
|
||||
interface FileViewerProviderProps {
|
||||
issueId: string;
|
||||
children: ReactNode;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function FileViewerProvider({ issueId, children }: FileViewerProviderProps) {
|
||||
export function FileViewerProvider({ issueId, children, enabled = true }: FileViewerProviderProps) {
|
||||
if (!enabled) return <>{children}</>;
|
||||
return <EnabledFileViewerProvider issueId={issueId}>{children}</EnabledFileViewerProvider>;
|
||||
}
|
||||
|
||||
function EnabledFileViewerProvider({ issueId, children }: Omit<FileViewerProviderProps, "enabled">) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const state = useMemo(() => readFileViewerStateFromSearch(location.search), [location.search]);
|
||||
|
||||
@@ -210,6 +210,8 @@ export function InstanceExperimentalSettings() {
|
||||
experimentalQuery.data?.enableStreamlinedLeftNavigation === true;
|
||||
const enableIssuePlanDecompositions =
|
||||
experimentalQuery.data?.enableIssuePlanDecompositions === true;
|
||||
const enableExperimentalFileViewer =
|
||||
experimentalQuery.data?.enableExperimentalFileViewer === true;
|
||||
const enableCloudSync = experimentalQuery.data?.enableCloudSync === true;
|
||||
const autoRestartDevServerWhenIdle = experimentalQuery.data?.autoRestartDevServerWhenIdle === true;
|
||||
const enableIssueGraphLivenessAutoRecovery =
|
||||
@@ -286,6 +288,27 @@ export function InstanceExperimentalSettings() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-border bg-card p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-sm font-semibold">Experimental File Viewer</h2>
|
||||
<p className="max-w-2xl text-sm text-muted-foreground">
|
||||
Show task detail controls for browsing and previewing workspace files relative to a task.
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
checked={enableExperimentalFileViewer}
|
||||
onCheckedChange={() =>
|
||||
toggleMutation.mutate({
|
||||
enableExperimentalFileViewer: !enableExperimentalFileViewer,
|
||||
})
|
||||
}
|
||||
disabled={toggleMutation.isPending}
|
||||
aria-label="Toggle experimental file viewer setting"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-border bg-card p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -75,6 +75,7 @@ const mockPushToast = vi.hoisted(() => vi.fn());
|
||||
const mockIssuesListRender = vi.hoisted(() => vi.fn());
|
||||
const mockIssueChatThreadRender = vi.hoisted(() => vi.fn());
|
||||
const mockImageGalleryRender = vi.hoisted(() => vi.fn());
|
||||
const mockIssueWorkspaceCardRender = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../api/issues", () => ({
|
||||
issuesApi: mockIssuesApi,
|
||||
@@ -282,7 +283,10 @@ vi.mock("../components/IssueRunLedger", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../components/IssueWorkspaceCard", () => ({
|
||||
IssueWorkspaceCard: () => <div>Workspace</div>,
|
||||
IssueWorkspaceCard: (props: { onBrowseFiles?: () => void; onOpenFileByPath?: () => void }) => {
|
||||
mockIssueWorkspaceCardRender(props);
|
||||
return <div>Workspace</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../components/ImageGalleryModal", () => ({
|
||||
@@ -934,11 +938,13 @@ describe("IssueDetail", () => {
|
||||
});
|
||||
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
|
||||
enableIssuePlanDecompositions: false,
|
||||
enableExperimentalFileViewer: false,
|
||||
});
|
||||
mockIssuesApi.listAcceptedPlanDecompositions.mockResolvedValue([]);
|
||||
mockIssuesListRender.mockClear();
|
||||
mockIssueChatThreadRender.mockClear();
|
||||
mockImageGalleryRender.mockClear();
|
||||
mockIssueWorkspaceCardRender.mockClear();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -994,10 +1000,55 @@ describe("IssueDetail", () => {
|
||||
expect(mockIssuesApi.listAcceptedPlanDecompositions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hides file viewer entry points by default", async () => {
|
||||
mockIssuesApi.get.mockResolvedValue(createIssue());
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<IssueDetail />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
await flushReact();
|
||||
await flushReact();
|
||||
|
||||
expect(container.querySelector('[aria-label="Open file in this issue"]')).toBeNull();
|
||||
const latestWorkspaceProps = mockIssueWorkspaceCardRender.mock.calls.at(-1)?.[0];
|
||||
expect(latestWorkspaceProps?.onBrowseFiles).toBeUndefined();
|
||||
expect(latestWorkspaceProps?.onOpenFileByPath).toBeUndefined();
|
||||
});
|
||||
|
||||
it("shows file viewer entry points when the experimental flag is enabled", async () => {
|
||||
mockIssuesApi.get.mockResolvedValue(createIssue());
|
||||
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
|
||||
enableIssuePlanDecompositions: false,
|
||||
enableExperimentalFileViewer: true,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<IssueDetail />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
await flushReact();
|
||||
await flushReact();
|
||||
|
||||
expect(container.querySelector('[aria-label="Open file in this issue"]')).not.toBeNull();
|
||||
const latestWorkspaceProps = mockIssueWorkspaceCardRender.mock.calls.at(-1)?.[0];
|
||||
expect(latestWorkspaceProps?.onBrowseFiles).toEqual(expect.any(Function));
|
||||
expect(latestWorkspaceProps?.onOpenFileByPath).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it("shows the plan decomposition panel when the experimental flag is enabled", async () => {
|
||||
mockIssuesApi.get.mockResolvedValue(createIssue());
|
||||
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
|
||||
enableIssuePlanDecompositions: true,
|
||||
enableExperimentalFileViewer: false,
|
||||
});
|
||||
mockIssuesApi.listAcceptedPlanDecompositions.mockResolvedValue([
|
||||
{
|
||||
|
||||
@@ -1520,6 +1520,7 @@ export function IssueDetail() {
|
||||
const feedbackDataSharingPreference = instanceGeneralSettings?.feedbackDataSharingPreference ?? "prompt";
|
||||
const showPlanDecompositionsSection =
|
||||
instanceExperimentalSettings?.enableIssuePlanDecompositions === true;
|
||||
const fileViewerEnabled = instanceExperimentalSettings?.enableExperimentalFileViewer === true;
|
||||
const { orderedProjects } = useProjectOrder({
|
||||
projects: projects ?? [],
|
||||
companyId: selectedCompanyId,
|
||||
@@ -2952,6 +2953,7 @@ export function IssueDetail() {
|
||||
setPendingCommentComposerFocusKey((current) => current + 1);
|
||||
}
|
||||
if (action === "open_file_viewer") {
|
||||
if (!fileViewerEnabled) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setFileViewerPromptOpen(true);
|
||||
@@ -2967,7 +2969,7 @@ export function IssueDetail() {
|
||||
document.removeEventListener("focusin", handleFocusIn, true);
|
||||
document.removeEventListener("keydown", handleKeyDown, true);
|
||||
};
|
||||
}, [keyboardShortcutsEnabled, navigate, sourceBreadcrumb.href]);
|
||||
}, [fileViewerEnabled, keyboardShortcutsEnabled, navigate, sourceBreadcrumb.href]);
|
||||
|
||||
useEffect(() => {
|
||||
const hash = location.hash;
|
||||
@@ -3016,6 +3018,7 @@ export function IssueDetail() {
|
||||
}, [detailTab, pendingCommentComposerFocusKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileViewerEnabled) return;
|
||||
const handleOpenFileViewer = () => {
|
||||
setFileViewerPromptOpen(true);
|
||||
};
|
||||
@@ -3026,7 +3029,7 @@ export function IssueDetail() {
|
||||
handleOpenFileViewer as EventListener,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
}, [fileViewerEnabled]);
|
||||
|
||||
const promotedOutputAttachmentIds = useMemo(() => getPromotedOutputAttachmentIds(workProducts), [workProducts]);
|
||||
const attachmentList = useMemo(
|
||||
@@ -3469,7 +3472,7 @@ export function IssueDetail() {
|
||||
);
|
||||
|
||||
return (
|
||||
<FileViewerProvider issueId={issue.id}>
|
||||
<FileViewerProvider issueId={issue.id} enabled={fileViewerEnabled}>
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* Parent chain breadcrumb */}
|
||||
{ancestors.length > 0 && (
|
||||
@@ -3719,15 +3722,17 @@ export function IssueDetail() {
|
||||
<Archive className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => setFileViewerPromptOpen(true)}
|
||||
title="Open file... (g f)"
|
||||
aria-label="Open file in this issue"
|
||||
>
|
||||
<FileCode2 className="h-4 w-4" />
|
||||
</Button>
|
||||
{fileViewerEnabled ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => setFileViewerPromptOpen(true)}
|
||||
title="Open file... (g f)"
|
||||
aria-label="Open file in this issue"
|
||||
>
|
||||
<FileCode2 className="h-4 w-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
@@ -4052,11 +4057,11 @@ export function IssueDetail() {
|
||||
issue={issue}
|
||||
project={resolvedProject}
|
||||
onUpdate={(data) => updateIssue.mutate(data)}
|
||||
onBrowseFiles={() => setFileViewerPromptOpen(true)}
|
||||
onOpenFileByPath={() => setFileViewerPromptOpen(true)}
|
||||
onBrowseFiles={fileViewerEnabled ? () => setFileViewerPromptOpen(true) : undefined}
|
||||
onOpenFileByPath={fileViewerEnabled ? () => setFileViewerPromptOpen(true) : undefined}
|
||||
/>
|
||||
|
||||
{issue.workProducts && issue.workProducts.length > 0 && (() => {
|
||||
{fileViewerEnabled && issue.workProducts && issue.workProducts.length > 0 && (() => {
|
||||
const workProductsWithFileRefs = issue.workProducts
|
||||
.map((product) => ({ product, fileRef: extractWorkspaceFileRefFromWorkProduct(product) }))
|
||||
.filter(({ fileRef }) => fileRef !== null);
|
||||
@@ -4397,11 +4402,13 @@ export function IssueDetail() {
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<IssueFileViewer
|
||||
issueId={issue.id}
|
||||
promptOpen={fileViewerPromptOpen}
|
||||
onPromptOpenChange={setFileViewerPromptOpen}
|
||||
/>
|
||||
{fileViewerEnabled ? (
|
||||
<IssueFileViewer
|
||||
issueId={issue.id}
|
||||
promptOpen={fileViewerPromptOpen}
|
||||
onPromptOpenChange={setFileViewerPromptOpen}
|
||||
/>
|
||||
) : null}
|
||||
<ScrollToBottom />
|
||||
</div>
|
||||
</FileViewerProvider>
|
||||
|
||||
Reference in new issue
Block a user