add delete and edit list
This commit is contained in:
parent
8a33e1c644
commit
78ce18f4d3
5 changed files with 276 additions and 7 deletions
81
frontend/src/components/kanban/DeleteListModal.tsx
Normal file
81
frontend/src/components/kanban/DeleteListModal.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { ModalContentProps } from '../../types';
|
||||
import Trash2Icon from '../icons/Trash2Icon';
|
||||
|
||||
interface DeleteListModalProps extends ModalContentProps {
|
||||
listName: string;
|
||||
onDelete: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function DeleteListModal({ onClose, onDelete, listName }: DeleteListModalProps) {
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await onDelete();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete list:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg shadow-xl w-full max-w-md border border-gray-700">
|
||||
<div className="flex justify-between items-center p-6 border-b border-gray-700">
|
||||
<h2 id="modal-title" className="text-xl font-bold text-white">
|
||||
Delete List
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-red-600/20 rounded-full flex items-center justify-center">
|
||||
<span className="w-6 h-6">
|
||||
<Trash2Icon />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Delete "{listName}"?
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
This will permanently delete the list and all cards in it. This action cannot be
|
||||
undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-6 py-3 text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="bg-red-600 hover:bg-red-700 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||
>
|
||||
Delete List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
108
frontend/src/components/kanban/EditListModal.tsx
Normal file
108
frontend/src/components/kanban/EditListModal.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { ModalContentProps } from '../../types';
|
||||
import { useToast } from '../../context/toasts/useToast';
|
||||
|
||||
const listSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, 'List name is required')
|
||||
.max(100, 'List name must be less than 100 characters'),
|
||||
});
|
||||
|
||||
type ListFormData = z.infer<typeof listSchema>;
|
||||
|
||||
interface EditListModalProps extends ModalContentProps {
|
||||
listName: string;
|
||||
onSave: (name: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function EditListModal({ onClose, onSave, listName }: EditListModalProps) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<ListFormData>({
|
||||
resolver: zodResolver(listSchema),
|
||||
defaultValues: {
|
||||
name: listName,
|
||||
},
|
||||
});
|
||||
|
||||
const { addNotification } = useToast();
|
||||
|
||||
const onSubmit = async (data: ListFormData) => {
|
||||
try {
|
||||
await onSave(data.name);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to update list';
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: errorMessage,
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg shadow-xl w-full max-w-md border border-gray-700">
|
||||
<div className="flex justify-between items-center p-6 border-b border-gray-700">
|
||||
<h2 id="modal-title" className="text-xl font-bold text-white">
|
||||
Edit List
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-6">
|
||||
<div className="mb-6">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
List Name <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
{...register('name')}
|
||||
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="e.g., To Do, In Progress, Done"
|
||||
autoFocus
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-sm text-red-400">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-6 py-3 text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,10 @@ import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
|||
import { ListWithCards, Card as CardType } from '../../types/kanban';
|
||||
import { KanbanCard } from './KanbanCard';
|
||||
import { CreateCardModal } from './CreateCardModal';
|
||||
import { EditListModal } from './EditListModal';
|
||||
import { DeleteListModal } from './DeleteListModal';
|
||||
import Edit2Icon from '../icons/Edit2Icon';
|
||||
import Trash2Icon from '../icons/Trash2Icon';
|
||||
import { useModal } from '../../context/modals/useModal';
|
||||
|
||||
interface KanbanColumnProps {
|
||||
|
|
@ -10,9 +14,18 @@ interface KanbanColumnProps {
|
|||
cards: CardType[];
|
||||
onOpenCardModal: (card: CardType) => void;
|
||||
onCardCreate: (data: { name: string; description?: string }) => Promise<void>;
|
||||
onListEdit?: (name: string) => Promise<void>;
|
||||
onListDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function KanbanColumn({ list, cards, onOpenCardModal, onCardCreate }: KanbanColumnProps) {
|
||||
export function KanbanColumn({
|
||||
list,
|
||||
cards,
|
||||
onOpenCardModal,
|
||||
onCardCreate,
|
||||
onListEdit,
|
||||
onListDelete,
|
||||
}: KanbanColumnProps) {
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: `LIST_${list.id}`,
|
||||
});
|
||||
|
|
@ -23,14 +36,66 @@ export function KanbanColumn({ list, cards, onOpenCardModal, onCardCreate }: Kan
|
|||
openModal((props) => <CreateCardModal {...props} onCreate={onCardCreate} />);
|
||||
};
|
||||
|
||||
const handleEditList = () => {
|
||||
if (!onListEdit) return;
|
||||
openModal((props) => (
|
||||
<EditListModal
|
||||
{...props}
|
||||
listName={list.name}
|
||||
onSave={async (name) => {
|
||||
await onListEdit(name);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const handleDeleteList = () => {
|
||||
if (!onListDelete) return;
|
||||
openModal((props) => (
|
||||
<DeleteListModal
|
||||
{...props}
|
||||
listName={list.name}
|
||||
onDelete={async () => {
|
||||
await onListDelete();
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-4 min-w-[300px] max-w-[300px] border border-gray-700 flex flex-col">
|
||||
<h2 className="text-white font-bold text-lg mb-4 flex items-center justify-between">
|
||||
{list.name}
|
||||
<span className="bg-gray-600 text-gray-300 text-xs px-2 py-1 rounded-full">
|
||||
{cards.length}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-white font-bold text-lg">{list.name}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{onListEdit && (
|
||||
<button
|
||||
onClick={handleEditList}
|
||||
className="text-gray-400 hover:text-blue-400 transition-colors"
|
||||
title="Edit list"
|
||||
>
|
||||
<span className="w-4 h-4">
|
||||
<Edit2Icon />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{onListDelete && (
|
||||
<button
|
||||
onClick={handleDeleteList}
|
||||
className="text-gray-400 hover:text-red-400 transition-colors"
|
||||
title="Delete list"
|
||||
>
|
||||
<span className="w-4 h-4">
|
||||
<Trash2Icon />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="bg-gray-600 text-gray-300 text-xs px-2 py-1 rounded-full inline-block">
|
||||
{cards.length} cards
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<SortableContext
|
||||
id={list.id.toString()}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export function useListMutations(boardId: number, onUpdate: () => void) {
|
|||
() => updateList(listId, { ...data, closed: false }),
|
||||
'Updating list...'
|
||||
);
|
||||
onUpdate(); // Refresh board data after update
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'List Updated',
|
||||
|
|
@ -64,6 +65,7 @@ export function useListMutations(boardId: number, onUpdate: () => void) {
|
|||
message: 'List deleted successfully.',
|
||||
duration: 3000,
|
||||
});
|
||||
onUpdate(); // Refresh board data after delete
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to delete list';
|
||||
addNotification({
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function BoardDetail() {
|
|||
const { id } = useParams<{ id: string }>();
|
||||
const { board, fetchBoard } = useBoard(parseInt(id || '0'));
|
||||
const { createCard, moveCard } = useCardMutations(parseInt(id || '0'), fetchBoard);
|
||||
const { createList } = useListMutations(parseInt(id || '0'), fetchBoard);
|
||||
const { createList, updateList, deleteList } = useListMutations(parseInt(id || '0'), fetchBoard);
|
||||
const { openModal } = useModal();
|
||||
|
||||
const [activeCard, setActiveCard] = useState<CardType | null>(null);
|
||||
|
|
@ -176,6 +176,17 @@ export function BoardDetail() {
|
|||
));
|
||||
};
|
||||
|
||||
const handleEditList = async (listId: number, name: string) => {
|
||||
const list = board?.lists.find((l) => l.id === listId);
|
||||
if (list) {
|
||||
await updateList(listId, { name, pos: list.pos });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteList = async (listId: number) => {
|
||||
await deleteList(listId);
|
||||
};
|
||||
|
||||
const handleAddCard =
|
||||
(listId: number) => async (data: { name: string; description?: string }) => {
|
||||
await createCard(listId, {
|
||||
|
|
@ -231,6 +242,8 @@ export function BoardDetail() {
|
|||
cards={list.cards}
|
||||
onOpenCardModal={handleOpenCardModal}
|
||||
onCardCreate={handleAddCard(list.id)}
|
||||
onListEdit={(name) => handleEditList(list.id, name)}
|
||||
onListDelete={() => handleDeleteList(list.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue