feat: add board group models and update related interfaces
This commit is contained in:
@@ -2,7 +2,14 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Activity, BarChart3, Bot, LayoutGrid, Network } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
BarChart3,
|
||||
Bot,
|
||||
Folder,
|
||||
LayoutGrid,
|
||||
Network,
|
||||
} from "lucide-react";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import {
|
||||
@@ -13,14 +20,16 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
export function DashboardSidebar() {
|
||||
const pathname = usePathname();
|
||||
const healthQuery = useHealthzHealthzGet<healthzHealthzGetResponse, ApiError>({
|
||||
query: {
|
||||
refetchInterval: 30_000,
|
||||
refetchOnMount: "always",
|
||||
retry: false,
|
||||
const healthQuery = useHealthzHealthzGet<healthzHealthzGetResponse, ApiError>(
|
||||
{
|
||||
query: {
|
||||
refetchInterval: 30_000,
|
||||
refetchOnMount: "always",
|
||||
retry: false,
|
||||
},
|
||||
request: { cache: "no-store" },
|
||||
},
|
||||
request: { cache: "no-store" },
|
||||
});
|
||||
);
|
||||
|
||||
const okValue = healthQuery.data?.data?.ok;
|
||||
const systemStatus: "unknown" | "operational" | "degraded" =
|
||||
@@ -51,7 +60,7 @@ export function DashboardSidebar() {
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname === "/dashboard"
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
@@ -63,19 +72,31 @@ export function DashboardSidebar() {
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/gateways")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<Network className="h-4 w-4" />
|
||||
Gateways
|
||||
</Link>
|
||||
<Link
|
||||
href="/board-groups"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/board-groups")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<Folder className="h-4 w-4" />
|
||||
Board groups
|
||||
</Link>
|
||||
<Link
|
||||
href="/boards"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/boards")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
@@ -87,7 +108,7 @@ export function DashboardSidebar() {
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/activity")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
@@ -99,7 +120,7 @@ export function DashboardSidebar() {
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname.startsWith("/agents")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<Bot className="h-4 w-4" />
|
||||
@@ -114,7 +135,7 @@ export function DashboardSidebar() {
|
||||
"h-2 w-2 rounded-full",
|
||||
systemStatus === "operational" && "bg-emerald-500",
|
||||
systemStatus === "degraded" && "bg-rose-500",
|
||||
systemStatus === "unknown" && "bg-slate-300"
|
||||
systemStatus === "unknown" && "bg-slate-300",
|
||||
)}
|
||||
/>
|
||||
{statusLabel}
|
||||
|
||||
@@ -2,10 +2,21 @@
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, isClerkEnabled } from "@/auth/clerk";
|
||||
import {
|
||||
SignInButton,
|
||||
SignedIn,
|
||||
SignedOut,
|
||||
isClerkEnabled,
|
||||
} from "@/auth/clerk";
|
||||
|
||||
const ArrowIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M6 12L10 8L6 4"
|
||||
stroke="currentColor"
|
||||
@@ -81,16 +92,14 @@ export function LandingHero() {
|
||||
</div>
|
||||
|
||||
<div className="hero-features">
|
||||
{[
|
||||
"Agent-First Operations",
|
||||
"Approval Queues",
|
||||
"Live Signals",
|
||||
].map((label) => (
|
||||
<div key={label} className="hero-feature">
|
||||
<div className="feature-icon">✓</div>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
))}
|
||||
{["Agent-First Operations", "Approval Queues", "Live Signals"].map(
|
||||
(label) => (
|
||||
<div key={label} className="hero-feature">
|
||||
<div className="feature-icon">✓</div>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,7 +113,9 @@ export function LandingHero() {
|
||||
</div>
|
||||
<div className="surface-subtitle">
|
||||
<h3>Ship work without losing the thread.</h3>
|
||||
<p>Tasks, approvals, and agent status stay synced across the board.</p>
|
||||
<p>
|
||||
Tasks, approvals, and agent status stay synced across the board.
|
||||
</p>
|
||||
</div>
|
||||
<div className="metrics-row">
|
||||
{[
|
||||
@@ -266,4 +277,3 @@ export function LandingHero() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { TaskCard } from "@/components/molecules/TaskCard";
|
||||
import { parseApiDatetime } from "@/lib/datetime";
|
||||
@@ -140,6 +147,7 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const cardRefsSnapshot = cardRefs.current;
|
||||
if (animationRafRef.current !== null) {
|
||||
window.cancelAnimationFrame(animationRafRef.current);
|
||||
animationRafRef.current = null;
|
||||
@@ -149,7 +157,7 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
cleanupTimeoutRef.current = null;
|
||||
}
|
||||
for (const taskId of animatedTaskIdsRef.current) {
|
||||
const element = cardRefs.current.get(taskId);
|
||||
const element = cardRefsSnapshot.get(taskId);
|
||||
if (!element) continue;
|
||||
element.style.transform = "";
|
||||
element.style.transition = "";
|
||||
@@ -182,7 +190,7 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
const dx = prev.left - next.left;
|
||||
const dy = prev.top - next.top;
|
||||
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue;
|
||||
const element = cardRefs.current.get(taskId);
|
||||
const element = cardRefsSnapshot.get(taskId);
|
||||
if (!element) continue;
|
||||
moved.push({ taskId, element, dx, dy });
|
||||
}
|
||||
@@ -229,7 +237,7 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
cleanupTimeoutRef.current = null;
|
||||
}
|
||||
for (const taskId of animatedTaskIdsRef.current) {
|
||||
const element = cardRefs.current.get(taskId);
|
||||
const element = cardRefsSnapshot.get(taskId);
|
||||
if (!element) continue;
|
||||
element.style.transform = "";
|
||||
element.style.transition = "";
|
||||
@@ -302,10 +310,10 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
};
|
||||
|
||||
const handleDragLeave = (status: TaskStatus) => () => {
|
||||
if (activeColumn === status) {
|
||||
setActiveColumn(null);
|
||||
}
|
||||
};
|
||||
if (activeColumn === status) {
|
||||
setActiveColumn(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -343,9 +351,14 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
? columnTasks.filter((task) => {
|
||||
if (reviewBucket === "blocked") return Boolean(task.is_blocked);
|
||||
if (reviewBucket === "approval_needed")
|
||||
return (task.approvals_pending_count ?? 0) > 0 && !task.is_blocked;
|
||||
return (
|
||||
(task.approvals_pending_count ?? 0) > 0 && !task.is_blocked
|
||||
);
|
||||
if (reviewBucket === "waiting_lead")
|
||||
return !task.is_blocked && (task.approvals_pending_count ?? 0) === 0;
|
||||
return (
|
||||
!task.is_blocked &&
|
||||
(task.approvals_pending_count ?? 0) === 0
|
||||
);
|
||||
return true;
|
||||
})
|
||||
: columnTasks;
|
||||
@@ -393,7 +406,11 @@ export const TaskBoard = memo(function TaskBoard({
|
||||
label: "Lead review",
|
||||
count: reviewCounts.waiting_lead,
|
||||
},
|
||||
{ key: "blocked", label: "Blocked", count: reviewCounts.blocked },
|
||||
{
|
||||
key: "blocked",
|
||||
label: "Blocked",
|
||||
count: reviewCounts.blocked,
|
||||
},
|
||||
] as const
|
||||
).map((option) => (
|
||||
<button
|
||||
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
Trello,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function UserMenu({ className }: { className?: string }) {
|
||||
|
||||
Reference in New Issue
Block a user