feat(frontend): Implement Issue management and Kanban board
Add comprehensive Issue management functionality with drag-and-drop Kanban board. Changes: - Created Issue API client (issues.ts) with CRUD operations - Implemented React Query hooks for Issue data management - Added IssueCard component with drag-and-drop support using @dnd-kit - Created KanbanColumn component with droppable zones - Built CreateIssueDialog with form validation using zod - Implemented Kanban page at /projects/[id]/kanban with DnD status changes - Added missing UI components (textarea, select, skeleton) - Enhanced API client with helper methods (get, post, put, patch, delete) - Installed dependencies: @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, @radix-ui/react-select, sonner - Fixed SignalR ConnectionManager TypeScript error - Preserved legacy KanbanBoard component for backward compatibility Features: - Drag and drop issues between Backlog, Todo, InProgress, and Done columns - Real-time status updates via API - Issue creation with type (Story, Task, Bug, Epic) and priority - Visual feedback with priority colors and type icons - Toast notifications for user actions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -133,3 +133,31 @@ apiClient.interceptors.response.use(
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// API helper functions
|
||||
export const api = {
|
||||
get: async <T>(url: string, config?: any): Promise<T> => {
|
||||
const response = await apiClient.get(url, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
post: async <T>(url: string, data?: any, config?: any): Promise<T> => {
|
||||
const response = await apiClient.post(url, data, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
put: async <T>(url: string, data?: any, config?: any): Promise<T> => {
|
||||
const response = await apiClient.put(url, data, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
patch: async <T>(url: string, data?: any, config?: any): Promise<T> => {
|
||||
const response = await apiClient.patch(url, data, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async <T>(url: string, config?: any): Promise<T> => {
|
||||
const response = await apiClient.delete(url, config);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
84
lib/api/issues.ts
Normal file
84
lib/api/issues.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { api } from './client';
|
||||
|
||||
export interface Issue {
|
||||
id: string;
|
||||
projectId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: 'Story' | 'Task' | 'Bug' | 'Epic';
|
||||
status: 'Backlog' | 'Todo' | 'InProgress' | 'Done';
|
||||
priority: 'Low' | 'Medium' | 'High' | 'Critical';
|
||||
assigneeId?: string;
|
||||
reporterId: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateIssueDto {
|
||||
title: string;
|
||||
description: string;
|
||||
type: string;
|
||||
priority: string;
|
||||
}
|
||||
|
||||
export interface UpdateIssueDto {
|
||||
title: string;
|
||||
description: string;
|
||||
priority: string;
|
||||
}
|
||||
|
||||
export interface ChangeStatusDto {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface AssignIssueDto {
|
||||
assigneeId: string | null;
|
||||
}
|
||||
|
||||
export const issuesApi = {
|
||||
list: async (projectId: string, status?: string): Promise<Issue[]> => {
|
||||
const params = status ? `?status=${status}` : '';
|
||||
return api.get<Issue[]>(`/api/v1/projects/${projectId}/issues${params}`);
|
||||
},
|
||||
|
||||
getById: async (projectId: string, id: string): Promise<Issue> => {
|
||||
return api.get<Issue>(`/api/v1/projects/${projectId}/issues/${id}`);
|
||||
},
|
||||
|
||||
create: async (projectId: string, data: CreateIssueDto): Promise<Issue> => {
|
||||
return api.post<Issue>(`/api/v1/projects/${projectId}/issues`, data);
|
||||
},
|
||||
|
||||
update: async (
|
||||
projectId: string,
|
||||
id: string,
|
||||
data: UpdateIssueDto
|
||||
): Promise<Issue> => {
|
||||
return api.put<Issue>(`/api/v1/projects/${projectId}/issues/${id}`, data);
|
||||
},
|
||||
|
||||
changeStatus: async (
|
||||
projectId: string,
|
||||
id: string,
|
||||
status: string
|
||||
): Promise<void> => {
|
||||
return api.put<void>(
|
||||
`/api/v1/projects/${projectId}/issues/${id}/status`,
|
||||
{ status }
|
||||
);
|
||||
},
|
||||
|
||||
assign: async (
|
||||
projectId: string,
|
||||
id: string,
|
||||
assigneeId: string | null
|
||||
): Promise<void> => {
|
||||
return api.put<void>(`/api/v1/projects/${projectId}/issues/${id}/assign`, {
|
||||
assigneeId,
|
||||
});
|
||||
},
|
||||
|
||||
delete: async (projectId: string, id: string): Promise<void> => {
|
||||
return api.delete<void>(`/api/v1/projects/${projectId}/issues/${id}`);
|
||||
},
|
||||
};
|
||||
102
lib/hooks/use-issues.ts
Normal file
102
lib/hooks/use-issues.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { issuesApi, Issue, CreateIssueDto, UpdateIssueDto } from '@/lib/api/issues';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function useIssues(projectId: string, status?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['issues', projectId, status],
|
||||
queryFn: () => issuesApi.list(projectId, status),
|
||||
enabled: !!projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useIssue(projectId: string, issueId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['issue', projectId, issueId],
|
||||
queryFn: () => issuesApi.getById(projectId, issueId),
|
||||
enabled: !!projectId && !!issueId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateIssue(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateIssueDto) => issuesApi.create(projectId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issues', projectId] });
|
||||
toast.success('Issue created successfully');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to create issue');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateIssue(projectId: string, issueId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateIssueDto) =>
|
||||
issuesApi.update(projectId, issueId, data),
|
||||
onSuccess: (updatedIssue) => {
|
||||
queryClient.setQueryData(['issue', projectId, issueId], updatedIssue);
|
||||
queryClient.invalidateQueries({ queryKey: ['issues', projectId] });
|
||||
toast.success('Issue updated successfully');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to update issue');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useChangeIssueStatus(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ issueId, status }: { issueId: string; status: string }) =>
|
||||
issuesApi.changeStatus(projectId, issueId, status),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issues', projectId] });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to change issue status');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignIssue(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
issueId,
|
||||
assigneeId,
|
||||
}: {
|
||||
issueId: string;
|
||||
assigneeId: string | null;
|
||||
}) => issuesApi.assign(projectId, issueId, assigneeId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issues', projectId] });
|
||||
toast.success('Issue assigned successfully');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to assign issue');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteIssue(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (issueId: string) => issuesApi.delete(projectId, issueId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issues', projectId] });
|
||||
toast.success('Issue deleted successfully');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to delete issue');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -78,8 +78,10 @@ export class SignalRConnectionManager {
|
||||
}
|
||||
|
||||
off(methodName: string, callback?: (...args: any[]) => void): void {
|
||||
if (this.connection) {
|
||||
if (this.connection && callback) {
|
||||
this.connection.off(methodName, callback);
|
||||
} else if (this.connection) {
|
||||
this.connection.off(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user