@moontra/moonui-pro 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +1475 -155
- package/package.json +1 -1
- package/src/components/index.ts +2 -1
- package/src/components/kanban/add-card-modal.tsx +502 -0
- package/src/components/kanban/card-detail-modal.tsx +769 -0
- package/src/components/kanban/index.ts +13 -0
- package/src/components/kanban/{index.tsx → kanban.tsx} +393 -133
- package/src/components/kanban/types.ts +111 -0
- package/src/hooks/use-toast.ts +15 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { Kanban } from './kanban'
|
|
2
|
+
export { CardDetailModal } from './card-detail-modal'
|
|
3
|
+
export { AddCardModal } from './add-card-modal'
|
|
4
|
+
export type {
|
|
5
|
+
KanbanCard,
|
|
6
|
+
KanbanColumn,
|
|
7
|
+
KanbanProps,
|
|
8
|
+
KanbanAssignee,
|
|
9
|
+
KanbanLabel,
|
|
10
|
+
KanbanFilter,
|
|
11
|
+
KanbanActivity,
|
|
12
|
+
KanbanChecklist
|
|
13
|
+
} from './types'
|
|
@@ -65,119 +65,22 @@ import {
|
|
|
65
65
|
} from 'lucide-react'
|
|
66
66
|
import { cn } from '../../lib/utils'
|
|
67
67
|
import { useSubscription } from '../../hooks/use-subscription'
|
|
68
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog'
|
|
69
|
+
import { ColorPicker } from '../ui/color-picker'
|
|
70
|
+
import { CardDetailModal } from './card-detail-modal'
|
|
71
|
+
import { AddCardModal } from './add-card-modal'
|
|
72
|
+
import type {
|
|
73
|
+
KanbanAssignee,
|
|
74
|
+
KanbanLabel,
|
|
75
|
+
KanbanChecklist,
|
|
76
|
+
KanbanActivity,
|
|
77
|
+
KanbanCard,
|
|
78
|
+
KanbanColumn,
|
|
79
|
+
KanbanFilter,
|
|
80
|
+
KanbanProps
|
|
81
|
+
} from './types'
|
|
82
|
+
import { useToast } from '../../hooks/use-toast'
|
|
68
83
|
|
|
69
|
-
// Enhanced types
|
|
70
|
-
interface KanbanAssignee {
|
|
71
|
-
id: string
|
|
72
|
-
name: string
|
|
73
|
-
avatar?: string
|
|
74
|
-
email?: string
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface KanbanLabel {
|
|
78
|
-
id: string
|
|
79
|
-
name: string
|
|
80
|
-
color: string
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface KanbanChecklist {
|
|
84
|
-
id: string
|
|
85
|
-
title: string
|
|
86
|
-
items: {
|
|
87
|
-
id: string
|
|
88
|
-
text: string
|
|
89
|
-
completed: boolean
|
|
90
|
-
}[]
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface KanbanActivity {
|
|
94
|
-
id: string
|
|
95
|
-
user: KanbanAssignee
|
|
96
|
-
action: string
|
|
97
|
-
timestamp: Date
|
|
98
|
-
details?: string
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
interface KanbanCard {
|
|
102
|
-
id: string
|
|
103
|
-
title: string
|
|
104
|
-
description?: string
|
|
105
|
-
coverImage?: string
|
|
106
|
-
assignees?: KanbanAssignee[]
|
|
107
|
-
dueDate?: Date
|
|
108
|
-
startDate?: Date
|
|
109
|
-
priority?: 'low' | 'medium' | 'high' | 'urgent'
|
|
110
|
-
tags?: string[]
|
|
111
|
-
labels?: KanbanLabel[]
|
|
112
|
-
attachments?: {
|
|
113
|
-
id: string
|
|
114
|
-
name: string
|
|
115
|
-
type: string
|
|
116
|
-
url: string
|
|
117
|
-
size: number
|
|
118
|
-
}[]
|
|
119
|
-
comments?: number
|
|
120
|
-
completed?: boolean
|
|
121
|
-
progress?: number
|
|
122
|
-
checklist?: KanbanChecklist
|
|
123
|
-
activities?: KanbanActivity[]
|
|
124
|
-
customFields?: Record<string, any>
|
|
125
|
-
position: number
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
interface KanbanColumn {
|
|
129
|
-
id: string
|
|
130
|
-
title: string
|
|
131
|
-
color?: string
|
|
132
|
-
cards: KanbanCard[]
|
|
133
|
-
limit?: number
|
|
134
|
-
collapsed?: boolean
|
|
135
|
-
locked?: boolean
|
|
136
|
-
template?: 'todo' | 'inProgress' | 'done' | 'custom'
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
interface KanbanFilter {
|
|
140
|
-
id: string
|
|
141
|
-
name: string
|
|
142
|
-
query: string
|
|
143
|
-
assignees?: string[]
|
|
144
|
-
labels?: string[]
|
|
145
|
-
priority?: string[]
|
|
146
|
-
tags?: string[]
|
|
147
|
-
dueDate?: {
|
|
148
|
-
from?: Date
|
|
149
|
-
to?: Date
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
interface KanbanProps {
|
|
154
|
-
columns: KanbanColumn[]
|
|
155
|
-
onCardMove?: (cardId: string, fromColumn: string, toColumn: string, newPosition: number) => void
|
|
156
|
-
onCardClick?: (card: KanbanCard) => void
|
|
157
|
-
onCardEdit?: (card: KanbanCard) => void
|
|
158
|
-
onCardDelete?: (card: KanbanCard) => void
|
|
159
|
-
onCardUpdate?: (card: KanbanCard) => void
|
|
160
|
-
onAddCard?: (columnId: string, card?: Partial<KanbanCard>) => void
|
|
161
|
-
onAddColumn?: (column?: Partial<KanbanColumn>) => void
|
|
162
|
-
onColumnUpdate?: (column: KanbanColumn) => void
|
|
163
|
-
onColumnDelete?: (columnId: string) => void
|
|
164
|
-
onBulkAction?: (action: string, cardIds: string[]) => void
|
|
165
|
-
onExport?: (format: 'json' | 'csv') => void
|
|
166
|
-
className?: string
|
|
167
|
-
showAddColumn?: boolean
|
|
168
|
-
showCardDetails?: boolean
|
|
169
|
-
showFilters?: boolean
|
|
170
|
-
showSearch?: boolean
|
|
171
|
-
enableKeyboardShortcuts?: boolean
|
|
172
|
-
cardTemplates?: Partial<KanbanCard>[]
|
|
173
|
-
columnTemplates?: Partial<KanbanColumn>[]
|
|
174
|
-
filters?: KanbanFilter[]
|
|
175
|
-
defaultFilter?: string
|
|
176
|
-
loading?: boolean
|
|
177
|
-
disabled?: boolean
|
|
178
|
-
labels?: KanbanLabel[]
|
|
179
|
-
users?: KanbanAssignee[]
|
|
180
|
-
}
|
|
181
84
|
|
|
182
85
|
// Constants
|
|
183
86
|
const PRIORITY_CONFIG = {
|
|
@@ -584,8 +487,21 @@ export function Kanban({
|
|
|
584
487
|
const [draggedOverColumn, setDraggedOverColumn] = useState<string | null>(null)
|
|
585
488
|
const [isCreatingColumn, setIsCreatingColumn] = useState(false)
|
|
586
489
|
const [newColumnTitle, setNewColumnTitle] = useState('')
|
|
490
|
+
|
|
491
|
+
// Modal states
|
|
492
|
+
const [selectedCard, setSelectedCard] = useState<KanbanCard | null>(null)
|
|
493
|
+
const [addCardColumnId, setAddCardColumnId] = useState<string | null>(null)
|
|
494
|
+
const [editingColumnId, setEditingColumnId] = useState<string | null>(null)
|
|
495
|
+
const [editingColumnTitle, setEditingColumnTitle] = useState('')
|
|
496
|
+
const [wipLimitModalOpen, setWipLimitModalOpen] = useState(false)
|
|
497
|
+
const [wipLimitColumnId, setWipLimitColumnId] = useState<string | null>(null)
|
|
498
|
+
const [wipLimit, setWipLimit] = useState<number | undefined>()
|
|
499
|
+
const [colorPickerOpen, setColorPickerOpen] = useState(false)
|
|
500
|
+
const [colorPickerColumnId, setColorPickerColumnId] = useState<string | null>(null)
|
|
501
|
+
const [selectedColor, setSelectedColor] = useState('#6B7280')
|
|
587
502
|
|
|
588
503
|
const { scrollRef, startAutoScroll, stopAutoScroll } = useAutoScroll()
|
|
504
|
+
const { toast } = useToast()
|
|
589
505
|
|
|
590
506
|
// Filter cards based on search and filters
|
|
591
507
|
const filteredColumns = useMemo(() => {
|
|
@@ -729,17 +645,221 @@ export function Kanban({
|
|
|
729
645
|
const handleColumnAction = (column: KanbanColumn, action: string) => {
|
|
730
646
|
switch (action) {
|
|
731
647
|
case 'rename':
|
|
732
|
-
|
|
648
|
+
setEditingColumnId(column.id)
|
|
649
|
+
setEditingColumnTitle(column.title)
|
|
733
650
|
break
|
|
734
651
|
case 'delete':
|
|
735
652
|
onColumnDelete?.(column.id)
|
|
653
|
+
toast({
|
|
654
|
+
title: "Column deleted",
|
|
655
|
+
description: `"${column.title}" has been deleted`
|
|
656
|
+
})
|
|
736
657
|
break
|
|
737
658
|
case 'collapse':
|
|
738
|
-
|
|
659
|
+
const updatedColumn = { ...column, collapsed: !column.collapsed }
|
|
660
|
+
onColumnUpdate?.(updatedColumn)
|
|
661
|
+
setColumns(columns.map(col => col.id === column.id ? updatedColumn : col))
|
|
739
662
|
break
|
|
740
663
|
case 'setLimit':
|
|
741
|
-
|
|
664
|
+
setWipLimitColumnId(column.id)
|
|
665
|
+
setWipLimit(column.limit)
|
|
666
|
+
setWipLimitModalOpen(true)
|
|
667
|
+
break
|
|
668
|
+
case 'changeColor':
|
|
669
|
+
setColorPickerColumnId(column.id)
|
|
670
|
+
setSelectedColor(column.color || '#6B7280')
|
|
671
|
+
setColorPickerOpen(true)
|
|
742
672
|
break
|
|
673
|
+
case 'sortByPriority':
|
|
674
|
+
const sortedCards = [...column.cards].sort((a, b) => {
|
|
675
|
+
const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 }
|
|
676
|
+
return (priorityOrder[b.priority || 'medium'] || 2) - (priorityOrder[a.priority || 'medium'] || 2)
|
|
677
|
+
})
|
|
678
|
+
const sortedColumn = { ...column, cards: sortedCards }
|
|
679
|
+
onColumnUpdate?.(sortedColumn)
|
|
680
|
+
setColumns(columns.map(col => col.id === column.id ? sortedColumn : col))
|
|
681
|
+
toast({
|
|
682
|
+
title: "Cards sorted",
|
|
683
|
+
description: "Cards sorted by priority"
|
|
684
|
+
})
|
|
685
|
+
break
|
|
686
|
+
case 'sortByDueDate':
|
|
687
|
+
const dateCards = [...column.cards].sort((a, b) => {
|
|
688
|
+
if (!a.dueDate && !b.dueDate) return 0
|
|
689
|
+
if (!a.dueDate) return 1
|
|
690
|
+
if (!b.dueDate) return -1
|
|
691
|
+
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
|
|
692
|
+
})
|
|
693
|
+
const dateColumn = { ...column, cards: dateCards }
|
|
694
|
+
onColumnUpdate?.(dateColumn)
|
|
695
|
+
setColumns(columns.map(col => col.id === column.id ? dateColumn : col))
|
|
696
|
+
toast({
|
|
697
|
+
title: "Cards sorted",
|
|
698
|
+
description: "Cards sorted by due date"
|
|
699
|
+
})
|
|
700
|
+
break
|
|
701
|
+
case 'sortAlphabetically':
|
|
702
|
+
const alphaCards = [...column.cards].sort((a, b) => a.title.localeCompare(b.title))
|
|
703
|
+
const alphaColumn = { ...column, cards: alphaCards }
|
|
704
|
+
onColumnUpdate?.(alphaColumn)
|
|
705
|
+
setColumns(columns.map(col => col.id === column.id ? alphaColumn : col))
|
|
706
|
+
toast({
|
|
707
|
+
title: "Cards sorted",
|
|
708
|
+
description: "Cards sorted alphabetically"
|
|
709
|
+
})
|
|
710
|
+
break
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Card handlers
|
|
715
|
+
const handleCardClick = (card: KanbanCard) => {
|
|
716
|
+
if (onCardClick) {
|
|
717
|
+
onCardClick(card)
|
|
718
|
+
} else {
|
|
719
|
+
setSelectedCard(card)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const handleCardUpdate = (updatedCard: KanbanCard) => {
|
|
724
|
+
onCardUpdate?.(updatedCard)
|
|
725
|
+
setColumns(columns.map(col => ({
|
|
726
|
+
...col,
|
|
727
|
+
cards: col.cards.map(card => card.id === updatedCard.id ? updatedCard : card)
|
|
728
|
+
})))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const handleAddCard = (columnId: string, newCard?: Partial<KanbanCard>) => {
|
|
732
|
+
if (onAddCard) {
|
|
733
|
+
onAddCard(columnId, newCard)
|
|
734
|
+
} else {
|
|
735
|
+
setAddCardColumnId(columnId)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const handleAddNewCard = (card: Partial<KanbanCard>) => {
|
|
740
|
+
if (!addCardColumnId) return
|
|
741
|
+
|
|
742
|
+
const newCard: KanbanCard = {
|
|
743
|
+
id: Date.now().toString(),
|
|
744
|
+
title: card.title || 'New Card',
|
|
745
|
+
position: Date.now(),
|
|
746
|
+
...card
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
setColumns(columns.map(col => {
|
|
750
|
+
if (col.id === addCardColumnId) {
|
|
751
|
+
return {
|
|
752
|
+
...col,
|
|
753
|
+
cards: [...col.cards, newCard]
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return col
|
|
757
|
+
}))
|
|
758
|
+
|
|
759
|
+
onAddCard?.(addCardColumnId, newCard)
|
|
760
|
+
toast({
|
|
761
|
+
title: "Card added",
|
|
762
|
+
description: `"${newCard.title}" has been added`
|
|
763
|
+
})
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Column updates
|
|
767
|
+
const handleColumnRename = (columnId: string) => {
|
|
768
|
+
const column = columns.find(col => col.id === columnId)
|
|
769
|
+
if (!column || !editingColumnTitle.trim()) return
|
|
770
|
+
|
|
771
|
+
const updatedColumn = { ...column, title: editingColumnTitle.trim() }
|
|
772
|
+
onColumnUpdate?.(updatedColumn)
|
|
773
|
+
setColumns(columns.map(col => col.id === columnId ? updatedColumn : col))
|
|
774
|
+
setEditingColumnId(null)
|
|
775
|
+
setEditingColumnTitle('')
|
|
776
|
+
|
|
777
|
+
toast({
|
|
778
|
+
title: "Column renamed",
|
|
779
|
+
description: `Column renamed to "${editingColumnTitle.trim()}"`
|
|
780
|
+
})
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const handleWipLimitUpdate = () => {
|
|
784
|
+
const column = columns.find(col => col.id === wipLimitColumnId)
|
|
785
|
+
if (!column) return
|
|
786
|
+
|
|
787
|
+
const updatedColumn = { ...column, limit: wipLimit }
|
|
788
|
+
onColumnUpdate?.(updatedColumn)
|
|
789
|
+
setColumns(columns.map(col => col.id === wipLimitColumnId ? updatedColumn : col))
|
|
790
|
+
setWipLimitModalOpen(false)
|
|
791
|
+
|
|
792
|
+
toast({
|
|
793
|
+
title: "WIP limit updated",
|
|
794
|
+
description: wipLimit ? `WIP limit set to ${wipLimit}` : "WIP limit removed"
|
|
795
|
+
})
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const handleColorUpdate = () => {
|
|
799
|
+
const column = columns.find(col => col.id === colorPickerColumnId)
|
|
800
|
+
if (!column) return
|
|
801
|
+
|
|
802
|
+
const updatedColumn = { ...column, color: selectedColor }
|
|
803
|
+
onColumnUpdate?.(updatedColumn)
|
|
804
|
+
setColumns(columns.map(col => col.id === colorPickerColumnId ? updatedColumn : col))
|
|
805
|
+
setColorPickerOpen(false)
|
|
806
|
+
|
|
807
|
+
toast({
|
|
808
|
+
title: "Column color updated",
|
|
809
|
+
description: "Column color has been changed"
|
|
810
|
+
})
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Export functionality
|
|
814
|
+
const handleExport = (format: 'json' | 'csv') => {
|
|
815
|
+
if (onExport) {
|
|
816
|
+
onExport(format)
|
|
817
|
+
} else {
|
|
818
|
+
if (format === 'json') {
|
|
819
|
+
const data = JSON.stringify(columns, null, 2)
|
|
820
|
+
const blob = new Blob([data], { type: 'application/json' })
|
|
821
|
+
const url = URL.createObjectURL(blob)
|
|
822
|
+
const a = document.createElement('a')
|
|
823
|
+
a.href = url
|
|
824
|
+
a.download = 'kanban-board.json'
|
|
825
|
+
document.body.appendChild(a)
|
|
826
|
+
a.click()
|
|
827
|
+
document.body.removeChild(a)
|
|
828
|
+
URL.revokeObjectURL(url)
|
|
829
|
+
|
|
830
|
+
toast({
|
|
831
|
+
title: "Board exported",
|
|
832
|
+
description: "Board exported as JSON file"
|
|
833
|
+
})
|
|
834
|
+
} else if (format === 'csv') {
|
|
835
|
+
let csv = 'Column,Card Title,Description,Priority,Assignees,Due Date,Tags\n'
|
|
836
|
+
columns.forEach(column => {
|
|
837
|
+
column.cards.forEach(card => {
|
|
838
|
+
csv += `"${column.title}",`
|
|
839
|
+
csv += `"${card.title}",`
|
|
840
|
+
csv += `"${card.description || ''}",`
|
|
841
|
+
csv += `"${card.priority || ''}",`
|
|
842
|
+
csv += `"${card.assignees?.map(a => a.name).join(', ') || ''}",`
|
|
843
|
+
csv += `"${card.dueDate ? new Date(card.dueDate).toLocaleDateString() : ''}",`
|
|
844
|
+
csv += `"${card.tags?.join(', ') || ''}"\n`
|
|
845
|
+
})
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
const blob = new Blob([csv], { type: 'text/csv' })
|
|
849
|
+
const url = URL.createObjectURL(blob)
|
|
850
|
+
const a = document.createElement('a')
|
|
851
|
+
a.href = url
|
|
852
|
+
a.download = 'kanban-board.csv'
|
|
853
|
+
document.body.appendChild(a)
|
|
854
|
+
a.click()
|
|
855
|
+
document.body.removeChild(a)
|
|
856
|
+
URL.revokeObjectURL(url)
|
|
857
|
+
|
|
858
|
+
toast({
|
|
859
|
+
title: "Board exported",
|
|
860
|
+
description: "Board exported as CSV file"
|
|
861
|
+
})
|
|
862
|
+
}
|
|
743
863
|
}
|
|
744
864
|
}
|
|
745
865
|
|
|
@@ -855,8 +975,7 @@ export function Kanban({
|
|
|
855
975
|
)}
|
|
856
976
|
|
|
857
977
|
{/* Export */}
|
|
858
|
-
|
|
859
|
-
<DropdownMenu>
|
|
978
|
+
<DropdownMenu>
|
|
860
979
|
<DropdownMenuTrigger asChild>
|
|
861
980
|
<Button variant="outline" size="sm">
|
|
862
981
|
<Download className="mr-2 h-4 w-4" />
|
|
@@ -864,15 +983,14 @@ export function Kanban({
|
|
|
864
983
|
</Button>
|
|
865
984
|
</DropdownMenuTrigger>
|
|
866
985
|
<DropdownMenuContent align="end">
|
|
867
|
-
<DropdownMenuItem onClick={() =>
|
|
986
|
+
<DropdownMenuItem onClick={() => handleExport('json')}>
|
|
868
987
|
Export as JSON
|
|
869
988
|
</DropdownMenuItem>
|
|
870
|
-
<DropdownMenuItem onClick={() =>
|
|
989
|
+
<DropdownMenuItem onClick={() => handleExport('csv')}>
|
|
871
990
|
Export as CSV
|
|
872
991
|
</DropdownMenuItem>
|
|
873
992
|
</DropdownMenuContent>
|
|
874
993
|
</DropdownMenu>
|
|
875
|
-
)}
|
|
876
994
|
</div>
|
|
877
995
|
</div>
|
|
878
996
|
|
|
@@ -940,8 +1058,30 @@ export function Kanban({
|
|
|
940
1058
|
|
|
941
1059
|
{/* Column title */}
|
|
942
1060
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
943
|
-
{column.
|
|
944
|
-
|
|
1061
|
+
{editingColumnId === column.id ? (
|
|
1062
|
+
<Input
|
|
1063
|
+
value={editingColumnTitle}
|
|
1064
|
+
onChange={(e) => setEditingColumnTitle(e.target.value)}
|
|
1065
|
+
onBlur={() => handleColumnRename(column.id)}
|
|
1066
|
+
onKeyDown={(e) => {
|
|
1067
|
+
if (e.key === 'Enter') {
|
|
1068
|
+
handleColumnRename(column.id)
|
|
1069
|
+
}
|
|
1070
|
+
if (e.key === 'Escape') {
|
|
1071
|
+
setEditingColumnId(null)
|
|
1072
|
+
setEditingColumnTitle('')
|
|
1073
|
+
}
|
|
1074
|
+
}}
|
|
1075
|
+
className="h-6 w-32 text-sm"
|
|
1076
|
+
autoFocus
|
|
1077
|
+
onClick={(e) => e.stopPropagation()}
|
|
1078
|
+
/>
|
|
1079
|
+
) : (
|
|
1080
|
+
<>
|
|
1081
|
+
{column.title}
|
|
1082
|
+
{column.locked && <Lock className="h-3 w-3" />}
|
|
1083
|
+
</>
|
|
1084
|
+
)}
|
|
945
1085
|
</CardTitle>
|
|
946
1086
|
|
|
947
1087
|
{/* Card count */}
|
|
@@ -986,14 +1126,27 @@ export function Kanban({
|
|
|
986
1126
|
<Timer className="mr-2 h-4 w-4" />
|
|
987
1127
|
Set WIP limit
|
|
988
1128
|
</DropdownMenuItem>
|
|
989
|
-
<DropdownMenuItem>
|
|
1129
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'changeColor')}>
|
|
990
1130
|
<Palette className="mr-2 h-4 w-4" />
|
|
991
1131
|
Change color
|
|
992
1132
|
</DropdownMenuItem>
|
|
993
|
-
<
|
|
994
|
-
<
|
|
995
|
-
|
|
996
|
-
|
|
1133
|
+
<DropdownMenuSub>
|
|
1134
|
+
<DropdownMenuSubTrigger>
|
|
1135
|
+
<ArrowUpDown className="mr-2 h-4 w-4" />
|
|
1136
|
+
Sort cards
|
|
1137
|
+
</DropdownMenuSubTrigger>
|
|
1138
|
+
<DropdownMenuSubContent>
|
|
1139
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByPriority')}>
|
|
1140
|
+
By Priority
|
|
1141
|
+
</DropdownMenuItem>
|
|
1142
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByDueDate')}>
|
|
1143
|
+
By Due Date
|
|
1144
|
+
</DropdownMenuItem>
|
|
1145
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'sortAlphabetically')}>
|
|
1146
|
+
Alphabetically
|
|
1147
|
+
</DropdownMenuItem>
|
|
1148
|
+
</DropdownMenuSubContent>
|
|
1149
|
+
</DropdownMenuSub>
|
|
997
1150
|
</DropdownMenuSubContent>
|
|
998
1151
|
</DropdownMenuSub>
|
|
999
1152
|
<DropdownMenuSeparator />
|
|
@@ -1056,7 +1209,7 @@ export function Kanban({
|
|
|
1056
1209
|
e.stopPropagation()
|
|
1057
1210
|
onCardDelete?.(card)
|
|
1058
1211
|
}}
|
|
1059
|
-
onClick={() =>
|
|
1212
|
+
onClick={() => handleCardClick(card)}
|
|
1060
1213
|
showDetails={showCardDetails}
|
|
1061
1214
|
disabled={disabled}
|
|
1062
1215
|
/>
|
|
@@ -1082,14 +1235,14 @@ export function Kanban({
|
|
|
1082
1235
|
<DropdownMenuContent align="start" className="w-48">
|
|
1083
1236
|
<DropdownMenuLabel>Card Templates</DropdownMenuLabel>
|
|
1084
1237
|
<DropdownMenuSeparator />
|
|
1085
|
-
<DropdownMenuItem onClick={() =>
|
|
1238
|
+
<DropdownMenuItem onClick={() => handleAddCard(column.id)}>
|
|
1086
1239
|
<FileText className="mr-2 h-4 w-4" />
|
|
1087
1240
|
Blank card
|
|
1088
1241
|
</DropdownMenuItem>
|
|
1089
1242
|
{cardTemplates.map((template, index) => (
|
|
1090
1243
|
<DropdownMenuItem
|
|
1091
1244
|
key={index}
|
|
1092
|
-
onClick={() =>
|
|
1245
|
+
onClick={() => handleAddCard(column.id, template)}
|
|
1093
1246
|
>
|
|
1094
1247
|
<Star className="mr-2 h-4 w-4" />
|
|
1095
1248
|
{template.title || `Template ${index + 1}`}
|
|
@@ -1207,8 +1360,115 @@ export function Kanban({
|
|
|
1207
1360
|
</motion.div>
|
|
1208
1361
|
)}
|
|
1209
1362
|
</div>
|
|
1363
|
+
|
|
1364
|
+
{/* Modals */}
|
|
1365
|
+
{/* Card Detail Modal */}
|
|
1366
|
+
{selectedCard && (
|
|
1367
|
+
<CardDetailModal
|
|
1368
|
+
card={selectedCard}
|
|
1369
|
+
isOpen={!!selectedCard}
|
|
1370
|
+
onClose={() => setSelectedCard(null)}
|
|
1371
|
+
onUpdate={handleCardUpdate}
|
|
1372
|
+
onDelete={(card) => {
|
|
1373
|
+
onCardDelete?.(card)
|
|
1374
|
+
toast({
|
|
1375
|
+
title: "Card deleted",
|
|
1376
|
+
description: `"${card.title}" has been deleted`
|
|
1377
|
+
})
|
|
1378
|
+
}}
|
|
1379
|
+
availableAssignees={users}
|
|
1380
|
+
availableLabels={labels}
|
|
1381
|
+
currentColumn={columns.find(col => col.cards.some(c => c.id === selectedCard.id))?.title}
|
|
1382
|
+
availableColumns={columns.map(col => ({ id: col.id, title: col.title }))}
|
|
1383
|
+
/>
|
|
1384
|
+
)}
|
|
1385
|
+
|
|
1386
|
+
{/* Add Card Modal */}
|
|
1387
|
+
{addCardColumnId && (
|
|
1388
|
+
<AddCardModal
|
|
1389
|
+
isOpen={!!addCardColumnId}
|
|
1390
|
+
onClose={() => setAddCardColumnId(null)}
|
|
1391
|
+
onAdd={handleAddNewCard}
|
|
1392
|
+
columnId={addCardColumnId}
|
|
1393
|
+
columnTitle={columns.find(col => col.id === addCardColumnId)?.title || ''}
|
|
1394
|
+
availableAssignees={users}
|
|
1395
|
+
availableLabels={labels}
|
|
1396
|
+
templates={cardTemplates}
|
|
1397
|
+
/>
|
|
1398
|
+
)}
|
|
1399
|
+
|
|
1400
|
+
{/* WIP Limit Modal */}
|
|
1401
|
+
<Dialog open={wipLimitModalOpen} onOpenChange={setWipLimitModalOpen}>
|
|
1402
|
+
<DialogContent className="sm:max-w-md">
|
|
1403
|
+
<DialogHeader>
|
|
1404
|
+
<DialogTitle>Set WIP Limit</DialogTitle>
|
|
1405
|
+
</DialogHeader>
|
|
1406
|
+
<div className="space-y-4 py-4">
|
|
1407
|
+
<div className="space-y-2">
|
|
1408
|
+
<Label htmlFor="wip-limit">Work In Progress Limit</Label>
|
|
1409
|
+
<Input
|
|
1410
|
+
id="wip-limit"
|
|
1411
|
+
type="number"
|
|
1412
|
+
min="0"
|
|
1413
|
+
value={wipLimit || ''}
|
|
1414
|
+
onChange={(e) => setWipLimit(e.target.value ? parseInt(e.target.value) : undefined)}
|
|
1415
|
+
placeholder="Enter a number (leave empty to remove limit)"
|
|
1416
|
+
/>
|
|
1417
|
+
<p className="text-sm text-muted-foreground">
|
|
1418
|
+
Set a maximum number of cards allowed in this column. Leave empty to remove the limit.
|
|
1419
|
+
</p>
|
|
1420
|
+
</div>
|
|
1421
|
+
</div>
|
|
1422
|
+
<DialogFooter>
|
|
1423
|
+
<Button variant="outline" onClick={() => setWipLimitModalOpen(false)}>
|
|
1424
|
+
Cancel
|
|
1425
|
+
</Button>
|
|
1426
|
+
<Button onClick={handleWipLimitUpdate}>
|
|
1427
|
+
Save
|
|
1428
|
+
</Button>
|
|
1429
|
+
</DialogFooter>
|
|
1430
|
+
</DialogContent>
|
|
1431
|
+
</Dialog>
|
|
1432
|
+
|
|
1433
|
+
{/* Color Picker Modal */}
|
|
1434
|
+
<Dialog open={colorPickerOpen} onOpenChange={setColorPickerOpen}>
|
|
1435
|
+
<DialogContent className="sm:max-w-md">
|
|
1436
|
+
<DialogHeader>
|
|
1437
|
+
<DialogTitle>Change Column Color</DialogTitle>
|
|
1438
|
+
</DialogHeader>
|
|
1439
|
+
<div className="space-y-4 py-4">
|
|
1440
|
+
<div className="space-y-2">
|
|
1441
|
+
<Label>Select a color</Label>
|
|
1442
|
+
<div className="grid grid-cols-8 gap-2">
|
|
1443
|
+
{[
|
|
1444
|
+
'#6B7280', '#EF4444', '#F59E0B', '#10B981',
|
|
1445
|
+
'#3B82F6', '#8B5CF6', '#EC4899', '#06B6D4',
|
|
1446
|
+
'#F43F5E', '#84CC16', '#14B8A6', '#6366F1',
|
|
1447
|
+
'#A855F7', '#F472B6', '#0EA5E9', '#22D3EE'
|
|
1448
|
+
].map(color => (
|
|
1449
|
+
<button
|
|
1450
|
+
key={color}
|
|
1451
|
+
className={cn(
|
|
1452
|
+
"w-10 h-10 rounded-md border-2 transition-all",
|
|
1453
|
+
selectedColor === color ? "border-primary scale-110" : "border-transparent"
|
|
1454
|
+
)}
|
|
1455
|
+
style={{ backgroundColor: color }}
|
|
1456
|
+
onClick={() => setSelectedColor(color)}
|
|
1457
|
+
/>
|
|
1458
|
+
))}
|
|
1459
|
+
</div>
|
|
1460
|
+
</div>
|
|
1461
|
+
</div>
|
|
1462
|
+
<DialogFooter>
|
|
1463
|
+
<Button variant="outline" onClick={() => setColorPickerOpen(false)}>
|
|
1464
|
+
Cancel
|
|
1465
|
+
</Button>
|
|
1466
|
+
<Button onClick={handleColorUpdate}>
|
|
1467
|
+
Save
|
|
1468
|
+
</Button>
|
|
1469
|
+
</DialogFooter>
|
|
1470
|
+
</DialogContent>
|
|
1471
|
+
</Dialog>
|
|
1210
1472
|
</div>
|
|
1211
1473
|
)
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
export default Kanban
|
|
1474
|
+
}
|