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/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]/[teamKey]/page.tsx b/client/app/(dashboard)/[workspaceSlug]/[teamKey]/page.tsx
deleted file mode 100644
index e69de29..0000000
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/(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/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 034e9f5..bdac8f2 100644
--- a/client/components/features/issues/IssuesTable.tsx
+++ b/client/components/features/issues/IssuesTable.tsx
@@ -1,92 +1,33 @@
'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 (
-
-
-
-
- |
- 0}
- onCheckedChange={handleSelectAll}
- />
- |
- Title |
- State |
- Priority |
- Due Date |
- Assignee |
- Created |
- |
-
-
-
- {sortedIssues.map((issue) => (
-
- ))}
-
-
-
- {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/IssueRow.tsx b/client/components/features/issues/table/IssueRow.tsx
new file mode 100644
index 0000000..ef22eff
--- /dev/null
+++ b/client/components/features/issues/table/IssueRow.tsx
@@ -0,0 +1,52 @@
+"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
new file mode 100644
index 0000000..2e0e33d
--- /dev/null
+++ b/client/components/features/issues/table/issue-columns.tsx
@@ -0,0 +1,153 @@
+"use client"
+
+import { ColumnDef } from "@tanstack/react-table"
+import { Issue } from "@/types/issue"
+import {
+ PriorityCell,
+ KeyCell,
+ TitleCell,
+ AssigneeCell,
+ StateCell
+} from "@/components/ui/atoms/rowElements"
+import { Calendar, Bug, Star, Wrench, ArrowUp } from "lucide-react"
+
+// 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
+ }
+}
+
+// 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: "priority",
+ header: "Priority",
+ enableSorting: true,
+ cell: ({ row }) => {
+ const issue = row.original
+ const priority = row.getValue("priority") as number
+
+ return (
+
+ )
+ },
+ },
+ {
+ accessorKey: "key",
+ header: "Key",
+ enableSorting: true,
+ cell: ({ row }) => {
+ const issue = row.original
+ return
+ },
+ },
+ {
+ accessorKey: "title",
+ header: "Title",
+ enableSorting: true,
+ cell: ({ row }) => {
+ const issue = row.original
+ return
+ },
+ },
+ {
+ 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)}
+
+ ))
+ ) : (
+
+
+
+ )}
+
+ )
+ },
+ },
+ {
+ 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 issue = row.original
+
+ return (
+
+ )
+ },
+ },
+ {
+ accessorKey: "state",
+ header: "State",
+ enableSorting: true,
+ cell: ({ row }) => {
+ const issue = row.original
+ const state = row.getValue("state") as string
+
+ return (
+
+ )
+ },
+ },
+ {
+ accessorKey: "createdAt",
+ header: "Created",
+ enableSorting: true,
+ cell: ({ row }) => {
+ const createdAt = row.getValue("createdAt") as string
+ return (
+
+ {formatDate(createdAt)}
+
+ )
+ },
+ },
+]
\ 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..bedd806
--- /dev/null
+++ b/client/components/ui/atoms/rowElements/AssigneeCell.tsx
@@ -0,0 +1,23 @@
+"use client"
+
+import { AssigneeMenu } from "@/components/ui/molecules/menus/AssigneeMenu"
+
+interface Assignee {
+ id: string
+ name?: string
+ email?: string
+}
+
+interface AssigneeCellProps {
+ issueId: string
+ assignee?: Assignee
+}
+
+export function AssigneeCell({ issueId, assignee }: AssigneeCellProps) {
+ return (
+
+ )
+}
\ 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..19299c6
--- /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 = `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..e79560e
--- /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 }: 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..d61ce3c
--- /dev/null
+++ b/client/components/ui/atoms/rowElements/PriorityCell.tsx
@@ -0,0 +1,17 @@
+"use client"
+
+import { PriorityMenu } from "@/components/ui/molecules/menus/PriorityMenu"
+
+interface PriorityCellProps {
+ issueId: string
+ priority: number
+}
+
+export function PriorityCell({ issueId, priority }: PriorityCellProps) {
+ return (
+
+ )
+}
\ 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..cf891bc
--- /dev/null
+++ b/client/components/ui/atoms/rowElements/StateCell.tsx
@@ -0,0 +1,17 @@
+"use client"
+
+import { StateMenu } from "@/components/ui/molecules/menus/StateMenu"
+
+interface StateCellProps {
+ issueId: string
+ state: string
+}
+
+export function StateCell({ issueId, state }: StateCellProps) {
+ return (
+
+ )
+}
\ 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..3e91c57
--- /dev/null
+++ b/client/components/ui/molecules/menus/AssigneeMenu.tsx
@@ -0,0 +1,116 @@
+"use client"
+
+import { Avatar, AvatarFallback } from "@/components/ui/atoms/avatar"
+import { User, Check } from "lucide-react"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuGroup,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/atoms/dropdown-menu"
+import { useWorkspaceStore } from "@/stores/workspaceStore"
+import { useIssueStore } from "@/stores/issueStore"
+import { useEffect } from "react"
+
+interface Assignee {
+ id: string
+ name?: string
+ email?: string
+}
+
+interface AssigneeMenuProps {
+ issueId: string
+ assignee?: Assignee
+}
+
+export function AssigneeMenu({ issueId, assignee }: AssigneeMenuProps) {
+ const { members, fetchMembers, currentWorkspace } = useWorkspaceStore()
+ const { assignIssue, getIssueById } = useIssueStore()
+
+ // Get the current issue to access assigneeId
+ const currentIssue = getIssueById(issueId)
+
+ // Fetch members when component mounts or workspace changes
+ useEffect(() => {
+ if (currentWorkspace) {
+ console.log('Fetching members for workspace:', currentWorkspace.id)
+ fetchMembers()
+ }
+ }, [currentWorkspace, fetchMembers])
+
+ const onAssigneeChange = async (assigneeId: string | null) => {
+ try {
+ await assignIssue(issueId, assigneeId || '')
+ } catch (error) {
+ console.error('Failed to change assignee:', error)
+ }
+ }
+
+ // Check if a member is currently assigned
+ const isAssigned = (memberId: string) => {
+ // Check both assigneeId and assignee object
+ return currentIssue?.assigneeId === memberId || assignee?.id === memberId
+ }
+
+ return (
+
+
+
+ {assignee ? (
+
+
+ {assignee.name?.charAt(0).toUpperCase() || assignee.email?.charAt(0).toUpperCase() || '?'}
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {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)}
+ className="flex items-center gap-2 cursor-pointer"
+ >
+
+
+
+ Unassign
+ {!currentIssue?.assigneeId && !assignee && (
+
+ )}
+
+
+
+ )
+}
\ 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..418a15d
--- /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 }: 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..dfaa93c
--- /dev/null
+++ b/client/components/ui/molecules/menus/PriorityMenu.tsx
@@ -0,0 +1,97 @@
+"use client"
+
+import { Badge } from "@/components/ui/atoms/badge"
+import { Check } from "lucide-react"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuGroup,
+ DropdownMenuTrigger,
+} from "@/components/ui/atoms/dropdown-menu"
+import { useIssueStore } from "@/stores/issueStore"
+import { cn } from "@/lib/utils"
+
+interface PriorityMenuProps {
+ issueId: string
+ priority: number
+}
+
+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'
+ }
+}
+
+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(currentPriority)}
+
+
+
+
+ {priorityOptions.map((option) => (
+ onPriorityChange(option.value)}
+ className="flex items-center gap-2 cursor-pointer"
+ >
+
+ {getPriorityLabel(option.value)}
+
+ {option.label}
+ {currentPriority === option.value && (
+
+ )}
+
+ ))}
+
+
+
+ )
+}
\ 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..10dd063
--- /dev/null
+++ b/client/components/ui/molecules/menus/StateMenu.tsx
@@ -0,0 +1,78 @@
+"use client"
+
+import { Clock, Check, Archive, X, List } from "lucide-react"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuGroup,
+ DropdownMenuTrigger,
+} from "@/components/ui/atoms/dropdown-menu"
+import { useIssueStore } from "@/stores/issueStore"
+
+interface StateMenuProps {
+ issueId: string
+ state: string
+}
+
+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 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)
+ }
+ }
+
+ return (
+
+
+
+ {getStateIcon(currentState)}
+
+
+
+
+ {stateOptions.map((option) => (
+ onStateChange(option.value)}
+ className="flex items-center gap-2 cursor-pointer"
+ >
+ {option.icon}
+ {option.label}
+ {currentState === option.value && (
+
+ )}
+
+ ))}
+
+
+
+ )
+}
\ No newline at end of file
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
diff --git a/client/components/ui/organisms/data-table.tsx b/client/components/ui/organisms/data-table.tsx
new file mode 100644
index 0000000..5f048ee
--- /dev/null
+++ b/client/components/ui/organisms/data-table.tsx
@@ -0,0 +1,183 @@
+"use client"
+
+import * as React from "react"
+import {
+ ColumnDef,
+ getCoreRowModel,
+ useReactTable,
+ getSortedRowModel,
+ SortingState,
+ getFilteredRowModel,
+ ColumnFiltersState,
+ getPaginationRowModel,
+} from "@tanstack/react-table"
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableRow,
+} from "@/components/ui/molecules/table"
+import { Button } from "@/components/ui/atoms/button"
+import { Input } from "@/components/ui/atoms/input"
+import { cn } from "@/lib/utils"
+import { IssueRow } from "@/components/features/issues/table/IssueRow"
+
+interface DataTableProps {
+ columns: ColumnDef[]
+ data: TData[]
+ searchKey?: string
+ searchPlaceholder?: string
+ showSearch?: boolean
+ showPagination?: boolean
+ 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({
+ columns,
+ data,
+ searchKey,
+ searchPlaceholder = "Search...",
+ showSearch = true,
+ showPagination = true,
+ className,
+ emptyMessage = "No data found",
+ groupBy = "state"
+}: DataTableProps) {
+ const [sorting, setSorting] = React.useState([])
+ const [columnFilters, setColumnFilters] = React.useState([])
+
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ state: {
+ sorting,
+ columnFilters,
+ },
+ })
+
+ // 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 (
+
+ {showSearch && searchKey && (
+
+
+ table.getColumn(searchKey)?.setFilterValue(event.target.value)
+ }
+ className="max-w-sm"
+ />
+
+ )}
+
+ {/* Table */}
+
+
+
+ {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' : ''}
+
+
+
+
+
+ {/* 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}
+
+
+ )}
+
+
+
+
+ {showPagination && (
+
+
+ {table.getFilteredRowModel().rows.length} row(s) total.
+
+
+
+
+
+
+ )}
+
+ )
+}
\ 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",
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)