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 = ({ 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(null); const [createLabelText, setCreateLabelText] = useState(''); const [teamMembersSorted, setTeamMembersSorted] = useState({ data: [], total: 0, }); const [assigneeDropdownOpen, setAssigneeDropdownOpen] = useState(false); const [showDrawer, setShowDrawer] = useState(false); const [selectedLabels, setSelectedLabels] = useState([]); // 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: , })), }, { key: '2', label: t('priority'), children: priorityList.map(priority => ({ key: priority.id, onClick: () => handleChangePriority(priority), label: , })), }, { key: '3', label: t('phase'), children: phaseList.map(phase => ({ key: phase.id, onClick: () => handleChangePhase(phase), label: , })), }, ]; useEffect(() => { if (members?.data && assigneeDropdownOpen) { let sortedMembers = sortTeamMembers(members.data); setTeamMembersSorted({ data: sortedMembers, total: members.total }); } }, [assigneeDropdownOpen, members?.data]); const getAssigneesMenu = () => { return ( 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 = ( } 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 (
{totalSelected} task{totalSelected > 1 ? 's' : ''} selected {/* Status/Priority/Phase Change */}
); }; const BulkActionBar: React.FC = (props) => { // Render the bulk action bar through a portal to avoid suspense issues return createPortal( , document.body, 'bulk-action-bar' ); }; export default BulkActionBar;