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 { Tooltip } from 'antd';
|
||||||
import { Label } from '@/types/task-management.types';
|
import { Label } from '@/types/task-management.types';
|
||||||
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||||
import { ALPHA_CHANNEL } from '@/shared/constants';
|
|
||||||
|
|
||||||
interface CustomColordLabelProps {
|
interface CustomColordLabelProps {
|
||||||
label: Label | ITaskLabel;
|
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;
|
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name;
|
||||||
|
|
||||||
// Handle different color property names for different types
|
// 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
|
// Function to determine if we should use white or black text based on background color
|
||||||
const backgroundColor = baseColor + ALPHA_CHANNEL;
|
const getTextColor = (bgColor: string): string => {
|
||||||
const textColor = baseColor;
|
// 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 (
|
return (
|
||||||
<Tooltip title={label.name}>
|
<Tooltip title={label.name}>
|
||||||
@@ -29,7 +43,7 @@ const CustomColordLabel = React.forwardRef<HTMLSpanElement, CustomColordLabelPro
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
color: textColor,
|
color: textColor,
|
||||||
border: `1px solid ${baseColor}`,
|
border: `1px solid ${backgroundColor}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="truncate">{truncatedName}</span>
|
<span className="truncate">{truncatedName}</span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import { NumbersColorMap, ALPHA_CHANNEL } from '@/shared/constants';
|
import { NumbersColorMap } from '@/shared/constants';
|
||||||
|
|
||||||
interface CustomNumberLabelProps {
|
interface CustomNumberLabelProps {
|
||||||
labelList: string[];
|
labelList: string[];
|
||||||
@@ -12,24 +12,17 @@ interface CustomNumberLabelProps {
|
|||||||
const CustomNumberLabel = React.forwardRef<HTMLSpanElement, CustomNumberLabelProps>(
|
const CustomNumberLabel = React.forwardRef<HTMLSpanElement, CustomNumberLabelProps>(
|
||||||
({ labelList, namesString, isDarkMode = false, color }, ref) => {
|
({ labelList, namesString, isDarkMode = false, color }, ref) => {
|
||||||
// Use provided color, or fall back to NumbersColorMap based on first digit
|
// 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';
|
const firstDigit = namesString.match(/\d/)?.[0] || '0';
|
||||||
return NumbersColorMap[firstDigit] || NumbersColorMap['0'];
|
return NumbersColorMap[firstDigit] || NumbersColorMap['0'];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Add alpha channel to the base color
|
|
||||||
const backgroundColor = baseColor + ALPHA_CHANNEL;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={labelList.join(', ')}>
|
<Tooltip title={labelList.join(', ')}>
|
||||||
<span
|
<span
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium cursor-help"
|
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium cursor-help"
|
||||||
style={{
|
style={{ backgroundColor, color: 'white' }}
|
||||||
backgroundColor,
|
|
||||||
color: baseColor,
|
|
||||||
border: `1px solid ${baseColor}`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{namesString}
|
{namesString}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -959,8 +959,26 @@ const taskManagementSlice = createSlice({
|
|||||||
.addCase(fetchTasksV3.fulfilled, (state, action) => {
|
.addCase(fetchTasksV3.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
const { allTasks, groups, grouping } = action.payload;
|
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.groups = groups;
|
||||||
state.grouping = grouping;
|
state.grouping = grouping;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
updateTaskDescription,
|
updateTaskDescription,
|
||||||
updateSubTasks,
|
updateSubTasks,
|
||||||
updateTaskProgress,
|
updateTaskProgress,
|
||||||
|
updateTaskTimeTracking,
|
||||||
} from '@/features/tasks/tasks.slice';
|
} from '@/features/tasks/tasks.slice';
|
||||||
import {
|
import {
|
||||||
addTask,
|
addTask,
|
||||||
@@ -936,6 +937,8 @@ export const useTaskSocketHandlers = () => {
|
|||||||
const { task_id, start_time } = typeof data === 'string' ? JSON.parse(data) : data;
|
const { task_id, start_time } = typeof data === 'string' ? JSON.parse(data) : data;
|
||||||
if (!task_id) return;
|
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
|
// Update the task-management slice to include timer state
|
||||||
const currentTask = store.getState().taskManagement.entities[task_id];
|
const currentTask = store.getState().taskManagement.entities[task_id];
|
||||||
if (currentTask) {
|
if (currentTask) {
|
||||||
@@ -943,13 +946,16 @@ export const useTaskSocketHandlers = () => {
|
|||||||
...currentTask,
|
...currentTask,
|
||||||
timeTracking: {
|
timeTracking: {
|
||||||
...currentTask.timeTracking,
|
...currentTask.timeTracking,
|
||||||
activeTimer: start_time ? (typeof start_time === 'number' ? start_time : parseInt(start_time)) : Date.now(),
|
activeTimer: timerTimestamp,
|
||||||
},
|
},
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
dispatch(updateTask(updatedTask));
|
dispatch(updateTask(updatedTask));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also update the tasks slice activeTimers to keep both slices in sync
|
||||||
|
dispatch(updateTaskTimeTracking({ taskId: task_id, timeTracking: timerTimestamp }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error handling timer start event:', error);
|
logger.error('Error handling timer start event:', error);
|
||||||
}
|
}
|
||||||
@@ -975,6 +981,9 @@ export const useTaskSocketHandlers = () => {
|
|||||||
};
|
};
|
||||||
dispatch(updateTask(updatedTask));
|
dispatch(updateTask(updatedTask));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also update the tasks slice activeTimers to keep both slices in sync
|
||||||
|
dispatch(updateTaskTimeTracking({ taskId: task_id, timeTracking: null }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error handling timer stop event:', error);
|
logger.error('Error handling timer stop event:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
|
|||||||
|
|
||||||
// Timer management effect
|
// Timer management effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (started && localStarted && reduxStartTime) {
|
if (started && reduxStartTime) {
|
||||||
|
// Sync local state with Redux state
|
||||||
|
if (!localStarted) {
|
||||||
|
setLocalStarted(true);
|
||||||
|
}
|
||||||
clearTimerInterval();
|
clearTimerInterval();
|
||||||
timerTick();
|
timerTick();
|
||||||
intervalRef.current = setInterval(timerTick, 1000);
|
intervalRef.current = setInterval(timerTick, 1000);
|
||||||
|
|||||||
Reference in New Issue
Block a user