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 = (