Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 18 additions & 30 deletions packages/web/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,22 +256,18 @@ const PieChart = ({ data, title }: { data: Array<{label: string, value: number,
const Dashboard: React.FC<DashboardProps> = ({ filteredNodes, stats }) => {
return (
<div className="p-6 space-y-6">
{/* Total Tasks - Full Width Card */}
<div className="bg-gradient-to-br from-lime-500/15 via-lime-500/8 to-lime-500/5 rounded-lg p-8 border border-gray-700/30 border-l-4 border-l-lime-400/70 hover:from-lime-500/25 hover:via-lime-500/18 hover:to-lime-500/15 transition-all duration-200 hover:scale-[1.01] hover:-translate-y-1 cursor-pointer">
<div className="flex items-center justify-center">
<div className="flex-shrink-0">
<Sigma className="h-12 w-12 text-lime-400" />
</div>
<div className="ml-6 text-center">
<div className="text-2xl font-bold text-gray-300">Total Tasks</div>
<div className="text-4xl font-bold text-lime-400">{stats.total}</div>
</div>
</div>
{/* Summary strip — compact + content-sized (was a tall, near-empty banner) */}
<div className="bg-gradient-to-br from-lime-500/15 via-lime-500/8 to-lime-500/5 rounded-lg px-5 py-3 border border-gray-700/30 border-l-4 border-l-lime-400/70 flex items-center gap-3">
<Sigma className="h-7 w-7 text-lime-400 flex-shrink-0" />
<span className="text-sm font-semibold text-gray-300">Total Tasks</span>
<span className="ml-auto text-3xl font-bold text-lime-400 leading-none">{stats.total}</span>
</div>

{/* Stats Cards - Second Row */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className={`${getStatusGradientBackground('NOT_STARTED' as WorkItemStatus, 'dashboard')} rounded-lg p-6 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
{/* Status breakdown — one compact, dense grid (was 3 sparse rows of 3 with
single-digit numbers in oversized boxes). 2 cols on phones up to 5 on
desktop so the 9 statuses read as a tidy panel, not a wall of whitespace. */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
<div className={`${getStatusGradientBackground('NOT_STARTED' as WorkItemStatus, 'dashboard')} rounded-lg p-4 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{React.createElement(getStatusConfig('NOT_STARTED').icon!, { className: `h-8 w-8 ${getStatusConfig('NOT_STARTED').color}` })}
Expand All @@ -283,7 +279,7 @@ const Dashboard: React.FC<DashboardProps> = ({ filteredNodes, stats }) => {
</div>
</div>

<div className={`${getStatusGradientBackground('PROPOSED' as WorkItemStatus, 'dashboard')} rounded-lg p-6 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className={`${getStatusGradientBackground('PROPOSED' as WorkItemStatus, 'dashboard')} rounded-lg p-4 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{React.createElement(getStatusConfig('PROPOSED').icon!, { className: `h-8 w-8 ${getStatusConfig('PROPOSED').color}` })}
Expand All @@ -295,7 +291,7 @@ const Dashboard: React.FC<DashboardProps> = ({ filteredNodes, stats }) => {
</div>
</div>

<div className={`${getStatusGradientBackground('PLANNED' as WorkItemStatus, 'dashboard')} rounded-lg p-6 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className={`${getStatusGradientBackground('PLANNED' as WorkItemStatus, 'dashboard')} rounded-lg p-4 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{React.createElement(getStatusConfig('PLANNED').icon!, { className: `h-8 w-8 ${getStatusConfig('PLANNED').color}` })}
Expand All @@ -306,11 +302,7 @@ const Dashboard: React.FC<DashboardProps> = ({ filteredNodes, stats }) => {
</div>
</div>
</div>
</div>

{/* Stats Cards - Third Row */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className={`${getStatusGradientBackground('IN_PROGRESS' as WorkItemStatus, 'dashboard')} rounded-lg p-6 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className={`${getStatusGradientBackground('IN_PROGRESS' as WorkItemStatus, 'dashboard')} rounded-lg p-4 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{React.createElement(getStatusConfig('IN_PROGRESS').icon!, { className: `h-8 w-8 ${getStatusConfig('IN_PROGRESS').color}` })}
Expand All @@ -322,7 +314,7 @@ const Dashboard: React.FC<DashboardProps> = ({ filteredNodes, stats }) => {
</div>
</div>

<div className={`${getStatusGradientBackground('IN_REVIEW' as WorkItemStatus, 'dashboard')} rounded-lg p-6 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className={`${getStatusGradientBackground('IN_REVIEW' as WorkItemStatus, 'dashboard')} rounded-lg p-4 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{React.createElement(getStatusConfig('IN_REVIEW').icon!, { className: `h-8 w-8 ${getStatusConfig('IN_REVIEW').color}` })}
Expand All @@ -334,7 +326,7 @@ const Dashboard: React.FC<DashboardProps> = ({ filteredNodes, stats }) => {
</div>
</div>

<div className={`${getStatusGradientBackground('BLOCKED' as WorkItemStatus, 'dashboard')} rounded-lg p-6 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className={`${getStatusGradientBackground('BLOCKED' as WorkItemStatus, 'dashboard')} rounded-lg p-4 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{React.createElement(getStatusConfig('BLOCKED').icon!, { className: `h-8 w-8 ${getStatusConfig('BLOCKED').color}` })}
Expand All @@ -345,11 +337,7 @@ const Dashboard: React.FC<DashboardProps> = ({ filteredNodes, stats }) => {
</div>
</div>
</div>
</div>

{/* Stats Cards - Fourth Row */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className={`${getStatusGradientBackground('ON_HOLD' as WorkItemStatus, 'dashboard')} rounded-lg p-6 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className={`${getStatusGradientBackground('ON_HOLD' as WorkItemStatus, 'dashboard')} rounded-lg p-4 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{React.createElement(getStatusConfig('ON_HOLD').icon!, { className: `h-8 w-8 ${getStatusConfig('ON_HOLD').color}` })}
Expand All @@ -361,7 +349,7 @@ const Dashboard: React.FC<DashboardProps> = ({ filteredNodes, stats }) => {
</div>
</div>

<div className={`${getStatusGradientBackground('COMPLETED' as WorkItemStatus, 'dashboard')} rounded-lg p-6 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className={`${getStatusGradientBackground('COMPLETED' as WorkItemStatus, 'dashboard')} rounded-lg p-4 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{React.createElement(getStatusConfig('COMPLETED').icon!, { className: `h-8 w-8 ${getStatusConfig('COMPLETED').color}` })}
Expand All @@ -373,7 +361,7 @@ const Dashboard: React.FC<DashboardProps> = ({ filteredNodes, stats }) => {
</div>
</div>

<div className={`${getStatusGradientBackground('CANCELLED' as WorkItemStatus, 'dashboard')} rounded-lg p-6 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className={`${getStatusGradientBackground('CANCELLED' as WorkItemStatus, 'dashboard')} rounded-lg p-4 border border-gray-700/30 hover:scale-[1.02] hover:-translate-y-1 transition-all duration-200 cursor-pointer`}>
<div className="flex items-center">
<div className="flex-shrink-0">
{React.createElement(getStatusConfig('CANCELLED').icon!, { className: `h-8 w-8 ${getStatusConfig('CANCELLED').color}` })}
Expand Down
50 changes: 48 additions & 2 deletions packages/web/src/components/InteractiveGraphVisualization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { DeleteGraphModal } from './DeleteGraphModal';
import { ConnectWorkItemModal } from './ConnectWorkItemModal';
import { WorkItemDetailsModal } from './WorkItemDetailsModal';
import { NodeInspector } from './NodeInspector';
import { NodeQuickEdit, type QuickEditCommit } from './NodeQuickEdit';

import { WorkItem, WorkItemEdge } from '../types/graph';
import { RelationshipType, RELATIONSHIP_OPTIONS, getRelationshipConfig } from '../constants/workItemConstants';
Expand Down Expand Up @@ -429,6 +430,10 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i
const [selectedNodes, setSelectedNodes] = useState<Set<string>>(new Set());
// Inline rename: an input floats over the node — no modal (W2)
const [inlineEdit, setInlineEdit] = useState<{ nodeId: string; value: string; original: string; graphX: number; graphY: number } | null>(null);
// PR #87: quick in-context node editor (double-click a node) — edits every
// field without the heavy details modal. Holds the work item being edited.
const [quickEditNode, setQuickEditNode] = useState<any | null>(null);
const quickEditNodeId = quickEditNode?.id ?? null;

// D3 handlers are bound once at init and the init effect intentionally
// avoids re-initialising on mode changes — so handlers would capture STALE
Expand Down Expand Up @@ -2363,10 +2368,14 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i
clickableEdges.classed('dim-for-hover', false);
edgeLabelGroups.classed('dim-for-hover', false);
})
// Double-click a node → rename in place, no modal (W2)
// Double-click a node → quick in-context editor (title, description, type,
// priority, status) anchored to the node, no heavy modal (#87, W2). Single
// click+drag still moves the node (when unlocked); only a genuine
// double-click opens this.
.on('dblclick.rename', (event: MouseEvent, d: any) => {
event.stopPropagation();
setInlineEdit({ nodeId: d.id, value: d.title || '', original: d.title || '', graphX: d.x || 0, graphY: d.y || 0 });
setInlineEdit(null);
setQuickEditNode(d);
});


Expand Down Expand Up @@ -4405,6 +4414,15 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i
}, [expandedNodeId]);
useEffect(() => { setExpandedNode(null); }, [currentGraphId]);

// Esc closes the quick editor; switching graphs dismisses it (node is gone).
useEffect(() => {
if (!quickEditNodeId) return undefined;
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setQuickEditNode(null); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [quickEditNodeId]);
useEffect(() => { setQuickEditNode(null); }, [currentGraphId]);

// Expose reset function to parent component
useEffect(() => {
if (onResetLayout) {
Expand Down Expand Up @@ -4924,6 +4942,34 @@ export function InteractiveGraphVisualization({ onResetLayout, onNodeSelected, i
);
})()}

{/* #87: quick in-context node editor — anchored to the node, edits every
field with immediate optimistic saves + undo, no heavy modal. */}
{quickEditNode && (() => {
const simNode = (simulationRef.current?.nodes() as any[])?.find((n: any) => n.id === quickEditNode.id);
const node = simNode || quickEditNode;
const gx = node.x ?? 0;
const gy = node.y ?? 0;
const left = gx * currentTransform.scale + currentTransform.x;
const top = gy * currentTransform.scale + currentTransform.y;
const onCommit = (c: QuickEditCommit) => {
updateWorkItemMutation({ variables: { where: { id: quickEditNode.id }, update: c.update } })
.then(() => {
undoStackRef.current.push({
label: c.label,
undo: async () => {
await updateWorkItemMutation({ variables: { where: { id: quickEditNode.id }, update: c.prev } });
},
});
})
.catch(() => showError(`Could not ${c.label.toLowerCase()}`));
};
return (
<div className="absolute z-50" style={{ left, top, transform: 'translate(-50%, -50%)' }}>
<NodeQuickEdit node={node} onCommit={onCommit} onClose={() => setQuickEditNode(null)} />
</div>
);
})()}

{/* PR-3: expand-in-place peek — a readable Card/Contents/Diagram panel
anchored to its node on the canvas, on top of everything. Position is
driven each frame by the rAF sync effect above. */}
Expand Down
Loading
Loading