feat(task-management): add progress statistics and visual representation for task groups
- Implemented progress calculations for tasks grouped by priority and phase, including todo, doing, and done counts. - Introduced a new GroupProgressBar component to visually represent task progress in the TaskGroupHeader. - Updated TaskGroupHeader and TaskListV2Table to integrate progress data and display the progress bar conditionally based on grouping. - Enhanced local storage handling for grouping preferences in the task management feature.
This commit is contained in:
@@ -1174,9 +1174,39 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate progress stats for priority and phase grouping
|
||||
if (groupBy === GroupBy.PRIORITY || groupBy === GroupBy.PHASE) {
|
||||
Object.values(groupedResponse).forEach((group: any) => {
|
||||
if (group.tasks && group.tasks.length > 0) {
|
||||
const todoCount = group.tasks.filter((task: any) => {
|
||||
// For tasks, we need to check their original status category
|
||||
const originalTask = tasks.find(t => t.id === task.id);
|
||||
return originalTask?.status_category?.is_todo;
|
||||
}).length;
|
||||
|
||||
const doingCount = group.tasks.filter((task: any) => {
|
||||
const originalTask = tasks.find(t => t.id === task.id);
|
||||
return originalTask?.status_category?.is_doing;
|
||||
}).length;
|
||||
|
||||
const doneCount = group.tasks.filter((task: any) => {
|
||||
const originalTask = tasks.find(t => t.id === task.id);
|
||||
return originalTask?.status_category?.is_done;
|
||||
}).length;
|
||||
|
||||
const total = group.tasks.length;
|
||||
|
||||
// Calculate progress percentages
|
||||
group.todo_progress = total > 0 ? +((todoCount / total) * 100).toFixed(0) : 0;
|
||||
group.doing_progress = total > 0 ? +((doingCount / total) * 100).toFixed(0) : 0;
|
||||
group.done_progress = total > 0 ? +((doneCount / total) * 100).toFixed(0) : 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create unmapped group if there are tasks without proper phase assignment
|
||||
if (unmappedTasks.length > 0 && groupBy === GroupBy.PHASE) {
|
||||
groupedResponse[UNMAPPED.toLowerCase()] = {
|
||||
const unmappedGroup = {
|
||||
id: UNMAPPED,
|
||||
title: UNMAPPED,
|
||||
groupType: groupBy,
|
||||
@@ -1189,7 +1219,36 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
sort_index: 999, // Put unmapped group at the end
|
||||
todo_progress: 0,
|
||||
doing_progress: 0,
|
||||
done_progress: 0,
|
||||
};
|
||||
|
||||
// Calculate progress stats for unmapped group
|
||||
if (unmappedTasks.length > 0) {
|
||||
const todoCount = unmappedTasks.filter((task: any) => {
|
||||
const originalTask = tasks.find(t => t.id === task.id);
|
||||
return originalTask?.status_category?.is_todo;
|
||||
}).length;
|
||||
|
||||
const doingCount = unmappedTasks.filter((task: any) => {
|
||||
const originalTask = tasks.find(t => t.id === task.id);
|
||||
return originalTask?.status_category?.is_doing;
|
||||
}).length;
|
||||
|
||||
const doneCount = unmappedTasks.filter((task: any) => {
|
||||
const originalTask = tasks.find(t => t.id === task.id);
|
||||
return originalTask?.status_category?.is_done;
|
||||
}).length;
|
||||
|
||||
const total = unmappedTasks.length;
|
||||
|
||||
unmappedGroup.todo_progress = total > 0 ? +((todoCount / total) * 100).toFixed(0) : 0;
|
||||
unmappedGroup.doing_progress = total > 0 ? +((doingCount / total) * 100).toFixed(0) : 0;
|
||||
unmappedGroup.done_progress = total > 0 ? +((doneCount / total) * 100).toFixed(0) : 0;
|
||||
}
|
||||
|
||||
groupedResponse[UNMAPPED.toLowerCase()] = unmappedGroup;
|
||||
}
|
||||
|
||||
// Sort tasks within each group by order
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface GroupProgressBarProps {
|
||||
todoProgress: number;
|
||||
doingProgress: number;
|
||||
doneProgress: number;
|
||||
groupType: string;
|
||||
}
|
||||
|
||||
const GroupProgressBar: React.FC<GroupProgressBarProps> = ({
|
||||
todoProgress,
|
||||
doingProgress,
|
||||
doneProgress,
|
||||
groupType
|
||||
}) => {
|
||||
const { t } = useTranslation('task-management');
|
||||
|
||||
// Only show for priority and phase grouping
|
||||
if (groupType !== 'priority' && groupType !== 'phase') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const total = todoProgress + doingProgress + doneProgress;
|
||||
|
||||
// Don't show if no progress values exist
|
||||
if (total === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Compact progress text */}
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap font-medium">
|
||||
{doneProgress}% {t('done')}
|
||||
</span>
|
||||
|
||||
{/* Compact progress bar */}
|
||||
<div className="w-20 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5 overflow-hidden">
|
||||
<div className="h-full flex">
|
||||
{/* Todo section - light gray */}
|
||||
{todoProgress > 0 && (
|
||||
<div
|
||||
className="bg-gray-300 dark:bg-gray-600 transition-all duration-300"
|
||||
style={{ width: `${(todoProgress / total) * 100}%` }}
|
||||
title={`${t('todo')}: ${todoProgress}%`}
|
||||
/>
|
||||
)}
|
||||
{/* Doing section - blue */}
|
||||
{doingProgress > 0 && (
|
||||
<div
|
||||
className="bg-blue-500 transition-all duration-300"
|
||||
style={{ width: `${(doingProgress / total) * 100}%` }}
|
||||
title={`${t('inProgress')}: ${doingProgress}%`}
|
||||
/>
|
||||
)}
|
||||
{/* Done section - green */}
|
||||
{doneProgress > 0 && (
|
||||
<div
|
||||
className="bg-green-500 transition-all duration-300"
|
||||
style={{ width: `${(doneProgress / total) * 100}%` }}
|
||||
title={`${t('done')}: ${doneProgress}%`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Small legend dots with better spacing */}
|
||||
<div className="flex items-center gap-1">
|
||||
{todoProgress > 0 && (
|
||||
<div
|
||||
className="w-1.5 h-1.5 bg-gray-300 dark:bg-gray-600 rounded-full"
|
||||
title={`${t('todo')}: ${todoProgress}%`}
|
||||
/>
|
||||
)}
|
||||
{doingProgress > 0 && (
|
||||
<div
|
||||
className="w-1.5 h-1.5 bg-blue-500 rounded-full"
|
||||
title={`${t('inProgress')}: ${doingProgress}%`}
|
||||
/>
|
||||
)}
|
||||
{doneProgress > 0 && (
|
||||
<div
|
||||
className="w-1.5 h-1.5 bg-green-500 rounded-full"
|
||||
title={`${t('done')}: ${doneProgress}%`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupProgressBar;
|
||||
@@ -3,6 +3,7 @@ import { useDroppable } from '@dnd-kit/core';
|
||||
// @ts-ignore: Heroicons module types
|
||||
import { ChevronDownIcon, ChevronRightIcon, EllipsisHorizontalIcon, PencilIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||
import { Checkbox, Dropdown, Menu, Input, Modal, Badge, Flex } from 'antd';
|
||||
import GroupProgressBar from './GroupProgressBar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getContrastColor } from '@/utils/colorUtils';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
@@ -27,6 +28,10 @@ interface TaskGroupHeaderProps {
|
||||
name: string;
|
||||
count: number;
|
||||
color?: string; // Color for the group indicator
|
||||
todo_progress?: number;
|
||||
doing_progress?: number;
|
||||
done_progress?: number;
|
||||
groupType?: string;
|
||||
};
|
||||
isCollapsed: boolean;
|
||||
onToggle: () => void;
|
||||
@@ -44,7 +49,7 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
const { isOwnerOrAdmin } = useAuthService();
|
||||
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
const [categoryModalVisible, setCategoryModalVisible] = useState(false);
|
||||
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [isChangingCategory, setIsChangingCategory] = useState(false);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
@@ -94,7 +99,12 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
|
||||
// Handle inline name editing
|
||||
const handleNameSave = useCallback(async () => {
|
||||
if (!editingName.trim() || editingName.trim() === group.name || isRenaming) return;
|
||||
// If no changes or already renaming, just exit editing mode
|
||||
if (!editingName.trim() || editingName.trim() === group.name || isRenaming) {
|
||||
setIsEditingName(false);
|
||||
setEditingName(group.name);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRenaming(true);
|
||||
try {
|
||||
@@ -122,12 +132,12 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
|
||||
// Refresh task list to get updated group names
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
setIsEditingName(false);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error renaming group:', error);
|
||||
setEditingName(group.name);
|
||||
} finally {
|
||||
setIsEditingName(false);
|
||||
setIsRenaming(false);
|
||||
}
|
||||
}, [editingName, group.name, group.id, currentGrouping, projectId, dispatch, trackMixpanelEvent, isRenaming]);
|
||||
@@ -150,9 +160,8 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
}, [group.name, handleNameSave]);
|
||||
|
||||
const handleNameBlur = useCallback(() => {
|
||||
setIsEditingName(false);
|
||||
setEditingName(group.name);
|
||||
}, [group.name]);
|
||||
handleNameSave();
|
||||
}, [handleNameSave]);
|
||||
|
||||
// Handle dropdown menu actions
|
||||
const handleRenameGroup = useCallback(() => {
|
||||
@@ -161,10 +170,7 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
setEditingName(group.name);
|
||||
}, [group.name]);
|
||||
|
||||
const handleChangeCategory = useCallback(() => {
|
||||
setDropdownVisible(false);
|
||||
setCategoryModalVisible(true);
|
||||
}, []);
|
||||
|
||||
|
||||
// Handle category change
|
||||
const handleCategoryChange = useCallback(async (categoryId: string, e?: React.MouseEvent) => {
|
||||
@@ -182,7 +188,6 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
// Refresh status list and tasks
|
||||
dispatch(fetchStatuses(projectId));
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
setCategoryModalVisible(false);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error changing category:', error);
|
||||
@@ -209,19 +214,30 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
|
||||
// Only show "Change Category" when grouped by status
|
||||
if (currentGrouping === 'status') {
|
||||
items.push({
|
||||
key: 'changeCategory',
|
||||
icon: <ArrowPathIcon className="h-4 w-4" />,
|
||||
label: t('changeCategory'),
|
||||
const categorySubMenuItems = statusCategories.map((category) => ({
|
||||
key: `category-${category.id}`,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color={category.color_code} />
|
||||
<span>{category.name}</span>
|
||||
</div>
|
||||
),
|
||||
onClick: (e: any) => {
|
||||
e?.domEvent?.stopPropagation();
|
||||
handleChangeCategory();
|
||||
handleCategoryChange(category.id || '', e?.domEvent);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
items.push({
|
||||
key: 'changeCategory',
|
||||
icon: <ArrowPathIcon className="h-4 w-4" />,
|
||||
label: t('changeCategory'),
|
||||
children: categorySubMenuItems,
|
||||
} as any);
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [currentGrouping, handleRenameGroup, handleChangeCategory, isOwnerOrAdmin]);
|
||||
}, [currentGrouping, handleRenameGroup, handleCategoryChange, isOwnerOrAdmin, statusCategories, t]);
|
||||
|
||||
// Make the group header droppable
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
@@ -232,75 +248,146 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`inline-flex w-max items-center px-1 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-t border-b border-gray-200 dark:border-gray-700 rounded-t-md pr-2 ${
|
||||
isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isOver ? `${headerBackgroundColor}dd` : headerBackgroundColor,
|
||||
color: headerTextColor,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 25, // Higher than task rows but lower than column headers (z-30)
|
||||
height: '36px',
|
||||
minHeight: '36px',
|
||||
maxHeight: '36px'
|
||||
}}
|
||||
onClick={onToggle}
|
||||
>
|
||||
{/* Drag Handle Space - ultra minimal width */}
|
||||
<div style={{ width: '20px' }} className="flex items-center justify-center">
|
||||
{/* Chevron button */}
|
||||
<button
|
||||
className="p-0 rounded-sm hover:shadow-lg hover:scale-105 transition-all duration-300 ease-out"
|
||||
style={{ backgroundColor: 'transparent', color: headerTextColor }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="transition-transform duration-300 ease-out"
|
||||
style={{
|
||||
transform: isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
||||
transformOrigin: 'center'
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`inline-flex w-max items-center px-1 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-t border-b border-gray-200 dark:border-gray-700 rounded-t-md pr-2 ${
|
||||
isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isOver ? `${headerBackgroundColor}dd` : headerBackgroundColor,
|
||||
color: headerTextColor,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 25, // Higher than task rows but lower than column headers (z-30)
|
||||
height: '36px',
|
||||
minHeight: '36px',
|
||||
maxHeight: '36px'
|
||||
}}
|
||||
onClick={onToggle}
|
||||
>
|
||||
{/* Drag Handle Space - ultra minimal width */}
|
||||
<div style={{ width: '20px' }} className="flex items-center justify-center">
|
||||
{/* Chevron button */}
|
||||
<button
|
||||
className="p-0 rounded-sm hover:shadow-lg hover:scale-105 transition-all duration-300 ease-out"
|
||||
style={{ backgroundColor: 'transparent', color: headerTextColor }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon className="h-3 w-3" style={{ color: headerTextColor }} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="transition-transform duration-300 ease-out"
|
||||
style={{
|
||||
transform: isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
||||
transformOrigin: 'center'
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon className="h-3 w-3" style={{ color: headerTextColor }} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Select All Checkbox Space - ultra minimal width */}
|
||||
<div style={{ width: '28px' }} className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
indeterminate={isPartiallySelected}
|
||||
onChange={handleSelectAllChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
color: headerTextColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Select All Checkbox Space - ultra minimal width */}
|
||||
<div style={{ width: '28px' }} className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
indeterminate={isPartiallySelected}
|
||||
onChange={handleSelectAllChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
color: headerTextColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Group indicator and name - no gap at all */}
|
||||
{/* Group indicator and name - no gap at all */}
|
||||
<div className="flex items-center flex-1 ml-1">
|
||||
{/* Group name and count */}
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className="text-sm font-semibold pr-2"
|
||||
style={{ color: headerTextColor }}
|
||||
>
|
||||
{group.name}
|
||||
</span>
|
||||
{isEditingName ? (
|
||||
<Input
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
onBlur={handleNameBlur}
|
||||
autoFocus
|
||||
size="small"
|
||||
className="text-sm font-semibold"
|
||||
style={{
|
||||
width: 'auto',
|
||||
minWidth: '100px',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: headerTextColor,
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="text-sm font-semibold pr-2 cursor-pointer hover:underline"
|
||||
style={{ color: headerTextColor }}
|
||||
onClick={handleNameClick}
|
||||
>
|
||||
{group.name}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm font-semibold ml-1" style={{ color: headerTextColor }}>
|
||||
({group.count})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Three-dot menu - only show for status and phase grouping */}
|
||||
{menuItems.length > 0 && (currentGrouping === 'status' || currentGrouping === 'phase') && (
|
||||
<div className="flex items-center ml-2">
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
trigger={['click']}
|
||||
open={dropdownVisible}
|
||||
onOpenChange={setDropdownVisible}
|
||||
placement="bottomRight"
|
||||
overlayStyle={{ zIndex: 1000 }}
|
||||
>
|
||||
<button
|
||||
className="p-1 rounded-sm hover:bg-black hover:bg-opacity-10 transition-colors duration-200"
|
||||
style={{ color: headerTextColor }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDropdownVisible(!dropdownVisible);
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Progress Bar - sticky to the right edge during horizontal scroll */}
|
||||
{(currentGrouping === 'priority' || currentGrouping === 'phase') &&
|
||||
(group.todo_progress || group.doing_progress || group.done_progress) && (
|
||||
<div
|
||||
className="flex items-center bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm px-3 py-1.5 ml-auto"
|
||||
style={{
|
||||
position: 'sticky',
|
||||
right: '16px',
|
||||
zIndex: 35, // Higher than header
|
||||
minWidth: '160px',
|
||||
height: '30px'
|
||||
}}
|
||||
>
|
||||
<GroupProgressBar
|
||||
todoProgress={group.todo_progress || 0}
|
||||
doingProgress={group.doing_progress || 0}
|
||||
doneProgress={group.done_progress || 0}
|
||||
groupType={group.groupType || currentGrouping || ''}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -452,6 +452,10 @@ const TaskListV2Section: React.FC = () => {
|
||||
name: group.title,
|
||||
count: group.actualCount,
|
||||
color: group.color,
|
||||
todo_progress: group.todo_progress,
|
||||
doing_progress: group.doing_progress,
|
||||
done_progress: group.done_progress,
|
||||
groupType: group.groupType,
|
||||
}}
|
||||
isCollapsed={isGroupCollapsed}
|
||||
onToggle={() => handleGroupCollapse(group.id)}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const GROUP_BY_OPTIONS: IGroupByOption[] = [
|
||||
{ label: 'Phase', value: IGroupBy.PHASE },
|
||||
];
|
||||
|
||||
const LOCALSTORAGE_GROUP_KEY = 'worklenz.enhanced-kanban.group_by';
|
||||
const LOCALSTORAGE_GROUP_KEY = 'worklenz.kanban.group_by';
|
||||
|
||||
export const getCurrentGroup = (): IGroupBy => {
|
||||
const key = localStorage.getItem(LOCALSTORAGE_GROUP_KEY);
|
||||
|
||||
@@ -17,8 +17,36 @@ interface LocalGroupingState {
|
||||
collapsedGroups: string[];
|
||||
}
|
||||
|
||||
// Local storage constants
|
||||
const LOCALSTORAGE_GROUP_KEY = 'worklenz.tasklist.group_by';
|
||||
|
||||
// Utility functions for local storage
|
||||
const loadGroupingFromLocalStorage = (): GroupingType | null => {
|
||||
try {
|
||||
const stored = localStorage.getItem(LOCALSTORAGE_GROUP_KEY);
|
||||
if (stored && ['status', 'priority', 'phase'].includes(stored)) {
|
||||
return stored as GroupingType;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load grouping from localStorage:', error);
|
||||
}
|
||||
return 'status'; // Default to 'status' instead of null
|
||||
};
|
||||
|
||||
const saveGroupingToLocalStorage = (grouping: GroupingType | null): void => {
|
||||
try {
|
||||
if (grouping) {
|
||||
localStorage.setItem(LOCALSTORAGE_GROUP_KEY, grouping);
|
||||
} else {
|
||||
localStorage.removeItem(LOCALSTORAGE_GROUP_KEY);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to save grouping to localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const initialState: LocalGroupingState = {
|
||||
currentGrouping: null,
|
||||
currentGrouping: loadGroupingFromLocalStorage(),
|
||||
customPhases: ['Planning', 'Development', 'Testing', 'Deployment'],
|
||||
groupOrder: {
|
||||
status: ['todo', 'doing', 'done'],
|
||||
@@ -35,6 +63,7 @@ const groupingSlice = createSlice({
|
||||
reducers: {
|
||||
setCurrentGrouping: (state, action: PayloadAction<GroupingType | null>) => {
|
||||
state.currentGrouping = action.payload;
|
||||
saveGroupingToLocalStorage(action.payload);
|
||||
},
|
||||
|
||||
addCustomPhase: (state, action: PayloadAction<string>) => {
|
||||
|
||||
Reference in New Issue
Block a user