kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 213 lines 7.2 kB view raw
1import { CheckCircle2, Circle, GripVertical, Plus, Trash2 } from "lucide-react"; 2import { useState } from "react"; 3import { useTranslation } from "react-i18next"; 4import { Button } from "@/components/ui/button"; 5import { Input } from "@/components/ui/input"; 6import { Switch } from "@/components/ui/switch"; 7import { useCreateColumn } from "@/hooks/mutations/column/use-create-column"; 8import { useDeleteColumn } from "@/hooks/mutations/column/use-delete-column"; 9import { useReorderColumns } from "@/hooks/mutations/column/use-reorder-columns"; 10import { useUpdateColumn } from "@/hooks/mutations/column/use-update-column"; 11import { useGetColumns } from "@/hooks/queries/column/use-get-columns"; 12import { toast } from "@/lib/toast"; 13 14type ColumnEditorProps = { 15 projectId: string; 16}; 17 18export default function ColumnEditor({ projectId }: ColumnEditorProps) { 19 const { t } = useTranslation(); 20 const { data: columns, isLoading } = useGetColumns(projectId); 21 const { mutateAsync: createColumn } = useCreateColumn(); 22 const { mutateAsync: updateColumn } = useUpdateColumn(); 23 const { mutateAsync: deleteColumn } = useDeleteColumn(); 24 const { mutateAsync: reorderColumns } = useReorderColumns(); 25 const [newColumnName, setNewColumnName] = useState(""); 26 const [draggedIndex, setDraggedIndex] = useState<number | null>(null); 27 28 const handleCreate = async () => { 29 if (!newColumnName.trim()) return; 30 try { 31 await createColumn({ 32 projectId, 33 data: { name: newColumnName.trim() }, 34 }); 35 setNewColumnName(""); 36 toast.success(t("settings:columnEditor.toastCreated")); 37 } catch (error) { 38 toast.error( 39 error instanceof Error 40 ? error.message 41 : t("settings:columnEditor.toastCreateError"), 42 ); 43 } 44 }; 45 46 const handleRename = async (id: string, name: string) => { 47 try { 48 await updateColumn({ id, projectId, data: { name } }); 49 toast.success(t("settings:columnEditor.toastRenamed")); 50 } catch (error) { 51 toast.error( 52 error instanceof Error 53 ? error.message 54 : t("settings:columnEditor.toastRenameError"), 55 ); 56 } 57 }; 58 59 const handleToggleFinal = async (id: string, isFinal: boolean) => { 60 try { 61 await updateColumn({ id, projectId, data: { isFinal } }); 62 toast.success( 63 isFinal 64 ? t("settings:columnEditor.toastFinalOn") 65 : t("settings:columnEditor.toastFinalOff"), 66 ); 67 } catch (error) { 68 toast.error( 69 error instanceof Error 70 ? error.message 71 : t("settings:columnEditor.toastUpdateError"), 72 ); 73 } 74 }; 75 76 const handleDelete = async (id: string) => { 77 try { 78 await deleteColumn({ id, projectId }); 79 toast.success(t("settings:columnEditor.toastDeleted")); 80 } catch (error) { 81 toast.error( 82 error instanceof Error 83 ? error.message 84 : t("settings:columnEditor.toastDeleteError"), 85 ); 86 } 87 }; 88 89 const handleDragStart = (index: number) => { 90 setDraggedIndex(index); 91 }; 92 93 const handleDragOver = (e: React.DragEvent, index: number) => { 94 e.preventDefault(); 95 if (draggedIndex === null || draggedIndex === index || !columns) return; 96 97 const reordered = [...columns]; 98 const [removed] = reordered.splice(draggedIndex, 1); 99 reordered.splice(index, 0, removed); 100 101 const updates = reordered.map((col, i) => ({ id: col.id, position: i })); 102 reorderColumns({ projectId, columns: updates }); 103 setDraggedIndex(index); 104 }; 105 106 const handleDragEnd = () => { 107 setDraggedIndex(null); 108 }; 109 110 if (isLoading) { 111 return ( 112 <div className="text-sm text-muted-foreground"> 113 {t("settings:columnEditor.loading")} 114 </div> 115 ); 116 } 117 118 return ( 119 <div className="space-y-3"> 120 <div className="space-y-1"> 121 {columns?.map((col, index) => ( 122 // biome-ignore lint/a11y/useSemanticElements: false positive for role="listitem" 123 <div 124 key={col.id} 125 role="listitem" 126 draggable 127 onDragStart={() => handleDragStart(index)} 128 onDragOver={(e) => handleDragOver(e, index)} 129 onDragEnd={handleDragEnd} 130 className="flex items-center gap-2 p-2 border border-border rounded-md bg-sidebar hover:bg-sidebar-accent/50 transition-colors" 131 > 132 <GripVertical className="w-4 h-4 text-muted-foreground cursor-grab shrink-0" /> 133 <Input 134 defaultValue={col.name} 135 className="h-8 text-sm flex-1" 136 onBlur={(e) => { 137 if (e.target.value !== col.name) { 138 handleRename(col.id, e.target.value); 139 } 140 }} 141 onKeyDown={(e) => { 142 if (e.key === "Enter") { 143 e.preventDefault(); 144 e.currentTarget.blur(); 145 } 146 }} 147 /> 148 <div className="flex items-center gap-1.5 shrink-0"> 149 <div 150 className="flex items-center gap-2" 151 title={t("settings:columnEditor.doneColumnTooltip")} 152 > 153 {col.isFinal ? ( 154 <CheckCircle2 className="w-3.5 h-3.5 text-muted-foreground" /> 155 ) : ( 156 <Circle className="w-3.5 h-3.5 text-muted-foreground" /> 157 )} 158 <span className="text-xs text-muted-foreground whitespace-nowrap"> 159 {t("settings:columnEditor.doneColumn")} 160 </span> 161 <Switch 162 checked={col.isFinal} 163 onCheckedChange={(checked) => 164 handleToggleFinal(col.id, checked) 165 } 166 aria-label={t("settings:columnEditor.markDoneAria", { 167 name: col.name, 168 })} 169 className="scale-75" 170 /> 171 <span className="text-[11px] text-muted-foreground w-8"> 172 {col.isFinal 173 ? t("settings:columnEditor.on") 174 : t("settings:columnEditor.off")} 175 </span> 176 </div> 177 <Button 178 variant="ghost" 179 size="sm" 180 className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive" 181 onClick={() => handleDelete(col.id)} 182 > 183 <Trash2 className="w-3.5 h-3.5" /> 184 </Button> 185 </div> 186 </div> 187 ))} 188 </div> 189 190 <div className="flex items-center gap-2"> 191 <Input 192 placeholder={t("settings:columnEditor.newColumnPlaceholder")} 193 value={newColumnName} 194 onChange={(e) => setNewColumnName(e.target.value)} 195 className="h-8 text-sm flex-1" 196 onKeyDown={(e) => { 197 if (e.key === "Enter") handleCreate(); 198 }} 199 /> 200 <Button 201 variant="outline" 202 size="sm" 203 onClick={handleCreate} 204 disabled={!newColumnName.trim()} 205 className="h-8 gap-1" 206 > 207 <Plus className="w-3.5 h-3.5" /> 208 {t("settings:columnEditor.add")} 209 </Button> 210 </div> 211 </div> 212 ); 213}