@object-ui/plugin-kanban 3.0.3 → 3.1.1
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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +10 -0
- package/dist/{KanbanEnhanced-BPIKjTDv.js → KanbanEnhanced-CXDSLlGR.js} +338 -324
- package/dist/KanbanImpl-4dgoNPtI.js +350 -0
- package/dist/index-CyNcIIS1.js +1077 -0
- package/dist/index.js +9 -4
- package/dist/index.umd.cjs +4 -4
- package/dist/src/CardTemplates.d.ts +25 -0
- package/dist/src/CardTemplates.d.ts.map +1 -0
- package/dist/src/InlineQuickAdd.d.ts +29 -0
- package/dist/src/InlineQuickAdd.d.ts.map +1 -0
- package/dist/src/KanbanEnhanced.d.ts +12 -1
- package/dist/src/KanbanEnhanced.d.ts.map +1 -1
- package/dist/src/KanbanImpl.d.ts +15 -1
- package/dist/src/KanbanImpl.d.ts.map +1 -1
- package/dist/src/ObjectKanban.d.ts.map +1 -1
- package/dist/src/index.d.ts +22 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/types.d.ts +97 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/useColumnWidths.d.ts +30 -0
- package/dist/src/useColumnWidths.d.ts.map +1 -0
- package/dist/src/useCrossSwimlaneMove.d.ts +46 -0
- package/dist/src/useCrossSwimlaneMove.d.ts.map +1 -0
- package/dist/src/useQuickAddReorder.d.ts +28 -0
- package/dist/src/useQuickAddReorder.d.ts.map +1 -0
- package/package.json +9 -9
- package/src/CardTemplates.tsx +123 -0
- package/src/InlineQuickAdd.tsx +189 -0
- package/src/KanbanEnhanced.tsx +140 -9
- package/src/KanbanImpl.tsx +266 -23
- package/src/ObjectKanban.tsx +39 -24
- package/src/__tests__/KanbanGrouping.test.tsx +164 -0
- package/src/__tests__/KanbanSwimlanes.test.tsx +194 -0
- package/src/__tests__/ObjectKanbanTitle.test.tsx +93 -0
- package/src/__tests__/SwimlanePersistence.test.tsx +159 -0
- package/src/__tests__/performance-benchmark.test.tsx +14 -14
- package/src/__tests__/phase13-features.test.tsx +387 -0
- package/src/index.tsx +49 -6
- package/src/types.ts +106 -1
- package/src/useColumnWidths.ts +125 -0
- package/src/useCrossSwimlaneMove.ts +116 -0
- package/src/useQuickAddReorder.ts +107 -0
- package/dist/KanbanImpl-BfOKAnJS.js +0 -194
- package/dist/index-CWGTi2xn.js +0 -600
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as React from "react"
|
|
10
|
+
import { Button, Input } from "@object-ui/components"
|
|
11
|
+
import { Check, X } from "lucide-react"
|
|
12
|
+
import type { InlineFieldDefinition } from "./types"
|
|
13
|
+
|
|
14
|
+
const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')
|
|
15
|
+
|
|
16
|
+
export interface InlineQuickAddProps {
|
|
17
|
+
/** Column where the card will be added */
|
|
18
|
+
columnId: string
|
|
19
|
+
/** Field definitions for inline editing */
|
|
20
|
+
fields: InlineFieldDefinition[]
|
|
21
|
+
/** Called with field values when the form is submitted */
|
|
22
|
+
onSubmit: (columnId: string, values: Record<string, any>) => void
|
|
23
|
+
/** Called when the form is cancelled */
|
|
24
|
+
onCancel: () => void
|
|
25
|
+
/** Pre-filled values (e.g. from a card template) */
|
|
26
|
+
defaultValues?: Record<string, any>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* InlineQuickAdd renders form fields directly inside the kanban column
|
|
31
|
+
* without opening a dialog or modal. Submit with Enter or the Save button;
|
|
32
|
+
* cancel with Escape or the Cancel button.
|
|
33
|
+
*/
|
|
34
|
+
export const InlineQuickAdd: React.FC<InlineQuickAddProps> = ({
|
|
35
|
+
columnId,
|
|
36
|
+
fields,
|
|
37
|
+
onSubmit,
|
|
38
|
+
onCancel,
|
|
39
|
+
defaultValues,
|
|
40
|
+
}) => {
|
|
41
|
+
const [values, setValues] = React.useState<Record<string, any>>(() => {
|
|
42
|
+
const initial: Record<string, any> = {}
|
|
43
|
+
for (const field of fields) {
|
|
44
|
+
initial[field.name] = defaultValues?.[field.name] ?? field.defaultValue ?? ""
|
|
45
|
+
}
|
|
46
|
+
return initial
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const firstInputRef = React.useRef<HTMLInputElement | HTMLSelectElement>(null)
|
|
50
|
+
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
// Auto-focus first field on mount
|
|
53
|
+
const timer = setTimeout(() => firstInputRef.current?.focus(), 0)
|
|
54
|
+
return () => clearTimeout(timer)
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
const handleChange = React.useCallback((name: string, value: any) => {
|
|
58
|
+
setValues(prev => ({ ...prev, [name]: value }))
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
const handleSubmit = React.useCallback(() => {
|
|
62
|
+
// Require at least one non-empty value
|
|
63
|
+
const hasValue = Object.values(values).some(v =>
|
|
64
|
+
typeof v === "string" ? v.trim().length > 0 : v != null
|
|
65
|
+
)
|
|
66
|
+
if (hasValue) {
|
|
67
|
+
onSubmit(columnId, values)
|
|
68
|
+
}
|
|
69
|
+
}, [columnId, values, onSubmit])
|
|
70
|
+
|
|
71
|
+
const handleKeyDown = React.useCallback(
|
|
72
|
+
(e: React.KeyboardEvent) => {
|
|
73
|
+
if (e.key === "Enter") {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
handleSubmit()
|
|
76
|
+
} else if (e.key === "Escape") {
|
|
77
|
+
e.preventDefault()
|
|
78
|
+
onCancel()
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[handleSubmit, onCancel],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className="mt-2 rounded-lg border border-primary/30 bg-card p-3 space-y-2 shadow-sm"
|
|
87
|
+
onKeyDown={handleKeyDown}
|
|
88
|
+
role="form"
|
|
89
|
+
aria-label="Quick add card"
|
|
90
|
+
>
|
|
91
|
+
{fields.map((field, idx) => (
|
|
92
|
+
<div key={field.name} className="space-y-1">
|
|
93
|
+
<label
|
|
94
|
+
htmlFor={`qa-${columnId}-${field.name}`}
|
|
95
|
+
className="text-xs font-mono text-muted-foreground"
|
|
96
|
+
>
|
|
97
|
+
{field.label ?? field.name}
|
|
98
|
+
</label>
|
|
99
|
+
{renderField(field, values[field.name], handleChange, idx === 0 ? firstInputRef : undefined, `qa-${columnId}-${field.name}`)}
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
<div className="flex items-center gap-2 pt-1">
|
|
103
|
+
<Button
|
|
104
|
+
type="button"
|
|
105
|
+
size="sm"
|
|
106
|
+
className="h-7 text-xs gap-1"
|
|
107
|
+
onClick={handleSubmit}
|
|
108
|
+
>
|
|
109
|
+
<Check className="h-3 w-3" />
|
|
110
|
+
Save
|
|
111
|
+
</Button>
|
|
112
|
+
<Button
|
|
113
|
+
type="button"
|
|
114
|
+
variant="ghost"
|
|
115
|
+
size="sm"
|
|
116
|
+
className="h-7 text-xs gap-1"
|
|
117
|
+
onClick={onCancel}
|
|
118
|
+
>
|
|
119
|
+
<X className="h-3 w-3" />
|
|
120
|
+
Cancel
|
|
121
|
+
</Button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Renders a single field based on its type. */
|
|
128
|
+
function renderField(
|
|
129
|
+
field: InlineFieldDefinition,
|
|
130
|
+
value: any,
|
|
131
|
+
onChange: (name: string, value: any) => void,
|
|
132
|
+
ref?: React.Ref<any>,
|
|
133
|
+
id?: string,
|
|
134
|
+
) {
|
|
135
|
+
const commonClasses = "text-sm h-8"
|
|
136
|
+
|
|
137
|
+
switch (field.type) {
|
|
138
|
+
case "select":
|
|
139
|
+
return (
|
|
140
|
+
<select
|
|
141
|
+
ref={ref as React.Ref<HTMLSelectElement>}
|
|
142
|
+
id={id}
|
|
143
|
+
value={value ?? ""}
|
|
144
|
+
onChange={e => onChange(field.name, e.target.value)}
|
|
145
|
+
className={cn(
|
|
146
|
+
commonClasses,
|
|
147
|
+
"w-full rounded-md border border-input bg-background px-2 py-1 text-sm ring-offset-background",
|
|
148
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
<option value="">{field.placeholder ?? `Select ${field.label ?? field.name}...`}</option>
|
|
152
|
+
{(field.options ?? []).map(opt => (
|
|
153
|
+
<option key={opt.value} value={opt.value}>
|
|
154
|
+
{opt.label}
|
|
155
|
+
</option>
|
|
156
|
+
))}
|
|
157
|
+
</select>
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
case "number":
|
|
161
|
+
return (
|
|
162
|
+
<Input
|
|
163
|
+
ref={ref as React.Ref<HTMLInputElement>}
|
|
164
|
+
id={id}
|
|
165
|
+
type="number"
|
|
166
|
+
value={value ?? ""}
|
|
167
|
+
onChange={e => onChange(field.name, e.target.value === "" ? "" : Number(e.target.value))}
|
|
168
|
+
placeholder={field.placeholder ?? `Enter ${field.label ?? field.name}...`}
|
|
169
|
+
className={commonClasses}
|
|
170
|
+
/>
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
case "text":
|
|
174
|
+
default:
|
|
175
|
+
return (
|
|
176
|
+
<Input
|
|
177
|
+
ref={ref as React.Ref<HTMLInputElement>}
|
|
178
|
+
id={id}
|
|
179
|
+
type="text"
|
|
180
|
+
value={value ?? ""}
|
|
181
|
+
onChange={e => onChange(field.name, e.target.value)}
|
|
182
|
+
placeholder={field.placeholder ?? `Enter ${field.label ?? field.name}...`}
|
|
183
|
+
className={commonClasses}
|
|
184
|
+
/>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default InlineQuickAdd
|
package/src/KanbanEnhanced.tsx
CHANGED
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
verticalListSortingStrategy,
|
|
26
26
|
} from "@dnd-kit/sortable"
|
|
27
27
|
import { CSS } from "@dnd-kit/utilities"
|
|
28
|
-
import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Button } from "@object-ui/components"
|
|
29
|
-
import { ChevronDown, ChevronRight, AlertTriangle } from "lucide-react"
|
|
28
|
+
import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Input } from "@object-ui/components"
|
|
29
|
+
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"
|
|
30
30
|
|
|
31
31
|
const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')
|
|
32
32
|
|
|
@@ -35,6 +35,7 @@ export interface KanbanCard {
|
|
|
35
35
|
title: string
|
|
36
36
|
description?: string
|
|
37
37
|
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline" }>
|
|
38
|
+
coverImage?: string
|
|
38
39
|
[key: string]: any
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -47,6 +48,14 @@ export interface KanbanColumn {
|
|
|
47
48
|
collapsed?: boolean
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export interface ConditionalFormattingRule {
|
|
52
|
+
field: string
|
|
53
|
+
operator: 'equals' | 'not_equals' | 'contains' | 'in'
|
|
54
|
+
value: string | string[]
|
|
55
|
+
backgroundColor?: string
|
|
56
|
+
borderColor?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
export interface KanbanEnhancedProps {
|
|
51
60
|
columns: KanbanColumn[]
|
|
52
61
|
onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void
|
|
@@ -54,9 +63,47 @@ export interface KanbanEnhancedProps {
|
|
|
54
63
|
enableVirtualScrolling?: boolean
|
|
55
64
|
virtualScrollThreshold?: number
|
|
56
65
|
className?: string
|
|
66
|
+
quickAdd?: boolean
|
|
67
|
+
onQuickAdd?: (columnId: string, title: string) => void
|
|
68
|
+
conditionalFormatting?: ConditionalFormattingRule[]
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
function
|
|
71
|
+
function getCardStyles(card: KanbanCard, rules?: ConditionalFormattingRule[]): React.CSSProperties {
|
|
72
|
+
if (!rules || rules.length === 0) return {}
|
|
73
|
+
|
|
74
|
+
for (const rule of rules) {
|
|
75
|
+
const fieldValue = card[rule.field]
|
|
76
|
+
if (fieldValue === undefined || fieldValue === null) continue
|
|
77
|
+
|
|
78
|
+
let matches = false
|
|
79
|
+
const strValue = String(fieldValue)
|
|
80
|
+
|
|
81
|
+
switch (rule.operator) {
|
|
82
|
+
case 'equals':
|
|
83
|
+
matches = strValue === String(rule.value)
|
|
84
|
+
break
|
|
85
|
+
case 'not_equals':
|
|
86
|
+
matches = strValue !== String(rule.value)
|
|
87
|
+
break
|
|
88
|
+
case 'contains':
|
|
89
|
+
matches = strValue.toLowerCase().includes(String(rule.value).toLowerCase())
|
|
90
|
+
break
|
|
91
|
+
case 'in':
|
|
92
|
+
matches = Array.isArray(rule.value) && rule.value.includes(strValue)
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (matches) {
|
|
97
|
+
return {
|
|
98
|
+
...(rule.backgroundColor ? { backgroundColor: rule.backgroundColor } : {}),
|
|
99
|
+
...(rule.borderColor ? { borderColor: rule.borderColor } : {}),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function SortableCard({ card, conditionalFormatting }: { card: KanbanCard; conditionalFormatting?: ConditionalFormattingRule[] }) {
|
|
60
107
|
const {
|
|
61
108
|
attributes,
|
|
62
109
|
listeners,
|
|
@@ -72,9 +119,21 @@ function SortableCard({ card }: { card: KanbanCard }) {
|
|
|
72
119
|
opacity: isDragging ? 0.5 : undefined,
|
|
73
120
|
}
|
|
74
121
|
|
|
122
|
+
const cardStyles = getCardStyles(card, conditionalFormatting)
|
|
123
|
+
|
|
75
124
|
return (
|
|
76
125
|
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
|
77
|
-
<Card className="mb-2 cursor-grab active:cursor-grabbing border-border bg-card/60 hover:border-primary/40 hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 group">
|
|
126
|
+
<Card className="mb-2 cursor-grab active:cursor-grabbing border-border bg-card/60 hover:border-primary/40 hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 group" style={cardStyles}>
|
|
127
|
+
{card.coverImage && (
|
|
128
|
+
<div className="w-full h-32 overflow-hidden rounded-t-lg">
|
|
129
|
+
<img
|
|
130
|
+
src={card.coverImage}
|
|
131
|
+
alt=""
|
|
132
|
+
className="w-full h-full object-cover"
|
|
133
|
+
loading="lazy"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
78
137
|
<CardHeader className="p-4">
|
|
79
138
|
<CardTitle className="text-sm font-medium font-mono tracking-tight text-foreground group-hover:text-primary transition-colors">{card.title}</CardTitle>
|
|
80
139
|
{card.description && (
|
|
@@ -99,7 +158,7 @@ function SortableCard({ card }: { card: KanbanCard }) {
|
|
|
99
158
|
)
|
|
100
159
|
}
|
|
101
160
|
|
|
102
|
-
function VirtualizedCardList({ cards, parentRef }: { cards: KanbanCard[]; parentRef: React.RefObject<HTMLDivElement | null
|
|
161
|
+
function VirtualizedCardList({ cards, parentRef, conditionalFormatting }: { cards: KanbanCard[]; parentRef: React.RefObject<HTMLDivElement | null>; conditionalFormatting?: ConditionalFormattingRule[] }) {
|
|
103
162
|
const rowVirtualizer = useVirtualizer({
|
|
104
163
|
count: cards.length,
|
|
105
164
|
getScrollElement: () => parentRef.current,
|
|
@@ -128,7 +187,7 @@ function VirtualizedCardList({ cards, parentRef }: { cards: KanbanCard[]; parent
|
|
|
128
187
|
transform: `translateY(${virtualItem.start}px)`,
|
|
129
188
|
}}
|
|
130
189
|
>
|
|
131
|
-
<SortableCard card={card} />
|
|
190
|
+
<SortableCard card={card} conditionalFormatting={conditionalFormatting} />
|
|
132
191
|
</div>
|
|
133
192
|
)
|
|
134
193
|
})}
|
|
@@ -136,16 +195,79 @@ function VirtualizedCardList({ cards, parentRef }: { cards: KanbanCard[]; parent
|
|
|
136
195
|
)
|
|
137
196
|
}
|
|
138
197
|
|
|
198
|
+
function QuickAddForm({ columnId, onAdd }: { columnId: string; onAdd: (columnId: string, title: string) => void }) {
|
|
199
|
+
const [isAdding, setIsAdding] = React.useState(false)
|
|
200
|
+
const [title, setTitle] = React.useState('')
|
|
201
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
202
|
+
|
|
203
|
+
const handleSubmit = () => {
|
|
204
|
+
const trimmed = title.trim()
|
|
205
|
+
if (trimmed) {
|
|
206
|
+
onAdd(columnId, trimmed)
|
|
207
|
+
setTitle('')
|
|
208
|
+
}
|
|
209
|
+
setIsAdding(false)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
213
|
+
if (e.key === 'Enter') {
|
|
214
|
+
e.preventDefault()
|
|
215
|
+
handleSubmit()
|
|
216
|
+
} else if (e.key === 'Escape') {
|
|
217
|
+
setTitle('')
|
|
218
|
+
setIsAdding(false)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!isAdding) {
|
|
223
|
+
return (
|
|
224
|
+
<Button
|
|
225
|
+
variant="ghost"
|
|
226
|
+
size="sm"
|
|
227
|
+
className="w-full mt-2 text-muted-foreground hover:text-foreground"
|
|
228
|
+
onClick={() => {
|
|
229
|
+
setIsAdding(true)
|
|
230
|
+
setTimeout(() => inputRef.current?.focus(), 0)
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
234
|
+
Add Card
|
|
235
|
+
</Button>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="mt-2 space-y-2">
|
|
241
|
+
<Input
|
|
242
|
+
ref={inputRef}
|
|
243
|
+
value={title}
|
|
244
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
245
|
+
onKeyDown={handleKeyDown}
|
|
246
|
+
onBlur={handleSubmit}
|
|
247
|
+
placeholder="Enter card title..."
|
|
248
|
+
className="text-sm"
|
|
249
|
+
autoFocus
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
139
255
|
function KanbanColumnEnhanced({
|
|
140
256
|
column,
|
|
141
257
|
cards,
|
|
142
258
|
onToggle,
|
|
143
259
|
enableVirtual,
|
|
260
|
+
quickAdd,
|
|
261
|
+
onQuickAdd,
|
|
262
|
+
conditionalFormatting,
|
|
144
263
|
}: {
|
|
145
264
|
column: KanbanColumn
|
|
146
265
|
cards: KanbanCard[]
|
|
147
266
|
onToggle: (collapsed: boolean) => void
|
|
148
267
|
enableVirtual: boolean
|
|
268
|
+
quickAdd?: boolean
|
|
269
|
+
onQuickAdd?: (columnId: string, title: string) => void
|
|
270
|
+
conditionalFormatting?: ConditionalFormattingRule[]
|
|
149
271
|
}) {
|
|
150
272
|
const safeCards = cards || []
|
|
151
273
|
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
@@ -221,15 +343,18 @@ function KanbanColumnEnhanced({
|
|
|
221
343
|
strategy={verticalListSortingStrategy}
|
|
222
344
|
>
|
|
223
345
|
{enableVirtual ? (
|
|
224
|
-
<VirtualizedCardList cards={safeCards} parentRef={scrollRef} />
|
|
346
|
+
<VirtualizedCardList cards={safeCards} parentRef={scrollRef} conditionalFormatting={conditionalFormatting} />
|
|
225
347
|
) : (
|
|
226
348
|
<div className="space-y-2">
|
|
227
349
|
{safeCards.map((card) => (
|
|
228
|
-
<SortableCard key={card.id} card={card} />
|
|
350
|
+
<SortableCard key={card.id} card={card} conditionalFormatting={conditionalFormatting} />
|
|
229
351
|
))}
|
|
230
352
|
</div>
|
|
231
353
|
)}
|
|
232
354
|
</SortableContext>
|
|
355
|
+
{quickAdd && onQuickAdd && (
|
|
356
|
+
<QuickAddForm columnId={column.id} onAdd={onQuickAdd} />
|
|
357
|
+
)}
|
|
233
358
|
</div>
|
|
234
359
|
)}
|
|
235
360
|
</div>
|
|
@@ -243,6 +368,9 @@ export function KanbanEnhanced({
|
|
|
243
368
|
enableVirtualScrolling = false,
|
|
244
369
|
virtualScrollThreshold = 50,
|
|
245
370
|
className,
|
|
371
|
+
quickAdd,
|
|
372
|
+
onQuickAdd,
|
|
373
|
+
conditionalFormatting,
|
|
246
374
|
}: KanbanEnhancedProps) {
|
|
247
375
|
const [activeCard, setActiveCard] = React.useState<KanbanCard | null>(null)
|
|
248
376
|
|
|
@@ -380,12 +508,15 @@ export function KanbanEnhanced({
|
|
|
380
508
|
cards={column.cards}
|
|
381
509
|
onToggle={(collapsed) => handleColumnToggle(column.id, collapsed)}
|
|
382
510
|
enableVirtual={shouldUseVirtual}
|
|
511
|
+
quickAdd={quickAdd}
|
|
512
|
+
onQuickAdd={onQuickAdd}
|
|
513
|
+
conditionalFormatting={conditionalFormatting}
|
|
383
514
|
/>
|
|
384
515
|
)
|
|
385
516
|
})}
|
|
386
517
|
</div>
|
|
387
518
|
<DragOverlay>
|
|
388
|
-
{activeCard ? <SortableCard card={activeCard} /> : null}
|
|
519
|
+
{activeCard ? <SortableCard card={activeCard} conditionalFormatting={conditionalFormatting} /> : null}
|
|
389
520
|
</DragOverlay>
|
|
390
521
|
</DndContext>
|
|
391
522
|
)
|