Add configuration tabs to project and agent pages
This commit is contained in:
@@ -121,6 +121,7 @@ function boardRoutes() {
|
|||||||
<Route path="projects/:projectId/overview" element={<ProjectDetail />} />
|
<Route path="projects/:projectId/overview" element={<ProjectDetail />} />
|
||||||
<Route path="projects/:projectId/issues" element={<ProjectDetail />} />
|
<Route path="projects/:projectId/issues" element={<ProjectDetail />} />
|
||||||
<Route path="projects/:projectId/issues/:filter" element={<ProjectDetail />} />
|
<Route path="projects/:projectId/issues/:filter" element={<ProjectDetail />} />
|
||||||
|
<Route path="projects/:projectId/configuration" element={<ProjectDetail />} />
|
||||||
<Route path="issues" element={<Issues />} />
|
<Route path="issues" element={<Issues />} />
|
||||||
<Route path="issues/all" element={<Navigate to="/issues" replace />} />
|
<Route path="issues/all" element={<Navigate to="/issues" replace />} />
|
||||||
<Route path="issues/active" element={<Navigate to="/issues" replace />} />
|
<Route path="issues/active" element={<Navigate to="/issues" replace />} />
|
||||||
@@ -235,6 +236,7 @@ export function App() {
|
|||||||
<Route path="projects/:projectId/overview" element={<UnprefixedBoardRedirect />} />
|
<Route path="projects/:projectId/overview" element={<UnprefixedBoardRedirect />} />
|
||||||
<Route path="projects/:projectId/issues" element={<UnprefixedBoardRedirect />} />
|
<Route path="projects/:projectId/issues" element={<UnprefixedBoardRedirect />} />
|
||||||
<Route path="projects/:projectId/issues/:filter" element={<UnprefixedBoardRedirect />} />
|
<Route path="projects/:projectId/issues/:filter" element={<UnprefixedBoardRedirect />} />
|
||||||
|
<Route path="projects/:projectId/configuration" element={<UnprefixedBoardRedirect />} />
|
||||||
<Route path=":companyPrefix" element={<Layout />}>
|
<Route path=":companyPrefix" element={<Layout />}>
|
||||||
{boardRoutes()}
|
{boardRoutes()}
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||||
import { useParams, useNavigate, Link, useBeforeUnload } from "@/lib/router";
|
import { useParams, useNavigate, Link, Navigate, useBeforeUnload } from "@/lib/router";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { agentsApi, type AgentKey, type ClaudeLoginResult } from "../api/agents";
|
import { agentsApi, type AgentKey, type ClaudeLoginResult } from "../api/agents";
|
||||||
import { heartbeatsApi } from "../api/heartbeats";
|
import { heartbeatsApi } from "../api/heartbeats";
|
||||||
@@ -14,6 +14,7 @@ import { useDialog } from "../context/DialogContext";
|
|||||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
import { AgentConfigForm } from "../components/AgentConfigForm";
|
import { AgentConfigForm } from "../components/AgentConfigForm";
|
||||||
|
import { PageTabBar } from "../components/PageTabBar";
|
||||||
import { adapterLabels, roleLabels } from "../components/agent-config-primitives";
|
import { adapterLabels, roleLabels } from "../components/agent-config-primitives";
|
||||||
import { getUIAdapter, buildTranscript } from "../adapters";
|
import { getUIAdapter, buildTranscript } from "../adapters";
|
||||||
import type { TranscriptEntry } from "../adapters";
|
import type { TranscriptEntry } from "../adapters";
|
||||||
@@ -28,6 +29,7 @@ import { ScrollToBottom } from "../components/ScrollToBottom";
|
|||||||
import { formatCents, formatDate, relativeTime, formatTokens } from "../lib/utils";
|
import { formatCents, formatDate, relativeTime, formatTokens } from "../lib/utils";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tabs } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -53,7 +55,6 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Settings,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
||||||
@@ -173,12 +174,12 @@ function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBeh
|
|||||||
container.scrollTo({ top: container.scrollHeight, behavior });
|
container.scrollTo({ top: container.scrollHeight, behavior });
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentDetailView = "overview" | "configure" | "runs";
|
type AgentDetailView = "dashboard" | "configuration" | "runs";
|
||||||
|
|
||||||
function parseAgentDetailView(value: string | null): AgentDetailView {
|
function parseAgentDetailView(value: string | null): AgentDetailView {
|
||||||
if (value === "configure" || value === "configuration") return "configure";
|
if (value === "configure" || value === "configuration") return "configuration";
|
||||||
if (value === "runs") return value;
|
if (value === "runs") return value;
|
||||||
return "overview";
|
return "dashboard";
|
||||||
}
|
}
|
||||||
|
|
||||||
function usageNumber(usage: Record<string, unknown> | null, ...keys: string[]) {
|
function usageNumber(usage: Record<string, unknown> | null, ...keys: string[]) {
|
||||||
@@ -304,17 +305,18 @@ export function AgentDetail() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!agent) return;
|
if (!agent) return;
|
||||||
if (routeAgentRef === canonicalAgentRef) return;
|
|
||||||
if (urlRunId) {
|
if (urlRunId) {
|
||||||
navigate(`/agents/${canonicalAgentRef}/runs/${urlRunId}`, { replace: true });
|
if (routeAgentRef !== canonicalAgentRef) {
|
||||||
|
navigate(`/agents/${canonicalAgentRef}/runs/${urlRunId}`, { replace: true });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (urlTab) {
|
const canonicalTab = activeView === "configuration" ? "configuration" : "dashboard";
|
||||||
navigate(`/agents/${canonicalAgentRef}/${urlTab}`, { replace: true });
|
if (routeAgentRef !== canonicalAgentRef || urlTab !== canonicalTab) {
|
||||||
|
navigate(`/agents/${canonicalAgentRef}/${canonicalTab}`, { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigate(`/agents/${canonicalAgentRef}`, { replace: true });
|
}, [agent, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, activeView, navigate]);
|
||||||
}, [agent, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, navigate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!agent?.companyId || agent.companyId === selectedCompanyId) return;
|
if (!agent?.companyId || agent.companyId === selectedCompanyId) return;
|
||||||
@@ -397,17 +399,19 @@ export function AgentDetail() {
|
|||||||
{ label: "Agents", href: "/agents" },
|
{ label: "Agents", href: "/agents" },
|
||||||
];
|
];
|
||||||
const agentName = agent?.name ?? routeAgentRef ?? "Agent";
|
const agentName = agent?.name ?? routeAgentRef ?? "Agent";
|
||||||
if (activeView === "overview" && !urlRunId) {
|
if (activeView === "dashboard" && !urlRunId) {
|
||||||
crumbs.push({ label: agentName });
|
crumbs.push({ label: agentName });
|
||||||
} else {
|
} else {
|
||||||
crumbs.push({ label: agentName, href: `/agents/${canonicalAgentRef}` });
|
crumbs.push({ label: agentName, href: `/agents/${canonicalAgentRef}/dashboard` });
|
||||||
if (urlRunId) {
|
if (urlRunId) {
|
||||||
crumbs.push({ label: "Runs", href: `/agents/${canonicalAgentRef}/runs` });
|
crumbs.push({ label: "Runs", href: `/agents/${canonicalAgentRef}/runs` });
|
||||||
crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` });
|
crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` });
|
||||||
} else if (activeView === "configure") {
|
} else if (activeView === "configuration") {
|
||||||
crumbs.push({ label: "Configure" });
|
crumbs.push({ label: "Configuration" });
|
||||||
} else if (activeView === "runs") {
|
} else if (activeView === "runs") {
|
||||||
crumbs.push({ label: "Runs" });
|
crumbs.push({ label: "Runs" });
|
||||||
|
} else {
|
||||||
|
crumbs.push({ label: "Dashboard" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setBreadcrumbs(crumbs);
|
setBreadcrumbs(crumbs);
|
||||||
@@ -416,7 +420,7 @@ export function AgentDetail() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
closePanel();
|
closePanel();
|
||||||
return () => closePanel();
|
return () => closePanel();
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [closePanel]);
|
||||||
|
|
||||||
useBeforeUnload(
|
useBeforeUnload(
|
||||||
useCallback((event) => {
|
useCallback((event) => {
|
||||||
@@ -429,8 +433,11 @@ export function AgentDetail() {
|
|||||||
if (isLoading) return <PageSkeleton variant="detail" />;
|
if (isLoading) return <PageSkeleton variant="detail" />;
|
||||||
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
||||||
if (!agent) return null;
|
if (!agent) return null;
|
||||||
|
if (!urlRunId && !urlTab) {
|
||||||
|
return <Navigate to={`/agents/${canonicalAgentRef}/dashboard`} replace />;
|
||||||
|
}
|
||||||
const isPendingApproval = agent.status === "pending_approval";
|
const isPendingApproval = agent.status === "pending_approval";
|
||||||
const showConfigActionBar = activeView === "configure" && configDirty;
|
const showConfigActionBar = activeView === "configuration" && configDirty;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-6", isMobile && showConfigActionBar && "pb-24")}>
|
<div className={cn("space-y-6", isMobile && showConfigActionBar && "pb-24")}>
|
||||||
@@ -514,16 +521,6 @@ export function AgentDetail() {
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-44 p-1" align="end">
|
<PopoverContent className="w-44 p-1" align="end">
|
||||||
<button
|
|
||||||
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50"
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/agents/${canonicalAgentRef}/configure`);
|
|
||||||
setMoreOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Settings className="h-3 w-3" />
|
|
||||||
Configure Agent
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50"
|
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -559,6 +556,22 @@ export function AgentDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!urlRunId && (
|
||||||
|
<Tabs
|
||||||
|
value={activeView === "configuration" ? "configuration" : "dashboard"}
|
||||||
|
onValueChange={(value) => navigate(`/agents/${canonicalAgentRef}/${value}`)}
|
||||||
|
>
|
||||||
|
<PageTabBar
|
||||||
|
items={[
|
||||||
|
{ value: "dashboard", label: "Dashboard" },
|
||||||
|
{ value: "configuration", label: "Configuration" },
|
||||||
|
]}
|
||||||
|
value={activeView === "configuration" ? "configuration" : "dashboard"}
|
||||||
|
onValueChange={(value) => navigate(`/agents/${canonicalAgentRef}/${value}`)}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
|
||||||
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
||||||
{isPendingApproval && (
|
{isPendingApproval && (
|
||||||
<p className="text-sm text-amber-500">
|
<p className="text-sm text-amber-500">
|
||||||
@@ -623,7 +636,7 @@ export function AgentDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* View content */}
|
{/* View content */}
|
||||||
{activeView === "overview" && (
|
{activeView === "dashboard" && (
|
||||||
<AgentOverview
|
<AgentOverview
|
||||||
agent={agent}
|
agent={agent}
|
||||||
runs={heartbeats ?? []}
|
runs={heartbeats ?? []}
|
||||||
@@ -636,7 +649,7 @@ export function AgentDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeView === "configure" && (
|
{activeView === "configuration" && (
|
||||||
<AgentConfigurePage
|
<AgentConfigurePage
|
||||||
agent={agent}
|
agent={agent}
|
||||||
agentId={agent.id}
|
agentId={agent.id}
|
||||||
@@ -824,7 +837,6 @@ function AgentOverview({
|
|||||||
{/* Configuration Summary */}
|
{/* Configuration Summary */}
|
||||||
<ConfigSummary
|
<ConfigSummary
|
||||||
agent={agent}
|
agent={agent}
|
||||||
agentRouteId={agentRouteId}
|
|
||||||
reportsToAgent={reportsToAgent}
|
reportsToAgent={reportsToAgent}
|
||||||
directReports={directReports}
|
directReports={directReports}
|
||||||
/>
|
/>
|
||||||
@@ -838,12 +850,10 @@ function AgentOverview({
|
|||||||
|
|
||||||
function ConfigSummary({
|
function ConfigSummary({
|
||||||
agent,
|
agent,
|
||||||
agentRouteId,
|
|
||||||
reportsToAgent,
|
reportsToAgent,
|
||||||
directReports,
|
directReports,
|
||||||
}: {
|
}: {
|
||||||
agent: Agent;
|
agent: Agent;
|
||||||
agentRouteId: string;
|
|
||||||
reportsToAgent: Agent | null;
|
reportsToAgent: Agent | null;
|
||||||
directReports: Agent[];
|
directReports: Agent[];
|
||||||
}) {
|
}) {
|
||||||
@@ -852,16 +862,7 @@ function ConfigSummary({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<h3 className="text-sm font-medium">Configuration</h3>
|
||||||
<h3 className="text-sm font-medium">Configuration</h3>
|
|
||||||
<Link
|
|
||||||
to={`/agents/${agentRouteId}/configure`}
|
|
||||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors no-underline"
|
|
||||||
>
|
|
||||||
<Settings className="h-3 w-3" />
|
|
||||||
Manage →
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="border border-border rounded-lg p-4 space-y-3">
|
<div className="border border-border rounded-lg p-4 space-y-3">
|
||||||
<h4 className="text-xs text-muted-foreground font-medium">Agent Details</h4>
|
<h4 className="text-xs text-muted-foreground font-medium">Agent Details</h4>
|
||||||
|
|||||||
@@ -16,15 +16,13 @@ import { InlineEditor } from "../components/InlineEditor";
|
|||||||
import { StatusBadge } from "../components/StatusBadge";
|
import { StatusBadge } from "../components/StatusBadge";
|
||||||
import { IssuesList } from "../components/IssuesList";
|
import { IssuesList } from "../components/IssuesList";
|
||||||
import { PageSkeleton } from "../components/PageSkeleton";
|
import { PageSkeleton } from "../components/PageSkeleton";
|
||||||
|
import { PageTabBar } from "../components/PageTabBar";
|
||||||
import { projectRouteRef, cn } from "../lib/utils";
|
import { projectRouteRef, cn } from "../lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Tabs } from "@/components/ui/tabs";
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import { SlidersHorizontal } from "lucide-react";
|
|
||||||
|
|
||||||
/* ── Top-level tab types ── */
|
/* ── Top-level tab types ── */
|
||||||
|
|
||||||
type ProjectTab = "overview" | "list";
|
type ProjectTab = "overview" | "list" | "configuration";
|
||||||
|
|
||||||
function resolveProjectTab(pathname: string, projectId: string): ProjectTab | null {
|
function resolveProjectTab(pathname: string, projectId: string): ProjectTab | null {
|
||||||
const segments = pathname.split("/").filter(Boolean);
|
const segments = pathname.split("/").filter(Boolean);
|
||||||
@@ -32,6 +30,7 @@ function resolveProjectTab(pathname: string, projectId: string): ProjectTab | nu
|
|||||||
if (projectsIdx === -1 || segments[projectsIdx + 1] !== projectId) return null;
|
if (projectsIdx === -1 || segments[projectsIdx + 1] !== projectId) return null;
|
||||||
const tab = segments[projectsIdx + 2];
|
const tab = segments[projectsIdx + 2];
|
||||||
if (tab === "overview") return "overview";
|
if (tab === "overview") return "overview";
|
||||||
|
if (tab === "configuration") return "configuration";
|
||||||
if (tab === "issues") return "list";
|
if (tab === "issues") return "list";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -198,9 +197,8 @@ export function ProjectDetail() {
|
|||||||
filter?: string;
|
filter?: string;
|
||||||
}>();
|
}>();
|
||||||
const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany();
|
const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany();
|
||||||
const { openPanel, closePanel, panelVisible, setPanelVisible } = usePanel();
|
const { closePanel } = usePanel();
|
||||||
const { setBreadcrumbs } = useBreadcrumbs();
|
const { setBreadcrumbs } = useBreadcrumbs();
|
||||||
const [mobilePropsOpen, setMobilePropsOpen] = useState(false);
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -264,6 +262,10 @@ export function ProjectDetail() {
|
|||||||
navigate(`/projects/${canonicalProjectRef}/overview`, { replace: true });
|
navigate(`/projects/${canonicalProjectRef}/overview`, { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (activeTab === "configuration") {
|
||||||
|
navigate(`/projects/${canonicalProjectRef}/configuration`, { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (activeTab === "list") {
|
if (activeTab === "list") {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
navigate(`/projects/${canonicalProjectRef}/issues/${filter}`, { replace: true });
|
navigate(`/projects/${canonicalProjectRef}/issues/${filter}`, { replace: true });
|
||||||
@@ -276,11 +278,9 @@ export function ProjectDetail() {
|
|||||||
}, [project, routeProjectRef, canonicalProjectRef, activeTab, filter, navigate]);
|
}, [project, routeProjectRef, canonicalProjectRef, activeTab, filter, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (project) {
|
closePanel();
|
||||||
openPanel(<ProjectProperties project={project} onUpdate={(data) => updateProject.mutate(data)} />);
|
|
||||||
}
|
|
||||||
return () => closePanel();
|
return () => closePanel();
|
||||||
}, [project]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [closePanel]);
|
||||||
|
|
||||||
// Redirect bare /projects/:id to /projects/:id/issues
|
// Redirect bare /projects/:id to /projects/:id/issues
|
||||||
if (routeProjectRef && activeTab === null) {
|
if (routeProjectRef && activeTab === null) {
|
||||||
@@ -294,6 +294,8 @@ export function ProjectDetail() {
|
|||||||
const handleTabChange = (tab: ProjectTab) => {
|
const handleTabChange = (tab: ProjectTab) => {
|
||||||
if (tab === "overview") {
|
if (tab === "overview") {
|
||||||
navigate(`/projects/${canonicalProjectRef}/overview`);
|
navigate(`/projects/${canonicalProjectRef}/overview`);
|
||||||
|
} else if (tab === "configuration") {
|
||||||
|
navigate(`/projects/${canonicalProjectRef}/configuration`);
|
||||||
} else {
|
} else {
|
||||||
navigate(`/projects/${canonicalProjectRef}/issues`);
|
navigate(`/projects/${canonicalProjectRef}/issues`);
|
||||||
}
|
}
|
||||||
@@ -314,54 +316,20 @@ export function ProjectDetail() {
|
|||||||
as="h2"
|
as="h2"
|
||||||
className="text-xl font-bold"
|
className="text-xl font-bold"
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-xs"
|
|
||||||
className="ml-auto md:hidden shrink-0"
|
|
||||||
onClick={() => setMobilePropsOpen(true)}
|
|
||||||
title="Properties"
|
|
||||||
>
|
|
||||||
<SlidersHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-xs"
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 ml-auto transition-opacity duration-200 hidden md:flex",
|
|
||||||
panelVisible ? "opacity-0 pointer-events-none w-0 overflow-hidden" : "opacity-100",
|
|
||||||
)}
|
|
||||||
onClick={() => setPanelVisible(true)}
|
|
||||||
title="Show properties"
|
|
||||||
>
|
|
||||||
<SlidersHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Top-level project tabs */}
|
<Tabs value={activeTab ?? "list"} onValueChange={(value) => handleTabChange(value as ProjectTab)}>
|
||||||
<div className="flex items-center gap-1 border-b border-border">
|
<PageTabBar
|
||||||
<button
|
items={[
|
||||||
className={`px-3 py-2 text-sm font-medium transition-colors border-b-2 ${
|
{ value: "overview", label: "Overview" },
|
||||||
activeTab === "overview"
|
{ value: "list", label: "List" },
|
||||||
? "border-foreground text-foreground"
|
{ value: "configuration", label: "Configuration" },
|
||||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
]}
|
||||||
}`}
|
value={activeTab ?? "list"}
|
||||||
onClick={() => handleTabChange("overview")}
|
onValueChange={(value) => handleTabChange(value as ProjectTab)}
|
||||||
>
|
/>
|
||||||
Overview
|
</Tabs>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`px-3 py-2 text-sm font-medium transition-colors border-b-2 ${
|
|
||||||
activeTab === "list"
|
|
||||||
? "border-foreground text-foreground"
|
|
||||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
}`}
|
|
||||||
onClick={() => handleTabChange("list")}
|
|
||||||
>
|
|
||||||
List
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab content */}
|
|
||||||
{activeTab === "overview" && (
|
{activeTab === "overview" && (
|
||||||
<OverviewContent
|
<OverviewContent
|
||||||
project={project}
|
project={project}
|
||||||
@@ -377,19 +345,9 @@ export function ProjectDetail() {
|
|||||||
<ProjectIssuesList projectId={project.id} companyId={resolvedCompanyId} />
|
<ProjectIssuesList projectId={project.id} companyId={resolvedCompanyId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mobile properties drawer */}
|
{activeTab === "configuration" && (
|
||||||
<Sheet open={mobilePropsOpen} onOpenChange={setMobilePropsOpen}>
|
<ProjectProperties project={project} onUpdate={(data) => updateProject.mutate(data)} />
|
||||||
<SheetContent side="bottom" className="max-h-[85dvh] pb-[env(safe-area-inset-bottom)]">
|
)}
|
||||||
<SheetHeader>
|
|
||||||
<SheetTitle className="text-sm">Properties</SheetTitle>
|
|
||||||
</SheetHeader>
|
|
||||||
<ScrollArea className="flex-1 overflow-y-auto">
|
|
||||||
<div className="px-4 pb-4">
|
|
||||||
<ProjectProperties project={project} onUpdate={(data) => updateProject.mutate(data)} />
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user