feat(web): panneau Fichiers — navigation projet + téléchargements fichier/dossier ZIP — #211
This commit is contained in:
@ -31,16 +31,7 @@ import { useGateways } from "@/app/di";
|
||||
import { useProjectWorkState } from "@/features/workstate/useProjectWorkState";
|
||||
import { LayoutGrid, LayoutTabs } from "@/features/layout";
|
||||
import { ProjectContextPanel } from "@/features/projects/ProjectContextPanel";
|
||||
import {
|
||||
PANEL_TITLE,
|
||||
floatingPanels,
|
||||
isDockedTo,
|
||||
panelsDockedTo,
|
||||
placementOf,
|
||||
type PanelId,
|
||||
type ViewPlacement,
|
||||
type ViewPlacements,
|
||||
} from "@/features/projects/viewPlacement";
|
||||
import { isDockedTo, type ViewPlacement } from "@/features/projects/viewPlacement";
|
||||
import { AgentsPanel } from "@/features/agents";
|
||||
import { TemplatesPanel } from "@/features/templates";
|
||||
import { SkillsPanel } from "@/features/skills";
|
||||
@ -69,6 +60,7 @@ import { WebTicketsView } from "./tickets/WebTicketsView";
|
||||
import { WebSprintsView } from "./tickets/WebSprintsView";
|
||||
import { WebProjectsPanel } from "./WebProjectsPanel";
|
||||
import { WebMenuSheet } from "./WebMenuSheet";
|
||||
import { ProjectFilesPanel } from "./ProjectFilesPanel";
|
||||
import {
|
||||
WEB_PANEL_META,
|
||||
type WebOpenablePanelId,
|
||||
@ -205,19 +197,50 @@ function useMobileWorkspace(): boolean {
|
||||
return mobile;
|
||||
}
|
||||
|
||||
const WEB_PANEL_SIZE: Record<PanelId, FloatingWindowSize> = {
|
||||
type WebDesktopPanelId = WebProjectPanelId;
|
||||
type WebDesktopPlacements = Partial<Record<WebDesktopPanelId, ViewPlacement>>;
|
||||
|
||||
const WEB_PANEL_SIZE: Record<WebDesktopPanelId, FloatingWindowSize> = {
|
||||
projects: "md",
|
||||
context: "lg",
|
||||
work: "lg",
|
||||
tickets: "lg",
|
||||
sprints: "lg",
|
||||
agents: "lg",
|
||||
templates: "lg",
|
||||
skills: "lg",
|
||||
permissions: "md",
|
||||
memory: "lg",
|
||||
git: "lg",
|
||||
files: "lg",
|
||||
};
|
||||
|
||||
function webPanelTitle(panel: WebDesktopPanelId): string {
|
||||
return WEB_PANEL_META[panel].label;
|
||||
}
|
||||
|
||||
function webPlacementOf(
|
||||
placements: WebDesktopPlacements,
|
||||
panel: WebDesktopPanelId,
|
||||
): ViewPlacement {
|
||||
return placements[panel] ?? "closed";
|
||||
}
|
||||
|
||||
function webPanelsDockedTo(
|
||||
placements: WebDesktopPlacements,
|
||||
side: "left" | "right",
|
||||
): WebDesktopPanelId[] {
|
||||
return (Object.keys(WEB_PANEL_META) as WebDesktopPanelId[]).filter((panel) =>
|
||||
isDockedTo(webPlacementOf(placements, panel), side),
|
||||
);
|
||||
}
|
||||
|
||||
function webFloatingPanels(placements: WebDesktopPlacements): WebDesktopPanelId[] {
|
||||
return (Object.keys(WEB_PANEL_META) as WebDesktopPanelId[]).filter(
|
||||
(panel) => webPlacementOf(placements, panel) === "floating",
|
||||
);
|
||||
}
|
||||
|
||||
function WebDesktopWorkspace({
|
||||
projects,
|
||||
openId,
|
||||
@ -243,7 +266,7 @@ function WebDesktopWorkspace({
|
||||
onRefreshProjects: () => void;
|
||||
onViewSprintTickets: (sprintId: string) => void;
|
||||
}) {
|
||||
const [placements, setPlacements] = useState<ViewPlacements>({
|
||||
const [placements, setPlacements] = useState<WebDesktopPlacements>({
|
||||
work: { dock: "right" },
|
||||
});
|
||||
const [leftDockWidth, setLeftDockWidth] = useState(340);
|
||||
@ -259,11 +282,11 @@ function WebDesktopWorkspace({
|
||||
setViewerConversation(null);
|
||||
}, [openId]);
|
||||
|
||||
function setPlacement(panel: PanelId, placement: Exclude<ViewPlacement, "detached">) {
|
||||
function setPlacement(panel: WebDesktopPanelId, placement: Exclude<ViewPlacement, "detached">) {
|
||||
setPlacements((prev) => {
|
||||
const next: ViewPlacements = { ...prev };
|
||||
const next: WebDesktopPlacements = { ...prev };
|
||||
if (placement === "floating") {
|
||||
for (const other of Object.keys(next) as PanelId[]) {
|
||||
for (const other of Object.keys(next) as WebDesktopPanelId[]) {
|
||||
if (other !== panel && next[other] === "floating") delete next[other];
|
||||
}
|
||||
}
|
||||
@ -272,7 +295,7 @@ function WebDesktopWorkspace({
|
||||
});
|
||||
}
|
||||
|
||||
function closePanel(panel: PanelId) {
|
||||
function closePanel(panel: WebDesktopPanelId) {
|
||||
setPlacements((prev) => {
|
||||
if (!(panel in prev)) return prev;
|
||||
const next = { ...prev };
|
||||
@ -285,7 +308,7 @@ function WebDesktopWorkspace({
|
||||
setPlacements((prev) => {
|
||||
let changed = false;
|
||||
const next = { ...prev };
|
||||
for (const panel of Object.keys(next) as PanelId[]) {
|
||||
for (const panel of Object.keys(next) as WebDesktopPanelId[]) {
|
||||
if (next[panel] === "floating") {
|
||||
delete next[panel];
|
||||
changed = true;
|
||||
@ -301,22 +324,23 @@ function WebDesktopWorkspace({
|
||||
dismissFloating();
|
||||
}
|
||||
|
||||
const panelOrder: PanelId[] = [
|
||||
const panelOrder: WebDesktopPanelId[] = [
|
||||
"context",
|
||||
"work",
|
||||
"tickets",
|
||||
"sprints",
|
||||
"agents",
|
||||
"templates",
|
||||
"skills",
|
||||
"permissions",
|
||||
"memory",
|
||||
"git",
|
||||
"files",
|
||||
"projects",
|
||||
];
|
||||
const panelToWebId = (panel: PanelId): WebProjectPanelId => panel;
|
||||
|
||||
function placementSubmenu(panel: PanelId): MenuBarItem[] {
|
||||
const placement = placementOf(placements, panel);
|
||||
function placementSubmenu(panel: WebDesktopPanelId): MenuBarItem[] {
|
||||
const placement = webPlacementOf(placements, panel);
|
||||
return [
|
||||
{
|
||||
id: "closed",
|
||||
@ -351,8 +375,8 @@ function WebDesktopWorkspace({
|
||||
label: "Panneaux",
|
||||
items: panelOrder.map((panel) => ({
|
||||
id: panel,
|
||||
label: PANEL_TITLE[panel],
|
||||
active: placementOf(placements, panel) !== "closed",
|
||||
label: webPanelTitle(panel),
|
||||
active: webPlacementOf(placements, panel) !== "closed",
|
||||
onSelect: () => {},
|
||||
submenu: placementSubmenu(panel),
|
||||
})),
|
||||
@ -369,14 +393,14 @@ function WebDesktopWorkspace({
|
||||
/>
|
||||
);
|
||||
|
||||
function renderPanel(panel: PanelId) {
|
||||
function renderPanel(panel: WebDesktopPanelId) {
|
||||
if (panel === "projects") return projectsManager;
|
||||
if (!openId) {
|
||||
return <p className="text-sm text-muted">Ouvrir un projet pour utiliser ce panneau.</p>;
|
||||
}
|
||||
return (
|
||||
<WebProjectPanelBody
|
||||
panel={panelToWebId(panel) as WebOpenablePanelId}
|
||||
panel={panel as WebOpenablePanelId}
|
||||
projectId={openId}
|
||||
root={openRoot}
|
||||
focusSprintId={focusSprintId}
|
||||
@ -385,9 +409,9 @@ function WebDesktopWorkspace({
|
||||
);
|
||||
}
|
||||
|
||||
function DockControls({ panel }: { panel: PanelId }) {
|
||||
const placement = placementOf(placements, panel);
|
||||
const title = PANEL_TITLE[panel];
|
||||
function DockControls({ panel }: { panel: WebDesktopPanelId }) {
|
||||
const placement = webPlacementOf(placements, panel);
|
||||
const title = webPanelTitle(panel);
|
||||
const activeClass = "bg-raised text-content ring-1 ring-border-strong";
|
||||
return (
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
@ -433,16 +457,16 @@ function WebDesktopWorkspace({
|
||||
);
|
||||
}
|
||||
|
||||
function renderDockedView(panel: PanelId) {
|
||||
function renderDockedView(panel: WebDesktopPanelId) {
|
||||
return (
|
||||
<section
|
||||
key={panel}
|
||||
aria-label={`${PANEL_TITLE[panel]} panel`}
|
||||
aria-label={`${webPanelTitle(panel)} panel`}
|
||||
className="flex min-h-0 flex-1 flex-col border-b border-border last:border-b-0"
|
||||
>
|
||||
<header className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-2">
|
||||
<span className="truncate text-sm font-medium text-content">
|
||||
{PANEL_TITLE[panel]}
|
||||
{webPanelTitle(panel)}
|
||||
</span>
|
||||
<DockControls panel={panel} />
|
||||
</header>
|
||||
@ -451,12 +475,12 @@ function WebDesktopWorkspace({
|
||||
);
|
||||
}
|
||||
|
||||
const leftPanels = panelsDockedTo(placements, "left");
|
||||
const rightPanels = panelsDockedTo(placements, "right");
|
||||
const floatingList = floatingPanels(placements);
|
||||
const leftPanels = webPanelsDockedTo(placements, "left");
|
||||
const rightPanels = webPanelsDockedTo(placements, "right");
|
||||
const floatingList = webFloatingPanels(placements);
|
||||
const activeLayoutKind = activeLayout?.kind ?? "terminal";
|
||||
const showInlineProjects =
|
||||
!openProject && placementOf(placements, "projects") === "closed";
|
||||
!openProject && webPlacementOf(placements, "projects") === "closed";
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -564,7 +588,7 @@ function WebDesktopWorkspace({
|
||||
<FloatingWindow
|
||||
key={panel}
|
||||
open
|
||||
title={PANEL_TITLE[panel]}
|
||||
title={webPanelTitle(panel)}
|
||||
size={WEB_PANEL_SIZE[panel]}
|
||||
onClose={() => closePanel(panel)}
|
||||
>
|
||||
@ -611,6 +635,7 @@ function WebMobileTaskFlowShell({
|
||||
{ id: "work", label: "Travail" },
|
||||
{ id: "tickets", label: "Tickets" },
|
||||
{ id: "sprints", label: "Sprints" },
|
||||
{ id: "files", label: "Fichiers" },
|
||||
{ id: "agents", label: "Agent" },
|
||||
];
|
||||
|
||||
@ -665,7 +690,7 @@ function WebMobileTaskFlowShell({
|
||||
<nav
|
||||
data-testid="web-mobile-bottom-nav"
|
||||
aria-label="Navigation mobile"
|
||||
className="grid shrink-0 grid-cols-5 border-t border-border bg-surface pb-[env(safe-area-inset-bottom)]"
|
||||
className="grid shrink-0 grid-cols-6 border-t border-border bg-surface pb-[env(safe-area-inset-bottom)]"
|
||||
>
|
||||
{navItems.map((item) => {
|
||||
const disabled = item.id !== "projects" && !openId;
|
||||
@ -858,6 +883,8 @@ export function WebProjectPanelBody({
|
||||
);
|
||||
case "git":
|
||||
return <GitPanel projectId={projectId} />;
|
||||
case "files":
|
||||
return <ProjectFilesPanel projectId={projectId} mobile={mobile} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user