feat(dashboard): dock file viewer, planning workflow switcher, gm mobile layout
- Files dock: clicking a file opens it inline (read-only FileEditor) with Back + pop-out-to-resizable-modal controls (DockFilesView). - Planning view shows the same board WorkflowSwitcher in the same Header workflow slot (PlanningWorkflowSwitcherSlot, portaled). - Embedded Git Manager uses the mobile single-column layout in the narrow dock (section tabs strip + full-width content). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,7 @@ import { subscribeSse } from "./sse-bus";
|
||||
import { AUTH_TOKEN_RECOVERY_REQUIRED_EVENT } from "./auth";
|
||||
import { AuthTokenRecoveryDialog } from "./components/AuthTokenRecoveryDialog";
|
||||
import { PlanningModeModal } from "./components/PlanningModeModal";
|
||||
import { PlanningWorkflowSwitcherSlot } from "./components/PlanningWorkflowSwitcherSlot";
|
||||
|
||||
// ChatView's CSS is imported eagerly so the styles bundle into the main
|
||||
// CSS file. Without this, the lazy ChatView JS chunk loaded its own CSS
|
||||
@@ -1823,6 +1824,11 @@ function AppInner() {
|
||||
};
|
||||
return (
|
||||
<PageErrorBoundary>
|
||||
{/*
|
||||
FNXC:Navigation 2026-06-22-00:00:
|
||||
Planning shows the same board WorkflowSwitcher in the same Header workflow slot as Board/List (portaled by PlanningWorkflowSwitcherSlot), so workflow selection is reachable from the left-sidebar Planning destination.
|
||||
*/}
|
||||
<PlanningWorkflowSwitcherSlot projectId={currentProject?.id} onOpenWorkflowEditor={openWorkflowEditorWithNav} />
|
||||
<PlanningModeModal
|
||||
isOpen={true}
|
||||
onClose={closePlanningView}
|
||||
|
||||
63
packages/dashboard/app/components/DockFilesView.css
Normal file
63
packages/dashboard/app/components/DockFilesView.css
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
FNXC:RightDockFiles 2026-06-22-00:00:
|
||||
The inline Files viewer fills the right-dock body and scrolls internally so the read-only FileEditor never overflows the dock.
|
||||
The header is a compact bar: BACK on the left, a truncating file name in the middle, POP-OUT on the right.
|
||||
*/
|
||||
.dock-files-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dock-files-view--viewer {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dock-files-viewer__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.dock-files-viewer__title {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dock-files-viewer__back,
|
||||
.dock-files-viewer__popout {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.dock-files-viewer__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dock-files-viewer__body .file-editor-container {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dock-files-viewer__status {
|
||||
padding: var(--space-md);
|
||||
font-size: var(--font-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.dock-files-viewer__status--error {
|
||||
color: var(--danger, var(--text));
|
||||
}
|
||||
125
packages/dashboard/app/components/DockFilesView.tsx
Normal file
125
packages/dashboard/app/components/DockFilesView.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowLeft, Maximize2 } from "lucide-react";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
import type { PluginDashboardViewContext } from "../plugins/types";
|
||||
import { fetchWorkspaceFileContent } from "../api";
|
||||
import { useWorkspaceFileBrowser } from "../hooks/useWorkspaceFileBrowser";
|
||||
import { FileBrowser } from "./FileBrowser";
|
||||
import { FileEditor } from "./FileEditor";
|
||||
import "./DockFilesView.css";
|
||||
|
||||
interface DockFilesViewProps {
|
||||
projectId?: string;
|
||||
openFile?: PluginDashboardViewContext["openFile"];
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:RightDockFiles 2026-06-22-00:00:
|
||||
The right-dock Files tool opens a clicked file INLINE inside the dock as a read-only viewer instead of immediately launching the resizable/movable FileBrowserModal.
|
||||
Clicking a file in the tree sets local `selectedFile` (it does NOT call `openFile`); the inline viewer reuses the read-only `FileEditor` so markdown previews and syntax highlighting match the rest of the app.
|
||||
The viewer header carries a BACK button (clears `selectedFile`, returning to the tree) and a POP-OUT button that calls `openFile(path, { workspace: "project" })` to escalate to the existing resizable/movable modal. This preserves the modal path; it is now opt-in via pop-out rather than the default click behavior.
|
||||
*/
|
||||
export function DockFilesView({ projectId, openFile }: DockFilesViewProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const { entries, currentPath, setPath, loading, error, refresh } = useWorkspaceFileBrowser("project", true, projectId);
|
||||
|
||||
// FNXC:RightDockFiles — selected file drives the inline read-only viewer; null returns to the tree.
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [content, setContent] = useState<string>("");
|
||||
const [contentLoading, setContentLoading] = useState(false);
|
||||
const [contentError, setContentError] = useState<string | null>(null);
|
||||
|
||||
// Load the selected file's content read-only from the project workspace.
|
||||
useEffect(() => {
|
||||
if (!selectedFile) {
|
||||
setContent("");
|
||||
setContentError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setContentLoading(true);
|
||||
setContentError(null);
|
||||
|
||||
fetchWorkspaceFileContent("project", selectedFile, projectId)
|
||||
.then((response) => {
|
||||
if (cancelled) return;
|
||||
setContent(response.content);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
setContentError(getErrorMessage(err) || t("editor.failedToLoadFile", "Failed to load file"));
|
||||
setContent("");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setContentLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedFile, projectId, t]);
|
||||
|
||||
const handleBack = useCallback(() => setSelectedFile(null), []);
|
||||
const handlePopOut = useCallback(() => {
|
||||
if (selectedFile) openFile?.(selectedFile, { workspace: "project" });
|
||||
}, [openFile, selectedFile]);
|
||||
|
||||
if (selectedFile) {
|
||||
const fileName = selectedFile.split("/").pop() || selectedFile;
|
||||
return (
|
||||
<div className="dock-files-view dock-files-view--viewer" data-testid="right-dock-files-view">
|
||||
<div className="dock-files-viewer__header">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-icon dock-files-viewer__back"
|
||||
onClick={handleBack}
|
||||
aria-label={t("fileViewer.back", "Back to files")}
|
||||
title={t("fileViewer.back", "Back to files")}
|
||||
data-testid="right-dock-files-back"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
</button>
|
||||
<span className="dock-files-viewer__title" title={selectedFile}>{fileName}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-icon dock-files-viewer__popout"
|
||||
onClick={handlePopOut}
|
||||
aria-label={t("fileViewer.popOut", "Open in resizable window")}
|
||||
title={t("fileViewer.popOut", "Open in resizable window")}
|
||||
data-testid="right-dock-files-popout"
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dock-files-viewer__body">
|
||||
{contentLoading ? (
|
||||
<div className="dock-files-viewer__status">{t("common.loading", "Loading...")}</div>
|
||||
) : contentError ? (
|
||||
<div className="dock-files-viewer__status dock-files-viewer__status--error">{contentError}</div>
|
||||
) : (
|
||||
<FileEditor content={content} onChange={() => {}} readOnly filePath={selectedFile} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dock-files-view" data-testid="right-dock-files-view">
|
||||
<FileBrowser
|
||||
entries={entries}
|
||||
currentPath={currentPath}
|
||||
onSelectFile={(path) => setSelectedFile(path)}
|
||||
onNavigate={setPath}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onRetry={refresh}
|
||||
workspace="project"
|
||||
onRefresh={refresh}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { fetchBoardWorkflows, type BoardWorkflowDefinition, type BoardWorkflowsPayload } from "../api";
|
||||
import { subscribeSse } from "../sse-bus";
|
||||
import { WorkflowSwitcher } from "./WorkflowSwitcher";
|
||||
import type { WorkflowStatusCounts } from "./workflowStatusCounts";
|
||||
import { readBoardWorkflowsCache, writeBoardWorkflowsCache } from "../utils/boardWorkflowsCache";
|
||||
|
||||
/*
|
||||
FNXC:PlanningWorkflowSwitcher 2026-06-22-00:00:
|
||||
The Planning view must surface the SAME workflow dropdown as the Board, in the SAME location (the Header `#header-workflow-slot`). Board owns its own switcher only while the board is active, so Planning needs a self-contained mirror that fetches/caches board-workflows, tracks local selection, and portals the identical `board-workflow-toolbar > board-workflow-selector > WorkflowSwitcher` markup into the header slot. We intentionally do NOT import Board (the board switcher is tied to board lifecycle/state).
|
||||
|
||||
Self-contained replication of Board's board-workflows fetch/cache/SSE-refresh path (Board.tsx ~370-470, ~607-637): refresh on mount, visibility/focus, and `workflow:created|updated|deleted` SSE, guarded by a monotonic sequence ref and persisted via the shared session cache. Gate render exactly like Board: only show when there is something to switch (workflow mode on AND >= 2 workflow options).
|
||||
*/
|
||||
|
||||
interface PlanningWorkflowSwitcherSlotProps {
|
||||
projectId?: string;
|
||||
onOpenWorkflowEditor?: () => void;
|
||||
onCreateWorkflow?: () => void;
|
||||
}
|
||||
|
||||
// Counts require live task/column data that Planning does not thread here.
|
||||
// WorkflowSwitcher renders zero counts for an empty map, so pass a stable empty Map
|
||||
// rather than threading tasks into the Planning view.
|
||||
const EMPTY_COUNTS: Map<string, WorkflowStatusCounts> = new Map();
|
||||
|
||||
export function PlanningWorkflowSwitcherSlot({ projectId, onOpenWorkflowEditor, onCreateWorkflow }: PlanningWorkflowSwitcherSlotProps) {
|
||||
const [boardWorkflowsState, setBoardWorkflowsState] = useState<{ projectId?: string; payload: BoardWorkflowsPayload } | null>(() => {
|
||||
const cached = readBoardWorkflowsCache(projectId);
|
||||
return cached ? { projectId, payload: cached } : null;
|
||||
});
|
||||
const boardWorkflows = boardWorkflowsState?.projectId === projectId && boardWorkflowsState ? boardWorkflowsState.payload : null;
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null);
|
||||
|
||||
// Header may mount its workflow slot after this component, so resolve it on mount
|
||||
// and re-resolve via a short polling effect until it attaches. Render only via portal.
|
||||
const [headerWorkflowSlot, setHeaderWorkflowSlot] = useState<HTMLElement | null>(() => {
|
||||
if (typeof document === "undefined") return null;
|
||||
return document.getElementById("header-workflow-slot");
|
||||
});
|
||||
|
||||
// Stale-response guard: drop out-of-order board-workflows responses.
|
||||
const boardWorkflowsFetchSeqRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const cached = readBoardWorkflowsCache(projectId);
|
||||
setBoardWorkflowsState(cached ? { projectId, payload: cached } : null);
|
||||
}, [projectId]);
|
||||
|
||||
/*
|
||||
FNXC:PlanningWorkflowSwitcher 2026-06-22-00:00:
|
||||
Opening the switcher must refresh the payload because task workflow assignment changes do not emit workflow-definition SSE events. Shared by mount, visibility/focus, and workflow-definition SSE refetches so the stale guard and cache writes stay identical to Board.
|
||||
*/
|
||||
const refreshBoardWorkflows = useCallback(() => {
|
||||
const seq = ++boardWorkflowsFetchSeqRef.current;
|
||||
fetchBoardWorkflows(projectId)
|
||||
.then((payload) => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload });
|
||||
writeBoardWorkflowsCache(projectId, payload);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload: { flagEnabled: false, defaultWorkflowId: "builtin:coding", workflows: [], taskWorkflowIds: {} } });
|
||||
}
|
||||
});
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshBoardWorkflows();
|
||||
const onVisible = () => {
|
||||
if (typeof document === "undefined" || document.visibilityState === "visible") refreshBoardWorkflows();
|
||||
};
|
||||
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
|
||||
if (typeof window !== "undefined") window.addEventListener("focus", onVisible);
|
||||
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
||||
const unsubscribe = subscribeSse(`/api/events${query}`, {
|
||||
events: {
|
||||
"workflow:created": refreshBoardWorkflows,
|
||||
"workflow:updated": refreshBoardWorkflows,
|
||||
"workflow:deleted": refreshBoardWorkflows,
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
boardWorkflowsFetchSeqRef.current++;
|
||||
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
|
||||
if (typeof window !== "undefined") window.removeEventListener("focus", onVisible);
|
||||
unsubscribe();
|
||||
};
|
||||
}, [projectId, refreshBoardWorkflows]);
|
||||
|
||||
// Attach to the header slot once the Header mounts it. Poll briefly until present.
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
const resolve = () => {
|
||||
const slot = document.getElementById("header-workflow-slot");
|
||||
setHeaderWorkflowSlot((prev) => (prev === slot ? prev : slot));
|
||||
return slot;
|
||||
};
|
||||
if (resolve()) return;
|
||||
const interval = window.setInterval(() => {
|
||||
if (resolve()) window.clearInterval(interval);
|
||||
}, 250);
|
||||
return () => window.clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const flagOn = boardWorkflows?.flagEnabled === true;
|
||||
const workflowMode = flagOn && Boolean(boardWorkflows?.workflows.length);
|
||||
|
||||
const workflowOptions = useMemo<BoardWorkflowDefinition[]>(() => {
|
||||
if (!workflowMode || !boardWorkflows) return [];
|
||||
return [...boardWorkflows.workflows].sort((a, b) => {
|
||||
if (a.id === boardWorkflows.defaultWorkflowId) return -1;
|
||||
if (b.id === boardWorkflows.defaultWorkflowId) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [boardWorkflows, workflowMode]);
|
||||
|
||||
const selectedWorkflow = useMemo<BoardWorkflowDefinition | null>(() => {
|
||||
if (!workflowMode) return null;
|
||||
return workflowOptions.find((workflow) => workflow.id === selectedWorkflowId)
|
||||
?? workflowOptions.find((workflow) => workflow.id === boardWorkflows?.defaultWorkflowId)
|
||||
?? workflowOptions[0]
|
||||
?? null;
|
||||
}, [boardWorkflows?.defaultWorkflowId, selectedWorkflowId, workflowMode, workflowOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workflowMode) {
|
||||
setSelectedWorkflowId(null);
|
||||
return;
|
||||
}
|
||||
if (selectedWorkflow && selectedWorkflow.id !== selectedWorkflowId) {
|
||||
setSelectedWorkflowId(selectedWorkflow.id);
|
||||
}
|
||||
}, [selectedWorkflow, selectedWorkflowId, workflowMode]);
|
||||
|
||||
// Gate: only render when there is something to switch (>= 2 options), matching Board's "show only when switchable" intent.
|
||||
if (!workflowMode || !selectedWorkflow || workflowOptions.length < 2 || !headerWorkflowSlot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workflowToolbar = (
|
||||
<div className="board-workflow-toolbar">
|
||||
<div className="board-workflow-selector">
|
||||
<WorkflowSwitcher
|
||||
workflows={workflowOptions}
|
||||
value={selectedWorkflow.id}
|
||||
onChange={setSelectedWorkflowId}
|
||||
counts={EMPTY_COUNTS}
|
||||
onOpen={refreshBoardWorkflows}
|
||||
onEditWorkflow={onOpenWorkflowEditor}
|
||||
onCreateWorkflow={onCreateWorkflow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return createPortal(workflowToolbar, headerWorkflowSlot);
|
||||
}
|
||||
@@ -577,6 +577,11 @@
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
/*
|
||||
FNXC:Navigation 2026-06-22-00:10:
|
||||
Anchor for the right dock, which is absolutely positioned so it overlays the page content instead of shrinking it.
|
||||
*/
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dashboard-project-shell--with-sidebar {
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
FNXC:Navigation 2026-06-21-00:00:
|
||||
The right dock CSS uses a mobile media query as a belt-and-suspenders guard only. The authoritative mobile gate is the JS `rightDockActive` value from `useViewportMode`, which also covers phone classes that a width-only query cannot classify reliably.
|
||||
*/
|
||||
/*
|
||||
FNXC:Navigation 2026-06-22-00:10:
|
||||
The right dock OVERLAYS the page content (floats over the right edge) instead of being a flex sibling that shrinks the main content. It is absolutely positioned against the project shell (which is position:relative) so opening/closing or resizing it never reflows the page beneath. z-index sits above content but below the docked terminal/modals.
|
||||
*/
|
||||
.right-dock {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
min-width: min(100%, var(--right-dock-min-width, calc(var(--space-2xl) * 8)));
|
||||
max-width: min(100%, var(--right-dock-max-width, calc(var(--space-2xl) * 22)));
|
||||
|
||||
@@ -1966,6 +1966,53 @@ The embedded host fills its right-dock container; the inner shell drops overlay-
|
||||
position: static;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitManager 2026-06-22-00:10:
|
||||
The embedded Git Manager renders inside the narrow right dock, so it must use the mobile single-column layout (section tabs as a horizontal strip above a full-width content pane) regardless of viewport width, mirroring the max-width:768px rules.
|
||||
*/
|
||||
.gm-modal--embedded .gm-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gm-modal--embedded .gm-sidebar {
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: calc(var(--space-2xl) + var(--space-md));
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.gm-modal--embedded .gm-nav-item {
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-left: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
min-width: calc(var(--space-2xl) + var(--space-xl));
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gm-modal--embedded .gm-nav-item.active {
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: var(--todo);
|
||||
}
|
||||
|
||||
.gm-modal--embedded .gm-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.gm-modal--embedded .gm-create-form {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Main layout: sidebar + content */
|
||||
.gm-layout {
|
||||
display: flex;
|
||||
|
||||
@@ -9,11 +9,10 @@ import {
|
||||
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
|
||||
import type { PluginDashboardViewEntry } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { useWorkspaceFileBrowser } from "../hooks/useWorkspaceFileBrowser";
|
||||
import { buildPluginTaskViewId } from "../plugins/pluginViewRegistry";
|
||||
import { PluginDashboardViewHost } from "../plugins/PluginDashboardViewHost";
|
||||
import type { DetailTaskTab, PluginDashboardViewContext } from "../plugins/types";
|
||||
import { FileBrowser } from "./FileBrowser";
|
||||
import { DockFilesView } from "./DockFilesView";
|
||||
import { PageErrorBoundary } from "./ErrorBoundary";
|
||||
import { getPluginNavIcon } from "./pluginNavIcon";
|
||||
import { UsageIndicator } from "./UsageIndicator";
|
||||
@@ -88,26 +87,6 @@ function wrapOverflowView(node: ReactNode): ReactNode {
|
||||
);
|
||||
}
|
||||
|
||||
function InlineFilesView({ projectId, openFile }: Pick<OverflowViewRenderProps, "projectId" | "openFile">) {
|
||||
const { entries, currentPath, setPath, loading, error, refresh } = useWorkspaceFileBrowser("project", true, projectId);
|
||||
return (
|
||||
<div data-testid="right-dock-files-view">
|
||||
<FileBrowser
|
||||
entries={entries}
|
||||
currentPath={currentPath}
|
||||
onSelectFile={(path) => openFile?.(path, { workspace: "project" })}
|
||||
onNavigate={setPath}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onRetry={refresh}
|
||||
workspace="project"
|
||||
onRefresh={refresh}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Navigation 2026-06-21-00:00:
|
||||
The right dock and its expand modal must resolve every hosted overflow destination through this registry so toolbar gating, component choice, and props cannot drift between the compact panel and full-size modal surfaces.
|
||||
@@ -166,7 +145,7 @@ export const STATIC_OVERFLOW_VIEW_ENTRIES: readonly OverflowViewEntry[] = [
|
||||
label: "Files",
|
||||
icon: Folder,
|
||||
testId: "right-dock-tab-files",
|
||||
render: (props) => wrapOverflowView(<InlineFilesView projectId={props.projectId} openFile={props.openFile} />),
|
||||
render: (props) => wrapOverflowView(<DockFilesView projectId={props.projectId} openFile={props.openFile} />),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user