Files
ColaFlow-Web/lib/signalr/ConnectionManager.ts
Yaojia Wang de697d436b 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>
2025-11-04 11:50:01 +01:00

170 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as signalR from '@microsoft/signalr';
import { tokenManager } from '@/lib/api/client';
import { SIGNALR_CONFIG } from './config';
export type ConnectionState =
| 'disconnected'
| 'connecting'
| 'connected'
| 'reconnecting';
export class SignalRConnectionManager {
private connection: signalR.HubConnection | null = null;
private hubUrl: string;
private reconnectAttempt = 0;
private stateListeners: Array<(state: ConnectionState) => void> = [];
constructor(hubUrl: string) {
this.hubUrl = hubUrl;
}
async start(): Promise<void> {
if (
this.connection &&
this.connection.state === signalR.HubConnectionState.Connected
) {
console.log('[SignalR] Already connected');
return;
}
const token = tokenManager.getAccessToken();
if (!token) {
console.warn('[SignalR] No access token found, cannot connect');
return;
}
this.connection = new signalR.HubConnectionBuilder()
.withUrl(this.hubUrl, {
accessTokenFactory: () => token,
// 备用方案:使用 query stringWebSocket 升级需要)
// transport: signalR.HttpTransportType.WebSockets,
})
.configureLogging(
signalR.LogLevel[
SIGNALR_CONFIG.LOG_LEVEL as keyof typeof signalR.LogLevel
]
)
.withAutomaticReconnect(SIGNALR_CONFIG.RECONNECT_DELAYS)
.build();
this.setupConnectionHandlers();
try {
this.notifyStateChange('connecting');
await this.connection.start();
console.log(`[SignalR] Connected to ${this.hubUrl}`);
this.notifyStateChange('connected');
this.reconnectAttempt = 0;
} catch (error) {
console.error('[SignalR] Connection error:', error);
this.notifyStateChange('disconnected');
this.scheduleReconnect();
}
}
async stop(): Promise<void> {
if (this.connection) {
await this.connection.stop();
this.connection = null;
this.notifyStateChange('disconnected');
console.log('[SignalR] Disconnected');
}
}
on(methodName: string, callback: (...args: any[]) => void): void {
if (this.connection) {
this.connection.on(methodName, callback);
}
}
off(methodName: string, callback?: (...args: any[]) => void): void {
if (this.connection && callback) {
this.connection.off(methodName, callback);
} else if (this.connection) {
this.connection.off(methodName);
}
}
async invoke(methodName: string, ...args: any[]): Promise<any> {
if (
!this.connection ||
this.connection.state !== signalR.HubConnectionState.Connected
) {
throw new Error('SignalR connection is not established');
}
return await this.connection.invoke(methodName, ...args);
}
onStateChange(listener: (state: ConnectionState) => void): () => void {
this.stateListeners.push(listener);
// 返回 unsubscribe 函数
return () => {
this.stateListeners = this.stateListeners.filter((l) => l !== listener);
};
}
private setupConnectionHandlers(): void {
if (!this.connection) return;
this.connection.onclose((error) => {
console.log('[SignalR] Connection closed', error);
this.notifyStateChange('disconnected');
this.scheduleReconnect();
});
this.connection.onreconnecting((error) => {
console.log('[SignalR] Reconnecting...', error);
this.notifyStateChange('reconnecting');
});
this.connection.onreconnected((connectionId) => {
console.log('[SignalR] Reconnected', connectionId);
this.notifyStateChange('connected');
this.reconnectAttempt = 0;
});
}
private scheduleReconnect(): void {
if (this.reconnectAttempt >= SIGNALR_CONFIG.RECONNECT_DELAYS.length) {
console.error('[SignalR] Max reconnect attempts reached');
return;
}
const delay = SIGNALR_CONFIG.RECONNECT_DELAYS[this.reconnectAttempt];
this.reconnectAttempt++;
console.log(
`[SignalR] Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempt})`
);
setTimeout(() => {
this.start();
}, delay);
}
private notifyStateChange(state: ConnectionState): void {
this.stateListeners.forEach((listener) => listener(state));
}
get connectionId(): string | null {
return this.connection?.connectionId ?? null;
}
get state(): ConnectionState {
if (!this.connection) return 'disconnected';
switch (this.connection.state) {
case signalR.HubConnectionState.Connected:
return 'connected';
case signalR.HubConnectionState.Connecting:
return 'connecting';
case signalR.HubConnectionState.Reconnecting:
return 'reconnecting';
default:
return 'disconnected';
}
}
}