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:
@@ -20,5 +20,20 @@
|
||||
"hitEnterToCreate": "Enter drücken zum Erstellen",
|
||||
"pendingInvitation": "Einladung ausstehend",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -20,5 +20,20 @@
|
||||
"hitEnterToCreate": "Press Enter to create",
|
||||
"pendingInvitation": "Pending Invitation",
|
||||
"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."
|
||||
}
|
||||
@@ -20,5 +20,20 @@
|
||||
"hitEnterToCreate": "Presione Enter para crear",
|
||||
"pendingInvitation": "Invitación Pendiente",
|
||||
"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."
|
||||
}
|
||||
@@ -20,5 +20,20 @@
|
||||
"hitEnterToCreate": "Pressione Enter para criar",
|
||||
"pendingInvitation": "Convite Pendente",
|
||||
"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."
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@/app/store';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -143,9 +144,14 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
onBulkExport,
|
||||
onBulkSetDueDate,
|
||||
}) => {
|
||||
const { t } = useTranslation('task-management');
|
||||
const { t } = useTranslation('tasks/task-table-bulk-actions');
|
||||
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
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [loadingStates, setLoadingStates] = useState({
|
||||
@@ -178,6 +184,41 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
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
|
||||
const handleStatusChange = useCallback(async () => {
|
||||
updateLoadingState('status', true);
|
||||
@@ -289,54 +330,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
whiteSpace: 'nowrap' as const,
|
||||
}), [isDarkMode]);
|
||||
|
||||
// Quick actions dropdown menu
|
||||
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) {
|
||||
if (!totalSelected || Number(totalSelected) < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -356,7 +350,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
marginRight: '6px'
|
||||
}}
|
||||
/>
|
||||
{totalSelected} {totalSelected === 1 ? 'task' : 'tasks'} selected
|
||||
{t('TASKS_SELECTED', { count: totalSelected })}
|
||||
</Text>
|
||||
|
||||
<Divider
|
||||
@@ -370,10 +364,13 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
|
||||
{/* Actions in same order as original component */}
|
||||
<Space size={2}>
|
||||
{/* Change Status/Priority/Phase */}
|
||||
<Tooltip title="Change Status/Priority/Phase" placement="top">
|
||||
{/* Change Status */}
|
||||
<Tooltip title={t('CHANGE_STATUS')} placement="top">
|
||||
<Dropdown
|
||||
menu={quickActionsMenu}
|
||||
menu={{
|
||||
items: statusMenuItems,
|
||||
onClick: handleStatusMenuClick
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="top"
|
||||
arrow
|
||||
@@ -396,7 +393,75 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
}}
|
||||
size="small"
|
||||
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>
|
||||
</Tooltip>
|
||||
@@ -404,7 +469,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
{/* Change Labels */}
|
||||
<ActionButton
|
||||
icon={<TagsOutlined />}
|
||||
tooltip="Add Labels"
|
||||
tooltip={t('ADD_LABELS')}
|
||||
onClick={() => onBulkAddLabels?.([])}
|
||||
loading={loadingStates.labels}
|
||||
isDarkMode={isDarkMode}
|
||||
@@ -413,7 +478,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
{/* Assign to Me */}
|
||||
<ActionButton
|
||||
icon={<UserAddOutlined />}
|
||||
tooltip="Assign to Me"
|
||||
tooltip={t('ASSIGN_TO_ME')}
|
||||
onClick={handleAssignToMe}
|
||||
loading={loadingStates.assignToMe}
|
||||
isDarkMode={isDarkMode}
|
||||
@@ -422,7 +487,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
{/* Change Assignees */}
|
||||
<ActionButton
|
||||
icon={<UsergroupAddOutlined />}
|
||||
tooltip="Assign Members"
|
||||
tooltip={t('ASSIGN_MEMBERS')}
|
||||
onClick={() => onBulkAssignMembers?.([])}
|
||||
loading={loadingStates.assignMembers}
|
||||
isDarkMode={isDarkMode}
|
||||
@@ -431,7 +496,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
{/* Archive */}
|
||||
<ActionButton
|
||||
icon={<InboxOutlined />}
|
||||
tooltip="Archive"
|
||||
tooltip={t('ARCHIVE')}
|
||||
onClick={handleArchive}
|
||||
loading={loadingStates.archive}
|
||||
isDarkMode={isDarkMode}
|
||||
@@ -439,17 +504,17 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
|
||||
{/* Delete */}
|
||||
<Popconfirm
|
||||
title={`Delete ${totalSelected} ${totalSelected === 1 ? 'task' : 'tasks'}?`}
|
||||
description="This action cannot be undone."
|
||||
title={t('DELETE_TASKS_CONFIRM', { count: totalSelected })}
|
||||
description={t('DELETE_TASKS_WARNING')}
|
||||
onConfirm={handleDelete}
|
||||
okText="Delete"
|
||||
cancelText="Cancel"
|
||||
okText={t('DELETE')}
|
||||
cancelText={t('CANCEL')}
|
||||
okType="danger"
|
||||
placement="top"
|
||||
>
|
||||
<ActionButton
|
||||
icon={<DeleteOutlined />}
|
||||
tooltip="Delete"
|
||||
tooltip={t('DELETE')}
|
||||
loading={loadingStates.delete}
|
||||
danger
|
||||
isDarkMode={isDarkMode}
|
||||
@@ -468,7 +533,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
{/* Clear Selection */}
|
||||
<ActionButton
|
||||
icon={<CloseOutlined />}
|
||||
tooltip="Clear Selection"
|
||||
tooltip={t('CLEAR_SELECTION')}
|
||||
onClick={onClearSelection}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
@@ -481,8 +546,8 @@ OptimizedBulkActionBarContent.displayName = 'OptimizedBulkActionBarContent';
|
||||
|
||||
// Portal wrapper for performance isolation
|
||||
const OptimizedBulkActionBar: React.FC<OptimizedBulkActionBarProps> = React.memo((props) => {
|
||||
// Only render portal if tasks are selected for better performance
|
||||
if (props.totalSelected === 0) {
|
||||
console.log('BulkActionBar totalSelected:', props.totalSelected, typeof props.totalSelected);
|
||||
if (!props.totalSelected || Number(props.totalSelected) < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||
import TaskRow from './task-row';
|
||||
// import BulkActionBar from './bulk-action-bar';
|
||||
import OptimizedBulkActionBar from './optimized-bulk-action-bar';
|
||||
// import OptimizedBulkActionBar from './optimized-bulk-action-bar';
|
||||
import VirtualizedTaskList from './virtualized-task-list';
|
||||
import { AppDispatch } from '@/app/store';
|
||||
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
|
||||
const handleClearSelection = useCallback(() => {
|
||||
dispatch(deselectAll());
|
||||
dispatch(clearSelection());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleBulkStatusChange = useCallback(async (statusId: string) => {
|
||||
@@ -480,6 +482,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_change_status);
|
||||
dispatch(deselectAll());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -501,6 +504,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_change_priority);
|
||||
dispatch(deselectAll());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -522,6 +526,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_change_phase);
|
||||
dispatch(deselectAll());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -540,6 +545,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_assign_me);
|
||||
dispatch(deselectAll());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -571,6 +577,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_assign_members);
|
||||
dispatch(deselectAll());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -595,6 +602,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_update_labels);
|
||||
dispatch(deselectAll());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
dispatch(fetchLabels());
|
||||
}
|
||||
@@ -614,6 +622,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_archive);
|
||||
dispatch(deselectAll());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -632,6 +641,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_bulk_delete);
|
||||
dispatch(deselectAll());
|
||||
dispatch(clearSelection());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user