feat(task-management): enhance task timer synchronization and color handling
- Updated `CustomColordLabel` and `CustomNumberLabel` components to improve color handling by removing the alpha channel logic and implementing a dynamic text color based on background luminance. - Enhanced task management slice to preserve timer state when fetching tasks, ensuring active timers are maintained across updates. - Modified socket handlers to synchronize timer state between task slices, improving consistency in task time tracking. - Refactored `useTaskTimer` hook to streamline local and Redux state synchronization for timer management.
This commit is contained in:
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Label } from '@/types/task-management.types';
|
||||
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||
import { ALPHA_CHANNEL } from '@/shared/constants';
|
||||
|
||||
interface CustomColordLabelProps {
|
||||
label: Label | ITaskLabel;
|
||||
@@ -15,11 +14,26 @@ const CustomColordLabel = React.forwardRef<HTMLSpanElement, CustomColordLabelPro
|
||||
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name;
|
||||
|
||||
// Handle different color property names for different types
|
||||
const baseColor = (label as Label).color || (label as ITaskLabel).color_code || '#6b7280'; // Default to gray-500 if no color
|
||||
const backgroundColor = (label as Label).color || (label as ITaskLabel).color_code || '#6b7280'; // Default to gray-500 if no color
|
||||
|
||||
// Add alpha channel to the base color
|
||||
const backgroundColor = baseColor + ALPHA_CHANNEL;
|
||||
const textColor = baseColor;
|
||||
// Function to determine if we should use white or black text based on background color
|
||||
const getTextColor = (bgColor: string): string => {
|
||||
// Remove # if present
|
||||
const color = bgColor.replace('#', '');
|
||||
|
||||
// Convert to RGB
|
||||
const r = parseInt(color.substr(0, 2), 16);
|
||||
const g = parseInt(color.substr(2, 2), 16);
|
||||
const b = parseInt(color.substr(4, 2), 16);
|
||||
|
||||
// Calculate luminance
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
|
||||
// Return white for dark backgrounds, black for light backgrounds
|
||||
return luminance > 0.5 ? '#000000' : '#ffffff';
|
||||
};
|
||||
|
||||
const textColor = getTextColor(backgroundColor);
|
||||
|
||||
return (
|
||||
<Tooltip title={label.name}>
|
||||
@@ -29,7 +43,7 @@ const CustomColordLabel = React.forwardRef<HTMLSpanElement, CustomColordLabelPro
|
||||
style={{
|
||||
backgroundColor,
|
||||
color: textColor,
|
||||
border: `1px solid ${baseColor}`,
|
||||
border: `1px solid ${backgroundColor}`,
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{truncatedName}</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { NumbersColorMap, ALPHA_CHANNEL } from '@/shared/constants';
|
||||
import { NumbersColorMap } from '@/shared/constants';
|
||||
|
||||
interface CustomNumberLabelProps {
|
||||
labelList: string[];
|
||||
@@ -12,24 +12,17 @@ interface CustomNumberLabelProps {
|
||||
const CustomNumberLabel = React.forwardRef<HTMLSpanElement, CustomNumberLabelProps>(
|
||||
({ labelList, namesString, isDarkMode = false, color }, ref) => {
|
||||
// Use provided color, or fall back to NumbersColorMap based on first digit
|
||||
const baseColor = color || (() => {
|
||||
const backgroundColor = color || (() => {
|
||||
const firstDigit = namesString.match(/\d/)?.[0] || '0';
|
||||
return NumbersColorMap[firstDigit] || NumbersColorMap['0'];
|
||||
})();
|
||||
|
||||
// Add alpha channel to the base color
|
||||
const backgroundColor = baseColor + ALPHA_CHANNEL;
|
||||
|
||||
return (
|
||||
<Tooltip title={labelList.join(', ')}>
|
||||
<span
|
||||
ref={ref}
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium cursor-help"
|
||||
style={{
|
||||
backgroundColor,
|
||||
color: baseColor,
|
||||
border: `1px solid ${baseColor}`,
|
||||
}}
|
||||
style={{ backgroundColor, color: 'white' }}
|
||||
>
|
||||
{namesString}
|
||||
</span>
|
||||
|
||||
@@ -959,8 +959,26 @@ const taskManagementSlice = createSlice({
|
||||
.addCase(fetchTasksV3.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
const { allTasks, groups, grouping } = action.payload;
|
||||
tasksAdapter.setAll(state as EntityState<Task, string>, allTasks || []); // Ensure allTasks is an array
|
||||
state.ids = (allTasks || []).map(task => task.id); // Also update ids
|
||||
|
||||
// Preserve existing timer state from old tasks before replacing
|
||||
const oldTasks = state.entities;
|
||||
const tasksWithTimers = (allTasks || []).map(task => {
|
||||
const oldTask = oldTasks[task.id];
|
||||
if (oldTask?.timeTracking?.activeTimer) {
|
||||
// Preserve the timer state from the old task
|
||||
return {
|
||||
...task,
|
||||
timeTracking: {
|
||||
...task.timeTracking,
|
||||
activeTimer: oldTask.timeTracking.activeTimer
|
||||
}
|
||||
};
|
||||
}
|
||||
return task;
|
||||
});
|
||||
|
||||
tasksAdapter.setAll(state as EntityState<Task, string>, tasksWithTimers); // Ensure allTasks is an array
|
||||
state.ids = tasksWithTimers.map(task => task.id); // Also update ids
|
||||
state.groups = groups;
|
||||
state.grouping = grouping;
|
||||
})
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
updateTaskDescription,
|
||||
updateSubTasks,
|
||||
updateTaskProgress,
|
||||
updateTaskTimeTracking,
|
||||
} from '@/features/tasks/tasks.slice';
|
||||
import {
|
||||
addTask,
|
||||
@@ -936,6 +937,8 @@ export const useTaskSocketHandlers = () => {
|
||||
const { task_id, start_time } = typeof data === 'string' ? JSON.parse(data) : data;
|
||||
if (!task_id) return;
|
||||
|
||||
const timerTimestamp = start_time ? (typeof start_time === 'number' ? start_time : parseInt(start_time)) : Date.now();
|
||||
|
||||
// Update the task-management slice to include timer state
|
||||
const currentTask = store.getState().taskManagement.entities[task_id];
|
||||
if (currentTask) {
|
||||
@@ -943,13 +946,16 @@ export const useTaskSocketHandlers = () => {
|
||||
...currentTask,
|
||||
timeTracking: {
|
||||
...currentTask.timeTracking,
|
||||
activeTimer: start_time ? (typeof start_time === 'number' ? start_time : parseInt(start_time)) : Date.now(),
|
||||
activeTimer: timerTimestamp,
|
||||
},
|
||||
updatedAt: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
dispatch(updateTask(updatedTask));
|
||||
}
|
||||
|
||||
// Also update the tasks slice activeTimers to keep both slices in sync
|
||||
dispatch(updateTaskTimeTracking({ taskId: task_id, timeTracking: timerTimestamp }));
|
||||
} catch (error) {
|
||||
logger.error('Error handling timer start event:', error);
|
||||
}
|
||||
@@ -975,6 +981,9 @@ export const useTaskSocketHandlers = () => {
|
||||
};
|
||||
dispatch(updateTask(updatedTask));
|
||||
}
|
||||
|
||||
// Also update the tasks slice activeTimers to keep both slices in sync
|
||||
dispatch(updateTaskTimeTracking({ taskId: task_id, timeTracking: null }));
|
||||
} catch (error) {
|
||||
logger.error('Error handling timer stop event:', error);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,11 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
||||
|
||||
// Timer management effect
|
||||
useEffect(() => {
|
||||
if (started && localStarted && reduxStartTime) {
|
||||
if (started && reduxStartTime) {
|
||||
// Sync local state with Redux state
|
||||
if (!localStarted) {
|
||||
setLocalStarted(true);
|
||||
}
|
||||
clearTimerInterval();
|
||||
timerTick();
|
||||
intervalRef.current = setInterval(timerTick, 1000);
|
||||
|
||||
Reference in New Issue
Block a user