feat(task-management): implement task reordering and group updates via API
- Added API methods for reordering tasks and updating task groups (status, priority, phase). - Enhanced task management slice with async thunks for handling task reordering and group movements. - Updated task list board to support real-time updates during drag-and-drop operations, emitting socket events for task sort order changes. - Refactored task-related components to utilize shared Ant Design imports for consistency and maintainability. - Removed unused Ant Design imports and optimized drag-and-drop CSS for improved performance.
This commit is contained in:
@@ -12,160 +12,130 @@ import { assignMemberIfNot } from "./on-quick-assign-or-remove";
|
|||||||
interface ChangeRequest {
|
interface ChangeRequest {
|
||||||
from_index: number; // from sort_order
|
from_index: number; // from sort_order
|
||||||
to_index: number; // to sort_order
|
to_index: number; // to sort_order
|
||||||
to_last_index: boolean;
|
project_id: string;
|
||||||
from_group: string;
|
from_group: string;
|
||||||
to_group: string;
|
to_group: string;
|
||||||
group_by: string;
|
group_by: string;
|
||||||
project_id: string;
|
to_last_index: boolean;
|
||||||
task: any;
|
task: {
|
||||||
|
id: string;
|
||||||
|
project_id: string;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
};
|
||||||
team_id: string;
|
team_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Connection pooling for better database performance
|
interface Config {
|
||||||
const dbPool = {
|
from_index: number;
|
||||||
query: async (text: string, params?: any[]) => {
|
to_index: number;
|
||||||
return await db.query(text, params);
|
task_id: string;
|
||||||
|
from_group: string | null;
|
||||||
|
to_group: string | null;
|
||||||
|
project_id: string;
|
||||||
|
group_by: string;
|
||||||
|
to_last_index: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyStatusChange(socket: Socket, config: Config) {
|
||||||
|
const userId = getLoggedInUserIdFromSocket(socket);
|
||||||
|
if (userId && config.to_group) {
|
||||||
|
void TasksController.notifyStatusChange(userId, config.task_id, config.to_group);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Cache for dependency checks to reduce database queries
|
async function emitSortOrderChange(data: ChangeRequest, socket: Socket) {
|
||||||
const dependencyCache = new Map<string, { result: boolean; timestamp: number }>();
|
const q = `
|
||||||
const CACHE_TTL = 5000; // 5 seconds cache
|
SELECT id, sort_order, completed_at
|
||||||
|
FROM tasks
|
||||||
|
WHERE project_id = $1
|
||||||
|
ORDER BY sort_order;
|
||||||
|
`;
|
||||||
|
const tasks = await db.query(q, [data.project_id]);
|
||||||
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), tasks.rows);
|
||||||
|
}
|
||||||
|
|
||||||
const clearExpiredCache = () => {
|
function updateUnmappedStatus(config: Config) {
|
||||||
const now = Date.now();
|
if (config.to_group === UNMAPPED)
|
||||||
for (const [key, value] of dependencyCache.entries()) {
|
config.to_group = null;
|
||||||
if (now - value.timestamp > CACHE_TTL) {
|
if (config.from_group === UNMAPPED)
|
||||||
dependencyCache.delete(key);
|
config.from_group = null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear expired cache entries every 10 seconds
|
export async function on_task_sort_order_change(_io: Server, socket: Socket, data: ChangeRequest) {
|
||||||
setInterval(clearExpiredCache, 10000);
|
|
||||||
|
|
||||||
const onTaskSortOrderChange = async (io: Server, socket: Socket, data: ChangeRequest) => {
|
|
||||||
try {
|
try {
|
||||||
const userId = getLoggedInUserIdFromSocket(socket);
|
const q = `SELECT handle_task_list_sort_order_change($1);`;
|
||||||
if (!userId) {
|
|
||||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { error: "User not authenticated" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const config: Config = {
|
||||||
from_index,
|
from_index: data.from_index,
|
||||||
to_index,
|
to_index: data.to_index,
|
||||||
to_last_index,
|
task_id: data.task.id,
|
||||||
from_group,
|
from_group: data.from_group,
|
||||||
to_group,
|
to_group: data.to_group,
|
||||||
group_by,
|
project_id: data.project_id,
|
||||||
project_id,
|
group_by: data.group_by,
|
||||||
task,
|
to_last_index: Boolean(data.to_last_index)
|
||||||
team_id
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Validate input data early to avoid expensive operations
|
|
||||||
if (!project_id || !task?.id || !team_id) {
|
|
||||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { error: "Missing required data" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Use cached dependency check if available
|
|
||||||
const cacheKey = `${project_id}-${userId}-${team_id}`;
|
|
||||||
const cachedDependency = dependencyCache.get(cacheKey);
|
|
||||||
|
|
||||||
let hasAccess = false;
|
|
||||||
if (cachedDependency && (Date.now() - cachedDependency.timestamp) < CACHE_TTL) {
|
|
||||||
hasAccess = cachedDependency.result;
|
|
||||||
} else {
|
|
||||||
// PERFORMANCE OPTIMIZATION: Optimized dependency check query
|
|
||||||
const dependencyResult = await dbPool.query(`
|
|
||||||
SELECT EXISTS(
|
|
||||||
SELECT 1 FROM project_members pm
|
|
||||||
INNER JOIN projects p ON p.id = pm.project_id
|
|
||||||
INNER JOIN team_members tm ON pm.team_member_id = tm.id
|
|
||||||
WHERE pm.project_id = $1
|
|
||||||
AND tm.user_id = $2
|
|
||||||
AND p.team_id = $3
|
|
||||||
) as has_access
|
|
||||||
`, [project_id, userId, team_id]);
|
|
||||||
|
|
||||||
hasAccess = dependencyResult.rows[0]?.has_access || false;
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
dependencyCache.set(cacheKey, { result: hasAccess, timestamp: Date.now() });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasAccess) {
|
|
||||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { error: "Access denied" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Execute database operation directly
|
|
||||||
await dbPool.query(`SELECT handle_task_list_sort_order_change($1)`, [JSON.stringify({
|
|
||||||
project_id,
|
|
||||||
task_id: task.id,
|
|
||||||
from_index,
|
|
||||||
to_index,
|
|
||||||
to_last_index,
|
|
||||||
from_group,
|
|
||||||
to_group,
|
|
||||||
group_by
|
|
||||||
})]);
|
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Optimized project updates notification
|
|
||||||
const projectUpdateData = {
|
|
||||||
project_id,
|
|
||||||
team_id,
|
|
||||||
user_id: userId,
|
|
||||||
update_type: "task_sort_order_change",
|
|
||||||
task_id: task.id,
|
|
||||||
from_group,
|
|
||||||
to_group,
|
|
||||||
group_by
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Emit to all users in the project room
|
if ((config.group_by === GroupBy.STATUS) && config.to_group) {
|
||||||
io.to(`project_${project_id}`).emit("project_updates", projectUpdateData);
|
const canContinue = await TasksControllerV2.checkForCompletedDependencies(config.task_id, config?.to_group);
|
||||||
|
if (!canContinue) {
|
||||||
// PERFORMANCE OPTIMIZATION: Optimized activity logging
|
return socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||||
const activityLogData = {
|
completed_deps: canContinue
|
||||||
task_id: task.id,
|
});
|
||||||
socket,
|
|
||||||
new_value: to_group,
|
|
||||||
old_value: from_group
|
|
||||||
};
|
|
||||||
|
|
||||||
// Log activity asynchronously to avoid blocking the response
|
|
||||||
setImmediate(async () => {
|
|
||||||
try {
|
|
||||||
if (group_by === "phase") {
|
|
||||||
await logPhaseChange(activityLogData);
|
|
||||||
} else if (group_by === "status") {
|
|
||||||
await logStatusChange(activityLogData);
|
|
||||||
} else if (group_by === "priority") {
|
|
||||||
await logPriorityChange(activityLogData);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log_error(error);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Send success response
|
notifyStatusChange(socket, config);
|
||||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
}
|
||||||
success: true,
|
|
||||||
task_id: task.id,
|
|
||||||
from_group,
|
|
||||||
to_group,
|
|
||||||
group_by
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (config.group_by === GroupBy.PHASE) {
|
||||||
|
updateUnmappedStatus(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.query(q, [JSON.stringify(config)]);
|
||||||
|
await emitSortOrderChange(data, socket);
|
||||||
|
|
||||||
|
if (config.group_by === GroupBy.STATUS) {
|
||||||
|
const userId = getLoggedInUserIdFromSocket(socket);
|
||||||
|
const isAlreadyAssigned = await TasksControllerV2.checkUserAssignedToTask(data.task.id, userId as string, data.team_id);
|
||||||
|
|
||||||
|
if (!isAlreadyAssigned) {
|
||||||
|
await assignMemberIfNot(data.task.id, userId as string, data.team_id, _io, socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.group_by === GroupBy.PHASE) {
|
||||||
|
void logPhaseChange({
|
||||||
|
task_id: data.task.id,
|
||||||
|
socket,
|
||||||
|
new_value: data.to_group,
|
||||||
|
old_value: data.from_group
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.group_by === GroupBy.STATUS) {
|
||||||
|
void logStatusChange({
|
||||||
|
task_id: data.task.id,
|
||||||
|
socket,
|
||||||
|
new_value: data.to_group,
|
||||||
|
old_value: data.from_group
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.group_by === GroupBy.PRIORITY) {
|
||||||
|
void logPriorityChange({
|
||||||
|
task_id: data.task.id,
|
||||||
|
socket,
|
||||||
|
new_value: data.to_group,
|
||||||
|
old_value: data.from_group
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyProjectUpdates(socket, config.task_id);
|
||||||
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log_error(error);
|
log_error(error);
|
||||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
|
||||||
error: "Internal server error"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default onTaskSortOrderChange;
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), []);
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ import { on_task_description_change } from "./commands/on-task-description-chang
|
|||||||
import { on_get_task_progress } from "./commands/on-get-task-progress";
|
import { on_get_task_progress } from "./commands/on-get-task-progress";
|
||||||
import { on_task_timer_start } from "./commands/on-task-timer-start";
|
import { on_task_timer_start } from "./commands/on-task-timer-start";
|
||||||
import { on_task_timer_stop } from "./commands/on-task-timer-stop";
|
import { on_task_timer_stop } from "./commands/on-task-timer-stop";
|
||||||
import on_task_sort_order_change from "./commands/on-task-sort-order-change";
|
import { on_task_sort_order_change } from "./commands/on-task-sort-order-change";
|
||||||
import { on_join_project_room as on_join_or_leave_project_room } from "./commands/on-join-or-leave-project-room";
|
import { on_join_project_room as on_join_or_leave_project_room } from "./commands/on-join-or-leave-project-room";
|
||||||
import { on_task_subscriber_change } from "./commands/on-task-subscriber-change";
|
import { on_task_subscriber_change } from "./commands/on-task-subscriber-change";
|
||||||
import { on_project_subscriber_change } from "./commands/on-project-subscriber-change";
|
import { on_project_subscriber_change } from "./commands/on-project-subscriber-change";
|
||||||
|
|||||||
@@ -159,4 +159,24 @@ export const tasksApiService = {
|
|||||||
const response = await apiClient.get(`${rootUrl}/progress-status/${projectId}`);
|
const response = await apiClient.get(`${rootUrl}/progress-status/${projectId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// API method to reorder tasks
|
||||||
|
reorderTasks: async (params: { taskIds: string[]; newOrder: number[]; projectId: string }): Promise<IServerResponse<{ done: boolean }>> => {
|
||||||
|
const response = await apiClient.post(`${rootUrl}/reorder`, {
|
||||||
|
task_ids: params.taskIds,
|
||||||
|
new_order: params.newOrder,
|
||||||
|
project_id: params.projectId,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// API method to update task group (status, priority, phase)
|
||||||
|
updateTaskGroup: async (params: { taskId: string; groupType: 'status' | 'priority' | 'phase'; groupValue: string; projectId: string }): Promise<IServerResponse<{ done: boolean }>> => {
|
||||||
|
const response = await apiClient.put(`${rootUrl}/${params.taskId}/group`, {
|
||||||
|
group_type: params.groupType,
|
||||||
|
group_value: params.groupValue,
|
||||||
|
project_id: params.projectId,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
/**
|
|
||||||
* Centralized Ant Design imports for Task Management components
|
|
||||||
*
|
|
||||||
* This file provides:
|
|
||||||
* - Tree-shaking optimization by importing only used components
|
|
||||||
* - Type safety with proper TypeScript types
|
|
||||||
* - Performance optimization through selective imports
|
|
||||||
* - Consistent component versions across task management
|
|
||||||
* - Easy maintenance and updates
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Core Components
|
|
||||||
export {
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Typography,
|
|
||||||
Card,
|
|
||||||
Spin,
|
|
||||||
Empty,
|
|
||||||
Space,
|
|
||||||
Tooltip,
|
|
||||||
Badge,
|
|
||||||
Popconfirm,
|
|
||||||
message,
|
|
||||||
Checkbox,
|
|
||||||
Dropdown,
|
|
||||||
Menu
|
|
||||||
} from 'antd/es';
|
|
||||||
|
|
||||||
// Date & Time Components
|
|
||||||
export {
|
|
||||||
DatePicker,
|
|
||||||
TimePicker
|
|
||||||
} from 'antd/es';
|
|
||||||
|
|
||||||
// Form Components (if needed for task management)
|
|
||||||
export {
|
|
||||||
Form,
|
|
||||||
InputNumber
|
|
||||||
} from 'antd/es';
|
|
||||||
|
|
||||||
// Layout Components
|
|
||||||
export {
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Divider,
|
|
||||||
Flex
|
|
||||||
} from 'antd/es';
|
|
||||||
|
|
||||||
// Icon Components (commonly used in task management)
|
|
||||||
export {
|
|
||||||
EditOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
MoreOutlined,
|
|
||||||
CheckOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
CalendarOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
TeamOutlined,
|
|
||||||
TagOutlined,
|
|
||||||
FlagOutlined,
|
|
||||||
BarsOutlined,
|
|
||||||
TableOutlined,
|
|
||||||
AppstoreOutlined,
|
|
||||||
FilterOutlined,
|
|
||||||
SortAscendingOutlined,
|
|
||||||
SortDescendingOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
SettingOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
EyeInvisibleOutlined,
|
|
||||||
CopyOutlined,
|
|
||||||
ExportOutlined,
|
|
||||||
ImportOutlined,
|
|
||||||
DownOutlined,
|
|
||||||
RightOutlined,
|
|
||||||
LeftOutlined,
|
|
||||||
UpOutlined,
|
|
||||||
DragOutlined,
|
|
||||||
HolderOutlined,
|
|
||||||
MessageOutlined,
|
|
||||||
PaperClipOutlined,
|
|
||||||
GroupOutlined,
|
|
||||||
InboxOutlined,
|
|
||||||
TagsOutlined,
|
|
||||||
UsergroupAddOutlined,
|
|
||||||
UserAddOutlined,
|
|
||||||
RetweetOutlined
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
|
|
||||||
// TypeScript Types
|
|
||||||
export type {
|
|
||||||
ButtonProps,
|
|
||||||
InputProps,
|
|
||||||
InputRef,
|
|
||||||
SelectProps,
|
|
||||||
TypographyProps,
|
|
||||||
CardProps,
|
|
||||||
SpinProps,
|
|
||||||
EmptyProps,
|
|
||||||
SpaceProps,
|
|
||||||
TooltipProps,
|
|
||||||
BadgeProps,
|
|
||||||
PopconfirmProps,
|
|
||||||
CheckboxProps,
|
|
||||||
CheckboxChangeEvent,
|
|
||||||
DropdownProps,
|
|
||||||
MenuProps,
|
|
||||||
DatePickerProps,
|
|
||||||
TimePickerProps,
|
|
||||||
FormProps,
|
|
||||||
FormInstance,
|
|
||||||
InputNumberProps,
|
|
||||||
RowProps,
|
|
||||||
ColProps,
|
|
||||||
DividerProps,
|
|
||||||
FlexProps
|
|
||||||
} from 'antd/es';
|
|
||||||
|
|
||||||
// Dayjs (used with DatePicker)
|
|
||||||
export { default as dayjs } from 'dayjs';
|
|
||||||
export type { Dayjs } from 'dayjs';
|
|
||||||
|
|
||||||
// Re-export commonly used Ant Design utilities
|
|
||||||
export {
|
|
||||||
ConfigProvider,
|
|
||||||
theme
|
|
||||||
} from 'antd';
|
|
||||||
|
|
||||||
// Custom hooks for task management (if any Ant Design specific hooks are needed)
|
|
||||||
export const useAntdBreakpoint = () => {
|
|
||||||
// You can add custom breakpoint logic here if needed
|
|
||||||
return {
|
|
||||||
xs: window.innerWidth < 576,
|
|
||||||
sm: window.innerWidth >= 576 && window.innerWidth < 768,
|
|
||||||
md: window.innerWidth >= 768 && window.innerWidth < 992,
|
|
||||||
lg: window.innerWidth >= 992 && window.innerWidth < 1200,
|
|
||||||
xl: window.innerWidth >= 1200 && window.innerWidth < 1600,
|
|
||||||
xxl: window.innerWidth >= 1600,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Import message separately to avoid circular dependency
|
|
||||||
import { message as antdMessage } from 'antd';
|
|
||||||
|
|
||||||
// Performance optimized message utility
|
|
||||||
export const taskMessage = {
|
|
||||||
success: (content: string) => antdMessage.success(content),
|
|
||||||
error: (content: string) => antdMessage.error(content),
|
|
||||||
warning: (content: string) => antdMessage.warning(content),
|
|
||||||
info: (content: string) => antdMessage.info(content),
|
|
||||||
loading: (content: string) => antdMessage.loading(content),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Commonly used Ant Design configurations for task management
|
|
||||||
export const taskManagementAntdConfig = {
|
|
||||||
// DatePicker default props for consistency
|
|
||||||
datePickerDefaults: {
|
|
||||||
format: 'MMM DD, YYYY',
|
|
||||||
placeholder: 'Set Date',
|
|
||||||
suffixIcon: null,
|
|
||||||
size: 'small' as const,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Button default props for task actions
|
|
||||||
taskButtonDefaults: {
|
|
||||||
size: 'small' as const,
|
|
||||||
type: 'text' as const,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Input default props for task editing
|
|
||||||
taskInputDefaults: {
|
|
||||||
size: 'small' as const,
|
|
||||||
variant: 'borderless' as const,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Select default props for dropdowns
|
|
||||||
taskSelectDefaults: {
|
|
||||||
size: 'small' as const,
|
|
||||||
variant: 'borderless' as const,
|
|
||||||
showSearch: true,
|
|
||||||
optionFilterProp: 'label' as const,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tooltip default props
|
|
||||||
tooltipDefaults: {
|
|
||||||
placement: 'top' as const,
|
|
||||||
mouseEnterDelay: 0.5,
|
|
||||||
mouseLeaveDelay: 0.1,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Dropdown default props
|
|
||||||
dropdownDefaults: {
|
|
||||||
trigger: ['click'] as const,
|
|
||||||
placement: 'bottomLeft' as const,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Theme tokens specifically for task management
|
|
||||||
export const taskManagementTheme = {
|
|
||||||
light: {
|
|
||||||
colorBgContainer: '#ffffff',
|
|
||||||
colorBorder: '#e5e7eb',
|
|
||||||
colorText: '#374151',
|
|
||||||
colorTextSecondary: '#6b7280',
|
|
||||||
colorPrimary: '#3b82f6',
|
|
||||||
colorSuccess: '#10b981',
|
|
||||||
colorWarning: '#f59e0b',
|
|
||||||
colorError: '#ef4444',
|
|
||||||
colorBgHover: '#f9fafb',
|
|
||||||
colorBgSelected: '#eff6ff',
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
colorBgContainer: '#1f2937',
|
|
||||||
colorBorder: '#374151',
|
|
||||||
colorText: '#f9fafb',
|
|
||||||
colorTextSecondary: '#d1d5db',
|
|
||||||
colorPrimary: '#60a5fa',
|
|
||||||
colorSuccess: '#34d399',
|
|
||||||
colorWarning: '#fbbf24',
|
|
||||||
colorError: '#f87171',
|
|
||||||
colorBgHover: '#374151',
|
|
||||||
colorBgSelected: '#1e40af',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Export default configuration object
|
|
||||||
export default {
|
|
||||||
config: taskManagementAntdConfig,
|
|
||||||
theme: taskManagementTheme,
|
|
||||||
message: taskMessage,
|
|
||||||
useBreakpoint: useAntdBreakpoint,
|
|
||||||
};
|
|
||||||
@@ -1,149 +1,40 @@
|
|||||||
/* DRAG AND DROP PERFORMANCE OPTIMIZATIONS */
|
/* MINIMAL DRAG AND DROP CSS - SHOW ONLY TASK NAME */
|
||||||
|
|
||||||
/* Force GPU acceleration for all drag operations */
|
/* Basic drag handle styling */
|
||||||
[data-dnd-draggable],
|
|
||||||
[data-dnd-drag-handle],
|
|
||||||
[data-dnd-overlay] {
|
|
||||||
transform: translateZ(0);
|
|
||||||
will-change: transform;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
perspective: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optimize drag handle for instant response */
|
|
||||||
.drag-handle-optimized {
|
.drag-handle-optimized {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
opacity: 0.6;
|
||||||
touch-action: none;
|
transition: opacity 0.2s ease;
|
||||||
-webkit-user-select: none;
|
}
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
.drag-handle-optimized:hover {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle-optimized:active {
|
.drag-handle-optimized:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disable all transitions during drag for instant response */
|
/* Simple drag overlay - just show task name */
|
||||||
[data-dnd-dragging="true"] *,
|
|
||||||
[data-dnd-dragging="true"] {
|
|
||||||
transition: none !important;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optimize drag overlay for smooth movement */
|
|
||||||
[data-dnd-overlay] {
|
[data-dnd-overlay] {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: fixed !important;
|
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
transform: translateZ(0);
|
|
||||||
will-change: transform;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reduce layout thrashing during drag */
|
/* Dark mode support for drag overlay */
|
||||||
.task-row-dragging {
|
.dark [data-dnd-overlay],
|
||||||
contain: layout style paint;
|
[data-theme="dark"] [data-dnd-overlay] {
|
||||||
will-change: transform;
|
background: #1f1f1f;
|
||||||
transform: translateZ(0);
|
border-color: #404040;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optimize virtualized lists during drag */
|
/* Hide drag handle during drag */
|
||||||
.react-window-list {
|
[data-dnd-dragging="true"] .drag-handle-optimized {
|
||||||
contain: layout style;
|
opacity: 0;
|
||||||
will-change: scroll-position;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-window-list-item {
|
|
||||||
contain: layout style;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disable hover effects during drag */
|
|
||||||
[data-dnd-dragging="true"] .task-row:hover {
|
|
||||||
background-color: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optimize cursor changes */
|
|
||||||
.task-row {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-row[data-dnd-dragging="true"] {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Performance optimizations for large lists */
|
|
||||||
.virtualized-task-container {
|
|
||||||
contain: layout style paint;
|
|
||||||
will-change: scroll-position;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reduce repaints during scroll */
|
|
||||||
.task-groups-container {
|
|
||||||
contain: layout style;
|
|
||||||
will-change: scroll-position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optimize sortable context */
|
|
||||||
[data-dnd-sortable-context] {
|
|
||||||
contain: layout style;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disable animations during drag operations */
|
|
||||||
[data-dnd-context] [data-dnd-dragging="true"] * {
|
|
||||||
transition: none !important;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optimize drop indicators */
|
|
||||||
.drop-indicator {
|
|
||||||
contain: layout style;
|
|
||||||
will-change: opacity;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Performance optimizations for touch devices */
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
.drag-handle-optimized {
|
|
||||||
min-height: 44px;
|
|
||||||
min-width: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode optimizations */
|
|
||||||
.dark [data-dnd-dragging="true"],
|
|
||||||
[data-theme="dark"] [data-dnd-dragging="true"] {
|
|
||||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reduce memory usage during drag */
|
|
||||||
[data-dnd-dragging="true"] img,
|
|
||||||
[data-dnd-dragging="true"] svg {
|
|
||||||
contain: layout style paint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optimize for high DPI displays */
|
|
||||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
|
||||||
[data-dnd-overlay] {
|
|
||||||
transform: translateZ(0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disable text selection during drag */
|
|
||||||
[data-dnd-dragging="true"] {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optimize for reduced motion preferences */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
[data-dnd-overlay],
|
|
||||||
[data-dnd-dragging="true"] {
|
|
||||||
transition: none !important;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
InboxOutlined,
|
InboxOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
} from './antd-imports';
|
} from '@/shared/antd-imports';
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
RightOutlined,
|
RightOutlined,
|
||||||
DownOutlined
|
DownOutlined
|
||||||
} from './antd-imports';
|
} from '@/shared/antd-imports';
|
||||||
import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types';
|
import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types';
|
||||||
import { taskManagementSelectors } from '@/features/task-management/task-management.slice';
|
import { taskManagementSelectors } from '@/features/task-management/task-management.slice';
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
reorderTasks,
|
reorderTasks,
|
||||||
moveTaskToGroup,
|
moveTaskToGroup,
|
||||||
optimisticTaskMove,
|
optimisticTaskMove,
|
||||||
|
reorderTasksInGroup,
|
||||||
setLoading,
|
setLoading,
|
||||||
fetchTasks,
|
fetchTasks,
|
||||||
fetchTasksV3,
|
fetchTasksV3,
|
||||||
@@ -39,6 +40,8 @@ import {
|
|||||||
} from '@/features/task-management/selection.slice';
|
} from '@/features/task-management/selection.slice';
|
||||||
import { Task } from '@/types/task-management.types';
|
import { Task } from '@/types/task-management.types';
|
||||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||||
|
import { useSocket } from '@/socket/socketContext';
|
||||||
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import TaskRow from './task-row';
|
import TaskRow from './task-row';
|
||||||
// import BulkActionBar from './bulk-action-bar';
|
// import BulkActionBar from './bulk-action-bar';
|
||||||
import OptimizedBulkActionBar from './optimized-bulk-action-bar';
|
import OptimizedBulkActionBar from './optimized-bulk-action-bar';
|
||||||
@@ -136,7 +139,6 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
const renderCountRef = useRef(0);
|
const renderCountRef = useRef(0);
|
||||||
const [shouldThrottle, setShouldThrottle] = useState(false);
|
const [shouldThrottle, setShouldThrottle] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
// Refs for performance optimization
|
// Refs for performance optimization
|
||||||
const dragOverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const dragOverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -144,6 +146,9 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
// Enable real-time socket updates for task changes
|
// Enable real-time socket updates for task changes
|
||||||
useTaskSocketHandlers();
|
useTaskSocketHandlers();
|
||||||
|
|
||||||
|
// Socket connection for drag and drop
|
||||||
|
const { socket, connected } = useSocket();
|
||||||
|
|
||||||
// Redux selectors using V3 API (pre-processed data, minimal loops)
|
// Redux selectors using V3 API (pre-processed data, minimal loops)
|
||||||
const tasks = useSelector(taskManagementSelectors.selectAll);
|
const tasks = useSelector(taskManagementSelectors.selectAll);
|
||||||
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
|
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
|
||||||
@@ -151,7 +156,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
const selectedTaskIds = useSelector(selectSelectedTaskIds);
|
const selectedTaskIds = useSelector(selectSelectedTaskIds);
|
||||||
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
|
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
|
||||||
const error = useSelector((state: RootState) => state.taskManagement.error);
|
const error = useSelector((state: RootState) => state.taskManagement.error);
|
||||||
|
|
||||||
// Bulk action selectors
|
// Bulk action selectors
|
||||||
const statusList = useSelector((state: RootState) => state.taskStatusReducer.status);
|
const statusList = useSelector((state: RootState) => state.taskStatusReducer.status);
|
||||||
const priorityList = useSelector((state: RootState) => state.priorityReducer.priorities);
|
const priorityList = useSelector((state: RootState) => state.priorityReducer.priorities);
|
||||||
@@ -234,8 +239,12 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add isDragging state
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(event: DragStartEvent) => {
|
(event: DragStartEvent) => {
|
||||||
|
setIsDragging(true);
|
||||||
const { active } = event;
|
const { active } = event;
|
||||||
const taskId = active.id as string;
|
const taskId = active.id as string;
|
||||||
|
|
||||||
@@ -244,13 +253,12 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
let activeGroupId: string | null = null;
|
let activeGroupId: string | null = null;
|
||||||
|
|
||||||
if (activeTask) {
|
if (activeTask) {
|
||||||
// Determine group ID based on current grouping
|
// Find which group contains this task by looking through all groups
|
||||||
if (currentGrouping === 'status') {
|
for (const group of taskGroups) {
|
||||||
activeGroupId = `status-${activeTask.status}`;
|
if (group.taskIds.includes(taskId)) {
|
||||||
} else if (currentGrouping === 'priority') {
|
activeGroupId = group.id;
|
||||||
activeGroupId = `priority-${activeTask.priority}`;
|
break;
|
||||||
} else if (currentGrouping === 'phase') {
|
}
|
||||||
activeGroupId = `phase-${activeTask.phase}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +267,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
activeGroupId,
|
activeGroupId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[tasks, currentGrouping]
|
[tasks, currentGrouping, taskGroups]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Throttled drag over handler for smoother performance
|
// Throttled drag over handler for smoother performance
|
||||||
@@ -270,15 +278,14 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
if (!over || !dragState.activeTask) return;
|
if (!over || !dragState.activeTask) return;
|
||||||
|
|
||||||
const activeTaskId = active.id as string;
|
const activeTaskId = active.id as string;
|
||||||
const overContainer = over.id as string;
|
const overId = over.id as string;
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Immediate response for instant UX
|
// Check if we're hovering over a task or a group container
|
||||||
// Only update if we're hovering over a different container
|
const targetTask = tasks.find(t => t.id === overId);
|
||||||
const targetTask = tasks.find(t => t.id === overContainer);
|
let targetGroupId = overId;
|
||||||
let targetGroupId = overContainer;
|
|
||||||
|
|
||||||
if (targetTask) {
|
if (targetTask) {
|
||||||
// PERFORMANCE OPTIMIZATION: Use switch instead of multiple if statements
|
// We're hovering over a task, determine its group
|
||||||
switch (currentGrouping) {
|
switch (currentGrouping) {
|
||||||
case 'status':
|
case 'status':
|
||||||
targetGroupId = `status-${targetTask.status}`;
|
targetGroupId = `status-${targetTask.status}`;
|
||||||
@@ -291,29 +298,13 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetGroupId !== dragState.activeGroupId) {
|
|
||||||
// PERFORMANCE OPTIMIZATION: Use findIndex for better performance
|
|
||||||
const targetGroupIndex = taskGroups.findIndex(g => g.id === targetGroupId);
|
|
||||||
if (targetGroupIndex !== -1) {
|
|
||||||
const targetGroup = taskGroups[targetGroupIndex];
|
|
||||||
dispatch(
|
|
||||||
optimisticTaskMove({
|
|
||||||
taskId: activeTaskId,
|
|
||||||
newGroupId: targetGroupId,
|
|
||||||
newIndex: targetGroup.taskIds.length,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 16), // 60fps throttling for smooth performance
|
}, 16), // 60fps throttling for smooth performance
|
||||||
[dragState, tasks, taskGroups, currentGrouping, dispatch]
|
[dragState, tasks, taskGroups, currentGrouping]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(event: DragEndEvent) => {
|
(event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
setIsDragging(false);
|
||||||
|
|
||||||
// Clear any pending drag over timeouts
|
// Clear any pending drag over timeouts
|
||||||
if (dragOverTimeoutRef.current) {
|
if (dragOverTimeoutRef.current) {
|
||||||
clearTimeout(dragOverTimeoutRef.current);
|
clearTimeout(dragOverTimeoutRef.current);
|
||||||
@@ -327,36 +318,27 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
activeGroupId: null,
|
activeGroupId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!over || !currentDragState.activeTask || !currentDragState.activeGroupId) {
|
if (!event.over || !currentDragState.activeTask || !currentDragState.activeGroupId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { active, over } = event;
|
||||||
const activeTaskId = active.id as string;
|
const activeTaskId = active.id as string;
|
||||||
const overContainer = over.id as string;
|
const overId = over.id as string;
|
||||||
|
|
||||||
// Parse the group ID to get group type and value - optimized
|
// Determine target group and position
|
||||||
const parseGroupId = (groupId: string) => {
|
let targetGroupId = overId;
|
||||||
const [groupType, ...groupValueParts] = groupId.split('-');
|
|
||||||
return {
|
|
||||||
groupType: groupType as 'status' | 'priority' | 'phase',
|
|
||||||
groupValue: groupValueParts.join('-'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine target group
|
|
||||||
let targetGroupId = overContainer;
|
|
||||||
let targetIndex = -1;
|
let targetIndex = -1;
|
||||||
|
|
||||||
// Check if dropping on a task or a group
|
// Check if dropping on a task or a group
|
||||||
const targetTask = tasks.find(t => t.id === overContainer);
|
const targetTask = tasks.find(t => t.id === overId);
|
||||||
if (targetTask) {
|
if (targetTask) {
|
||||||
// Dropping on a task, determine its group
|
// Dropping on a task, find which group contains this task
|
||||||
if (currentGrouping === 'status') {
|
for (const group of taskGroups) {
|
||||||
targetGroupId = `status-${targetTask.status}`;
|
if (group.taskIds.includes(targetTask.id)) {
|
||||||
} else if (currentGrouping === 'priority') {
|
targetGroupId = group.id;
|
||||||
targetGroupId = `priority-${targetTask.priority}`;
|
break;
|
||||||
} else if (currentGrouping === 'phase') {
|
}
|
||||||
targetGroupId = `phase-${targetTask.phase}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the index of the target task within its group
|
// Find the index of the target task within its group
|
||||||
@@ -364,23 +346,15 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
if (targetGroup) {
|
if (targetGroup) {
|
||||||
targetIndex = targetGroup.taskIds.indexOf(targetTask.id);
|
targetIndex = targetGroup.taskIds.indexOf(targetTask.id);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Dropping on a group container, add to the end
|
||||||
|
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||||
|
if (targetGroup) {
|
||||||
|
targetIndex = targetGroup.taskIds.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceGroupInfo = parseGroupId(currentDragState.activeGroupId);
|
// Find source and target groups
|
||||||
const targetGroupInfo = parseGroupId(targetGroupId);
|
|
||||||
|
|
||||||
// If moving between different groups, update the task's group property
|
|
||||||
if (currentDragState.activeGroupId !== targetGroupId) {
|
|
||||||
dispatch(
|
|
||||||
moveTaskToGroup({
|
|
||||||
taskId: activeTaskId,
|
|
||||||
groupType: targetGroupInfo.groupType,
|
|
||||||
groupValue: targetGroupInfo.groupValue,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle reordering within the same group or between groups
|
|
||||||
const sourceGroup = taskGroups.find(g => g.id === currentDragState.activeGroupId);
|
const sourceGroup = taskGroups.find(g => g.id === currentDragState.activeGroupId);
|
||||||
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||||
|
|
||||||
@@ -390,27 +364,41 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
|
|
||||||
// Only reorder if actually moving to a different position
|
// Only reorder if actually moving to a different position
|
||||||
if (sourceGroup.id !== targetGroup.id || sourceIndex !== finalTargetIndex) {
|
if (sourceGroup.id !== targetGroup.id || sourceIndex !== finalTargetIndex) {
|
||||||
// Calculate new order values - simplified
|
// Use the new reorderTasksInGroup action that properly handles group arrays
|
||||||
const allTasksInTargetGroup = targetGroup.taskIds.map(
|
|
||||||
(id: string) => tasks.find((t: any) => t.id === id)!
|
|
||||||
);
|
|
||||||
const newOrder = allTasksInTargetGroup.map((task, index) => {
|
|
||||||
if (index < finalTargetIndex) return task.order;
|
|
||||||
if (index === finalTargetIndex) return currentDragState.activeTask!.order;
|
|
||||||
return task.order + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dispatch reorder action
|
|
||||||
dispatch(
|
dispatch(
|
||||||
reorderTasks({
|
reorderTasksInGroup({
|
||||||
taskIds: [activeTaskId, ...allTasksInTargetGroup.map((t: any) => t.id)],
|
taskId: activeTaskId,
|
||||||
newOrder: [currentDragState.activeTask!.order, ...newOrder],
|
fromGroupId: currentDragState.activeGroupId,
|
||||||
|
toGroupId: targetGroupId,
|
||||||
|
fromIndex: sourceIndex,
|
||||||
|
toIndex: finalTargetIndex,
|
||||||
|
groupType: targetGroup.groupType,
|
||||||
|
groupValue: targetGroup.groupValue,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Emit socket event to backend
|
||||||
|
if (connected && socket && currentDragState.activeTask) {
|
||||||
|
const currentSession = JSON.parse(localStorage.getItem('session') || '{}');
|
||||||
|
|
||||||
|
const socketData = {
|
||||||
|
from_index: sourceIndex,
|
||||||
|
to_index: finalTargetIndex,
|
||||||
|
to_last_index: finalTargetIndex >= targetGroup.taskIds.length,
|
||||||
|
from_group: currentDragState.activeGroupId,
|
||||||
|
to_group: targetGroupId,
|
||||||
|
group_by: currentGrouping,
|
||||||
|
project_id: projectId,
|
||||||
|
task: currentDragState.activeTask,
|
||||||
|
team_id: currentSession.team_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), socketData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dragState, tasks, taskGroups, currentGrouping, dispatch]
|
[dragState, tasks, taskGroups, currentGrouping, dispatch, connected, socket, projectId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectTask = useCallback(
|
const handleSelectTask = useCallback(
|
||||||
@@ -651,17 +639,14 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
// Additional handlers for new actions
|
// Additional handlers for new actions
|
||||||
const handleBulkDuplicate = useCallback(async () => {
|
const handleBulkDuplicate = useCallback(async () => {
|
||||||
// This would need to be implemented in the API service
|
// This would need to be implemented in the API service
|
||||||
console.log('Bulk duplicate not yet implemented in API:', selectedTaskIds);
|
|
||||||
}, [selectedTaskIds]);
|
}, [selectedTaskIds]);
|
||||||
|
|
||||||
const handleBulkExport = useCallback(async () => {
|
const handleBulkExport = useCallback(async () => {
|
||||||
// This would need to be implemented in the API service
|
// This would need to be implemented in the API service
|
||||||
console.log('Bulk export not yet implemented in API:', selectedTaskIds);
|
|
||||||
}, [selectedTaskIds]);
|
}, [selectedTaskIds]);
|
||||||
|
|
||||||
const handleBulkSetDueDate = useCallback(async (date: string) => {
|
const handleBulkSetDueDate = useCallback(async (date: string) => {
|
||||||
// This would need to be implemented in the API service
|
// This would need to be implemented in the API service
|
||||||
console.log('Bulk set due date not yet implemented in API:', date, selectedTaskIds);
|
|
||||||
}, [selectedTaskIds]);
|
}, [selectedTaskIds]);
|
||||||
|
|
||||||
// Cleanup effect
|
// Cleanup effect
|
||||||
@@ -689,24 +674,19 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
|
autoScroll={false}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Task Filters */}
|
{/* Task Filters */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<ImprovedTaskFilters position="list" />
|
<ImprovedTaskFilters position="list" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Performance Analysis - Only show in development */}
|
{/* Performance Analysis - Only show in development */}
|
||||||
{/* {process.env.NODE_ENV === 'development' && (
|
{/* {process.env.NODE_ENV === 'development' && (
|
||||||
<PerformanceAnalysis projectId={projectId} />
|
<PerformanceAnalysis projectId={projectId} />
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
{/* Fixed Height Task Groups Container - Asana Style */}
|
{/* Fixed Height Task Groups Container - Asana Style */}
|
||||||
<div className="task-groups-container-fixed">
|
<div className="task-groups-container-fixed">
|
||||||
<div className="task-groups-scrollable">
|
<div className={`task-groups-scrollable${isDragging ? ' lock-scroll' : ''}`}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading-container">
|
<div className="loading-container">
|
||||||
<div className="flex justify-center items-center py-8">
|
<div className="flex justify-center items-center py-8">
|
||||||
@@ -775,14 +755,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
|
|
||||||
<DragOverlay
|
<DragOverlay
|
||||||
adjustScale={false}
|
adjustScale={false}
|
||||||
dropAnimation={{
|
dropAnimation={null}
|
||||||
duration: 200,
|
|
||||||
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
cursor: 'grabbing',
|
|
||||||
zIndex: 9999,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{dragOverlayContent}
|
{dragOverlayContent}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
@@ -1277,6 +1250,10 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
.react-window-list-item {
|
.react-window-list-item {
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-groups-scrollable.lock-scroll {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Task } from '@/types/task-management.types';
|
import { Task } from '@/types/task-management.types';
|
||||||
import { dayjs } from './antd-imports';
|
import { dayjs } from '@/shared/antd-imports';
|
||||||
|
|
||||||
// Performance constants
|
// Performance constants
|
||||||
export const PERFORMANCE_CONSTANTS = {
|
export const PERFORMANCE_CONSTANTS = {
|
||||||
|
|||||||
@@ -15,8 +15,15 @@ import {
|
|||||||
UserOutlined,
|
UserOutlined,
|
||||||
type InputRef,
|
type InputRef,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from './antd-imports';
|
} from '@/shared/antd-imports';
|
||||||
import { DownOutlined, RightOutlined, ExpandAltOutlined, CheckCircleOutlined, MinusCircleOutlined, EyeOutlined, RetweetOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
RightOutlined,
|
||||||
|
ExpandAltOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
MinusCircleOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
RetweetOutlined,
|
||||||
|
} from '@/shared/antd-imports';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Task } from '@/types/task-management.types';
|
import { Task } from '@/types/task-management.types';
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
@@ -68,22 +75,25 @@ const STATUS_COLORS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Memoized sub-components for maximum performance
|
// Memoized sub-components for maximum performance
|
||||||
const DragHandle = React.memo<{ isDarkMode: boolean; attributes: any; listeners: any }>(({ isDarkMode, attributes, listeners }) => (
|
const DragHandle = React.memo<{ isDarkMode: boolean; attributes: any; listeners: any }>(({ isDarkMode, attributes, listeners }) => {
|
||||||
<div
|
return (
|
||||||
className="drag-handle-optimized flex items-center justify-center w-6 h-6 opacity-60 hover:opacity-100"
|
<div
|
||||||
style={{
|
className="drag-handle-optimized flex items-center justify-center w-6 h-6 opacity-60 hover:opacity-100"
|
||||||
transition: 'opacity 0.1s ease', // Faster transition
|
style={{
|
||||||
}}
|
transition: 'opacity 0.1s ease', // Faster transition
|
||||||
data-dnd-drag-handle="true"
|
cursor: 'grab',
|
||||||
{...attributes}
|
}}
|
||||||
{...listeners}
|
data-dnd-drag-handle="true"
|
||||||
>
|
{...attributes}
|
||||||
<HolderOutlined
|
{...listeners}
|
||||||
className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
>
|
||||||
style={{ pointerEvents: 'none' }} // Prevent icon from interfering
|
<HolderOutlined
|
||||||
/>
|
className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
||||||
</div>
|
style={{ pointerEvents: 'none' }} // Prevent icon from interfering
|
||||||
));
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const TaskKey = React.memo<{ taskKey: string; isDarkMode: boolean }>(({ taskKey, isDarkMode }) => (
|
const TaskKey = React.memo<{ taskKey: string; isDarkMode: boolean }>(({ taskKey, isDarkMode }) => (
|
||||||
<span
|
<span
|
||||||
@@ -508,12 +518,8 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition: isDragging ? 'opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1)' : 'none',
|
opacity: isDragging ? 0.5 : 1,
|
||||||
opacity: isDragging ? 0.3 : 1,
|
|
||||||
zIndex: isDragging ? 1000 : 'auto',
|
zIndex: isDragging ? 1000 : 'auto',
|
||||||
// PERFORMANCE OPTIMIZATION: Force GPU acceleration
|
|
||||||
willChange: 'transform, opacity',
|
|
||||||
filter: isDragging ? 'blur(0.5px)' : 'none',
|
|
||||||
};
|
};
|
||||||
}, [transform, isDragging]);
|
}, [transform, isDragging]);
|
||||||
|
|
||||||
@@ -1223,58 +1229,27 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
// Compute theme class
|
// Compute theme class
|
||||||
const themeClass = isDarkMode ? 'dark' : '';
|
const themeClass = isDarkMode ? 'dark' : '';
|
||||||
|
|
||||||
if (isDragging) {
|
|
||||||
console.log('TaskRow isDragging:', task.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRAG OVERLAY: Render simplified version when dragging
|
// DRAG OVERLAY: Render simplified version when dragging
|
||||||
if (isDragOverlay) {
|
if (isDragOverlay) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`drag-overlay-simplified ${themeClass}`}
|
className="drag-overlay-simplified"
|
||||||
style={{
|
style={{
|
||||||
padding: '10px 16px',
|
padding: '8px 12px',
|
||||||
backgroundColor: isDarkMode ? 'rgba(42, 42, 42, 0.95)' : 'rgba(255, 255, 255, 0.95)',
|
backgroundColor: isDarkMode ? '#1f1f1f' : 'white',
|
||||||
border: `1px solid ${isDarkMode ? 'rgba(74, 158, 255, 0.8)' : 'rgba(24, 144, 255, 0.8)'}`,
|
border: `1px solid ${isDarkMode ? '#404040' : '#d9d9d9'}`,
|
||||||
borderRadius: '8px',
|
borderRadius: '4px',
|
||||||
boxShadow: isDarkMode
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||||
? '0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(74, 158, 255, 0.2)'
|
color: isDarkMode ? 'white' : 'black',
|
||||||
: '0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(24, 144, 255, 0.15)',
|
fontSize: '14px',
|
||||||
backdropFilter: 'blur(8px)',
|
fontWeight: '500',
|
||||||
maxWidth: '320px',
|
|
||||||
minWidth: '200px',
|
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
transform: 'scale(1.02)',
|
maxWidth: '300px',
|
||||||
transition: 'none',
|
|
||||||
willChange: 'transform',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
{task.title}
|
||||||
<div
|
|
||||||
className={`drag-handle-icon ${isDarkMode ? 'text-blue-400' : 'text-blue-600'}`}
|
|
||||||
style={{
|
|
||||||
fontSize: '14px',
|
|
||||||
opacity: 0.8,
|
|
||||||
transform: 'translateZ(0)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HolderOutlined />
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`task-title-drag ${isDarkMode ? 'text-gray-100' : 'text-gray-900'}`}
|
|
||||||
title={task.title}
|
|
||||||
style={{
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: 500,
|
|
||||||
letterSpacing: '0.01em',
|
|
||||||
lineHeight: '1.4',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{task.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
|
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { FixedSizeList as List, FixedSizeList } from 'react-window';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -205,7 +205,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
return group.taskIds
|
return group.taskIds
|
||||||
.map((taskId: string) => tasksById[taskId])
|
.map((taskId: string) => tasksById[taskId])
|
||||||
.filter((task: Task | undefined): task is Task => task !== undefined);
|
.filter((task: Task | undefined): task is Task => task !== undefined);
|
||||||
}, [group.taskIds, tasksById]);
|
}, [group.taskIds, tasksById, group.id]);
|
||||||
|
|
||||||
// Calculate selection state for the group checkbox
|
// Calculate selection state for the group checkbox
|
||||||
const selectionState = useMemo(() => {
|
const selectionState = useMemo(() => {
|
||||||
@@ -329,7 +329,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
}, [groupTasks.length]);
|
}, [groupTasks.length]);
|
||||||
|
|
||||||
// Build displayRows array
|
// Build displayRows array
|
||||||
const displayRows = [];
|
const displayRows: Array<{ type: 'task'; task: Task } | { type: 'add-subtask'; parentTask: Task }> = [];
|
||||||
for (let i = 0; i < groupTasks.length; i++) {
|
for (let i = 0; i < groupTasks.length; i++) {
|
||||||
const task = groupTasks[i];
|
const task = groupTasks[i];
|
||||||
displayRows.push({ type: 'task', task });
|
displayRows.push({ type: 'task', task });
|
||||||
@@ -340,6 +340,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const headerScrollRef = useRef<HTMLDivElement>(null);
|
const headerScrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const listRef = useRef<FixedSizeList>(null);
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Throttled scroll handler
|
// PERFORMANCE OPTIMIZATION: Throttled scroll handler
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
@@ -551,14 +552,17 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
contain: 'layout style', // CSS containment for better performance
|
contain: 'layout style', // CSS containment for better performance
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
|
<SortableContext
|
||||||
|
items={group.taskIds}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
{shouldVirtualize ? (
|
{shouldVirtualize ? (
|
||||||
<List
|
<List
|
||||||
height={availableTaskRowsHeight}
|
height={availableTaskRowsHeight}
|
||||||
itemCount={displayRows.length}
|
itemCount={displayRows.length}
|
||||||
itemSize={TASK_ROW_HEIGHT}
|
itemSize={TASK_ROW_HEIGHT}
|
||||||
width={totalTableWidth}
|
width={totalTableWidth}
|
||||||
ref={scrollContainerRef}
|
ref={listRef}
|
||||||
overscanCount={overscanCount}
|
overscanCount={overscanCount}
|
||||||
>
|
>
|
||||||
{({ index, style }) => {
|
{({ index, style }) => {
|
||||||
|
|||||||
@@ -208,6 +208,60 @@ export const refreshTaskProgress = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Async thunk to reorder tasks with API call
|
||||||
|
export const reorderTasksWithAPI = createAsyncThunk(
|
||||||
|
'taskManagement/reorderTasksWithAPI',
|
||||||
|
async ({ taskIds, newOrder, projectId }: { taskIds: string[]; newOrder: number[]; projectId: string }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
// Make API call to update task order
|
||||||
|
const response = await tasksApiService.reorderTasks({
|
||||||
|
taskIds,
|
||||||
|
newOrder,
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.done) {
|
||||||
|
return { taskIds, newOrder };
|
||||||
|
} else {
|
||||||
|
return rejectWithValue('Failed to reorder tasks');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Reorder Tasks API Error:', error);
|
||||||
|
return rejectWithValue('Failed to reorder tasks');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Async thunk to move task between groups with API call
|
||||||
|
export const moveTaskToGroupWithAPI = createAsyncThunk(
|
||||||
|
'taskManagement/moveTaskToGroupWithAPI',
|
||||||
|
async ({ taskId, groupType, groupValue, projectId }: {
|
||||||
|
taskId: string;
|
||||||
|
groupType: 'status' | 'priority' | 'phase';
|
||||||
|
groupValue: string;
|
||||||
|
projectId: string;
|
||||||
|
}, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
// Make API call to update task group
|
||||||
|
const response = await tasksApiService.updateTaskGroup({
|
||||||
|
taskId,
|
||||||
|
groupType,
|
||||||
|
groupValue,
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.done) {
|
||||||
|
return { taskId, groupType, groupValue };
|
||||||
|
} else {
|
||||||
|
return rejectWithValue('Failed to move task');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Move Task API Error:', error);
|
||||||
|
return rejectWithValue('Failed to move task');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const taskManagementSlice = createSlice({
|
const taskManagementSlice = createSlice({
|
||||||
name: 'taskManagement',
|
name: 'taskManagement',
|
||||||
initialState: tasksAdapter.getInitialState(initialState),
|
initialState: tasksAdapter.getInitialState(initialState),
|
||||||
@@ -328,15 +382,6 @@ const taskManagementSlice = createSlice({
|
|||||||
}>) => {
|
}>) => {
|
||||||
const { taskId, fromGroupId, toGroupId, taskUpdate } = action.payload;
|
const { taskId, fromGroupId, toGroupId, taskUpdate } = action.payload;
|
||||||
|
|
||||||
console.log('🔧 moveTaskBetweenGroups action:', {
|
|
||||||
taskId,
|
|
||||||
fromGroupId,
|
|
||||||
toGroupId,
|
|
||||||
taskUpdate,
|
|
||||||
hasGroups: !!state.groups,
|
|
||||||
groupsCount: state.groups?.length || 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the task entity with new values
|
// Update the task entity with new values
|
||||||
tasksAdapter.updateOne(state, {
|
tasksAdapter.updateOne(state, {
|
||||||
id: taskId,
|
id: taskId,
|
||||||
@@ -351,25 +396,15 @@ const taskManagementSlice = createSlice({
|
|||||||
// Remove task from old group
|
// Remove task from old group
|
||||||
const fromGroup = state.groups.find(group => group.id === fromGroupId);
|
const fromGroup = state.groups.find(group => group.id === fromGroupId);
|
||||||
if (fromGroup) {
|
if (fromGroup) {
|
||||||
const beforeCount = fromGroup.taskIds.length;
|
|
||||||
fromGroup.taskIds = fromGroup.taskIds.filter(id => id !== taskId);
|
fromGroup.taskIds = fromGroup.taskIds.filter(id => id !== taskId);
|
||||||
console.log(`🔧 Removed task from ${fromGroup.title}: ${beforeCount} -> ${fromGroup.taskIds.length}`);
|
|
||||||
} else {
|
|
||||||
console.warn('🚨 From group not found:', fromGroupId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add task to new group
|
// Add task to new group
|
||||||
const toGroup = state.groups.find(group => group.id === toGroupId);
|
const toGroup = state.groups.find(group => group.id === toGroupId);
|
||||||
if (toGroup) {
|
if (toGroup) {
|
||||||
const beforeCount = toGroup.taskIds.length;
|
|
||||||
// Add to the end of the group (newest last)
|
// Add to the end of the group (newest last)
|
||||||
toGroup.taskIds.push(taskId);
|
toGroup.taskIds.push(taskId);
|
||||||
console.log(`🔧 Added task to ${toGroup.title}: ${beforeCount} -> ${toGroup.taskIds.length}`);
|
|
||||||
} else {
|
|
||||||
console.warn('🚨 To group not found:', toGroupId);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn('🚨 No groups available for task movement');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -397,7 +432,76 @@ const taskManagementSlice = createSlice({
|
|||||||
changes.phase = groupValue;
|
changes.phase = groupValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the task entity
|
||||||
tasksAdapter.updateOne(state, { id: taskId, changes });
|
tasksAdapter.updateOne(state, { id: taskId, changes });
|
||||||
|
|
||||||
|
// Update groups if they exist
|
||||||
|
if (state.groups && state.groups.length > 0) {
|
||||||
|
// Find the target group
|
||||||
|
const targetGroup = state.groups.find(group => group.id === newGroupId);
|
||||||
|
if (targetGroup) {
|
||||||
|
// Remove task from all groups first
|
||||||
|
state.groups.forEach(group => {
|
||||||
|
group.taskIds = group.taskIds.filter(id => id !== taskId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add task to target group at the specified index
|
||||||
|
if (newIndex >= targetGroup.taskIds.length) {
|
||||||
|
targetGroup.taskIds.push(taskId);
|
||||||
|
} else {
|
||||||
|
targetGroup.taskIds.splice(newIndex, 0, taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Proper reorder action that handles both task entities and group arrays
|
||||||
|
reorderTasksInGroup: (state, action: PayloadAction<{
|
||||||
|
taskId: string;
|
||||||
|
fromGroupId: string;
|
||||||
|
toGroupId: string;
|
||||||
|
fromIndex: number;
|
||||||
|
toIndex: number;
|
||||||
|
groupType: 'status' | 'priority' | 'phase';
|
||||||
|
groupValue: string;
|
||||||
|
}>) => {
|
||||||
|
const { taskId, fromGroupId, toGroupId, fromIndex, toIndex, groupType, groupValue } = action.payload;
|
||||||
|
|
||||||
|
// Update the task entity
|
||||||
|
const changes: Partial<Task> = {
|
||||||
|
order: toIndex,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update group-specific field
|
||||||
|
if (groupType === 'status') {
|
||||||
|
changes.status = groupValue as Task['status'];
|
||||||
|
} else if (groupType === 'priority') {
|
||||||
|
changes.priority = groupValue as Task['priority'];
|
||||||
|
} else if (groupType === 'phase') {
|
||||||
|
changes.phase = groupValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tasksAdapter.updateOne(state, { id: taskId, changes });
|
||||||
|
|
||||||
|
// Update groups if they exist
|
||||||
|
if (state.groups && state.groups.length > 0) {
|
||||||
|
// Remove task from source group
|
||||||
|
const fromGroup = state.groups.find(group => group.id === fromGroupId);
|
||||||
|
if (fromGroup) {
|
||||||
|
fromGroup.taskIds = fromGroup.taskIds.filter(id => id !== taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add task to target group
|
||||||
|
const toGroup = state.groups.find(group => group.id === toGroupId);
|
||||||
|
if (toGroup) {
|
||||||
|
if (toIndex >= toGroup.taskIds.length) {
|
||||||
|
toGroup.taskIds.push(taskId);
|
||||||
|
} else {
|
||||||
|
toGroup.taskIds.splice(toIndex, 0, taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -483,6 +587,7 @@ export const {
|
|||||||
moveTaskToGroup,
|
moveTaskToGroup,
|
||||||
moveTaskBetweenGroups,
|
moveTaskBetweenGroups,
|
||||||
optimisticTaskMove,
|
optimisticTaskMove,
|
||||||
|
reorderTasksInGroup,
|
||||||
setLoading,
|
setLoading,
|
||||||
setError,
|
setError,
|
||||||
setSelectedPriorities,
|
setSelectedPriorities,
|
||||||
|
|||||||
@@ -87,7 +87,17 @@ export {
|
|||||||
TableOutlined,
|
TableOutlined,
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
FileOutlined,
|
FileOutlined,
|
||||||
MessageOutlined
|
MessageOutlined,
|
||||||
|
FlagOutlined,
|
||||||
|
GroupOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
InboxOutlined,
|
||||||
|
PaperClipOutlined,
|
||||||
|
HolderOutlined,
|
||||||
|
ExpandAltOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
MinusCircleOutlined,
|
||||||
|
RetweetOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
// Re-export all components with React
|
// Re-export all components with React
|
||||||
@@ -196,4 +206,48 @@ export default {
|
|||||||
config: antdConfig,
|
config: antdConfig,
|
||||||
message: appMessage,
|
message: appMessage,
|
||||||
notification: appNotification,
|
notification: appNotification,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Commonly used Ant Design configurations for task management
|
||||||
|
export const taskManagementAntdConfig = {
|
||||||
|
// DatePicker default props for consistency
|
||||||
|
datePickerDefaults: {
|
||||||
|
format: 'MMM DD, YYYY',
|
||||||
|
placeholder: 'Set Date',
|
||||||
|
suffixIcon: null,
|
||||||
|
size: 'small' as const,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Button default props for task actions
|
||||||
|
taskButtonDefaults: {
|
||||||
|
size: 'small' as const,
|
||||||
|
type: 'text' as const,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Input default props for task editing
|
||||||
|
taskInputDefaults: {
|
||||||
|
size: 'small' as const,
|
||||||
|
variant: 'borderless' as const,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Select default props for dropdowns
|
||||||
|
taskSelectDefaults: {
|
||||||
|
size: 'small' as const,
|
||||||
|
variant: 'borderless' as const,
|
||||||
|
showSearch: true,
|
||||||
|
optionFilterProp: 'label' as const,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tooltip default props
|
||||||
|
tooltipDefaults: {
|
||||||
|
placement: 'top' as const,
|
||||||
|
mouseEnterDelay: 0.5,
|
||||||
|
mouseLeaveDelay: 0.1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dropdown default props
|
||||||
|
dropdownDefaults: {
|
||||||
|
trigger: ['click'] as const,
|
||||||
|
placement: 'bottomLeft' as const,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user