From 6472fafcd28f17e7d9dc0c14cf43938cfacc1bb3 Mon Sep 17 00:00:00 2001 From: Shilendra Singh Date: Sat, 2 Aug 2025 22:19:27 +0530 Subject: [PATCH 1/6] feat: refactor IssuesTable component and integrate DataTable for improved issue management - Replaced the existing IssuesTable implementation with a new DataTable component for enhanced functionality. - Introduced issueColumns for better column management and display of issue attributes. - Updated package dependencies to include @tanstack/react-table for table functionalities. - Cleaned up unused state management and commented code for improved readability. --- backend/prisma/schema.prisma | 2 +- .../features/issues/IssuesTable.tsx | 94 ++------ .../features/issues/table/issue-columns.tsx | 178 +++++++++++++++ client/components/ui/organisms/data-table.tsx | 215 ++++++++++++++++++ client/package-lock.json | 40 +++- client/package.json | 1 + 6 files changed, 450 insertions(+), 80 deletions(-) create mode 100644 client/components/features/issues/table/issue-columns.tsx create mode 100644 client/components/ui/organisms/data-table.tsx diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f3dfe1f..f073994 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -107,7 +107,7 @@ model Issue { description String? state String @default("backlog") // "backlog", "todo", "in_progress", "done", "canceled" priority Int @default(0) // 0=no priority, 1=urgent, 2=high, 3=medium, 4=low - labels String[] @default([]) + labels String[] @default([]) dueDate DateTime? notes String? number Int // Linear-style issue number diff --git a/client/components/features/issues/IssuesTable.tsx b/client/components/features/issues/IssuesTable.tsx index 034e9f5..7c362fb 100644 --- a/client/components/features/issues/IssuesTable.tsx +++ b/client/components/features/issues/IssuesTable.tsx @@ -1,92 +1,34 @@ 'use client' -import React, { useState } from 'react' +import React from 'react' import { Issue } from '@/types/issue' -import { Checkbox } from '@/components/ui/atoms/checkbox' -import { IssueRow } from './IssueRow' -import { useIssueStore } from '@/stores/issueStore' +import { DataTable } from '@/components/ui/organisms/data-table' +import { issueColumns } from './table/issue-columns' +// import { useIssueStore } from '@/stores/issueStore' interface IssueTableProps { issues: Issue[] } const IssueTableComponent = ({ issues }: IssueTableProps) => { - const { setSelectedIssue } = useIssueStore() - const [selectedIssues, setSelectedIssues] = useState>(new Set()) + // const { setSelectedIssue } = useIssueStore() - // Sort issues by createdAt descending (latest first) - const sortedIssues = [...issues].sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ) - - const handleIssueSelect = (issue: Issue) => { - setSelectedIssue(issue.id) - } - - const handleIssueCheck = (issueId: string, checked: boolean) => { - setSelectedIssues(prev => { - const newSet = new Set(prev) - if (checked) { - newSet.add(issueId) - } else { - newSet.delete(issueId) - } - return newSet - }) - } - - const handleSelectAll = (checked: boolean) => { - if (checked) { - setSelectedIssues(new Set(sortedIssues.map(issue => issue.id))) - } else { - setSelectedIssues(new Set()) - } - } + // const handleRowClick = (issue: Issue) => { + // setSelectedIssue(issue.id) + // } return ( -
- - - - - - - - - - - - - - - {sortedIssues.map((issue) => ( - - ))} - -
- 0} - onCheckedChange={handleSelectAll} - /> - TitleStatePriorityDue DateAssigneeCreated
- - {sortedIssues.length === 0 && ( -
-
-

No issues found

-

Create your first issue to get started

-
-
- )} -
+ ) } export const IssueTable = React.memo(IssueTableComponent) -IssueTable.displayName = 'IssueTable' \ No newline at end of file +IssueTable.displayName = 'IssueTable' \ No newline at end of file diff --git a/client/components/features/issues/table/issue-columns.tsx b/client/components/features/issues/table/issue-columns.tsx new file mode 100644 index 0000000..7f71c23 --- /dev/null +++ b/client/components/features/issues/table/issue-columns.tsx @@ -0,0 +1,178 @@ +"use client" + +import { ColumnDef } from "@tanstack/react-table" +import { Issue } from "@/types/issue" +import { Badge } from "@/components/ui/atoms/badge" +import { Button } from "@/components/ui/atoms/button" +import { Calendar, MoreHorizontal } from "lucide-react" +import { ClientDate } from "@/components/ui/atoms/client-date" +import { cn } from "@/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/atoms/dropdown-menu" + +const getStateLabel = (state: string) => { + switch (state) { + case 'backlog': return 'Backlog' + case 'todo': return 'To Do' + case 'in_progress': return 'In Progress' + case 'done': return 'Done' + case 'canceled': return 'Canceled' + default: return state + } +} + +const getStateColor = (state: string) => { + switch (state) { + case 'backlog': return 'bg-gray-200 text-gray-700 border-gray-300' + case 'todo': return 'bg-blue-100 text-blue-700 border-blue-200' + case 'in_progress': return 'bg-yellow-100 text-yellow-700 border-yellow-200' + case 'done': return 'bg-green-100 text-green-700 border-green-200' + case 'canceled': return 'bg-red-100 text-red-700 border-red-200' + default: return 'bg-muted text-muted-foreground border-border' + } +} + +const getPriorityLabel = (priority: number) => { + switch (priority) { + case 1: return 'P1' + case 2: return 'P2' + case 3: return 'P3' + case 4: return 'P4' + case 0: return 'P0' + default: return `P${priority}` + } +} + +const getPriorityColor = (priority: number) => { + switch (priority) { + case 1: return 'bg-destructive/10 text-destructive border-destructive/20' + case 2: return 'bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20' + case 3: return 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20' + case 4: return 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20' + default: return 'bg-muted text-muted-foreground border-border' + } +} + +export const issueColumns: ColumnDef[] = [ + { + accessorKey: "title", + header: "Title", + enableSorting: true, + cell: ({ row }) => { + const issue = row.original + return ( +
+ {issue.title} + {issue.description && ( + + {issue.description} + + )} +
+ ) + }, + }, + { + accessorKey: "state", + header: "State", + enableSorting: true, + cell: ({ row }) => { + const state = row.getValue("state") as string + return ( + + {getStateLabel(state)} + + ) + }, + }, + { + accessorKey: "priority", + header: "Priority", + enableSorting: true, + cell: ({ row }) => { + const priority = row.getValue("priority") as number + return ( + + {getPriorityLabel(priority)} + + ) + }, + }, + { + accessorKey: "dueDate", + header: "Due Date", + enableSorting: true, + cell: ({ row }) => { + const dueDate = row.getValue("dueDate") as string + if (!dueDate) return No due date + + return ( +
+ + +
+ ) + }, + }, + { + accessorKey: "assignee", + header: "Assignee", + enableSorting: true, + cell: ({ row }) => { + const assignee = row.original.assignee + if (!assignee) return Unassigned + + return ( +
+
+ {assignee.name?.charAt(0) || assignee.email?.charAt(0)} +
+ {assignee.name || assignee.email} +
+ ) + }, + }, + { + accessorKey: "createdAt", + header: "Created", + enableSorting: true, + cell: ({ row }) => { + const createdAt = row.getValue("createdAt") as string + return + }, + }, + { + id: "actions", + enableHiding: false, + enableSorting: false, + cell: ({ row }) => { + const issue = row.original + + return ( + + + + + + Actions + navigator.clipboard.writeText(issue.id)}> + Copy issue ID + + + View details + Edit issue + + + ) + }, + }, + ] \ No newline at end of file diff --git a/client/components/ui/organisms/data-table.tsx b/client/components/ui/organisms/data-table.tsx new file mode 100644 index 0000000..5c5f083 --- /dev/null +++ b/client/components/ui/organisms/data-table.tsx @@ -0,0 +1,215 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getSortedRowModel, + SortingState, + getFilteredRowModel, + ColumnFiltersState, + getPaginationRowModel, + RowSelectionState, +} from "@tanstack/react-table" + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/molecules/table" +import { Button } from "@/components/ui/atoms/button" +import { Input } from "@/components/ui/atoms/input" +import { Checkbox } from "@/components/ui/atoms/checkbox" +import { ArrowUpDown, ChevronDown, ChevronUp } from "lucide-react" +import { cn } from "@/lib/utils" + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] + searchKey?: string + searchPlaceholder?: string + showSearch?: boolean + showPagination?: boolean + showRowSelection?: boolean + onRowSelectionChange?: (selection: RowSelectionState) => void + selectedRows?: RowSelectionState + className?: string + emptyMessage?: string +} + +export function DataTable({ + columns, + data, + searchKey, + searchPlaceholder = "Search...", + showSearch = true, + showPagination = true, + showRowSelection = false, + onRowSelectionChange, + selectedRows, + className, + emptyMessage = "No data found" +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState([]) + const [rowSelection, setRowSelection] = React.useState(selectedRows || {}) + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onRowSelectionChange: (updater) => { + const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater + setRowSelection(newSelection) + onRowSelectionChange?.(newSelection) + }, + state: { + sorting, + columnFilters, + rowSelection, + }, + }) + + React.useEffect(() => { + if (selectedRows) { + setRowSelection(selectedRows) + } + }, [selectedRows]) + + return ( +
+ {showSearch && searchKey && ( +
+ + table.getColumn(searchKey)?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> +
+ )} + + {/* Table */} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {showRowSelection && ( + + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + + )} + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? ( + header.column.getCanSort() ? ( + + ) : null + )} +
+ )} +
+ ) + })} +
+ ))} +
+ + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {showRowSelection && ( + + row.toggleSelected(!!value)} + aria-label="Select row" + /> + + )} + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + {emptyMessage} + + + )} + +
+
+ + {showPagination && ( +
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+ + +
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 9c20b71..0e00020 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -23,6 +23,7 @@ "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.83.0", + "@tanstack/react-table": "^8.21.3", "@tiptap/extension-link": "^3.0.1", "@tiptap/extension-placeholder": "^3.0.1", "@tiptap/extension-table": "^3.0.1", @@ -2476,6 +2477,39 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tiptap/core": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.0.7.tgz", @@ -6646,9 +6680,9 @@ } }, "node_modules/linkifyjs": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.1.tgz", - "integrity": "sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", "license": "MIT" }, "node_modules/locate-path": { diff --git a/client/package.json b/client/package.json index e8e228d..6aac049 100644 --- a/client/package.json +++ b/client/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.83.0", + "@tanstack/react-table": "^8.21.3", "@tiptap/extension-link": "^3.0.1", "@tiptap/extension-placeholder": "^3.0.1", "@tiptap/extension-table": "^3.0.1", From d5eb7f8b630ad38b4eac0f4e1cd4d11cadb4c1a6 Mon Sep 17 00:00:00 2001 From: Shilendra Singh Date: Sun, 3 Aug 2025 23:42:04 +0530 Subject: [PATCH 2/6] refactor: streamline IssuesTable and enhance DataTable functionality - Removed row selection feature from IssuesTable for a cleaner interface. - Refactored issueColumns to utilize new cell components for better organization and readability. - Introduced label icons and improved date formatting in issue display. - Enhanced DataTable to group issues by state, improving data presentation and usability. --- .../features/issues/IssuesTable.tsx | 1 - .../features/issues/table/IssueRow.tsx | 54 +++ .../features/issues/table/issue-columns.tsx | 310 +++++++++--------- .../ui/atoms/rowElements/AssigneeCell.tsx | 61 ++++ .../ui/atoms/rowElements/CreatedAtCell.tsx | 11 + .../ui/atoms/rowElements/KeyCell.tsx | 17 + .../ui/atoms/rowElements/LabelsCell.tsx | 64 ++++ .../ui/atoms/rowElements/PriorityCell.tsx | 69 ++++ .../ui/atoms/rowElements/StateCell.tsx | 56 ++++ .../ui/atoms/rowElements/TitleCell.tsx | 15 + .../components/ui/atoms/rowElements/index.ts | 7 + .../ui/molecules/menus/AssigneeMenu.tsx | 61 ++++ .../ui/molecules/menus/LabelsMenu.tsx | 64 ++++ .../ui/molecules/menus/PriorityMenu.tsx | 69 ++++ .../ui/molecules/menus/StateMenu.tsx | 71 ++++ client/components/ui/organisms/data-table.tsx | 162 ++++----- 16 files changed, 835 insertions(+), 257 deletions(-) create mode 100644 client/components/features/issues/table/IssueRow.tsx create mode 100644 client/components/ui/atoms/rowElements/AssigneeCell.tsx create mode 100644 client/components/ui/atoms/rowElements/CreatedAtCell.tsx create mode 100644 client/components/ui/atoms/rowElements/KeyCell.tsx create mode 100644 client/components/ui/atoms/rowElements/LabelsCell.tsx create mode 100644 client/components/ui/atoms/rowElements/PriorityCell.tsx create mode 100644 client/components/ui/atoms/rowElements/StateCell.tsx create mode 100644 client/components/ui/atoms/rowElements/TitleCell.tsx create mode 100644 client/components/ui/atoms/rowElements/index.ts create mode 100644 client/components/ui/molecules/menus/AssigneeMenu.tsx create mode 100644 client/components/ui/molecules/menus/LabelsMenu.tsx create mode 100644 client/components/ui/molecules/menus/PriorityMenu.tsx create mode 100644 client/components/ui/molecules/menus/StateMenu.tsx diff --git a/client/components/features/issues/IssuesTable.tsx b/client/components/features/issues/IssuesTable.tsx index 7c362fb..9c17c7f 100644 --- a/client/components/features/issues/IssuesTable.tsx +++ b/client/components/features/issues/IssuesTable.tsx @@ -23,7 +23,6 @@ const IssueTableComponent = ({ issues }: IssueTableProps) => { data={issues} showSearch={true} showPagination={true} - showRowSelection={true} emptyMessage="No issues found. Create a new issue." className="w-full" /> diff --git a/client/components/features/issues/table/IssueRow.tsx b/client/components/features/issues/table/IssueRow.tsx new file mode 100644 index 0000000..63f654c --- /dev/null +++ b/client/components/features/issues/table/IssueRow.tsx @@ -0,0 +1,54 @@ +"use client" + +import { TableCell, TableRow } from "@/components/ui/molecules/table" +import { flexRender, Row } from "@tanstack/react-table" + +interface IssueRowProps { + row: Row +} + +export function IssueRow({ row }: IssueRowProps) { + const cells = row.getVisibleCells() + + return ( + + {/* Priority */} + + {flexRender(cells[0].column.columnDef.cell, cells[0].getContext())} + + + {/* Key */} + + {flexRender(cells[1].column.columnDef.cell, cells[1].getContext())} + + + {/* Title - takes remaining space */} + +
+
+ {flexRender(cells[2].column.columnDef.cell, cells[2].getContext())} +
+
+ {flexRender(cells[6].column.columnDef.cell, cells[6].getContext())} +
+
+ +
+ + {/* Labels */} + + + + + {/* Assignee */} + + {flexRender(cells[5].column.columnDef.cell, cells[5].getContext())} + + + {/* Created Date */} + + {flexRender(cells[7].column.columnDef.cell, cells[7].getContext())} + +
+ ) +} \ No newline at end of file diff --git a/client/components/features/issues/table/issue-columns.tsx b/client/components/features/issues/table/issue-columns.tsx index 7f71c23..8c5a410 100644 --- a/client/components/features/issues/table/issue-columns.tsx +++ b/client/components/features/issues/table/issue-columns.tsx @@ -2,177 +2,169 @@ import { ColumnDef } from "@tanstack/react-table" import { Issue } from "@/types/issue" -import { Badge } from "@/components/ui/atoms/badge" -import { Button } from "@/components/ui/atoms/button" -import { Calendar, MoreHorizontal } from "lucide-react" -import { ClientDate } from "@/components/ui/atoms/client-date" -import { cn } from "@/lib/utils" import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/atoms/dropdown-menu" + PriorityCell, + KeyCell, + TitleCell, + AssigneeCell, + StateCell +} from "@/components/ui/atoms/rowElements" +import { Calendar, Bug, Star, Wrench, ArrowUp } from "lucide-react" -const getStateLabel = (state: string) => { - switch (state) { - case 'backlog': return 'Backlog' - case 'todo': return 'To Do' - case 'in_progress': return 'In Progress' - case 'done': return 'Done' - case 'canceled': return 'Canceled' - default: return state +// Label helpers +const getLabelIcon = (labelType: string) => { + switch (labelType) { + case 'bug': return + case 'feature': return + case 'fix': return + case 'enhancement': return + case 'due_date': return + default: return } } -const getStateColor = (state: string) => { - switch (state) { - case 'backlog': return 'bg-gray-200 text-gray-700 border-gray-300' - case 'todo': return 'bg-blue-100 text-blue-700 border-blue-200' - case 'in_progress': return 'bg-yellow-100 text-yellow-700 border-yellow-200' - case 'done': return 'bg-green-100 text-green-700 border-green-200' - case 'canceled': return 'bg-red-100 text-red-700 border-red-200' - default: return 'bg-muted text-muted-foreground border-border' - } -} - -const getPriorityLabel = (priority: number) => { - switch (priority) { - case 1: return 'P1' - case 2: return 'P2' - case 3: return 'P3' - case 4: return 'P4' - case 0: return 'P0' - default: return `P${priority}` - } -} - -const getPriorityColor = (priority: number) => { - switch (priority) { - case 1: return 'bg-destructive/10 text-destructive border-destructive/20' - case 2: return 'bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20' - case 3: return 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20' - case 4: return 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20' - default: return 'bg-muted text-muted-foreground border-border' - } +// Date formatter for "12 July" format +const formatDate = (dateString: string) => { + const date = new Date(dateString) + const day = date.getDate() + const month = date.toLocaleDateString('en-US', { month: 'long' }) + return `${day} ${month}` } export const issueColumns: ColumnDef[] = [ - { - accessorKey: "title", - header: "Title", - enableSorting: true, - cell: ({ row }) => { - const issue = row.original - return ( -
- {issue.title} - {issue.description && ( - - {issue.description} - - )} -
- ) - }, - }, - { - accessorKey: "state", - header: "State", - enableSorting: true, - cell: ({ row }) => { - const state = row.getValue("state") as string - return ( - - {getStateLabel(state)} - - ) - }, + { + accessorKey: "priority", + header: "Priority", + enableSorting: true, + cell: ({ row }) => { + const priority = row.getValue("priority") as number + const handlePriorityChange = (newPriority: number) => { + console.log('Priority changed to:', newPriority) + } + + return ( + + ) }, - { - accessorKey: "priority", - header: "Priority", - enableSorting: true, - cell: ({ row }) => { - const priority = row.getValue("priority") as number - return ( - - {getPriorityLabel(priority)} - - ) - }, + }, + { + accessorKey: "key", + header: "Key", + enableSorting: true, + cell: ({ row }) => { + const issue = row.original + return }, - { - accessorKey: "dueDate", - header: "Due Date", - enableSorting: true, - cell: ({ row }) => { - const dueDate = row.getValue("dueDate") as string - if (!dueDate) return No due date - - return ( -
- - -
- ) - }, + }, + { + accessorKey: "title", + header: "Title", + enableSorting: true, + cell: ({ row }) => { + const issue = row.original + return }, - { - accessorKey: "assignee", - header: "Assignee", - enableSorting: true, - cell: ({ row }) => { - const assignee = row.original.assignee - if (!assignee) return Unassigned - - return ( -
-
- {assignee.name?.charAt(0) || assignee.email?.charAt(0)} + }, + { + id: "labels", + header: "Labels", + enableSorting: false, + cell: ({ row }) => { + const issue = row.original + const labels = issue.labels || [] + + return ( +
+ {labels.length > 0 ? ( + labels.slice(0, 2).map((label, index) => ( +
+ {getLabelIcon(label)} +
+ )) + ) : ( +
+
- {assignee.name || assignee.email} -
- ) - }, + )} +
+ ) + }, + }, + { + accessorKey: "dueDate", + header: "Due Date", + enableSorting: true, + cell: ({ row }) => { + const dueDate = row.getValue("dueDate") as string + if (!dueDate) return No due date + + return ( + + {formatDate(dueDate)} + + ) + }, + }, + { + accessorKey: "assignee", + header: "Assignee", + enableSorting: true, + cell: ({ row }) => { + const assignee = row.original.assignee + const handleAssigneeChange = (assigneeId: string | null) => { + console.log('Assignee changed to:', assigneeId) + } + + const availableAssignees = [ + { id: '1', name: 'John Doe', email: 'john@example.com' }, + { id: '2', name: 'Jane Smith', email: 'jane@example.com' }, + ] + + return ( + + ) }, - { - accessorKey: "createdAt", - header: "Created", - enableSorting: true, - cell: ({ row }) => { - const createdAt = row.getValue("createdAt") as string - return - }, + }, + { + accessorKey: "state", + header: "State", + enableSorting: true, + cell: ({ row }) => { + const state = row.getValue("state") as string + const handleStateChange = (newState: string) => { + console.log('State changed to:', newState) + } + + return ( + + ) }, - { - id: "actions", - enableHiding: false, - enableSorting: false, - cell: ({ row }) => { - const issue = row.original - - return ( - - - - - - Actions - navigator.clipboard.writeText(issue.id)}> - Copy issue ID - - - View details - Edit issue - - - ) - }, + }, + { + accessorKey: "createdAt", + header: "Created", + enableSorting: true, + cell: ({ row }) => { + const createdAt = row.getValue("createdAt") as string + return ( + + {formatDate(createdAt)} + + ) }, - ] \ No newline at end of file + }, +] \ No newline at end of file diff --git a/client/components/ui/atoms/rowElements/AssigneeCell.tsx b/client/components/ui/atoms/rowElements/AssigneeCell.tsx new file mode 100644 index 0000000..97b0047 --- /dev/null +++ b/client/components/ui/atoms/rowElements/AssigneeCell.tsx @@ -0,0 +1,61 @@ +"use client" + +import { Avatar, AvatarFallback } from "@/components/ui/atoms/avatar" +import { User } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/atoms/dropdown-menu" + +interface Assignee { + id: string + name?: string + email: string +} + +interface AssigneeCellProps { + assignee?: Assignee + onAssigneeChange: (assigneeId: string | null) => void + availableAssignees: Assignee[] +} + +export function AssigneeCell({ assignee, onAssigneeChange, availableAssignees }: AssigneeCellProps) { + return ( + + +
+ {assignee ? ( + + + {assignee.name?.charAt(0).toUpperCase() || assignee.email?.charAt(0).toUpperCase() || '?'} + + + ) : ( +
+ +
+ )} +
+
+ + Assign to + {availableAssignees.map((user) => ( + onAssigneeChange(user.id)} + > + {user.name || user.email} + + ))} + + onAssigneeChange(null)}> + Unassign + + +
+ ) +} \ No newline at end of file diff --git a/client/components/ui/atoms/rowElements/CreatedAtCell.tsx b/client/components/ui/atoms/rowElements/CreatedAtCell.tsx new file mode 100644 index 0000000..09e18a9 --- /dev/null +++ b/client/components/ui/atoms/rowElements/CreatedAtCell.tsx @@ -0,0 +1,11 @@ +"use client" + +import { ClientDate } from "@/components/ui/atoms/client-date" + +interface CreatedAtCellProps { + createdAt: string +} + +export function CreatedAtCell({ createdAt }: CreatedAtCellProps) { + return +} \ No newline at end of file diff --git a/client/components/ui/atoms/rowElements/KeyCell.tsx b/client/components/ui/atoms/rowElements/KeyCell.tsx new file mode 100644 index 0000000..4889168 --- /dev/null +++ b/client/components/ui/atoms/rowElements/KeyCell.tsx @@ -0,0 +1,17 @@ +"use client" + +import { Issue } from "@/types/issue" + +interface KeyCellProps { + issue: Issue +} + +export function KeyCell({ issue }: KeyCellProps) { + const key = issue.key || `ISS-${issue.id.slice(-2)}` + + return ( + + {key} + + ) +} \ No newline at end of file diff --git a/client/components/ui/atoms/rowElements/LabelsCell.tsx b/client/components/ui/atoms/rowElements/LabelsCell.tsx new file mode 100644 index 0000000..2ba5b2a --- /dev/null +++ b/client/components/ui/atoms/rowElements/LabelsCell.tsx @@ -0,0 +1,64 @@ +"use client" + +import { Calendar, Bug, Star, Wrench, ArrowUp } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/atoms/dropdown-menu" + +interface Label { + id: string + type: string + name: string +} + +interface LabelsCellProps { + labels: Label[] + onLabelsChange: (labels: Label[]) => void +} + +const getLabelIcon = (labelType: string) => { + switch (labelType) { + case 'bug': return + case 'feature': return + case 'fix': return + case 'enhancement': return + case 'due_date': return + default: return + } +} + +export function LabelsCell({ labels, onLabelsChange }: LabelsCellProps) { + return ( + + +
+ {labels.length > 0 ? ( + labels.slice(0, 3).map((label, index) => ( +
+ {getLabelIcon(label.type)} +
+ )) + ) : ( +
+ +
+ )} +
+
+ + Labels + Add Bug Label + Add Feature Label + Add Fix Label + Add Enhancement Label + + Manage Labels + +
+ ) +} \ No newline at end of file diff --git a/client/components/ui/atoms/rowElements/PriorityCell.tsx b/client/components/ui/atoms/rowElements/PriorityCell.tsx new file mode 100644 index 0000000..1673653 --- /dev/null +++ b/client/components/ui/atoms/rowElements/PriorityCell.tsx @@ -0,0 +1,69 @@ +"use client" + +import { Badge } from "@/components/ui/atoms/badge" +import { cn } from "@/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/atoms/dropdown-menu" + +interface PriorityCellProps { + priority: number + onPriorityChange: (priority: number) => void +} + +const getPriorityLabel = (priority: number) => { + switch (priority) { + case 1: return 'P1' + case 2: return 'P2' + case 3: return 'P3' + case 4: return 'P4' + case 0: return 'P0' + default: return `P${priority}` + } +} + +const getPriorityColor = (priority: number) => { + switch (priority) { + case 1: return 'bg-destructive/10 text-destructive border-destructive/20' + case 2: return 'bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20' + case 3: return 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20' + case 4: return 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20' + default: return 'bg-muted text-muted-foreground border-border' + } +} + +export function PriorityCell({ priority, onPriorityChange }: PriorityCellProps) { + return ( + + + + {getPriorityLabel(priority)} + + + + Change Priority + onPriorityChange(1)}> + P1 - Urgent + + onPriorityChange(2)}> + P2 - High + + onPriorityChange(3)}> + P3 - Medium + + onPriorityChange(4)}> + P4 - Low + + onPriorityChange(0)}> + P0 - None + + + + ) +} \ No newline at end of file diff --git a/client/components/ui/atoms/rowElements/StateCell.tsx b/client/components/ui/atoms/rowElements/StateCell.tsx new file mode 100644 index 0000000..5965aeb --- /dev/null +++ b/client/components/ui/atoms/rowElements/StateCell.tsx @@ -0,0 +1,56 @@ +"use client" + +import { Clock, Check, Archive, X, List } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/atoms/dropdown-menu" + +interface StateCellProps { + state: string + onStateChange: (state: string) => void +} + +const getStateIcon = (state: string) => { + switch (state) { + case 'todo': return + case 'in_progress': return + case 'done': return + case 'backlog': return + case 'canceled': return + default: return + } +} + +export function StateCell({ state, onStateChange }: StateCellProps) { + return ( + + +
+ {getStateIcon(state)} +
+
+ + Change State + onStateChange('todo')}> + Todo + + onStateChange('in_progress')}> + In Progress + + onStateChange('done')}> + Done + + onStateChange('backlog')}> + Backlog + + onStateChange('canceled')}> + Canceled + + +
+ ) +} \ No newline at end of file diff --git a/client/components/ui/atoms/rowElements/TitleCell.tsx b/client/components/ui/atoms/rowElements/TitleCell.tsx new file mode 100644 index 0000000..f7fc0fa --- /dev/null +++ b/client/components/ui/atoms/rowElements/TitleCell.tsx @@ -0,0 +1,15 @@ +"use client" + +import { Issue } from "@/types/issue" + +interface TitleCellProps { + issue: Issue +} + +export function TitleCell({ issue }: TitleCellProps) { + return ( +
+ {issue.title} +
+ ) +} \ No newline at end of file diff --git a/client/components/ui/atoms/rowElements/index.ts b/client/components/ui/atoms/rowElements/index.ts new file mode 100644 index 0000000..a0d860b --- /dev/null +++ b/client/components/ui/atoms/rowElements/index.ts @@ -0,0 +1,7 @@ +export { PriorityCell } from './PriorityCell' +export { KeyCell } from './KeyCell' +export { TitleCell } from './TitleCell' +export { AssigneeCell } from './AssigneeCell' +export { LabelsCell } from './LabelsCell' +export { StateCell } from './StateCell' +export { CreatedAtCell } from './CreatedAtCell' \ No newline at end of file diff --git a/client/components/ui/molecules/menus/AssigneeMenu.tsx b/client/components/ui/molecules/menus/AssigneeMenu.tsx new file mode 100644 index 0000000..41634b0 --- /dev/null +++ b/client/components/ui/molecules/menus/AssigneeMenu.tsx @@ -0,0 +1,61 @@ +"use client" + +import { Avatar, AvatarFallback } from "@/components/ui/atoms/avatar" +import { User } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/atoms/dropdown-menu" + +interface Assignee { + id: string + name?: string + email: string +} + +interface AssigneeMenuProps { + assignee?: Assignee + onAssigneeChange: (assigneeId: string | null) => void + availableAssignees: Assignee[] +} + +export function AssigneeMenu({ assignee, onAssigneeChange, availableAssignees }: AssigneeMenuProps) { + return ( + + +
+ {assignee ? ( + + + {assignee.name?.charAt(0).toUpperCase() || assignee.email?.charAt(0).toUpperCase() || '?'} + + + ) : ( +
+ +
+ )} +
+
+ + Assign to + {availableAssignees.map((user) => ( + onAssigneeChange(user.id)} + > + {user.name || user.email} + + ))} + + onAssigneeChange(null)}> + Unassign + + +
+ ) +} \ No newline at end of file diff --git a/client/components/ui/molecules/menus/LabelsMenu.tsx b/client/components/ui/molecules/menus/LabelsMenu.tsx new file mode 100644 index 0000000..2eaa16f --- /dev/null +++ b/client/components/ui/molecules/menus/LabelsMenu.tsx @@ -0,0 +1,64 @@ +"use client" + +import { Calendar, Bug, Star, Wrench, ArrowUp } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/atoms/dropdown-menu" + +interface Label { + id: string + type: string + name: string +} + +interface LabelsMenuProps { + labels: Label[] + onLabelsChange: (labels: Label[]) => void +} + +const getLabelIcon = (labelType: string) => { + switch (labelType) { + case 'bug': return + case 'feature': return + case 'fix': return + case 'enhancement': return + case 'due_date': return + default: return + } +} + +export function LabelsMenu({ labels, onLabelsChange }: LabelsMenuProps) { + return ( + + +
+ {labels.length > 0 ? ( + labels.slice(0, 3).map((label, index) => ( +
+ {getLabelIcon(label.type)} +
+ )) + ) : ( +
+ +
+ )} +
+
+ + Labels + Add Bug Label + Add Feature Label + Add Fix Label + Add Enhancement Label + + Manage Labels + +
+ ) +} \ No newline at end of file diff --git a/client/components/ui/molecules/menus/PriorityMenu.tsx b/client/components/ui/molecules/menus/PriorityMenu.tsx new file mode 100644 index 0000000..9063a17 --- /dev/null +++ b/client/components/ui/molecules/menus/PriorityMenu.tsx @@ -0,0 +1,69 @@ +"use client" + +import { Badge } from "@/components/ui/atoms/badge" +import { cn } from "@/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/atoms/dropdown-menu" + +interface PriorityMenuProps { + priority: number + onPriorityChange: (priority: number) => void +} + +const getPriorityLabel = (priority: number) => { + switch (priority) { + case 1: return 'P1' + case 2: return 'P2' + case 3: return 'P3' + case 4: return 'P4' + case 0: return 'P0' + default: return `P${priority}` + } +} + +const getPriorityColor = (priority: number) => { + switch (priority) { + case 1: return 'bg-destructive/10 text-destructive border-destructive/20' + case 2: return 'bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20' + case 3: return 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20' + case 4: return 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20' + default: return 'bg-muted text-muted-foreground border-border' + } +} + +export function PriorityMenu({ priority, onPriorityChange }: PriorityMenuProps) { + return ( + + + + {getPriorityLabel(priority)} + + + + Change Priority + onPriorityChange(1)}> + P1 - Urgent + + onPriorityChange(2)}> + P2 - High + + onPriorityChange(3)}> + P3 - Medium + + onPriorityChange(4)}> + P4 - Low + + onPriorityChange(0)}> + P0 - None + + + + ) +} \ No newline at end of file diff --git a/client/components/ui/molecules/menus/StateMenu.tsx b/client/components/ui/molecules/menus/StateMenu.tsx new file mode 100644 index 0000000..25ab0d2 --- /dev/null +++ b/client/components/ui/molecules/menus/StateMenu.tsx @@ -0,0 +1,71 @@ +"use client" + +import { Badge } from "@/components/ui/atoms/badge" +import { Circle, Loader2, Check, Archive, X } from "lucide-react" +import { cn } from "@/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/atoms/dropdown-menu" + +interface StateMenuProps { + state: string + onStateChange: (state: string) => void +} + +const getStateIcon = (state: string) => { + switch (state) { + case 'todo': return + case 'in_progress': return + case 'done': return + case 'backlog': return + case 'canceled': return + default: return + } +} + +const getStateColor = (state: string) => { + switch (state) { + case 'backlog': return 'bg-gray-200 text-gray-700 border-gray-300' + case 'todo': return 'bg-blue-100 text-blue-700 border-blue-200' + case 'in_progress': return 'bg-yellow-100 text-yellow-700 border-yellow-200' + case 'done': return 'bg-green-100 text-green-700 border-green-200' + case 'canceled': return 'bg-red-100 text-red-700 border-red-200' + default: return 'bg-muted text-muted-foreground border-border' + } +} + +export function StateMenu({ state, onStateChange }: StateMenuProps) { + return ( + + + + {getStateIcon(state)} + + + + Change State + onStateChange('todo')}> + Todo + + onStateChange('in_progress')}> + In Progress + + onStateChange('done')}> + Done + + onStateChange('backlog')}> + Backlog + + onStateChange('canceled')}> + Canceled + + + + ) +} \ No newline at end of file diff --git a/client/components/ui/organisms/data-table.tsx b/client/components/ui/organisms/data-table.tsx index 5c5f083..5f048ee 100644 --- a/client/components/ui/organisms/data-table.tsx +++ b/client/components/ui/organisms/data-table.tsx @@ -3,7 +3,6 @@ import * as React from "react" import { ColumnDef, - flexRender, getCoreRowModel, useReactTable, getSortedRowModel, @@ -11,22 +10,18 @@ import { getFilteredRowModel, ColumnFiltersState, getPaginationRowModel, - RowSelectionState, } from "@tanstack/react-table" import { Table, TableBody, TableCell, - TableHead, - TableHeader, TableRow, } from "@/components/ui/molecules/table" import { Button } from "@/components/ui/atoms/button" import { Input } from "@/components/ui/atoms/input" -import { Checkbox } from "@/components/ui/atoms/checkbox" -import { ArrowUpDown, ChevronDown, ChevronUp } from "lucide-react" import { cn } from "@/lib/utils" +import { IssueRow } from "@/components/features/issues/table/IssueRow" interface DataTableProps { columns: ColumnDef[] @@ -35,11 +30,20 @@ interface DataTableProps { searchPlaceholder?: string showSearch?: boolean showPagination?: boolean - showRowSelection?: boolean - onRowSelectionChange?: (selection: RowSelectionState) => void - selectedRows?: RowSelectionState className?: string emptyMessage?: string + showRowSelection?: boolean + groupBy?: string +} + +// State order for grouping +const stateOrder = ['todo', 'in_progress', 'backlog', 'done', 'canceled'] +const stateLabels = { + todo: 'To Do', + in_progress: 'In Progress', + backlog: 'Backlog', + done: 'Done', + canceled: 'Canceled' } export function DataTable({ @@ -49,15 +53,12 @@ export function DataTable({ searchPlaceholder = "Search...", showSearch = true, showPagination = true, - showRowSelection = false, - onRowSelectionChange, - selectedRows, className, - emptyMessage = "No data found" + emptyMessage = "No data found", + groupBy = "state" }: DataTableProps) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) - const [rowSelection, setRowSelection] = React.useState(selectedRows || {}) const table = useReactTable({ data, @@ -68,23 +69,26 @@ export function DataTable({ getPaginationRowModel: getPaginationRowModel(), onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, - onRowSelectionChange: (updater) => { - const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater - setRowSelection(newSelection) - onRowSelectionChange?.(newSelection) - }, state: { sorting, columnFilters, - rowSelection, }, }) - React.useEffect(() => { - if (selectedRows) { - setRowSelection(selectedRows) - } - }, [selectedRows]) + // Group data by state + const groupedData = React.useMemo(() => { + const groups: Record = {} + + table.getFilteredRowModel().rows.forEach((row) => { + const state = (row.original as Record)[groupBy] as string || 'unknown' + if (!groups[state]) { + groups[state] = [] + } + groups[state].push(row.original) + }) + + return groups + }, [table.getFilteredRowModel().rows, groupBy]) return (
@@ -104,78 +108,43 @@ export function DataTable({ {/* Table */}
- - {table.getHeaderGroups().map((headerGroup) => ( - - {showRowSelection && ( - - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> - - )} - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder ? null : ( -
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} - {{ - asc: , - desc: , - }[header.column.getIsSorted() as string] ?? ( - header.column.getCanSort() ? ( - - ) : null - )} + + {Object.keys(groupedData).length > 0 ? ( + stateOrder.map((state) => { + const groupIssues = groupedData[state] + if (!groupIssues || groupIssues.length === 0) return null + + return ( + + {/* Group Header */} + + +
+ + {stateLabels[state as keyof typeof stateLabels]} + + + {groupIssues.length} issue{groupIssues.length !== 1 ? 's' : ''} +
- )} - - ) - })} -
- ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {showRowSelection && ( - - row.toggleSelected(!!value)} - aria-label="Select row" - /> - - )} - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) + + + + {/* Group Issues */} + {groupIssues.map((issue) => { + const row = table.getRowModel().rows.find(r => r.original === issue) + if (!row) return null + + return ( + key={row.id} row={row} /> + ) + })} +
+ ) + }) ) : ( - - + + {emptyMessage} @@ -187,8 +156,7 @@ export function DataTable({ {showPagination && (
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. + {table.getFilteredRowModel().rows.length} row(s) total.
- - Assign to - {availableAssignees.map((user) => ( - onAssigneeChange(user.id)} - > - {user.name || user.email} - - ))} + + + {members.length > 0 ? ( + members.map((member) => ( + onAssigneeChange(member.userId)} + className="flex items-center gap-2 cursor-pointer" + > + + + {member.user.name?.charAt(0).toUpperCase() || member.user.email?.charAt(0).toUpperCase() || '?'} + + + {member.user.name || member.user.email} + {isAssigned(member.userId) && ( + + )} + + )) + ) : ( + + No members available + + )} + - onAssigneeChange(null)}> - Unassign + onAssigneeChange(null)} + className="flex items-center gap-2 cursor-pointer" + > +
+ +
+ Unassign + {!currentIssue?.assigneeId && !assignee && ( + + )}
diff --git a/client/components/ui/molecules/menus/PriorityMenu.tsx b/client/components/ui/molecules/menus/PriorityMenu.tsx index 9063a17..dfaa93c 100644 --- a/client/components/ui/molecules/menus/PriorityMenu.tsx +++ b/client/components/ui/molecules/menus/PriorityMenu.tsx @@ -1,18 +1,20 @@ "use client" import { Badge } from "@/components/ui/atoms/badge" -import { cn } from "@/lib/utils" +import { Check } from "lucide-react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, + DropdownMenuGroup, DropdownMenuTrigger, } from "@/components/ui/atoms/dropdown-menu" +import { useIssueStore } from "@/stores/issueStore" +import { cn } from "@/lib/utils" interface PriorityMenuProps { + issueId: string priority: number - onPriorityChange: (priority: number) => void } const getPriorityLabel = (priority: number) => { @@ -36,33 +38,59 @@ const getPriorityColor = (priority: number) => { } } -export function PriorityMenu({ priority, onPriorityChange }: PriorityMenuProps) { +const priorityOptions = [ + { value: 1, label: 'Urgent', color: 'bg-destructive/10 text-destructive border-destructive/20' }, + { value: 2, label: 'High', color: 'bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20' }, + { value: 3, label: 'Medium', color: 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20' }, + { value: 4, label: 'Low', color: 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20' }, + { value: 0, label: 'None', color: 'bg-muted text-muted-foreground border-border' }, +] + +export function PriorityMenu({ issueId, priority }: PriorityMenuProps) { + const { updateIssue, getIssueById } = useIssueStore() + + // Get the current issue to access priority + const currentIssue = getIssueById(issueId) + const currentPriority = currentIssue?.priority ?? priority + + const onPriorityChange = async (newPriority: number) => { + try { + await updateIssue(issueId, { priority: newPriority }) + } catch (error) { + console.error('Failed to change priority:', error) + } + } + return ( - {getPriorityLabel(priority)} + {getPriorityLabel(currentPriority)} - - Change Priority - onPriorityChange(1)}> - P1 - Urgent - - onPriorityChange(2)}> - P2 - High - - onPriorityChange(3)}> - P3 - Medium - - onPriorityChange(4)}> - P4 - Low - - onPriorityChange(0)}> - P0 - None - + + + {priorityOptions.map((option) => ( + onPriorityChange(option.value)} + className="flex items-center gap-2 cursor-pointer" + > + + {getPriorityLabel(option.value)} + + {option.label} + {currentPriority === option.value && ( + + )} + + ))} + ) diff --git a/client/components/ui/molecules/menus/StateMenu.tsx b/client/components/ui/molecules/menus/StateMenu.tsx index 25ab0d2..10dd063 100644 --- a/client/components/ui/molecules/menus/StateMenu.tsx +++ b/client/components/ui/molecules/menus/StateMenu.tsx @@ -1,70 +1,77 @@ "use client" -import { Badge } from "@/components/ui/atoms/badge" -import { Circle, Loader2, Check, Archive, X } from "lucide-react" -import { cn } from "@/lib/utils" +import { Clock, Check, Archive, X, List } from "lucide-react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, + DropdownMenuGroup, DropdownMenuTrigger, } from "@/components/ui/atoms/dropdown-menu" +import { useIssueStore } from "@/stores/issueStore" interface StateMenuProps { + issueId: string state: string - onStateChange: (state: string) => void } const getStateIcon = (state: string) => { switch (state) { - case 'todo': return - case 'in_progress': return - case 'done': return - case 'backlog': return - case 'canceled': return - default: return + case 'todo': return + case 'in_progress': return + case 'done': return + case 'backlog': return + case 'canceled': return + default: return } } -const getStateColor = (state: string) => { - switch (state) { - case 'backlog': return 'bg-gray-200 text-gray-700 border-gray-300' - case 'todo': return 'bg-blue-100 text-blue-700 border-blue-200' - case 'in_progress': return 'bg-yellow-100 text-yellow-700 border-yellow-200' - case 'done': return 'bg-green-100 text-green-700 border-green-200' - case 'canceled': return 'bg-red-100 text-red-700 border-red-200' - default: return 'bg-muted text-muted-foreground border-border' +const stateOptions = [ + { value: 'todo', label: 'Todo', icon: }, + { value: 'in_progress', label: 'In Progress', icon: }, + { value: 'done', label: 'Done', icon: }, + { value: 'backlog', label: 'Backlog', icon: }, + { value: 'canceled', label: 'Canceled', icon: }, +] + +export function StateMenu({ issueId, state }: StateMenuProps) { + const { updateIssue, getIssueById } = useIssueStore() + + // Get the current issue to access state + const currentIssue = getIssueById(issueId) + const currentState = currentIssue?.state ?? state + + const onStateChange = async (newState: string) => { + try { + await updateIssue(issueId, { state: newState }) + } catch (error) { + console.error('Failed to change state:', error) + } } -} -export function StateMenu({ state, onStateChange }: StateMenuProps) { return ( - - {getStateIcon(state)} - +
+ {getStateIcon(currentState)} +
- - Change State - onStateChange('todo')}> - Todo - - onStateChange('in_progress')}> - In Progress - - onStateChange('done')}> - Done - - onStateChange('backlog')}> - Backlog - - onStateChange('canceled')}> - Canceled - + + + {stateOptions.map((option) => ( + onStateChange(option.value)} + className="flex items-center gap-2 cursor-pointer" + > + {option.icon} + {option.label} + {currentState === option.value && ( + + )} + + ))} +
) diff --git a/client/stores/workspaceStore.ts b/client/stores/workspaceStore.ts index 7d7b0a5..4dc95ec 100644 --- a/client/stores/workspaceStore.ts +++ b/client/stores/workspaceStore.ts @@ -92,8 +92,20 @@ export const useWorkspaceStore = create((set, get) => ({ const currentWorkspace = get().currentWorkspace if (!currentWorkspace) return try { - const res = await api.get(`/api/workspaces/${currentWorkspace.id}/members`) - set({ members: res.data.members || [] }) + const res = await api.get(`/api/workspace/${currentWorkspace.id}/members`) + // Transform the backend response to match the expected TeamMember structure + const backendMembers = res.data.data?.members || [] + const transformedMembers = backendMembers.map((member: { id: string; user_id: string; name?: string; email: string }) => ({ + id: member.id, + userId: member.user_id, + role: 'member' as const, // Default role since backend doesn't provide it + user: { + id: member.user_id, + name: member.name, + email: member.email + } + })) + set({ members: transformedMembers }) } catch (error) { set({ members: [] }) console.error('Failed to fetch members:', error) From df72285bb56dd9cef3a133165fd674780a062c71 Mon Sep 17 00:00:00 2001 From: Shilendra Singh Date: Tue, 5 Aug 2025 19:37:05 +0530 Subject: [PATCH 4/6] refactor: update issue views and styling for improved user experience - Modified primary color variables in globals.css for better visual consistency. - Replaced header sections in TeamIssuesPage and MyIssuesPage with a new ViewHeader component, enhancing layout and functionality. - Cleaned up unused imports and commented-out code in issue-related components for improved readability. - Streamlined IssueBoardView and IssueListView by removing unnecessary filtering and pagination logic, focusing on core functionalities. --- .../[workspaceSlug]/[teamKey]/issues/page.tsx | 37 ++++----- .../[workspaceSlug]/myissues/page.tsx | 28 +++++-- client/app/globals.css | 6 +- .../features/issues/IssueBoardView.tsx | 52 +----------- .../features/issues/IssueListView.tsx | 79 +------------------ .../features/issues/IssuesTable.tsx | 4 +- client/components/ui/organisms/ViewHeader.tsx | 71 +++++++++++++++++ 7 files changed, 117 insertions(+), 160 deletions(-) create mode 100644 client/components/ui/organisms/ViewHeader.tsx diff --git a/client/app/(dashboard)/[workspaceSlug]/[teamKey]/issues/page.tsx b/client/app/(dashboard)/[workspaceSlug]/[teamKey]/issues/page.tsx index a4094af..c35f87e 100644 --- a/client/app/(dashboard)/[workspaceSlug]/[teamKey]/issues/page.tsx +++ b/client/app/(dashboard)/[workspaceSlug]/[teamKey]/issues/page.tsx @@ -6,10 +6,12 @@ import { useIssueStore } from '@/stores/issueStore' import { IssueListView } from '@/components/features/issues/IssueListView' import { IssueBoardView } from '@/components/features/issues/IssueBoardView' import { IssueDetails } from '@/components/features/issues/IssueDetails' -import { CreateIssueButton } from '@/components/features/issues/CreateIssueButton' -import { ViewToggle } from '@/components/features/issues/ViewToggle' +// import { CreateIssueButton } from '@/components/features/issues/CreateIssueButton' +// import { ViewToggle } from '@/components/features/issues/ViewToggle' import { LoadingSpinner } from '@/components/ui/atoms/loading-spinner' import { use } from 'react' +import { ViewHeader } from '@/components/ui/organisms/ViewHeader' +import { Users } from 'lucide-react' interface TeamIssuesPageProps { params: Promise<{ @@ -59,26 +61,17 @@ export default function TeamIssuesPage({ params }: TeamIssuesPageProps) { return (
{/* Header */} -
-
-
-

{currentTeam.name} Issues

-
- -
- - -
-
- + } + color={currentTeam.color} + showViewToggle={true} + view={view} + setView={setView} + showCreateIssue={showCreateIssue} + setShowCreateIssue={setShowCreateIssue} + workspaceId={currentWorkspace?.id} + /> {/* Main Content */}
{view === 'list' ? ( diff --git a/client/app/(dashboard)/[workspaceSlug]/myissues/page.tsx b/client/app/(dashboard)/[workspaceSlug]/myissues/page.tsx index 6755a7f..978d826 100644 --- a/client/app/(dashboard)/[workspaceSlug]/myissues/page.tsx +++ b/client/app/(dashboard)/[workspaceSlug]/myissues/page.tsx @@ -5,15 +5,17 @@ import { useWorkspaceStore } from '@/stores/workspaceStore' import { useIssueStore } from '@/stores/issueStore' import { IssueListView } from '@/components/features/issues/IssueListView' import { IssueBoardView } from '@/components/features/issues/IssueBoardView' -import { IssueDetails } from '@/components/features/issues/IssueDetails' -import { CreateIssueButton } from '@/components/features/issues/CreateIssueButton' -import { ViewToggle } from '@/components/features/issues/ViewToggle' +// import { IssueDetails } from '@/components/features/issues/IssueDetails' +// import { CreateIssueButton } from '@/components/features/issues/CreateIssueButton' +// import { ViewToggle } from '@/components/features/issues/ViewToggle' import { LoadingSpinner } from '@/components/ui/atoms/loading-spinner' +import { ViewHeader } from '@/components/ui/organisms/ViewHeader' +import { User } from 'lucide-react' export default function MyIssuesPage() { const currentWorkspace = useWorkspaceStore((state) => state.currentWorkspace) const isLoading = useWorkspaceStore((state) => state.isLoading) - const { issues, loadingStates, selectedIssueId, fetchIssuesByWorkspace, setSelectedIssue } = useIssueStore() + const { issues, loadingStates, fetchIssuesByWorkspace } = useIssueStore() // Local UI state for view and create modal const [view, setView] = useState<'list' | 'board'>('list') @@ -37,7 +39,17 @@ export default function MyIssuesPage() { return (
{/* Header */} -
+ } + showViewToggle={true} + view={view} + setView={setView} + showCreateIssue={showCreateIssue} + setShowCreateIssue={setShowCreateIssue} + workspaceId={currentWorkspace?.id} + /> + {/*

My Issues

@@ -51,7 +63,7 @@ export default function MyIssuesPage() { size="xs" />
-
+
*/} {/* Main Content */}
@@ -63,13 +75,13 @@ export default function MyIssuesPage() {
{/* Issue Details Sidebar */} - {selectedIssueId && ( + {/* {selectedIssueId && ( setSelectedIssue(null)} /> - )} + )} */}
) } \ No newline at end of file diff --git a/client/app/globals.css b/client/app/globals.css index 9dc66fd..02001a7 100644 --- a/client/app/globals.css +++ b/client/app/globals.css @@ -167,7 +167,7 @@ --card-foreground: oklch(0 0 0); --popover: oklch(0.9401 0 0); --popover-foreground: oklch(0 0 0); - --primary: oklch(0.6267 0.1272 297.2303); + --primary: oklch(0.1346 0 129.63); --primary-foreground: oklch(1.0000 0 0); --secondary: oklch(0.6508 0.0955 296.5799); --secondary-foreground: oklch(1.0000 0 0); @@ -223,8 +223,8 @@ --card-foreground: oklch(1.0000 0 0); --popover: oklch(0.2686 0.0218 265.7834); --popover-foreground: oklch(1.0000 0 0); - --primary: oklch(0.5771 0.1491 288.6467); - --primary-foreground: oklch(1.0000 0 0); + --primary: oklch(0.8721 0 208); + --primary-foreground: oklch(0 0 0); --secondary: oklch(0.4538 0.0277 258.3696); --secondary-foreground: oklch(1.0000 0 0); --muted: oklch(0.2598 0.0202 264.1313); diff --git a/client/components/features/issues/IssueBoardView.tsx b/client/components/features/issues/IssueBoardView.tsx index 841e4d5..2aa0abc 100644 --- a/client/components/features/issues/IssueBoardView.tsx +++ b/client/components/features/issues/IssueBoardView.tsx @@ -1,14 +1,11 @@ 'use client' -import React, { useMemo, useState } from 'react' +import React, { useMemo } from 'react' import { DragDropContext, DropResult } from '@hello-pangea/dnd' import { IssueColumn } from '@/components/features/issues/IssueColumn' -import { IssueFilterBar } from '@/components/features/issues/IssueFilterBar' import { useIssueStore } from '@/stores/issueStore' -import { useIssueFiltering } from '@/hooks/useIssueFiltering' import { toast } from 'react-hot-toast' import { Issue } from '@/types/issue' -import { useTeamStore } from '@/stores/teamStore' interface IssueBoardViewProps { issues: Issue[] @@ -45,33 +42,10 @@ const COLUMNS = [ export function IssueBoardView({ issues }: IssueBoardViewProps) { const { updateIssue } = useIssueStore() - const { teams: teamList } = useTeamStore() - const [teamFilter, setTeamFilter] = useState('All') - const [sortOptions] = useState([ - { label: 'None', value: 'None' }, - { label: 'Priority', value: 'Priority' }, - { label: 'Assignee', value: 'Assignee' }, - { label: 'Created At', value: 'Created At' } - ]) - // Use filtering hook - const { - stateFilter, - setStateFilter, - priorityFilter, - setPriorityFilter, - assigneeFilter, - setAssigneeFilter, - sortOption, - setSortOption, - filteredIssues, - clearAllFilters, - hasActiveFilters, - filterSummary - } = useIssueFiltering() // Group filtered issues by state (using canonical state values) const groupedIssues = useMemo(() => { - return filteredIssues.reduce((acc, issue) => { + return issues.reduce((acc, issue) => { const state = issue.state if (!acc[state]) { acc[state] = [] @@ -79,7 +53,7 @@ export function IssueBoardView({ issues }: IssueBoardViewProps) { acc[state].push(issue) return acc }, {} as Record) - }, [filteredIssues]) + }, [issues]) // Handle drag end const handleDragEnd = async (result: DropResult) => { @@ -108,26 +82,6 @@ export function IssueBoardView({ issues }: IssueBoardViewProps) { return (
- {/* Filter Bar */} - - {/* Issue Columns */}
diff --git a/client/components/features/issues/IssueListView.tsx b/client/components/features/issues/IssueListView.tsx index fa9cb95..8e63392 100644 --- a/client/components/features/issues/IssueListView.tsx +++ b/client/components/features/issues/IssueListView.tsx @@ -1,14 +1,9 @@ 'use client' -import React, { useState } from 'react' +import React from 'react' import { Issue } from '@/types/issue' import { IssueTable } from '@/components/features/issues/IssuesTable' import { IssuesTableSkeleton } from '@/components/features/issues/IssuesTableSkeleton' -import { IssueFilterBar } from '@/components/features/issues/IssueFilterBar' -import { Pagination } from '@/components/features/issues/Pagination' -import { useIssueFiltering } from '@/hooks/useIssueFiltering' -import { usePagination } from '@/hooks/usePagination' -import { useTeamStore } from '@/stores/teamStore' interface IssueListViewProps { issues: Issue[] @@ -16,87 +11,19 @@ interface IssueListViewProps { } export function IssueListView({ issues, isLoading = false }: IssueListViewProps) { - const [currentPage, setCurrentPage] = useState(1) - const issuesPerPage = 15 - const { teams } = useTeamStore() - const [teamFilter, setTeamFilter] = useState('All') - - // Use filtering hook - const { - stateFilter, - setStateFilter, - priorityFilter, - setPriorityFilter, - assigneeFilter, - setAssigneeFilter, - sortOption, - setSortOption, - filteredIssues, - clearAllFilters, - hasActiveFilters, - filterSummary - } = useIssueFiltering() - - // Use pagination hook - const { currentItems: currentIssues, totalPages } = usePagination( - filteredIssues, - issuesPerPage, - currentPage - ) - - const sortOptions = [ - { value: "None", label: "None" }, - { value: "Due Date (Asc)", label: "Due Date (Asc)" }, - { value: "Due Date (Desc)", label: "Due Date (Desc)" }, - { value: "Priority (High → Low)", label: "Priority (High → Low)" }, - { value: "Priority (Low → High)", label: "Priority (Low → High)" }, - { value: "Title (A → Z)", label: "Title (A → Z)" }, - { value: "Title (Z → A)", label: "Title (Z → A)" }, - { value: "Date Created (Newest)", label: "Date Created (Newest)" }, - { value: "Date Created (Oldest)", label: "Date Created (Oldest)" } - ] return (
- {/* Filter Bar */} - {/* Issue Table */}
{isLoading ? ( ) : ( - + )}
- - {/* Pagination */} - {totalPages > 1 && ( -
- -
- )} +
) } \ No newline at end of file diff --git a/client/components/features/issues/IssuesTable.tsx b/client/components/features/issues/IssuesTable.tsx index 9c17c7f..bdac8f2 100644 --- a/client/components/features/issues/IssuesTable.tsx +++ b/client/components/features/issues/IssuesTable.tsx @@ -22,10 +22,10 @@ const IssueTableComponent = ({ issues }: IssueTableProps) => { columns={issueColumns} data={issues} showSearch={true} - showPagination={true} + showPagination={false} emptyMessage="No issues found. Create a new issue." className="w-full" - /> + /> ) } diff --git a/client/components/ui/organisms/ViewHeader.tsx b/client/components/ui/organisms/ViewHeader.tsx new file mode 100644 index 0000000..8590b59 --- /dev/null +++ b/client/components/ui/organisms/ViewHeader.tsx @@ -0,0 +1,71 @@ +'use client' + +import React from 'react' +import { ViewToggle } from '@/components/features/issues/ViewToggle' +import { CreateIssueButton } from '@/components/features/issues/CreateIssueButton' +import { cn } from '@/lib/utils' + +interface ViewHeaderProps { + title: string + icon?: React.ReactNode + color?: string + showViewToggle?: boolean + view?: 'list' | 'board' + setView?: (view: 'list' | 'board') => void + showCreateIssue?: boolean + setShowCreateIssue?: (show: boolean) => void + workspaceId?: string + actions?: React.ReactNode + className?: string +} + +export function ViewHeader({ + title, + icon, + color, + showViewToggle = false, + view, + setView, + showCreateIssue = false, + setShowCreateIssue, + workspaceId, + actions, + className +}: ViewHeaderProps) { + return ( +
+
+ {icon && ( +
+ {icon} +
+ )} + {color && ( +
+ )} +

{title}

+
+ +
+ {actions} + {showViewToggle && view && setView && ( + + )} + {showCreateIssue && setShowCreateIssue && workspaceId && ( + + )} +
+
+ ) +} \ No newline at end of file From ee0086391f1f9462edb52ad9c7de6e2db4cba42f Mon Sep 17 00:00:00 2001 From: Shilendra Singh Date: Tue, 5 Aug 2025 19:42:24 +0530 Subject: [PATCH 5/6] chore: remove unused page.tsx file from workspace directory --- client/app/(dashboard)/[workspaceSlug]/[teamKey]/page.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 client/app/(dashboard)/[workspaceSlug]/[teamKey]/page.tsx diff --git a/client/app/(dashboard)/[workspaceSlug]/[teamKey]/page.tsx b/client/app/(dashboard)/[workspaceSlug]/[teamKey]/page.tsx deleted file mode 100644 index e69de29..0000000 From 706721c357e74184acabf93c9fd3f5831b98d610 Mon Sep 17 00:00:00 2001 From: Shilendra Singh Date: Tue, 5 Aug 2025 19:47:10 +0530 Subject: [PATCH 6/6] feat: add TeamsPage component for team management interface - Introduced a new TeamsPage component with a header and placeholder content for future team management features. - Updated KeyCell and LabelsCell components to remove unused props, simplifying their interfaces. --- .../[workspaceSlug]/teams/page.tsx | 19 +++++++++++++++++++ .../ui/atoms/rowElements/KeyCell.tsx | 2 +- .../ui/atoms/rowElements/LabelsCell.tsx | 2 +- .../ui/molecules/menus/LabelsMenu.tsx | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/client/app/(dashboard)/[workspaceSlug]/teams/page.tsx b/client/app/(dashboard)/[workspaceSlug]/teams/page.tsx index e69de29..704f7b5 100644 --- a/client/app/(dashboard)/[workspaceSlug]/teams/page.tsx +++ b/client/app/(dashboard)/[workspaceSlug]/teams/page.tsx @@ -0,0 +1,19 @@ +'use client' + +import React from 'react' + +export default function TeamsPage() { + return ( +
+ {/* Header */} +
+

Teams

+
+ + {/* Main Content */} +
+ Teams management coming soon. +
+
+ ) +} \ No newline at end of file diff --git a/client/components/ui/atoms/rowElements/KeyCell.tsx b/client/components/ui/atoms/rowElements/KeyCell.tsx index 4889168..19299c6 100644 --- a/client/components/ui/atoms/rowElements/KeyCell.tsx +++ b/client/components/ui/atoms/rowElements/KeyCell.tsx @@ -7,7 +7,7 @@ interface KeyCellProps { } export function KeyCell({ issue }: KeyCellProps) { - const key = issue.key || `ISS-${issue.id.slice(-2)}` + const key = `ISS-${issue.id.slice(-2)}` return ( diff --git a/client/components/ui/atoms/rowElements/LabelsCell.tsx b/client/components/ui/atoms/rowElements/LabelsCell.tsx index 2ba5b2a..e79560e 100644 --- a/client/components/ui/atoms/rowElements/LabelsCell.tsx +++ b/client/components/ui/atoms/rowElements/LabelsCell.tsx @@ -32,7 +32,7 @@ const getLabelIcon = (labelType: string) => { } } -export function LabelsCell({ labels, onLabelsChange }: LabelsCellProps) { +export function LabelsCell({ labels }: LabelsCellProps) { return ( diff --git a/client/components/ui/molecules/menus/LabelsMenu.tsx b/client/components/ui/molecules/menus/LabelsMenu.tsx index 2eaa16f..418a15d 100644 --- a/client/components/ui/molecules/menus/LabelsMenu.tsx +++ b/client/components/ui/molecules/menus/LabelsMenu.tsx @@ -32,7 +32,7 @@ const getLabelIcon = (labelType: string) => { } } -export function LabelsMenu({ labels, onLabelsChange }: LabelsMenuProps) { +export function LabelsMenu({ labels }: LabelsMenuProps) { return (