feat(task-management): enhance bulk action bar with new features and translations

- Added new bulk action options for changing status, priority, and phase, as well as adding labels and assigning tasks.
- Integrated translations for English, German, Spanish, and Portuguese in the task table bulk actions.
- Updated the `OptimizedBulkActionBar` to utilize Redux state for status, priority, and phase selections.
- Improved user experience with dynamic dropdown menus for bulk actions and confirmation prompts.
- Refactored task selection handling to ensure proper state management during bulk actions.
This commit is contained in:
chamiakJ
2025-07-02 07:59:34 +05:30
parent 326f283d4e
commit ccde08b700
6 changed files with 205 additions and 70 deletions

View File

@@ -20,5 +20,20 @@
"hitEnterToCreate": "Enter drücken zum Erstellen", "hitEnterToCreate": "Enter drücken zum Erstellen",
"pendingInvitation": "Einladung ausstehend", "pendingInvitation": "Einladung ausstehend",
"noMatchingLabels": "Keine passenden Labels", "noMatchingLabels": "Keine passenden Labels",
"noLabels": "Keine Labels" "noLabels": "Keine Labels",
"CHANGE_STATUS": "Status ändern",
"CHANGE_PRIORITY": "Priorität ändern",
"CHANGE_PHASE": "Phase ändern",
"ADD_LABELS": "Labels hinzufügen",
"ASSIGN_TO_ME": "Mir zuweisen",
"ASSIGN_MEMBERS": "Mitglieder zuweisen",
"ARCHIVE": "Archivieren",
"DELETE": "Löschen",
"CANCEL": "Abbrechen",
"CLEAR_SELECTION": "Auswahl löschen",
"TASKS_SELECTED": "{{count}} Aufgabe ausgewählt",
"TASKS_SELECTED_plural": "{{count}} Aufgaben ausgewählt",
"DELETE_TASKS_CONFIRM": "{{count}} Aufgabe löschen?",
"DELETE_TASKS_CONFIRM_plural": "{{count}} Aufgaben löschen?",
"DELETE_TASKS_WARNING": "Diese Aktion kann nicht rückgängig gemacht werden."
} }

View File

@@ -20,5 +20,20 @@
"hitEnterToCreate": "Press Enter to create", "hitEnterToCreate": "Press Enter to create",
"pendingInvitation": "Pending Invitation", "pendingInvitation": "Pending Invitation",
"noMatchingLabels": "No matching labels", "noMatchingLabels": "No matching labels",
"noLabels": "No labels" "noLabels": "No labels",
"CHANGE_STATUS": "Change Status",
"CHANGE_PRIORITY": "Change Priority",
"CHANGE_PHASE": "Change Phase",
"ADD_LABELS": "Add Labels",
"ASSIGN_TO_ME": "Assign to Me",
"ASSIGN_MEMBERS": "Assign Members",
"ARCHIVE": "Archive",
"DELETE": "Delete",
"CANCEL": "Cancel",
"CLEAR_SELECTION": "Clear Selection",
"TASKS_SELECTED": "{{count}} task selected",
"TASKS_SELECTED_plural": "{{count}} tasks selected",
"DELETE_TASKS_CONFIRM": "Delete {{count}} task?",
"DELETE_TASKS_CONFIRM_plural": "Delete {{count}} tasks?",
"DELETE_TASKS_WARNING": "This action cannot be undone."
} }

View File

@@ -20,5 +20,20 @@
"hitEnterToCreate": "Presione Enter para crear", "hitEnterToCreate": "Presione Enter para crear",
"pendingInvitation": "Invitación Pendiente", "pendingInvitation": "Invitación Pendiente",
"noMatchingLabels": "No hay etiquetas coincidentes", "noMatchingLabels": "No hay etiquetas coincidentes",
"noLabels": "Sin etiquetas" "noLabels": "Sin etiquetas",
"CHANGE_STATUS": "Cambiar Estado",
"CHANGE_PRIORITY": "Cambiar Prioridad",
"CHANGE_PHASE": "Cambiar Fase",
"ADD_LABELS": "Agregar Etiquetas",
"ASSIGN_TO_ME": "Asignar a Mí",
"ASSIGN_MEMBERS": "Asignar Miembros",
"ARCHIVE": "Archivar",
"DELETE": "Eliminar",
"CANCEL": "Cancelar",
"CLEAR_SELECTION": "Limpiar Selección",
"TASKS_SELECTED": "{{count}} tarea seleccionada",
"TASKS_SELECTED_plural": "{{count}} tareas seleccionadas",
"DELETE_TASKS_CONFIRM": "¿Eliminar {{count}} tarea?",
"DELETE_TASKS_CONFIRM_plural": "¿Eliminar {{count}} tareas?",
"DELETE_TASKS_WARNING": "Esta acción no se puede deshacer."
} }

View File

@@ -20,5 +20,20 @@
"hitEnterToCreate": "Pressione Enter para criar", "hitEnterToCreate": "Pressione Enter para criar",
"pendingInvitation": "Convite Pendente", "pendingInvitation": "Convite Pendente",
"noMatchingLabels": "Nenhuma etiqueta correspondente", "noMatchingLabels": "Nenhuma etiqueta correspondente",
"noLabels": "Sem etiquetas" "noLabels": "Sem etiquetas",
"CHANGE_STATUS": "Alterar Status",
"CHANGE_PRIORITY": "Alterar Prioridade",
"CHANGE_PHASE": "Alterar Fase",
"ADD_LABELS": "Adicionar Etiquetas",
"ASSIGN_TO_ME": "Atribuir a Mim",
"ASSIGN_MEMBERS": "Atribuir Membros",
"ARCHIVE": "Arquivar",
"DELETE": "Deletar",
"CANCEL": "Cancelar",
"CLEAR_SELECTION": "Limpar Seleção",
"TASKS_SELECTED": "{{count}} tarefa selecionada",
"TASKS_SELECTED_plural": "{{count}} tarefas selecionadas",
"DELETE_TASKS_CONFIRM": "Deletar {{count}} tarefa?",
"DELETE_TASKS_CONFIRM_plural": "Deletar {{count}} tarefas?",
"DELETE_TASKS_WARNING": "Esta ação não pode ser desfeita."
} }

View File

@@ -30,6 +30,7 @@ import {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '@/app/store'; import { RootState } from '@/app/store';
import { useAppSelector } from '@/hooks/useAppSelector';
const { Text } = Typography; const { Text } = Typography;
@@ -143,9 +144,14 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
onBulkExport, onBulkExport,
onBulkSetDueDate, onBulkSetDueDate,
}) => { }) => {
const { t } = useTranslation('task-management'); const { t } = useTranslation('tasks/task-table-bulk-actions');
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark'); const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
// Get data from Redux store
const statusList = useAppSelector(state => state.taskStatusReducer.status);
const priorityList = useAppSelector(state => state.priorityReducer.priorities);
const phaseList = useAppSelector(state => state.phaseReducer.phaseList);
// Performance state management // Performance state management
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [loadingStates, setLoadingStates] = useState({ const [loadingStates, setLoadingStates] = useState({
@@ -178,6 +184,41 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
setLoadingStates(prev => ({ ...prev, [action]: loading })); setLoadingStates(prev => ({ ...prev, [action]: loading }));
}, []); }, []);
// Create dropdown menus
const statusMenuItems = useMemo(() =>
statusList.map(status => ({
key: status.id || '',
label: <Badge color={status.color_code} text={status.name} />,
})), [statusList]
);
const priorityMenuItems = useMemo(() =>
priorityList.map(priority => ({
key: priority.id || '',
label: <Badge color={priority.color_code} text={priority.name} />,
})), [priorityList]
);
const phaseMenuItems = useMemo(() =>
phaseList.map(phase => ({
key: phase.id || '',
label: <Badge color={phase.color_code} text={phase.name} />,
})), [phaseList]
);
// Menu click handlers
const handleStatusMenuClick = useCallback((e: any) => {
onBulkStatusChange?.(e.key);
}, [onBulkStatusChange]);
const handlePriorityMenuClick = useCallback((e: any) => {
onBulkPriorityChange?.(e.key);
}, [onBulkPriorityChange]);
const handlePhaseMenuClick = useCallback((e: any) => {
onBulkPhaseChange?.(e.key);
}, [onBulkPhaseChange]);
// Memoized handlers with loading states // Memoized handlers with loading states
const handleStatusChange = useCallback(async () => { const handleStatusChange = useCallback(async () => {
updateLoadingState('status', true); updateLoadingState('status', true);
@@ -289,54 +330,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
whiteSpace: 'nowrap' as const, whiteSpace: 'nowrap' as const,
}), [isDarkMode]); }), [isDarkMode]);
// Quick actions dropdown menu if (!totalSelected || Number(totalSelected) < 1) {
const quickActionsMenu = useMemo(() => ({
items: [
{
key: 'change-status',
label: 'Change Status',
icon: <RetweetOutlined />,
onClick: handleStatusChange,
},
{
key: 'change-priority',
label: 'Change Priority',
icon: <FlagOutlined />,
onClick: handlePriorityChange,
},
{
key: 'change-phase',
label: 'Change Phase',
icon: <RetweetOutlined />,
onClick: handlePhaseChange,
},
{
key: 'set-due-date',
label: 'Set Due Date',
icon: <CalendarOutlined />,
onClick: () => onBulkSetDueDate?.(new Date().toISOString()),
},
{
type: 'divider' as const,
key: 'divider-1',
},
{
key: 'duplicate',
label: 'Duplicate Tasks',
icon: <CopyOutlined />,
onClick: handleDuplicate,
},
{
key: 'export',
label: 'Export Tasks',
icon: <ExportOutlined />,
onClick: handleExport,
},
],
}), [handleStatusChange, handlePriorityChange, handlePhaseChange, handleDuplicate, handleExport, onBulkSetDueDate]);
// Don't render if no tasks selected
if (totalSelected === 0) {
return null; return null;
} }
@@ -356,7 +350,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
marginRight: '6px' marginRight: '6px'
}} }}
/> />
{totalSelected} {totalSelected === 1 ? 'task' : 'tasks'} selected {t('TASKS_SELECTED', { count: totalSelected })}
</Text> </Text>
<Divider <Divider
@@ -370,10 +364,13 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
{/* Actions in same order as original component */} {/* Actions in same order as original component */}
<Space size={2}> <Space size={2}>
{/* Change Status/Priority/Phase */} {/* Change Status */}
<Tooltip title="Change Status/Priority/Phase" placement="top"> <Tooltip title={t('CHANGE_STATUS')} placement="top">
<Dropdown <Dropdown
menu={quickActionsMenu} menu={{
items: statusMenuItems,
onClick: handleStatusMenuClick
}}
trigger={['click']} trigger={['click']}
placement="top" placement="top"
arrow arrow
@@ -396,7 +393,75 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
}} }}
size="small" size="small"
type="text" type="text"
loading={loadingStates.status || loadingStates.priority || loadingStates.phase} loading={loadingStates.status}
/>
</Dropdown>
</Tooltip>
{/* Change Priority */}
<Tooltip title={t('CHANGE_PRIORITY')} placement="top">
<Dropdown
menu={{
items: priorityMenuItems,
onClick: handlePriorityMenuClick
}}
trigger={['click']}
placement="top"
arrow
>
<Button
icon={<FlagOutlined />}
style={{
background: 'transparent',
color: isDarkMode ? '#e5e7eb' : '#374151',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '6px',
height: '32px',
width: '32px',
fontSize: '14px',
borderRadius: '6px',
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
}}
size="small"
type="text"
loading={loadingStates.priority}
/>
</Dropdown>
</Tooltip>
{/* Change Phase */}
<Tooltip title={t('CHANGE_PHASE')} placement="top">
<Dropdown
menu={{
items: phaseMenuItems,
onClick: handlePhaseMenuClick
}}
trigger={['click']}
placement="top"
arrow
>
<Button
icon={<BulbOutlined />}
style={{
background: 'transparent',
color: isDarkMode ? '#e5e7eb' : '#374151',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '6px',
height: '32px',
width: '32px',
fontSize: '14px',
borderRadius: '6px',
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
}}
size="small"
type="text"
loading={loadingStates.phase}
/> />
</Dropdown> </Dropdown>
</Tooltip> </Tooltip>
@@ -404,7 +469,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
{/* Change Labels */} {/* Change Labels */}
<ActionButton <ActionButton
icon={<TagsOutlined />} icon={<TagsOutlined />}
tooltip="Add Labels" tooltip={t('ADD_LABELS')}
onClick={() => onBulkAddLabels?.([])} onClick={() => onBulkAddLabels?.([])}
loading={loadingStates.labels} loading={loadingStates.labels}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
@@ -413,7 +478,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
{/* Assign to Me */} {/* Assign to Me */}
<ActionButton <ActionButton
icon={<UserAddOutlined />} icon={<UserAddOutlined />}
tooltip="Assign to Me" tooltip={t('ASSIGN_TO_ME')}
onClick={handleAssignToMe} onClick={handleAssignToMe}
loading={loadingStates.assignToMe} loading={loadingStates.assignToMe}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
@@ -422,7 +487,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
{/* Change Assignees */} {/* Change Assignees */}
<ActionButton <ActionButton
icon={<UsergroupAddOutlined />} icon={<UsergroupAddOutlined />}
tooltip="Assign Members" tooltip={t('ASSIGN_MEMBERS')}
onClick={() => onBulkAssignMembers?.([])} onClick={() => onBulkAssignMembers?.([])}
loading={loadingStates.assignMembers} loading={loadingStates.assignMembers}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
@@ -431,7 +496,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
{/* Archive */} {/* Archive */}
<ActionButton <ActionButton
icon={<InboxOutlined />} icon={<InboxOutlined />}
tooltip="Archive" tooltip={t('ARCHIVE')}
onClick={handleArchive} onClick={handleArchive}
loading={loadingStates.archive} loading={loadingStates.archive}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
@@ -439,17 +504,17 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
{/* Delete */} {/* Delete */}
<Popconfirm <Popconfirm
title={`Delete ${totalSelected} ${totalSelected === 1 ? 'task' : 'tasks'}?`} title={t('DELETE_TASKS_CONFIRM', { count: totalSelected })}
description="This action cannot be undone." description={t('DELETE_TASKS_WARNING')}
onConfirm={handleDelete} onConfirm={handleDelete}
okText="Delete" okText={t('DELETE')}
cancelText="Cancel" cancelText={t('CANCEL')}
okType="danger" okType="danger"
placement="top" placement="top"
> >
<ActionButton <ActionButton
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
tooltip="Delete" tooltip={t('DELETE')}
loading={loadingStates.delete} loading={loadingStates.delete}
danger danger
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
@@ -468,7 +533,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
{/* Clear Selection */} {/* Clear Selection */}
<ActionButton <ActionButton
icon={<CloseOutlined />} icon={<CloseOutlined />}
tooltip="Clear Selection" tooltip={t('CLEAR_SELECTION')}
onClick={onClearSelection} onClick={onClearSelection}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
/> />
@@ -481,8 +546,8 @@ OptimizedBulkActionBarContent.displayName = 'OptimizedBulkActionBarContent';
// Portal wrapper for performance isolation // Portal wrapper for performance isolation
const OptimizedBulkActionBar: React.FC<OptimizedBulkActionBarProps> = React.memo((props) => { const OptimizedBulkActionBar: React.FC<OptimizedBulkActionBarProps> = React.memo((props) => {
// Only render portal if tasks are selected for better performance console.log('BulkActionBar totalSelected:', props.totalSelected, typeof props.totalSelected);
if (props.totalSelected === 0) { if (!props.totalSelected || Number(props.totalSelected) < 1) {
return null; return null;
} }

View File

@@ -42,6 +42,7 @@ import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import TaskRow from './task-row'; import TaskRow from './task-row';
// import BulkActionBar from './bulk-action-bar'; // import BulkActionBar from './bulk-action-bar';
import OptimizedBulkActionBar from './optimized-bulk-action-bar'; import OptimizedBulkActionBar from './optimized-bulk-action-bar';
// import OptimizedBulkActionBar from './optimized-bulk-action-bar';
import VirtualizedTaskList from './virtualized-task-list'; import VirtualizedTaskList from './virtualized-task-list';
import { AppDispatch } from '@/app/store'; import { AppDispatch } from '@/app/store';
import { shallowEqual } from 'react-redux'; import { shallowEqual } from 'react-redux';
@@ -443,6 +444,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// Bulk action handlers - implementing real functionality from task-list-bulk-actions-bar // Bulk action handlers - implementing real functionality from task-list-bulk-actions-bar
const handleClearSelection = useCallback(() => { const handleClearSelection = useCallback(() => {
dispatch(deselectAll()); dispatch(deselectAll());
dispatch(clearSelection());
}, [dispatch]); }, [dispatch]);
const handleBulkStatusChange = useCallback(async (statusId: string) => { const handleBulkStatusChange = useCallback(async (statusId: string) => {
@@ -480,6 +482,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_status); trackMixpanelEvent(evt_project_task_list_bulk_change_status);
dispatch(deselectAll()); dispatch(deselectAll());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
} }
} catch (error) { } catch (error) {
@@ -501,6 +504,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_priority); trackMixpanelEvent(evt_project_task_list_bulk_change_priority);
dispatch(deselectAll()); dispatch(deselectAll());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
} }
} catch (error) { } catch (error) {
@@ -522,6 +526,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_phase); trackMixpanelEvent(evt_project_task_list_bulk_change_phase);
dispatch(deselectAll()); dispatch(deselectAll());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
} }
} catch (error) { } catch (error) {
@@ -540,6 +545,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_assign_me); trackMixpanelEvent(evt_project_task_list_bulk_assign_me);
dispatch(deselectAll()); dispatch(deselectAll());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
} }
} catch (error) { } catch (error) {
@@ -571,6 +577,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_assign_members); trackMixpanelEvent(evt_project_task_list_bulk_assign_members);
dispatch(deselectAll()); dispatch(deselectAll());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
} }
} catch (error) { } catch (error) {
@@ -595,6 +602,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_update_labels); trackMixpanelEvent(evt_project_task_list_bulk_update_labels);
dispatch(deselectAll()); dispatch(deselectAll());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
dispatch(fetchLabels()); dispatch(fetchLabels());
} }
@@ -614,6 +622,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_archive); trackMixpanelEvent(evt_project_task_list_bulk_archive);
dispatch(deselectAll()); dispatch(deselectAll());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
} }
} catch (error) { } catch (error) {
@@ -632,6 +641,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_delete); trackMixpanelEvent(evt_project_task_list_bulk_delete);
dispatch(deselectAll()); dispatch(deselectAll());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
} }
} catch (error) { } catch (error) {