diff --git a/worklenz-frontend/package-lock.json b/worklenz-frontend/package-lock.json index 9ef5d8fd..251dbd44 100644 --- a/worklenz-frontend/package-lock.json +++ b/worklenz-frontend/package-lock.json @@ -22,7 +22,7 @@ "@tanstack/react-table": "^8.20.6", "@tanstack/react-virtual": "^3.11.2", "@tinymce/tinymce-react": "^5.1.1", - "antd": "^5.24.9", + "antd": "^5.26.2", "axios": "^1.9.0", "chart.js": "^4.4.7", "chartjs-plugin-datalabels": "^2.2.0", @@ -1924,9 +1924,9 @@ } }, "node_modules/@rc-component/trigger": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.6.tgz", - "integrity": "sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.7.tgz", + "integrity": "sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2", @@ -2864,9 +2864,9 @@ } }, "node_modules/antd": { - "version": "5.24.9", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.24.9.tgz", - "integrity": "sha512-liB+Y/JwD5/KSKbK1Z1EVAbWcoWYvWJ1s97AbbT+mOdigpJQuWwH7kG8IXNEljI7onvj0DdD43TXhSRLUu9AMA==", + "version": "5.26.2", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.26.2.tgz", + "integrity": "sha512-C8dBgwSzXfUS5ousUN+mfcaGFhEOd9wuyhvmw0lQnU9gukpRoFe1B0UKzvr6Z50QgapIl+s03nYlQJUghKqVjQ==", "license": "MIT", "dependencies": { "@ant-design/colors": "^7.2.1", @@ -2880,7 +2880,7 @@ "@rc-component/mutate-observer": "^1.1.0", "@rc-component/qrcode": "~1.0.0", "@rc-component/tour": "~1.15.1", - "@rc-component/trigger": "^2.2.6", + "@rc-component/trigger": "^2.2.7", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.11", @@ -2888,7 +2888,7 @@ "rc-checkbox": "~3.5.0", "rc-collapse": "~3.9.0", "rc-dialog": "~9.6.0", - "rc-drawer": "~7.2.0", + "rc-drawer": "~7.3.0", "rc-dropdown": "~4.2.1", "rc-field-form": "~2.7.0", "rc-image": "~7.12.0", @@ -2908,13 +2908,13 @@ "rc-slider": "~11.1.8", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", - "rc-table": "~7.50.5", + "rc-table": "~7.51.1", "rc-tabs": "~15.6.1", "rc-textarea": "~1.10.0", "rc-tooltip": "~6.4.0", "rc-tree": "~5.13.1", "rc-tree-select": "~5.27.0", - "rc-upload": "~4.9.0", + "rc-upload": "~4.9.2", "rc-util": "^5.44.4", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" @@ -5529,9 +5529,9 @@ } }, "node_modules/rc-drawer": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz", - "integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", @@ -5942,9 +5942,9 @@ } }, "node_modules/rc-table": { - "version": "7.50.5", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.5.tgz", - "integrity": "sha512-FDZu8aolhSYd3v9KOc3lZOVAU77wmRRu44R0Wfb8Oj1dXRUsloFaXMSl6f7yuWZUxArJTli7k8TEOX2mvhDl4A==", + "version": "7.51.1", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.51.1.tgz", + "integrity": "sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -6055,9 +6055,9 @@ } }, "node_modules/rc-upload": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.0.tgz", - "integrity": "sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA==", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz", + "integrity": "sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", diff --git a/worklenz-frontend/package.json b/worklenz-frontend/package.json index ac7bc185..bb1d7f46 100644 --- a/worklenz-frontend/package.json +++ b/worklenz-frontend/package.json @@ -26,7 +26,7 @@ "@tanstack/react-table": "^8.20.6", "@tanstack/react-virtual": "^3.11.2", "@tinymce/tinymce-react": "^5.1.1", - "antd": "^5.24.9", + "antd": "^5.26.2", "axios": "^1.9.0", "chart.js": "^4.4.7", "chartjs-plugin-datalabels": "^2.2.0", diff --git a/worklenz-frontend/src/components/task-management/antd-imports.ts b/worklenz-frontend/src/components/task-management/antd-imports.ts new file mode 100644 index 00000000..16991148 --- /dev/null +++ b/worklenz-frontend/src/components/task-management/antd-imports.ts @@ -0,0 +1,237 @@ +/** + * Centralized Ant Design imports for Task Management components + * + * This file provides: + * - Tree-shaking optimization by importing only used components + * - Type safety with proper TypeScript types + * - Performance optimization through selective imports + * - Consistent component versions across task management + * - Easy maintenance and updates + */ + +// Core Components +export { + Button, + Input, + Select, + Typography, + Card, + Spin, + Empty, + Space, + Tooltip, + Badge, + Popconfirm, + message, + Checkbox, + Dropdown, + Menu +} from 'antd/es'; + +// Date & Time Components +export { + DatePicker, + TimePicker +} from 'antd/es'; + +// Form Components (if needed for task management) +export { + Form, + InputNumber +} from 'antd/es'; + +// Layout Components +export { + Row, + Col, + Divider, + Flex +} from 'antd/es'; + +// Icon Components (commonly used in task management) +export { + EditOutlined, + DeleteOutlined, + PlusOutlined, + MoreOutlined, + CheckOutlined, + CloseOutlined, + CalendarOutlined, + ClockCircleOutlined, + UserOutlined, + TeamOutlined, + TagOutlined, + FlagOutlined, + BarsOutlined, + TableOutlined, + AppstoreOutlined, + FilterOutlined, + SortAscendingOutlined, + SortDescendingOutlined, + SearchOutlined, + ReloadOutlined, + SettingOutlined, + EyeOutlined, + EyeInvisibleOutlined, + CopyOutlined, + ExportOutlined, + ImportOutlined, + DownOutlined, + RightOutlined, + LeftOutlined, + UpOutlined, + DragOutlined, + HolderOutlined, + MessageOutlined, + PaperClipOutlined, + GroupOutlined, + InboxOutlined, + TagsOutlined, + UsergroupAddOutlined, + UserAddOutlined, + RetweetOutlined +} from '@ant-design/icons'; + +// TypeScript Types +export type { + ButtonProps, + InputProps, + InputRef, + SelectProps, + TypographyProps, + CardProps, + SpinProps, + EmptyProps, + SpaceProps, + TooltipProps, + BadgeProps, + PopconfirmProps, + CheckboxProps, + CheckboxChangeEvent, + DropdownProps, + MenuProps, + DatePickerProps, + TimePickerProps, + FormProps, + FormInstance, + InputNumberProps, + RowProps, + ColProps, + DividerProps, + FlexProps +} from 'antd/es'; + +// Dayjs (used with DatePicker) +export { default as dayjs } from 'dayjs'; +export type { Dayjs } from 'dayjs'; + +// Re-export commonly used Ant Design utilities +export { + ConfigProvider, + theme +} from 'antd'; + +// Custom hooks for task management (if any Ant Design specific hooks are needed) +export const useAntdBreakpoint = () => { + // You can add custom breakpoint logic here if needed + return { + xs: window.innerWidth < 576, + sm: window.innerWidth >= 576 && window.innerWidth < 768, + md: window.innerWidth >= 768 && window.innerWidth < 992, + lg: window.innerWidth >= 992 && window.innerWidth < 1200, + xl: window.innerWidth >= 1200 && window.innerWidth < 1600, + xxl: window.innerWidth >= 1600, + }; +}; + +// Import message separately to avoid circular dependency +import { message as antdMessage } from 'antd'; + +// Performance optimized message utility +export const taskMessage = { + success: (content: string) => antdMessage.success(content), + error: (content: string) => antdMessage.error(content), + warning: (content: string) => antdMessage.warning(content), + info: (content: string) => antdMessage.info(content), + loading: (content: string) => antdMessage.loading(content), +}; + +// Commonly used Ant Design configurations for task management +export const taskManagementAntdConfig = { + // DatePicker default props for consistency + datePickerDefaults: { + format: 'MMM DD, YYYY', + placeholder: 'Set Date', + suffixIcon: null, + size: 'small' as const, + }, + + // Button default props for task actions + taskButtonDefaults: { + size: 'small' as const, + type: 'text' as const, + }, + + // Input default props for task editing + taskInputDefaults: { + size: 'small' as const, + variant: 'borderless' as const, + }, + + // Select default props for dropdowns + taskSelectDefaults: { + size: 'small' as const, + variant: 'borderless' as const, + showSearch: true, + optionFilterProp: 'label' as const, + }, + + // Tooltip default props + tooltipDefaults: { + placement: 'top' as const, + mouseEnterDelay: 0.5, + mouseLeaveDelay: 0.1, + }, + + // Dropdown default props + dropdownDefaults: { + trigger: ['click'] as const, + placement: 'bottomLeft' as const, + }, +}; + +// Theme tokens specifically for task management +export const taskManagementTheme = { + light: { + colorBgContainer: '#ffffff', + colorBorder: '#e5e7eb', + colorText: '#374151', + colorTextSecondary: '#6b7280', + colorPrimary: '#3b82f6', + colorSuccess: '#10b981', + colorWarning: '#f59e0b', + colorError: '#ef4444', + colorBgHover: '#f9fafb', + colorBgSelected: '#eff6ff', + }, + dark: { + colorBgContainer: '#1f2937', + colorBorder: '#374151', + colorText: '#f9fafb', + colorTextSecondary: '#d1d5db', + colorPrimary: '#60a5fa', + colorSuccess: '#34d399', + colorWarning: '#fbbf24', + colorError: '#f87171', + colorBgHover: '#374151', + colorBgSelected: '#1e40af', + }, +}; + +// Export default configuration object +export default { + config: taskManagementAntdConfig, + theme: taskManagementTheme, + message: taskMessage, + useBreakpoint: useAntdBreakpoint, +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-management/bulk-action-bar.tsx b/worklenz-frontend/src/components/task-management/bulk-action-bar.tsx index 1148e623..2359a77f 100644 --- a/worklenz-frontend/src/components/task-management/bulk-action-bar.tsx +++ b/worklenz-frontend/src/components/task-management/bulk-action-bar.tsx @@ -1,11 +1,13 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Button, Typography, Dropdown, Menu, Popconfirm, message, Tooltip, Badge, CheckboxChangeEvent, InputRef } from 'antd'; -import { +import { + Button, + Typography, + Dropdown, + Menu, + Popconfirm, + Tooltip, + Badge, DeleteOutlined, - EditOutlined, - TagOutlined, - UserOutlined, - CheckOutlined, CloseOutlined, MoreOutlined, RetweetOutlined, @@ -13,11 +15,11 @@ import { InboxOutlined, TagsOutlined, UsergroupAddOutlined, -} from '@ant-design/icons'; + type CheckboxChangeEvent, + type InputRef +} from './antd-imports'; 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'; diff --git a/worklenz-frontend/src/components/task-management/improved-task-filters.tsx b/worklenz-frontend/src/components/task-management/improved-task-filters.tsx index dd376ac7..9fefc16f 100644 --- a/worklenz-frontend/src/components/task-management/improved-task-filters.tsx +++ b/worklenz-frontend/src/components/task-management/improved-task-filters.tsx @@ -16,7 +16,7 @@ import { CheckOutlined, SettingOutlined, MoreOutlined, -} from '@ant-design/icons'; +} from './antd-imports'; import { RootState } from '@/app/store'; import { AppDispatch } from '@/app/store'; import { useAppSelector } from '@/hooks/useAppSelector'; @@ -27,7 +27,13 @@ import { SocketEvents } from '@/shared/socket-events'; import { colors } from '@/styles/colors'; import SingleAvatar from '@components/common/single-avatar/single-avatar'; import { useFilterDataLoader } from '@/hooks/useFilterDataLoader'; -import { Dropdown, Checkbox, Button, Space } from 'antd'; +import { + Dropdown, + Checkbox, + Button, + Space, + taskManagementAntdConfig +} from './antd-imports'; import { toggleField, TaskListField } from '@/features/task-management/taskListFields.slice'; // Import Redux actions @@ -40,50 +46,40 @@ import { ITaskPriority } from '@/types/tasks/taskPriority.types'; import { ITaskListColumn } from '@/types/tasks/taskList.types'; import { IGroupBy } from '@/features/tasks/tasks.slice'; -// Memoized selectors to prevent unnecessary re-renders -const selectPriorities = createSelector( - [(state: any) => state.priorityReducer.priorities], - (priorities) => priorities || [] -); - -const selectTaskPriorities = createSelector( - [(state: any) => state.taskReducer.priorities], - (priorities) => priorities || [] -); - -const selectBoardPriorities = createSelector( - [(state: any) => state.boardReducer.priorities], - (priorities) => priorities || [] -); - -const selectTaskLabels = createSelector( - [(state: any) => state.taskReducer.labels], - (labels) => labels || [] -); - -const selectBoardLabels = createSelector( - [(state: any) => state.boardReducer.labels], - (labels) => labels || [] -); - -const selectTaskAssignees = createSelector( - [(state: any) => state.taskReducer.taskAssignees], - (assignees) => assignees || [] -); - -const selectBoardAssignees = createSelector( - [(state: any) => state.boardReducer.taskAssignees], - (assignees) => assignees || [] -); - -const selectProject = createSelector( - [(state: any) => state.projectReducer.project], - (project) => project -); - -const selectSelectedPriorities = createSelector( - [(state: any) => state.taskManagement.selectedPriorities], - (selectedPriorities) => selectedPriorities || [] +// Optimized selectors with proper transformation logic +const selectFilterData = createSelector( + [ + (state: any) => state.priorityReducer.priorities, + (state: any) => state.taskReducer.priorities, + (state: any) => state.boardReducer.priorities, + (state: any) => state.taskReducer.labels, + (state: any) => state.boardReducer.labels, + (state: any) => state.taskReducer.taskAssignees, + (state: any) => state.boardReducer.taskAssignees, + (state: any) => state.projectReducer.project, + (state: any) => state.taskManagement.selectedPriorities, + ], + ( + priorities, + taskPriorities, + boardPriorities, + taskLabels, + boardLabels, + taskAssignees, + boardAssignees, + project, + selectedPriorities + ) => ({ + priorities: priorities || [], + taskPriorities: taskPriorities || [], + boardPriorities: boardPriorities || [], + taskLabels: taskLabels || [], + boardLabels: boardLabels || [], + taskAssignees: taskAssignees || [], + boardAssignees: boardAssignees || [], + project, + selectedPriorities: selectedPriorities || [], + }) ); // Types @@ -118,50 +114,29 @@ const useFilterData = (): FilterSection[] => { const [searchParams] = useSearchParams(); const { projectView } = useTabSearchParam(); - // Use memoized selectors to prevent unnecessary re-renders - const priorities = useAppSelector(selectPriorities); - const taskPriorities = useAppSelector(selectTaskPriorities); - const boardPriorities = useAppSelector(selectBoardPriorities); - const taskLabels = useAppSelector(selectTaskLabels); - const boardLabels = useAppSelector(selectBoardLabels); - const taskAssignees = useAppSelector(selectTaskAssignees); - const boardAssignees = useAppSelector(selectBoardAssignees); - const taskGroupBy = useAppSelector(state => state.taskReducer.groupBy); - const boardGroupBy = useAppSelector(state => state.boardReducer.groupBy); - const project = useAppSelector(selectProject); + // Use optimized selector to get all filter data at once + const filterData = useAppSelector(selectFilterData); const currentGrouping = useAppSelector(selectCurrentGrouping); - const selectedPriorities = useAppSelector(selectSelectedPriorities); const tab = searchParams.get('tab'); const currentProjectView = tab === 'tasks-list' ? 'list' : 'kanban'; - // Debug logging - console.log('Filter Data Debug:', { - priorities: priorities?.length, - taskAssignees: taskAssignees?.length, - boardAssignees: boardAssignees?.length, - labels: taskLabels?.length, - boardLabels: boardLabels?.length, - currentProjectView, - projectId: project?.id - }); - return useMemo(() => { - const currentPriorities = currentProjectView === 'list' ? taskPriorities : boardPriorities; - const currentLabels = currentProjectView === 'list' ? taskLabels : boardLabels; - const currentAssignees = currentProjectView === 'list' ? taskAssignees : boardAssignees; + const currentPriorities = currentProjectView === 'list' ? filterData.taskPriorities : filterData.boardPriorities; + const currentLabels = currentProjectView === 'list' ? filterData.taskLabels : filterData.boardLabels; + const currentAssignees = currentProjectView === 'list' ? filterData.taskAssignees : filterData.boardAssignees; const groupByValue = currentGrouping || 'status'; return [ { id: 'priority', label: 'Priority', - options: priorities.map((p: any) => ({ + options: filterData.priorities.map((p: any) => ({ value: p.id, label: p.name, color: p.color_code, })), - selectedValues: selectedPriorities, + selectedValues: filterData.selectedPriorities, multiSelect: true, searchable: false, icon: FlagOutlined, @@ -206,25 +181,15 @@ const useFilterData = (): FilterSection[] => { options: [ { id: 'status', label: t('statusText'), value: 'status' }, { id: 'priority', label: t('priorityText'), value: 'priority' }, - { id: 'phase', label: project?.phase_label || t('phaseText'), value: 'phase' }, + { id: 'phase', label: filterData.project?.phase_label || t('phaseText'), value: 'phase' }, ], }, ]; }, [ - priorities, - taskPriorities, - boardPriorities, - taskLabels, - boardLabels, - taskAssignees, - boardAssignees, - taskGroupBy, - boardGroupBy, - project, + filterData, currentProjectView, t, - currentGrouping, - selectedPriorities + currentGrouping ]); }; diff --git a/worklenz-frontend/src/components/task-management/task-group.tsx b/worklenz-frontend/src/components/task-management/task-group.tsx index 43e13d23..bbff1138 100644 --- a/worklenz-frontend/src/components/task-management/task-group.tsx +++ b/worklenz-frontend/src/components/task-management/task-group.tsx @@ -1,9 +1,15 @@ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { useDroppable } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { useSelector } from 'react-redux'; -import { Button, Typography } from 'antd'; -import { PlusOutlined, RightOutlined, DownOutlined } from '@ant-design/icons'; +import { + Button, + Typography, + taskManagementAntdConfig, + PlusOutlined, + RightOutlined, + DownOutlined +} from './antd-imports'; import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types'; import { taskManagementSelectors } from '@/features/task-management/task-management.slice'; import { RootState } from '@/app/store'; @@ -72,6 +78,8 @@ const TaskGroup: React.FC = React.memo(({ // Get field visibility from taskListFields slice const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[]; + + // Define all possible columns const allFixedColumns = [ { key: 'drag', label: '', width: 40, alwaysVisible: true }, @@ -81,12 +89,22 @@ const TaskGroup: React.FC = React.memo(({ ]; const allScrollableColumns = [ + { key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' }, { key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' }, { key: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' }, { key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' }, + { key: 'phase', label: 'Phase', width: 100, fieldKey: 'PHASE' }, { key: 'status', label: 'Status', width: 100, fieldKey: 'STATUS' }, { key: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' }, { key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' }, + { key: 'estimation', label: 'Estimation', width: 100, fieldKey: 'ESTIMATION' }, + { key: 'startDate', label: 'Start Date', width: 120, fieldKey: 'START_DATE' }, + { key: 'dueDate', label: 'Due Date', width: 120, fieldKey: 'DUE_DATE' }, + { key: 'dueTime', label: 'Due Time', width: 100, fieldKey: 'DUE_TIME' }, + { key: 'completedDate', label: 'Completed Date', width: 130, fieldKey: 'COMPLETED_DATE' }, + { key: 'createdDate', label: 'Created Date', width: 120, fieldKey: 'CREATED_DATE' }, + { key: 'lastUpdated', label: 'Last Updated', width: 130, fieldKey: 'LAST_UPDATED' }, + { key: 'reporter', label: 'Reporter', width: 100, fieldKey: 'REPORTER' }, ]; // Filter columns based on field visibility @@ -150,6 +168,8 @@ const TaskGroup: React.FC = React.memo(({ return { isAllSelected, isIndeterminate }; }, [groupTasks, selectedTaskIds]); + + // Get group color based on grouping type - memoized const groupColor = useMemo(() => { if (group.color) return group.color; @@ -218,8 +238,7 @@ const TaskGroup: React.FC = React.memo(({ style={{ backgroundColor: groupColor }} >