feat(task-management): implement new task management features with BulkActionBar and task grouping

- Introduced BulkActionBar component for bulk actions on selected tasks, including status, priority, and assignee changes.
- Added TaskGroup and TaskRow components to enhance task organization and display.
- Implemented grouping functionality with GroupingSelector for improved task categorization.
- Enhanced drag-and-drop capabilities for task reordering within groups.
- Updated styling and responsiveness across task management components for better user experience.
This commit is contained in:
chamikaJ
2025-06-20 10:56:48 +05:30
parent c1e6689beb
commit 5221061241
9 changed files with 607 additions and 185 deletions

View File

@@ -1,174 +0,0 @@
import React from 'react';
import { Card, Button, Space, Typography, Dropdown, Menu, Popconfirm, message } from 'antd';
import {
DeleteOutlined,
EditOutlined,
TagOutlined,
UserOutlined,
CheckOutlined,
CloseOutlined,
MoreOutlined,
} from '@ant-design/icons';
import { useDispatch, useSelector } from 'react-redux';
import { IGroupBy, bulkUpdateTasks, bulkDeleteTasks } from '@/features/tasks/tasks.slice';
import { AppDispatch, RootState } from '@/app/store';
const { Text } = Typography;
interface BulkActionBarProps {
selectedTaskIds: string[];
totalSelected: number;
currentGrouping: IGroupBy;
projectId: string;
onClearSelection?: () => void;
}
const BulkActionBar: React.FC<BulkActionBarProps> = ({
selectedTaskIds,
totalSelected,
currentGrouping,
projectId,
onClearSelection,
}) => {
const dispatch = useDispatch<AppDispatch>();
const { statuses, priorities } = useSelector((state: RootState) => state.taskReducer);
const handleBulkStatusChange = (statusId: string) => {
// dispatch(bulkUpdateTasks({ ids: selectedTaskIds, changes: { status: statusId } }));
message.success(`Updated ${totalSelected} tasks`);
onClearSelection?.();
};
const handleBulkPriorityChange = (priority: string) => {
// dispatch(bulkUpdateTasks({ ids: selectedTaskIds, changes: { priority } }));
message.success(`Updated ${totalSelected} tasks`);
onClearSelection?.();
};
const handleBulkDelete = () => {
// dispatch(bulkDeleteTasks(selectedTaskIds));
message.success(`Deleted ${totalSelected} tasks`);
onClearSelection?.();
};
const statusMenu = (
<Menu
onClick={({ key }) => handleBulkStatusChange(key)}
items={statuses.map(status => ({
key: status.id!,
label: (
<div className="flex items-center space-x-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: status.color_code }}
/>
<span>{status.name}</span>
</div>
),
}))}
/>
);
const priorityMenu = (
<Menu
onClick={({ key }) => handleBulkPriorityChange(key)}
items={[
{ key: 'critical', label: 'Critical', icon: <div className="w-2 h-2 rounded-full bg-red-500" /> },
{ key: 'high', label: 'High', icon: <div className="w-2 h-2 rounded-full bg-orange-500" /> },
{ key: 'medium', label: 'Medium', icon: <div className="w-2 h-2 rounded-full bg-yellow-500" /> },
{ key: 'low', label: 'Low', icon: <div className="w-2 h-2 rounded-full bg-green-500" /> },
]}
/>
);
const moreActionsMenu = (
<Menu
items={[
{
key: 'assign',
label: 'Assign to member',
icon: <UserOutlined />,
},
{
key: 'labels',
label: 'Add labels',
icon: <TagOutlined />,
},
{
key: 'archive',
label: 'Archive tasks',
icon: <EditOutlined />,
},
]}
/>
);
return (
<Card
size="small"
className="mb-4 bg-blue-50 border-blue-200"
styles={{ body: { padding: '8px 16px' } }}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Text strong className="text-blue-700">
{totalSelected} task{totalSelected > 1 ? 's' : ''} selected
</Text>
</div>
<Space>
{/* Status Change */}
{currentGrouping !== 'status' && (
<Dropdown overlay={statusMenu} trigger={['click']}>
<Button size="small" icon={<CheckOutlined />}>
Change Status
</Button>
</Dropdown>
)}
{/* Priority Change */}
{currentGrouping !== 'priority' && (
<Dropdown overlay={priorityMenu} trigger={['click']}>
<Button size="small" icon={<EditOutlined />}>
Set Priority
</Button>
</Dropdown>
)}
{/* More Actions */}
<Dropdown overlay={moreActionsMenu} trigger={['click']}>
<Button size="small" icon={<MoreOutlined />}>
More Actions
</Button>
</Dropdown>
{/* Delete */}
<Popconfirm
title={`Delete ${totalSelected} task${totalSelected > 1 ? 's' : ''}?`}
description="This action cannot be undone."
onConfirm={handleBulkDelete}
okText="Delete"
cancelText="Cancel"
okType="danger"
>
<Button size="small" danger icon={<DeleteOutlined />}>
Delete
</Button>
</Popconfirm>
{/* Clear Selection */}
<Button
size="small"
icon={<CloseOutlined />}
onClick={onClearSelection}
title="Clear selection"
>
Clear
</Button>
</Space>
</div>
</Card>
);
};
export default BulkActionBar;

View File

@@ -0,0 +1,590 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, Typography, Dropdown, Menu, Popconfirm, message, Tooltip, Badge, CheckboxChangeEvent, InputRef } from 'antd';
import {
DeleteOutlined,
EditOutlined,
TagOutlined,
UserOutlined,
CheckOutlined,
CloseOutlined,
MoreOutlined,
RetweetOutlined,
UserAddOutlined,
InboxOutlined,
TagsOutlined,
UsergroupAddOutlined,
} from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { IGroupBy, fetchTaskGroups } from '@/features/tasks/tasks.slice';
import { AppDispatch, RootState } from '@/app/store';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice';
import { taskListBulkActionsApiService } from '@/api/tasks/task-list-bulk-actions.api.service';
import {
evt_project_task_list_bulk_archive,
evt_project_task_list_bulk_assign_me,
evt_project_task_list_bulk_assign_members,
evt_project_task_list_bulk_change_phase,
evt_project_task_list_bulk_change_priority,
evt_project_task_list_bulk_change_status,
evt_project_task_list_bulk_delete,
evt_project_task_list_bulk_update_labels,
} from '@/shared/worklenz-analytics-events';
import {
IBulkTasksLabelsRequest,
IBulkTasksPhaseChangeRequest,
IBulkTasksPriorityChangeRequest,
IBulkTasksStatusChangeRequest,
} from '@/types/tasks/bulk-action-bar.types';
import { ITaskStatus } from '@/types/tasks/taskStatus.types';
import { ITaskPriority } from '@/types/tasks/taskPriority.types';
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
import { ITaskAssignee } from '@/types/tasks/task.types';
import { createPortal } from 'react-dom';
import TaskTemplateDrawer from '@/components/task-templates/task-template-drawer';
import AssigneesDropdown from '@/components/taskListCommon/task-list-bulk-actions-bar/components/AssigneesDropdown';
import LabelsDropdown from '@/components/taskListCommon/task-list-bulk-actions-bar/components/LabelsDropdown';
import { sortTeamMembers } from '@/utils/sort-team-members';
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
import { useAuthService } from '@/hooks/useAuth';
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
import alertService from '@/services/alerts/alertService';
import logger from '@/utils/errorLogger';
const { Text } = Typography;
interface BulkActionBarProps {
selectedTaskIds: string[];
totalSelected: number;
currentGrouping: IGroupBy;
projectId: string;
onClearSelection?: () => void;
}
const BulkActionBarContent: React.FC<BulkActionBarProps> = ({
selectedTaskIds,
totalSelected,
currentGrouping,
projectId,
onClearSelection,
}) => {
const dispatch = useAppDispatch();
const { t } = useTranslation('tasks/task-table-bulk-actions');
const { trackMixpanelEvent } = useMixpanelTracking();
// Add permission hooks
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
// loading state
const [loading, setLoading] = useState(false);
const [updatingLabels, setUpdatingLabels] = useState(false);
const [updatingAssignToMe, setUpdatingAssignToMe] = useState(false);
const [updatingAssignees, setUpdatingAssignees] = useState(false);
const [updatingArchive, setUpdatingArchive] = useState(false);
const [updatingDelete, setUpdatingDelete] = useState(false);
// Selectors
const { selectedTaskIdsList } = useAppSelector(state => state.bulkActionReducer);
const statusList = useAppSelector(state => state.taskStatusReducer.status);
const priorityList = useAppSelector(state => state.priorityReducer.priorities);
const phaseList = useAppSelector(state => state.phaseReducer.phaseList);
const labelsList = useAppSelector(state => state.taskLabelsReducer.labels);
const members = useAppSelector(state => state.teamMembersReducer.teamMembers);
const archived = useAppSelector(state => state.taskReducer.archived);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const labelsInputRef = useRef<InputRef>(null);
const [createLabelText, setCreateLabelText] = useState<string>('');
const [teamMembersSorted, setTeamMembersSorted] = useState<ITeamMembersViewModel>({
data: [],
total: 0,
});
const [assigneeDropdownOpen, setAssigneeDropdownOpen] = useState(false);
const [showDrawer, setShowDrawer] = useState(false);
const [selectedLabels, setSelectedLabels] = useState<ITaskLabel[]>([]);
// Handlers
const handleChangeStatus = async (status: ITaskStatus) => {
if (!status.id || !projectId) return;
try {
setLoading(true);
const body: IBulkTasksStatusChangeRequest = {
tasks: selectedTaskIds,
status_id: status.id,
};
const res = await taskListBulkActionsApiService.changeStatus(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_status);
dispatch(deselectAll());
dispatch(fetchTaskGroups(projectId));
onClearSelection?.();
}
for (const it of selectedTaskIds) {
const canContinue = await checkTaskDependencyStatus(it, status.id);
if (!canContinue) {
if (selectedTaskIds.length > 1) {
alertService.warning(
'Incomplete Dependencies!',
'Some tasks were not updated. Please ensure all dependent tasks are completed before proceeding.'
);
} else {
alertService.error(
'Task is not completed',
'Please complete the task dependencies before proceeding'
);
}
return;
}
}
} catch (error) {
logger.error('Error changing status:', error);
} finally {
setLoading(false);
}
};
const handleChangePriority = async (priority: ITaskPriority) => {
if (!priority.id || !projectId) return;
try {
setLoading(true);
const body: IBulkTasksPriorityChangeRequest = {
tasks: selectedTaskIds,
priority_id: priority.id,
};
const res = await taskListBulkActionsApiService.changePriority(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_priority);
dispatch(deselectAll());
dispatch(fetchTaskGroups(projectId));
onClearSelection?.();
}
} catch (error) {
logger.error('Error changing priority:', error);
} finally {
setLoading(false);
}
};
const handleChangePhase = async (phase: ITaskPhase) => {
if (!phase.id || !projectId) return;
try {
setLoading(true);
const body: IBulkTasksPhaseChangeRequest = {
tasks: selectedTaskIds,
phase_id: phase.id,
};
const res = await taskListBulkActionsApiService.changePhase(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_phase);
dispatch(deselectAll());
dispatch(fetchTaskGroups(projectId));
onClearSelection?.();
}
} catch (error) {
logger.error('Error changing phase:', error);
} finally {
setLoading(false);
}
};
const handleAssignToMe = async () => {
if (!projectId) return;
try {
setUpdatingAssignToMe(true);
const body = {
tasks: selectedTaskIds,
project_id: projectId,
};
const res = await taskListBulkActionsApiService.assignToMe(body);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_assign_me);
dispatch(deselectAll());
dispatch(fetchTaskGroups(projectId));
onClearSelection?.();
}
} catch (error) {
logger.error('Error assigning to me:', error);
} finally {
setUpdatingAssignToMe(false);
}
};
const handleArchive = async () => {
if (!projectId) return;
try {
setUpdatingArchive(true);
const body = {
tasks: selectedTaskIds,
project_id: projectId,
};
const res = await taskListBulkActionsApiService.archiveTasks(body, archived);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_archive);
dispatch(deselectAll());
dispatch(fetchTaskGroups(projectId));
onClearSelection?.();
}
} catch (error) {
logger.error('Error archiving tasks:', error);
} finally {
setUpdatingArchive(false);
}
};
const handleChangeAssignees = async (selectedAssignees: ITeamMemberViewModel[]) => {
if (!projectId) return;
try {
setUpdatingAssignees(true);
const body = {
tasks: selectedTaskIds,
project_id: projectId,
members: selectedAssignees.map(member => ({
id: member.id,
name: member.name || member.email || 'Unknown', // Fix: Ensure name is always a string
email: member.email || '',
avatar_url: member.avatar_url,
team_member_id: member.id,
project_member_id: member.id,
})) as ITaskAssignee[],
};
const res = await taskListBulkActionsApiService.assignTasks(body);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_assign_members);
dispatch(deselectAll());
dispatch(fetchTaskGroups(projectId));
onClearSelection?.();
}
} catch (error) {
logger.error('Error assigning tasks:', error);
} finally {
setUpdatingAssignees(false);
}
};
const handleDelete = async () => {
if (!projectId) return;
try {
setUpdatingDelete(true);
const body = {
tasks: selectedTaskIds,
project_id: projectId,
};
const res = await taskListBulkActionsApiService.deleteTasks(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_delete);
dispatch(deselectAll());
dispatch(fetchTaskGroups(projectId));
onClearSelection?.();
}
} catch (error) {
logger.error('Error deleting tasks:', error);
} finally {
setUpdatingDelete(false);
}
};
// Menu Generators
const getChangeOptionsMenu = () => [
{
key: '1',
label: t('status'),
children: statusList.map(status => ({
key: status.id,
onClick: () => handleChangeStatus(status),
label: <Badge color={status.color_code} text={status.name} />,
})),
},
{
key: '2',
label: t('priority'),
children: priorityList.map(priority => ({
key: priority.id,
onClick: () => handleChangePriority(priority),
label: <Badge color={priority.color_code} text={priority.name} />,
})),
},
{
key: '3',
label: t('phase'),
children: phaseList.map(phase => ({
key: phase.id,
onClick: () => handleChangePhase(phase),
label: <Badge color={phase.color_code} text={phase.name} />,
})),
},
];
useEffect(() => {
if (members?.data && assigneeDropdownOpen) {
let sortedMembers = sortTeamMembers(members.data);
setTeamMembersSorted({ data: sortedMembers, total: members.total });
}
}, [assigneeDropdownOpen, members?.data]);
const getAssigneesMenu = () => {
return (
<AssigneesDropdown
members={teamMembersSorted?.data || []}
themeMode={themeMode}
onApply={handleChangeAssignees}
onClose={() => setAssigneeDropdownOpen(false)}
t={t}
/>
);
};
const handleLabelChange = (e: CheckboxChangeEvent, label: ITaskLabel) => {
if (e.target.checked) {
setSelectedLabels(prev => [...prev, label]);
} else {
setSelectedLabels(prev => prev.filter(l => l.id !== label.id));
}
};
const applyLabels = async () => {
if (!projectId) return;
try {
setUpdatingLabels(true);
const body: IBulkTasksLabelsRequest = {
tasks: selectedTaskIds,
labels: selectedLabels,
text:
selectedLabels.length > 0
? null
: createLabelText.trim() !== ''
? createLabelText.trim()
: null,
};
const res = await taskListBulkActionsApiService.assignLabels(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_update_labels);
dispatch(deselectAll());
dispatch(fetchTaskGroups(projectId));
dispatch(fetchLabels()); // Fallback: refetch all labels
setCreateLabelText('');
setSelectedLabels([]);
onClearSelection?.();
}
} catch (error) {
logger.error('Error updating labels:', error);
} finally {
setUpdatingLabels(false);
}
};
const labelsDropdownContent = (
<LabelsDropdown
labelsList={labelsList}
themeMode={themeMode}
createLabelText={createLabelText}
selectedLabels={selectedLabels}
labelsInputRef={labelsInputRef as React.RefObject<InputRef>}
onLabelChange={handleLabelChange}
onCreateLabelTextChange={value => setCreateLabelText(value)}
onApply={applyLabels}
t={t}
loading={updatingLabels}
/>
);
const onAssigneeDropdownOpenChange = (open: boolean) => {
setAssigneeDropdownOpen(open);
};
const buttonStyle = {
background: 'transparent',
color: '#fff',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px 8px',
height: '32px',
fontSize: '16px',
};
return (
<div
style={{
position: 'fixed',
bottom: '30px',
left: '50%',
transform: 'translateX(-50%)',
zIndex: 1000,
background: '#252628',
borderRadius: '25px',
padding: '8px 16px',
boxShadow: '0 0 0 1px #434343, 0 4px 12px 0 rgba(0, 0, 0, 0.15)',
display: 'flex',
alignItems: 'center',
gap: '8px',
minWidth: 'fit-content',
}}
>
<Text style={{ color: '#fff', fontSize: '14px', fontWeight: 500, marginRight: '8px' }}>
{totalSelected} task{totalSelected > 1 ? 's' : ''} selected
</Text>
{/* Status/Priority/Phase Change */}
<Tooltip title="Change Status/Priority/Phase">
<Dropdown menu={{ items: getChangeOptionsMenu() }} trigger={['click']}>
<Button
icon={<RetweetOutlined />}
style={buttonStyle}
size="small"
loading={loading}
/>
</Dropdown>
</Tooltip>
{/* Labels */}
<Tooltip title="Add Labels">
<Dropdown
dropdownRender={() => labelsDropdownContent}
placement="top"
arrow
trigger={['click']}
onOpenChange={value => {
if (!value) {
setSelectedLabels([]);
}
}}
>
<Button
icon={<TagsOutlined />}
style={buttonStyle}
size="small"
loading={updatingLabels}
/>
</Dropdown>
</Tooltip>
{/* Assign to Me */}
<Tooltip title="Assign to Me">
<Button
icon={<UserAddOutlined />}
style={buttonStyle}
size="small"
onClick={handleAssignToMe}
loading={updatingAssignToMe}
/>
</Tooltip>
{/* Assign Members */}
<Tooltip title="Assign Members">
<Dropdown
dropdownRender={getAssigneesMenu}
open={assigneeDropdownOpen}
onOpenChange={onAssigneeDropdownOpenChange}
placement="top"
arrow
trigger={['click']}
>
<Button
icon={<UsergroupAddOutlined />}
style={buttonStyle}
size="small"
loading={updatingAssignees}
/>
</Dropdown>
</Tooltip>
{/* Archive */}
<Tooltip title={archived ? 'Unarchive' : 'Archive'}>
<Button
icon={<InboxOutlined />}
style={buttonStyle}
size="small"
onClick={handleArchive}
loading={updatingArchive}
/>
</Tooltip>
{/* Delete */}
<Tooltip title="Delete">
<Popconfirm
title={`Delete ${totalSelected} task${totalSelected > 1 ? 's' : ''}?`}
description="This action cannot be undone."
onConfirm={handleDelete}
okText="Delete"
cancelText="Cancel"
okType="danger"
>
<Button
icon={<DeleteOutlined />}
style={buttonStyle}
size="small"
loading={updatingDelete}
/>
</Popconfirm>
</Tooltip>
{/* More Actions - Only for Owner/Admin */}
{isOwnerOrAdmin && (
<Tooltip title="More Actions">
<Dropdown
trigger={['click']}
menu={{
items: [
{
key: 'createTemplate',
label: 'Create task template',
onClick: () => setShowDrawer(true),
},
],
}}
>
<Button
icon={<MoreOutlined />}
style={buttonStyle}
size="small"
/>
</Dropdown>
</Tooltip>
)}
{/* Clear Selection */}
<Tooltip title="Clear Selection">
<Button
icon={<CloseOutlined />}
onClick={onClearSelection}
style={buttonStyle}
size="small"
/>
</Tooltip>
{/* Task Template Drawer */}
{createPortal(
<TaskTemplateDrawer
showDrawer={showDrawer}
selectedTemplateId={null}
onClose={() => {
setShowDrawer(false);
dispatch(deselectAll());
onClearSelection?.();
}}
/>,
document.body,
'create-task-template'
)}
</div>
);
};
const BulkActionBar: React.FC<BulkActionBarProps> = (props) => {
// Render the bulk action bar through a portal to avoid suspense issues
return createPortal(
<BulkActionBarContent {...props} />,
document.body,
'bulk-action-bar'
);
};
export default BulkActionBar;

View File

@@ -8,7 +8,7 @@ import { ITaskListGroup } from '@/types/tasks/taskList.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { IGroupBy, COLUMN_KEYS } from '@/features/tasks/tasks.slice';
import { RootState } from '@/app/store';
import TaskRow from './TaskRow';
import TaskRow from './task-row';
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
const { Text } = Typography;
@@ -126,7 +126,10 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
{/* Column Headers */}
{!isCollapsed && totalTasks > 0 && (
<div className="task-group-column-headers">
<div
className="task-group-column-headers"
style={{ borderLeft: `4px solid ${getGroupColor()}` }}
>
<div className="task-group-column-headers-row">
<div className="task-table-fixed-columns">
<div
@@ -182,7 +185,10 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
{/* Tasks List */}
{!isCollapsed && (
<div className="task-group-body">
<div
className="task-group-body"
style={{ borderLeft: `4px solid ${getGroupColor()}` }}
>
{group.tasks.length === 0 ? (
<div className="task-group-empty">
<div className="task-table-fixed-columns">
@@ -262,7 +268,7 @@ const TaskGroup: React.FC<TaskGroupProps> = ({
display: inline-flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
border-radius: 6px 6px 0 0;
background-color: #f0f0f0;
color: white;
font-weight: 500;

View File

@@ -24,9 +24,9 @@ import {
reorderTasks,
} from '@/features/tasks/tasks.slice';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import TaskGroup from './TaskGroup';
import TaskRow from './TaskRow';
import BulkActionBar from './BulkActionBar';
import TaskGroup from './task-group';
import TaskRow from './task-row';
import BulkActionBar from './bulk-action-bar';
import { AppDispatch } from '@/app/store';
// Import the TaskListFilters component
@@ -242,6 +242,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
totalSelected={selectedTaskIds.length}
currentGrouping={groupBy}
projectId={projectId}
onClearSelection={() => setSelectedTaskIds([])}
/>
)}

View File

@@ -247,7 +247,7 @@ const TaskListBulkActionsBar = () => {
project_id: projectId,
members: selectedAssignees.map(member => ({
id: member.id,
name: member.name,
name: member.name || '',
email: member.email,
avatar_url: member.avatar_url,
})) as ITaskAssignee[],
@@ -437,7 +437,6 @@ const TaskListBulkActionsBar = () => {
placement="top"
arrow
trigger={['click']}
destroyOnHidden
onOpenChange={value => {
if (!value) {
setSelectedLabels([]);

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { Layout, Typography, Card, Space, Alert } from 'antd';
import { useDispatch } from 'react-redux';
import TaskListBoard from '@/components/task-management/TaskListBoard';
import TaskListBoard from '@/components/task-management/task-list-board';
import { AppDispatch } from '@/app/store';
const { Header, Content } = Layout;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import TaskListBoard from '@/components/task-management/TaskListBoard';
import TaskListBoard from '@/components/task-management/task-list-board';
const ProjectViewEnhancedTasks: React.FC = () => {
const { project } = useAppSelector(state => state.projectReducer);