From f81d2df0569a8701d55513ec92599231c29852c9 Mon Sep 17 00:00:00 2001 From: Happyesss <22csaiml002@jssaten.ac.in> Date: Mon, 18 May 2026 10:42:56 +0530 Subject: [PATCH 1/2] feat: enhance project syncing logic and UI in workflow components --- .../workflows/[workflowId]/client.tsx | 63 +++++++++++-------- .../components/manage-columns-form.tsx | 35 +++++++---- .../components/workflow-simple-view.tsx | 8 +-- 3 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/app/(dashboard)/workspaces/[workspaceId]/spaces/[spaceId]/workflows/[workflowId]/client.tsx b/src/app/(dashboard)/workspaces/[workspaceId]/spaces/[spaceId]/workflows/[workflowId]/client.tsx index 2223b0b1..32496a26 100644 --- a/src/app/(dashboard)/workspaces/[workspaceId]/spaces/[spaceId]/workflows/[workflowId]/client.tsx +++ b/src/app/(dashboard)/workspaces/[workspaceId]/spaces/[spaceId]/workflows/[workflowId]/client.tsx @@ -135,9 +135,12 @@ const WorkflowEditor = () => { const { mutateAsync: updateTransition } = useUpdateTransition(); const { mutateAsync: deleteTransitionMutation } = useDeleteTransition(); const { mutate: updateProject, isPending: isUpdatingProject } = useUpdateProject(); - const { mutate: syncFromProject, isPending: isSyncing } = useSyncFromProject(); + const { mutate: syncFromProject } = useSyncFromProject(); const { mutate: syncWithResolution } = useSyncWithResolution(); + // Track which project is currently syncing (instead of a single global boolean) + const [syncingProjectId, setSyncingProjectId] = useState(null); + const projects = useMemo(() => { if (!projectsData?.documents) return []; return projectsData.documents.filter((p) => p.spaceId === spaceId); @@ -148,15 +151,6 @@ const WorkflowEditor = () => { [projects, workflowId] ); - useEffect(() => { - if (!hasAutoSyncedProjects.current && connectedProjects.length > 0 && !workflowLoading) { - hasAutoSyncedProjects.current = true; - const projectToSync = connectedProjects[0]; - if (projectToSync) { - syncFromProject({ param: { workflowId, projectId: projectToSync.$id } }); - } - } - }, [connectedProjects, workflowLoading, workflowId, syncFromProject]); const connectedProjectIds = useMemo( () => connectedProjects.map((p) => p.$id), @@ -445,7 +439,6 @@ const WorkflowEditor = () => { { onSuccess: () => { setConnectProjectOpen(false); - syncFromProject({ param: { workflowId, projectId } }); }, } ); @@ -463,7 +456,13 @@ const WorkflowEditor = () => { ); const handleSyncFromProject = useCallback( - (projectId: string) => syncFromProject({ param: { workflowId, projectId } }), + (projectId: string) => { + setSyncingProjectId(projectId); + syncFromProject( + { param: { workflowId, projectId } }, + { onSettled: () => setSyncingProjectId(null) } + ); + }, [workflowId, syncFromProject] ); @@ -845,16 +844,7 @@ const WorkflowEditor = () => { )} - {/* Toggle side panel */} - + @@ -864,6 +854,18 @@ const WorkflowEditor = () => { Slides in/out with a CSS transition. Does NOT affect canvas width — canvas always stays full-bleed. */} + + {/* Floating tab to reopen panel when it is closed */} + {!panelOpen && ( + + )} +
- {/* Tab headers */} -
- + +
{/* Builder tab */} @@ -963,7 +974,7 @@ className=" onConnectProject={() => setConnectProjectOpen(true)} onDisconnectProject={handleDisconnectProject} onSyncFromProject={handleSyncFromProject} - isSyncing={isSyncing} + syncingProjectId={syncingProjectId ?? undefined} onRemoveStatus={handleRemoveStatus} />
diff --git a/src/features/custom-columns/components/manage-columns-form.tsx b/src/features/custom-columns/components/manage-columns-form.tsx index e69dd9c7..e903db7d 100644 --- a/src/features/custom-columns/components/manage-columns-form.tsx +++ b/src/features/custom-columns/components/manage-columns-form.tsx @@ -58,16 +58,31 @@ export const ManageColumnsForm = ({ onCancel }: ManageColumnsFormProps) => { workflowId: project?.workflowId || "" }); - // Create a list of workflow-based columns + // Must be declared before workflowColumns useMemo since the memo filters against it + const { data: customColumns, isLoading: isLoadingColumns } = useGetCustomColumns({ + workspaceId, + projectId: projectId || "" + }); + + // Create a list of workflow-based columns, excluding any that are already + // represented as project-level custom columns (auto-synced from workflow statuses). const workflowColumns = useMemo(() => { if (!workflowStatusesData?.documents) return []; - return workflowStatusesData.documents.map(status => ({ - id: status.key as TaskStatus, - name: status.name, - color: status.color, - statusType: status.statusType, - })); - }, [workflowStatusesData]); + + // Build a set of custom column names for this project (lower-cased for comparison) + const customColumnNames = new Set( + (customColumns?.documents || []).map(col => col.name.toLowerCase()) + ); + + return workflowStatusesData.documents + .filter(status => !customColumnNames.has(status.name.toLowerCase())) + .map(status => ({ + id: status.key as TaskStatus, + name: status.name, + color: status.color, + statusType: status.statusType, + })); + }, [workflowStatusesData, customColumns]); // Track pending changes const [pendingColumnToggles, setPendingColumnToggles] = useState>(new Set()); @@ -77,10 +92,6 @@ export const ManageColumnsForm = ({ onCancel }: ManageColumnsFormProps) => { const { mutate: createColumn, isPending: isCreating } = useCreateCustomColumn(); const { mutate: deleteColumn, isPending: isDeleting } = useDeleteCustomColumn(); - const { data: customColumns, isLoading: isLoadingColumns } = useGetCustomColumns({ - workspaceId, - projectId: projectId || "" - }); const { mutate: moveTasksFromDisabledColumn } = useMoveTasksFromDisabledColumn(); const { diff --git a/src/features/workflows/components/workflow-simple-view.tsx b/src/features/workflows/components/workflow-simple-view.tsx index 456f8775..98b37a82 100644 --- a/src/features/workflows/components/workflow-simple-view.tsx +++ b/src/features/workflows/components/workflow-simple-view.tsx @@ -118,7 +118,7 @@ interface WorkflowSimpleViewProps { onConnectProject?: () => void; onDisconnectProject?: (projectId: string) => void; onSyncFromProject?: (projectId: string) => void; - isSyncing?: boolean; + syncingProjectId?: string; onDragStatusStart?: (status: WorkflowStatus) => void; onRemoveStatus?: (statusId: string) => void; } @@ -171,7 +171,7 @@ export const WorkflowSimpleView = ({ onConnectProject, onDisconnectProject, onSyncFromProject, - isSyncing = false, + syncingProjectId, onDragStatusStart, onRemoveStatus: _onRemoveStatus, }: WorkflowSimpleViewProps) => { @@ -464,14 +464,14 @@ export const WorkflowSimpleView = ({ variant="ghost" size="icon" className="size-6 text-muted-foreground hover:text-primary transition-colors" - disabled={isSyncing} + disabled={syncingProjectId === project.$id} onClick={(e) => { e.preventDefault(); e.stopPropagation(); onSyncFromProject(project.$id); }} > - + Sync statuses from project From b41540b926e75e2d9159ab5a0f9ee34d82aad9a7 Mon Sep 17 00:00:00 2001 From: Happyesss <22csaiml002@jssaten.ac.in> Date: Mon, 18 May 2026 11:39:38 +0530 Subject: [PATCH 2/2] feat: add info modal for program overview and enhance workflow editor with auto-fit preview functionality --- .../[workspaceId]/programs/client.tsx | 96 +++++++++++++- .../workflows/[workflowId]/client.tsx | 34 ++++- .../[workspaceId]/programs/client.tsx | 122 +++++++++++++++++- .../components/activity-log-list.tsx | 4 +- .../components/activity-table-view.tsx | 4 +- .../workflows/components/workflow-ai-chat.tsx | 4 +- src/features/workflows/types.ts | 11 +- src/lib/ai-service.ts | 10 +- 8 files changed, 264 insertions(+), 21 deletions(-) diff --git a/src/app/(dashboard)/workspaces/[workspaceId]/programs/client.tsx b/src/app/(dashboard)/workspaces/[workspaceId]/programs/client.tsx index 5fc64597..636e9f5b 100644 --- a/src/app/(dashboard)/workspaces/[workspaceId]/programs/client.tsx +++ b/src/app/(dashboard)/workspaces/[workspaceId]/programs/client.tsx @@ -20,6 +20,11 @@ import { Shield, ChevronRight, Layers, + Info, + CheckCircle2, + BarChart3, + GitMerge, + Milestone, } from "lucide-react"; import Link from "next/link"; import { useState, useMemo } from "react"; @@ -51,6 +56,12 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { Separator } from "@/components/ui/separator"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { useWorkspaceId } from "@/features/workspaces/hooks/use-workspace-id"; import { useGetPrograms } from "@/features/programs/api/use-get-programs"; import { useDeleteProgram } from "@/features/programs/api/use-delete-program"; @@ -121,6 +132,7 @@ export const ProgramsClient = () => { const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [view, setView] = useState<"grid" | "list">("grid"); + const [showInfoModal, setShowInfoModal] = useState(false); const [DeleteDialog, confirmDelete] = useConfirm( "Delete Program", @@ -160,6 +172,79 @@ export const ProgramsClient = () => { + {/* Programs Info Modal */} + + + + +
+ +
+ What is a Program? +
+
+
+

+ A Program is a strategic umbrella that groups related projects under a shared objective. It gives your organization a bird's-eye view of work spanning multiple teams, timelines, and deliverables. +

+
+ {[ + { icon: Layers, title: "Multi-Project Oversight", desc: "Bundle related projects so stakeholders see progress in one place instead of switching between boards.", colorClass: "bg-purple-500/10 border-purple-500/20", iconColor: "text-purple-600 dark:text-purple-400" }, + { icon: Milestone, title: "Milestones", desc: "Define key delivery checkpoints — e.g. Beta Launch, Go-Live — shared across every project in the program.", colorClass: "bg-blue-500/10 border-blue-500/20", iconColor: "text-blue-600 dark:text-blue-400" }, + { icon: Users, title: "Cross-team Members", desc: "Add contributors from any project team so everyone on the initiative is visible in one place.", colorClass: "bg-emerald-500/10 border-emerald-500/20", iconColor: "text-emerald-600 dark:text-emerald-400" }, + { icon: BarChart3, title: "Analytics", desc: "Rolled-up completion rates, task counts, and timeline health for the entire program at a glance.", colorClass: "bg-amber-500/10 border-amber-500/20", iconColor: "text-amber-600 dark:text-amber-400" }, + ].map(({ icon: Icon, title, desc, colorClass, iconColor }) => ( +
+
+ +

{title}

+
+

{desc}

+
+ ))} +
+
+

+ + How it works in industry +

+
+ {[ + { company: "Product Company", program: "Q3 Platform Revamp", projects: ["API v2 Migration", "New Design System", "Mobile App Rewrite"] }, + { company: "E-commerce", program: "Holiday Season Launch", projects: ["Checkout Optimisation", "Inventory Sync", "Marketing Campaigns"] }, + { company: "Enterprise", program: "Cloud Migration", projects: ["Infra Lift & Shift", "Data Pipeline Rebuild", "Security Hardening"] }, + ].map((ex) => ( +
+
+ {ex.company} +

{ex.program}

+
+
+ {ex.projects.map((proj) => ( +
+ + {proj} +
+ ))} +
+
+ ))} +
+
+
+

Ready to get started?

+

Create your first program, link your projects, and start tracking progress at the strategic level.

+ {isAdmin && ( + + )} +
+
+
+
+ {/* Header */}
@@ -168,7 +253,16 @@ export const ProgramsClient = () => {
-

Programs

+
+

Programs

+ +

Manage strategic initiatives and portfolio health

diff --git a/src/app/(dashboard)/workspaces/[workspaceId]/spaces/[spaceId]/workflows/[workflowId]/client.tsx b/src/app/(dashboard)/workspaces/[workspaceId]/spaces/[spaceId]/workflows/[workflowId]/client.tsx index 32496a26..07d6536e 100644 --- a/src/app/(dashboard)/workspaces/[workspaceId]/spaces/[spaceId]/workflows/[workflowId]/client.tsx +++ b/src/app/(dashboard)/workspaces/[workspaceId]/spaces/[spaceId]/workflows/[workflowId]/client.tsx @@ -119,7 +119,7 @@ const WorkflowEditor = () => { // ───────────────────────────────────────────────────────────────────────── const hasSyncedOnMount = useRef(false); - const hasAutoSyncedProjects = useRef(false); + const _hasAutoSyncedProjects = useRef(false); useEffect(() => { if (!hasSyncedOnMount.current && workflowId) { hasSyncedOnMount.current = true; @@ -418,6 +418,28 @@ const WorkflowEditor = () => { handleEdgeEdit, handleEdgeDelete, ]); + // ── auto-fit to preview nodes when preview mode activates ──────────────── + const hasAutoFittedPreview = useRef(false); + useEffect(() => { + if (!previewSuggestion) { + hasAutoFittedPreview.current = false; + return; + } + if (hasAutoFittedPreview.current) return; + const previewNodes = nodes.filter((n) => (n.data as StatusNodeData)?.isPreview); + if (previewNodes.length === 0) return; + hasAutoFittedPreview.current = true; + const timer = setTimeout(() => { + reactFlowInstance.fitView({ + nodes: previewNodes.map((n) => ({ id: n.id })), + duration: 700, + padding: 0.35, + }); + }, 350); + return () => clearTimeout(timer); + }, [nodes, previewSuggestion, reactFlowInstance]); + // ───────────────────────────────────────────────────────────────────────── + const handleDelete = async () => { const ok = await confirmDelete(); if (!ok) return; @@ -443,7 +465,7 @@ const WorkflowEditor = () => { } ); }, - [workflowId, updateProject, syncFromProject, syncWithResolution] + [workflowId, updateProject, syncWithResolution] ); const handleDisconnectProject = useCallback( @@ -738,15 +760,15 @@ const WorkflowEditor = () => { @@ -998,6 +1020,8 @@ className=" } else { setPreviewSuggestion({ statuses: [], transitions: [suggestion as TransitionSuggestion] }); } + // Hide the panel so the full canvas is visible in preview mode + setPanelOpen(false); }} onApplyFullWorkflow={handleApplyFullWorkflow} /> diff --git a/src/app/(standalone)/workspaces/[workspaceId]/programs/client.tsx b/src/app/(standalone)/workspaces/[workspaceId]/programs/client.tsx index a98e26f9..fe8f9054 100644 --- a/src/app/(standalone)/workspaces/[workspaceId]/programs/client.tsx +++ b/src/app/(standalone)/workspaces/[workspaceId]/programs/client.tsx @@ -1,6 +1,6 @@ "use client"; -import { Plus, MoreVertical, Pencil, Trash2, FolderKanban, Calendar, ArrowRight, Target, TrendingUp, Clock, Search, Filter, Grid3x3, List } from "lucide-react"; +import { Plus, MoreVertical, Pencil, Trash2, FolderKanban, Calendar, ArrowRight, Target, TrendingUp, Clock, Search, Filter, Grid3x3, List, Info, CheckCircle2, Users, Layers, BarChart3, GitMerge, Milestone } from "lucide-react"; import Link from "next/link"; import { useState, useMemo } from "react"; @@ -10,6 +10,12 @@ import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Input } from "@/components/ui/input"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, @@ -92,6 +98,7 @@ export const ProgramsClient = () => { const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); + const [showInfoModal, setShowInfoModal] = useState(false); const [DeleteDialog, confirmDelete] = useConfirm( "Delete Program", @@ -141,13 +148,120 @@ export const ProgramsClient = () => { + {/* Programs Info Modal */} + + + + +
+ +
+ What is a Program? +
+
+ +
+ {/* Hero description */} +

+ A Program is a strategic umbrella that groups related projects together under a shared objective. It gives your organization a bird's-eye view of work that spans multiple teams, timelines, and deliverables. +

+ + {/* Key concepts */} +
+ {[ + { icon: Layers, color: "purple", title: "Multi-Project Oversight", desc: "Bundle related projects so stakeholders see progress in one place instead of switching between boards." }, + { icon: Milestone, color: "blue", title: "Milestones", desc: "Define key delivery checkpoints — e.g. Beta Launch, Go-Live — shared across every project in the program." }, + { icon: Users, color: "emerald", title: "Cross-team Members", desc: "Add contributors from any project team so everyone working on the initiative is visible." }, + { icon: BarChart3, color: "amber", title: "Analytics", desc: "Rolled-up completion rates, task counts, and timeline health for the entire program at a glance." }, + ].map(({ icon: Icon, color, title, desc }) => ( +
+
+ +

{title}

+
+

{desc}

+
+ ))} +
+ + {/* Industry examples */} +
+

+ + How it works in industry +

+
+ {[ + { + company: "Product Company", + program: "Q3 Platform Revamp", + projects: ["API v2 Migration", "New Design System", "Mobile App Rewrite"], + color: "purple", + }, + { + company: "E-commerce", + program: "Holiday Season Launch", + projects: ["Checkout Optimisation", "Inventory Sync", "Marketing Campaigns"], + color: "pink", + }, + { + company: "Enterprise", + program: "Cloud Migration", + projects: ["Infra Lift & Shift", "Data Pipeline Rebuild", "Security Hardening"], + color: "blue", + }, + ].map((ex) => ( +
+
+ {ex.company} +

{ex.program}

+
+
+ {ex.projects.map((proj) => ( +
+ + {proj} +
+ ))} +
+
+ ))} +
+
+ + {/* CTA */} +
+

Ready to get started?

+

Create your first program, link your projects, and start tracking progress at the strategic level.

+ +
+
+
+
+ {/* Header with Stats */}
-

- Programs -

+
+

+ Programs +

+ +

Strategic initiatives that drive your organization forward

diff --git a/src/features/audit-logs/components/activity-log-list.tsx b/src/features/audit-logs/components/activity-log-list.tsx index c568be85..b2c167e6 100644 --- a/src/features/audit-logs/components/activity-log-list.tsx +++ b/src/features/audit-logs/components/activity-log-list.tsx @@ -198,8 +198,8 @@ export const ActivityLogList = ({ activities, isLoading }: ActivityLogListProps) return (
- {activities.map((activity) => ( - + {activities.map((activity, index) => ( + ))}
diff --git a/src/features/audit-logs/components/activity-table-view.tsx b/src/features/audit-logs/components/activity-table-view.tsx index b0ba9595..d11e92bd 100644 --- a/src/features/audit-logs/components/activity-table-view.tsx +++ b/src/features/audit-logs/components/activity-table-view.tsx @@ -106,7 +106,7 @@ export const ActivityTableView = ({ activities }: ActivityTableViewProps) => { - {activities.map((activity) => { + {activities.map((activity, index) => { const Icon = getActivityIcon(activity.type); const colorClass = getActivityColor(activity.action); @@ -124,7 +124,7 @@ export const ActivityTableView = ({ activities }: ActivityTableViewProps) => { : "??"; return ( - +
{format(new Date(activity.timestamp), "MMM d, yyyy")} diff --git a/src/features/workflows/components/workflow-ai-chat.tsx b/src/features/workflows/components/workflow-ai-chat.tsx index 608ebd19..d332009f 100644 --- a/src/features/workflows/components/workflow-ai-chat.tsx +++ b/src/features/workflows/components/workflow-ai-chat.tsx @@ -298,7 +298,7 @@ export const WorkflowAIChat = ({ variant="outline" size="sm" className="h-8 text-xs justify-start font-normal" - onClick={() => handleAsk(prompt.text, prompt.type)} + onClick={() => handleAsk(prompt.text)} disabled={isProcessing || isLoadingContext} > @@ -456,7 +456,7 @@ export const WorkflowAIChat = ({ onClick={() => item.action?.data && handleApplyFullWorkflow(item.action.data as WorkflowSuggestion)} > - Apply All + Apply
diff --git a/src/features/workflows/types.ts b/src/features/workflows/types.ts index 3a25ee49..4744cb9d 100644 --- a/src/features/workflows/types.ts +++ b/src/features/workflows/types.ts @@ -528,15 +528,18 @@ export function convertStatusesToNodes( onRemove?: (id: string) => void, isPreview?: boolean ): StatusNode[] { - // Only render statuses that have valid canvas positions - const canvasStatuses = statuses.filter(hasCanvasPosition); + // For preview (AI suggestions), show all; otherwise require canvas positions + const canvasStatuses = isPreview ? statuses : statuses.filter(hasCanvasPosition); - return canvasStatuses.map((status: WorkflowStatusLike) => { + return canvasStatuses.map((status: WorkflowStatusLike, index: number) => { const nodeId = status.$id || status.key; // Use key as stable ID for suggestions + // Auto-assign staggered positions for AI suggestion nodes that lack coordinates + const x = status.positionX || (isPreview ? 100 + index * 260 : (status.position ? status.position * 280 : 100)); + const y = status.positionY || (isPreview ? 400 : 150); return { id: nodeId, type: "statusNode", - position: { x: status.positionX || (status.position ? status.position * 280 : 100), y: status.positionY || 150 }, + position: { x, y }, data: { id: nodeId, name: status.name, diff --git a/src/lib/ai-service.ts b/src/lib/ai-service.ts index 145938cf..ca00c927 100644 --- a/src/lib/ai-service.ts +++ b/src/lib/ai-service.ts @@ -189,7 +189,15 @@ export class AIService { } const data = await res.json(); - const text = data.candidates?.[0]?.content?.parts?.[0]?.text || ""; + // gemini-2.5-flash (and other thinking models) returns multiple parts: + // thought parts (thought: true) contain internal reasoning and must be excluded; + // only non-thought parts carry the actual output text. + const parts: Array<{ text?: string; thought?: boolean }> = + data.candidates?.[0]?.content?.parts || []; + const text = parts + .filter((p) => !p.thought) + .map((p) => p.text || "") + .join("") || ""; // Extract token usage from Gemini usageMetadata const usageMetadata = data.usageMetadata;