diff --git a/worklenz-frontend/public/locales/de/tasks/task-table-bulk-actions.json b/worklenz-frontend/public/locales/de/tasks/task-table-bulk-actions.json
index 3c2d7132..297987c5 100644
--- a/worklenz-frontend/public/locales/de/tasks/task-table-bulk-actions.json
+++ b/worklenz-frontend/public/locales/de/tasks/task-table-bulk-actions.json
@@ -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."
}
diff --git a/worklenz-frontend/public/locales/en/tasks/task-table-bulk-actions.json b/worklenz-frontend/public/locales/en/tasks/task-table-bulk-actions.json
index 2434aea4..4beab4f6 100644
--- a/worklenz-frontend/public/locales/en/tasks/task-table-bulk-actions.json
+++ b/worklenz-frontend/public/locales/en/tasks/task-table-bulk-actions.json
@@ -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."
}
\ No newline at end of file
diff --git a/worklenz-frontend/public/locales/es/tasks/task-table-bulk-actions.json b/worklenz-frontend/public/locales/es/tasks/task-table-bulk-actions.json
index 0040c407..94963c91 100644
--- a/worklenz-frontend/public/locales/es/tasks/task-table-bulk-actions.json
+++ b/worklenz-frontend/public/locales/es/tasks/task-table-bulk-actions.json
@@ -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."
}
\ No newline at end of file
diff --git a/worklenz-frontend/public/locales/pt/tasks/task-table-bulk-actions.json b/worklenz-frontend/public/locales/pt/tasks/task-table-bulk-actions.json
index 40147210..6581803a 100644
--- a/worklenz-frontend/public/locales/pt/tasks/task-table-bulk-actions.json
+++ b/worklenz-frontend/public/locales/pt/tasks/task-table-bulk-actions.json
@@ -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."
}
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/suspense-fallback/suspense-fallback.tsx b/worklenz-frontend/src/components/suspense-fallback/suspense-fallback.tsx
index 4f346af0..63ef5a56 100644
--- a/worklenz-frontend/src/components/suspense-fallback/suspense-fallback.tsx
+++ b/worklenz-frontend/src/components/suspense-fallback/suspense-fallback.tsx
@@ -1,7 +1,7 @@
import React, { memo } from 'react';
-import { Spin } from 'antd';
+import { Skeleton } from 'antd';
-// Lightweight loading component - removed heavy theme calculations
+// Lightweight loading component with skeleton animation
export const SuspenseFallback = memo(() => {
return (
{
justifyContent: 'center',
background: 'transparent',
zIndex: 9999,
+ padding: '20px',
}}
>
-
+
+
+
);
});
@@ -35,7 +42,13 @@ export const InlineSuspenseFallback = memo(() => {
minHeight: '200px',
}}
>
-
+
+
+
);
});
diff --git a/worklenz-frontend/src/components/task-management/optimized-bulk-action-bar.tsx b/worklenz-frontend/src/components/task-management/optimized-bulk-action-bar.tsx
index cb1d3d0a..8320c6a2 100644
--- a/worklenz-frontend/src/components/task-management/optimized-bulk-action-bar.tsx
+++ b/worklenz-frontend/src/components/task-management/optimized-bulk-action-bar.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
+import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import {
Button,
@@ -8,70 +8,23 @@ import {
Tooltip,
Space,
Badge,
- Divider,
- type InputRef,
- message
-} from '@/shared/antd-imports';
-import type { CheckboxChangeEvent } from 'antd/es/checkbox';
+ Divider
+} from 'antd';
import {
DeleteOutlined,
CloseOutlined,
- MoreOutlined,
- SyncOutlined,
- UserOutlined,
- BellOutlined,
- TagOutlined,
+ RetweetOutlined,
+ UserAddOutlined,
+ InboxOutlined,
+ TagsOutlined,
UsergroupAddOutlined,
- CheckOutlined,
- EditOutlined,
- FileOutlined,
- ImportOutlined,
- CalendarOutlined,
- BarChartOutlined,
- SettingOutlined
-} from '@/shared/antd-imports';
+ FlagOutlined,
+ BulbOutlined
+} from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { RootState } from '@/app/store';
-import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
-import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
-import { clearSelection } from '@/features/task-management/selection.slice';
-import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
-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 as createReactPortal } 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;
@@ -80,6 +33,17 @@ interface OptimizedBulkActionBarProps {
totalSelected: number;
projectId: string;
onClearSelection?: () => void;
+ onBulkStatusChange?: (statusId: string) => void;
+ onBulkPriorityChange?: (priorityId: string) => void;
+ onBulkPhaseChange?: (phaseId: string) => void;
+ onBulkAssignToMe?: () => void;
+ onBulkAssignMembers?: (memberIds: string[]) => void;
+ onBulkAddLabels?: (labelIds: string[]) => void;
+ onBulkArchive?: () => void;
+ onBulkDelete?: () => void;
+ onBulkDuplicate?: () => void;
+ onBulkExport?: () => void;
+ onBulkSetDueDate?: (date: string) => void;
}
// Performance-optimized memoized action button component
@@ -93,44 +57,51 @@ const ActionButton = React.memo<{
isDarkMode: boolean;
badge?: number;
}>(({ icon, tooltip, onClick, loading = false, danger = false, disabled = false, isDarkMode, badge }) => {
- const buttonClasses = useMemo(() => {
- const baseClasses = [
- 'flex items-center justify-center',
- 'h-8 w-8 p-1.5',
- 'text-sm rounded-md',
- 'transition-all duration-150 ease-out',
- 'border-none bg-transparent',
- 'hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2',
- disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
- ];
+ const buttonStyle = useMemo(() => ({
+ 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)',
+ cursor: disabled ? 'not-allowed' : 'pointer',
+ opacity: disabled ? 0.5 : 1,
+ ...(danger && {
+ color: '#ef4444',
+ }),
+ }), [isDarkMode, danger, disabled]);
- if (danger) {
- baseClasses.push(
- isDarkMode
- ? 'text-red-400 hover:bg-red-400/10 focus:ring-red-400/20'
- : 'text-red-500 hover:bg-red-500/10 focus:ring-red-500/20'
- );
- } else {
- baseClasses.push(
- isDarkMode
- ? 'text-gray-300 hover:bg-white/10 focus:ring-gray-400/20'
- : 'text-gray-600 hover:bg-black/5 focus:ring-gray-400/20'
- );
- }
+ const hoverStyle = useMemo(() => ({
+ backgroundColor: isDarkMode
+ ? (danger ? 'rgba(239, 68, 68, 0.1)' : 'rgba(255, 255, 255, 0.1)')
+ : (danger ? 'rgba(239, 68, 68, 0.1)' : 'rgba(0, 0, 0, 0.05)'),
+ transform: 'scale(1.05)',
+ }), [isDarkMode, danger]);
- return baseClasses.join(' ');
- }, [isDarkMode, danger, disabled]);
+ const [isHovered, setIsHovered] = useState(false);
+
+ const combinedStyle = useMemo(() => ({
+ ...buttonStyle,
+ ...(isHovered && !disabled ? hoverStyle : {}),
+ }), [buttonStyle, hoverStyle, isHovered, disabled]);
const ButtonComponent = (