@object-ui/plugin-kanban 3.3.0 → 3.3.2
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/CHANGELOG.md +19 -0
- package/README.md +24 -0
- package/dist/{KanbanEnhanced-TdUe0kQH.js → KanbanEnhanced-Do9ZB1Mh.js} +35 -32
- package/dist/{KanbanImpl-BtlPa7GE.js → KanbanImpl-BdocXM5T.js} +1 -1
- package/dist/{chevron-down-B6UH8BbF.js → chevron-down-C0JUlGjk.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/index.umd.cjs +2 -2
- package/dist/{plus-BTqoaaEC.js → plus-CHsXVJSY.js} +1 -1
- package/package.json +34 -11
- package/.turbo/turbo-build.log +0 -32
- package/src/CardTemplates.tsx +0 -123
- package/src/InlineQuickAdd.tsx +0 -189
- package/src/KanbanEnhanced.tsx +0 -525
- package/src/KanbanImpl.tsx +0 -597
- package/src/ObjectKanban.EdgeCases.stories.tsx +0 -168
- package/src/ObjectKanban.msw.test.tsx +0 -95
- package/src/ObjectKanban.stories.tsx +0 -152
- package/src/ObjectKanban.tsx +0 -276
- package/src/__tests__/KanbanEnhanced.test.tsx +0 -260
- package/src/__tests__/KanbanGrouping.test.tsx +0 -164
- package/src/__tests__/KanbanSwimlanes.test.tsx +0 -194
- package/src/__tests__/ObjectKanbanTitle.test.tsx +0 -93
- package/src/__tests__/SwimlanePersistence.test.tsx +0 -159
- package/src/__tests__/accessibility.test.tsx +0 -296
- package/src/__tests__/dnd-undo-integration.test.tsx +0 -525
- package/src/__tests__/performance-benchmark.test.tsx +0 -306
- package/src/__tests__/phase13-features.test.tsx +0 -387
- package/src/__tests__/view-states.test.tsx +0 -403
- package/src/index.test.ts +0 -112
- package/src/index.tsx +0 -327
- package/src/registration.test.tsx +0 -26
- package/src/types.ts +0 -185
- package/src/useColumnWidths.ts +0 -125
- package/src/useCrossSwimlaneMove.ts +0 -116
- package/src/useQuickAddReorder.ts +0 -107
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -62
- package/vitest.config.ts +0 -12
- package/vitest.setup.ts +0 -1
package/src/CardTemplates.tsx
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
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 } from "@object-ui/components"
|
|
11
|
-
import { ChevronDown, FileText, Plus } from "lucide-react"
|
|
12
|
-
import type { CardTemplate } from "./types"
|
|
13
|
-
|
|
14
|
-
const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')
|
|
15
|
-
|
|
16
|
-
export interface CardTemplatesProps {
|
|
17
|
-
/** Available card templates */
|
|
18
|
-
templates: CardTemplate[]
|
|
19
|
-
/** Called when a template (or "Custom") is selected */
|
|
20
|
-
onSelect: (template: CardTemplate | null) => void
|
|
21
|
-
/** Column the template applies to */
|
|
22
|
-
columnId: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* CardTemplates renders a dropdown button that lets users pick a
|
|
27
|
-
* predefined card template or start with a blank ("Custom") card.
|
|
28
|
-
* The selected template pre-fills the quick-add form values.
|
|
29
|
-
*/
|
|
30
|
-
export const CardTemplates: React.FC<CardTemplatesProps> = ({
|
|
31
|
-
templates,
|
|
32
|
-
onSelect,
|
|
33
|
-
columnId,
|
|
34
|
-
}) => {
|
|
35
|
-
const [open, setOpen] = React.useState(false)
|
|
36
|
-
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
37
|
-
|
|
38
|
-
// Close dropdown on outside click
|
|
39
|
-
React.useEffect(() => {
|
|
40
|
-
if (!open) return
|
|
41
|
-
const handler = (e: MouseEvent) => {
|
|
42
|
-
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
43
|
-
setOpen(false)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
document.addEventListener("mousedown", handler)
|
|
47
|
-
return () => document.removeEventListener("mousedown", handler)
|
|
48
|
-
}, [open])
|
|
49
|
-
|
|
50
|
-
// Close on Escape
|
|
51
|
-
const handleKeyDown = React.useCallback(
|
|
52
|
-
(e: React.KeyboardEvent) => {
|
|
53
|
-
if (e.key === "Escape") {
|
|
54
|
-
setOpen(false)
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
[],
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div ref={containerRef} className="relative" onKeyDown={handleKeyDown}>
|
|
62
|
-
<Button
|
|
63
|
-
variant="ghost"
|
|
64
|
-
size="sm"
|
|
65
|
-
className="w-full mt-2 text-muted-foreground hover:text-foreground gap-1"
|
|
66
|
-
onClick={() => setOpen(prev => !prev)}
|
|
67
|
-
aria-haspopup="listbox"
|
|
68
|
-
aria-expanded={open}
|
|
69
|
-
aria-label={`Add card to ${columnId}`}
|
|
70
|
-
>
|
|
71
|
-
<Plus className="h-4 w-4" />
|
|
72
|
-
Add Card
|
|
73
|
-
<ChevronDown className={cn("h-3 w-3 transition-transform", open ? "rotate-180" : "")} />
|
|
74
|
-
</Button>
|
|
75
|
-
|
|
76
|
-
{open && (
|
|
77
|
-
<div
|
|
78
|
-
className="absolute z-50 mt-1 w-full rounded-md border border-border bg-popover p-1 shadow-md"
|
|
79
|
-
role="listbox"
|
|
80
|
-
aria-label="Card templates"
|
|
81
|
-
>
|
|
82
|
-
{/* Custom blank card option */}
|
|
83
|
-
<button
|
|
84
|
-
type="button"
|
|
85
|
-
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
86
|
-
role="option"
|
|
87
|
-
aria-selected={false}
|
|
88
|
-
onClick={() => {
|
|
89
|
-
onSelect(null)
|
|
90
|
-
setOpen(false)
|
|
91
|
-
}}
|
|
92
|
-
>
|
|
93
|
-
<Plus className="h-4 w-4 text-muted-foreground" />
|
|
94
|
-
<span className="font-mono text-xs">Custom</span>
|
|
95
|
-
</button>
|
|
96
|
-
|
|
97
|
-
{templates.length > 0 && (
|
|
98
|
-
<div className="my-1 h-px bg-border" role="separator" />
|
|
99
|
-
)}
|
|
100
|
-
|
|
101
|
-
{templates.map(template => (
|
|
102
|
-
<button
|
|
103
|
-
key={template.id}
|
|
104
|
-
type="button"
|
|
105
|
-
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
106
|
-
role="option"
|
|
107
|
-
aria-selected={false}
|
|
108
|
-
onClick={() => {
|
|
109
|
-
onSelect(template)
|
|
110
|
-
setOpen(false)
|
|
111
|
-
}}
|
|
112
|
-
>
|
|
113
|
-
<FileText className="h-4 w-4 text-muted-foreground" />
|
|
114
|
-
<span className="font-mono text-xs truncate">{template.name}</span>
|
|
115
|
-
</button>
|
|
116
|
-
))}
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
</div>
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export default CardTemplates
|
package/src/InlineQuickAdd.tsx
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
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
|