feat(task-management): centralize Ant Design imports and enhance task components
- Introduced a new `antd-imports.ts` file to centralize Ant Design component imports, optimizing tree-shaking and improving maintainability. - Updated various task management components (e.g., TaskRow, TaskGroup, VirtualizedTaskList) to utilize centralized imports, ensuring consistent styling and configuration. - Enhanced task filtering and display features by adding new fields (e.g., start date, due date, estimation) to task components for improved usability. - Refactored date handling in task components to utilize memoization for performance optimization. - Improved overall styling and responsiveness of task management components, particularly in dark mode.
This commit is contained in:
40
worklenz-frontend/package-lock.json
generated
40
worklenz-frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
237
worklenz-frontend/src/components/task-management/antd-imports.ts
Normal file
237
worklenz-frontend/src/components/task-management/antd-imports.ts
Normal file
@@ -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,
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -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<TaskGroupProps> = 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<TaskGroupProps> = 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<TaskGroupProps> = 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<TaskGroupProps> = React.memo(({
|
||||
style={{ backgroundColor: groupColor }}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
{...taskManagementAntdConfig.taskButtonDefaults}
|
||||
icon={isCollapsed ? <RightOutlined /> : <DownOutlined />}
|
||||
onClick={handleToggleCollapse}
|
||||
className="task-group-header-button"
|
||||
@@ -290,6 +309,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
{...taskManagementAntdConfig.taskButtonDefaults}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddTask}
|
||||
className="mt-2"
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Example: Task Row Component using Centralized Ant Design Imports
|
||||
*
|
||||
* This file demonstrates how to migrate from direct antd imports to the centralized import system.
|
||||
*
|
||||
* BEFORE (Direct imports):
|
||||
* import { Input, Typography, DatePicker } from 'antd';
|
||||
* import type { InputRef } from 'antd';
|
||||
*
|
||||
* AFTER (Centralized imports):
|
||||
* import { Input, Typography, DatePicker, type InputRef, dayjs, taskManagementAntdConfig } from './antd-imports';
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Input,
|
||||
Typography,
|
||||
DatePicker,
|
||||
Button,
|
||||
Select,
|
||||
Tooltip,
|
||||
Badge,
|
||||
Space,
|
||||
Checkbox,
|
||||
UserOutlined,
|
||||
CalendarOutlined,
|
||||
ClockCircleOutlined,
|
||||
EditOutlined,
|
||||
MoreOutlined,
|
||||
dayjs,
|
||||
taskManagementAntdConfig,
|
||||
taskMessage,
|
||||
type InputRef,
|
||||
type DatePickerProps,
|
||||
type Dayjs
|
||||
} from './antd-imports';
|
||||
|
||||
// Your existing task type import
|
||||
import { Task } from '@/types/task-management.types';
|
||||
|
||||
interface TaskRowExampleProps {
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isDarkMode?: boolean;
|
||||
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
|
||||
}
|
||||
|
||||
const TaskRowExample: React.FC<TaskRowExampleProps> = ({
|
||||
task,
|
||||
projectId,
|
||||
isDarkMode = false,
|
||||
onTaskUpdate
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editingField, setEditingField] = useState<string | null>(null);
|
||||
|
||||
// Use centralized config for consistent DatePicker props
|
||||
const datePickerProps = useMemo(() => ({
|
||||
...taskManagementAntdConfig.datePickerDefaults,
|
||||
className: "w-full bg-transparent border-none shadow-none"
|
||||
}), []);
|
||||
|
||||
// Use centralized config for consistent Button props
|
||||
const buttonProps = useMemo(() => ({
|
||||
...taskManagementAntdConfig.taskButtonDefaults,
|
||||
icon: <EditOutlined />
|
||||
}), []);
|
||||
|
||||
// Handle date changes with centralized message system
|
||||
const handleDateChange = useCallback((date: Dayjs | null, field: 'startDate' | 'dueDate') => {
|
||||
if (onTaskUpdate) {
|
||||
onTaskUpdate(task.id, {
|
||||
[field]: date?.toISOString() || null
|
||||
});
|
||||
taskMessage.success(`${field === 'startDate' ? 'Start' : 'Due'} date updated`);
|
||||
}
|
||||
}, [task.id, onTaskUpdate]);
|
||||
|
||||
// Handle task title edit
|
||||
const handleTitleEdit = useCallback((newTitle: string) => {
|
||||
if (onTaskUpdate && newTitle.trim() !== task.title) {
|
||||
onTaskUpdate(task.id, { title: newTitle.trim() });
|
||||
taskMessage.success('Task title updated');
|
||||
}
|
||||
setIsEditing(false);
|
||||
}, [task.id, task.title, onTaskUpdate]);
|
||||
|
||||
// Memoized date values for performance
|
||||
const startDateValue = useMemo(() =>
|
||||
task.startDate ? dayjs(task.startDate) : undefined,
|
||||
[task.startDate]
|
||||
);
|
||||
|
||||
const dueDateValue = useMemo(() =>
|
||||
task.dueDate ? dayjs(task.dueDate) : undefined,
|
||||
[task.dueDate]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`task-row-example ${isDarkMode ? 'dark' : 'light'}`}>
|
||||
<div className="task-row-content">
|
||||
|
||||
{/* Task Selection Checkbox */}
|
||||
<div className="task-cell">
|
||||
<Checkbox
|
||||
onChange={(e) => {
|
||||
// Handle selection logic here
|
||||
console.log('Task selected:', e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Task Title */}
|
||||
<div className="task-cell task-title">
|
||||
{isEditing ? (
|
||||
<Input
|
||||
{...taskManagementAntdConfig.taskInputDefaults}
|
||||
defaultValue={task.title}
|
||||
autoFocus
|
||||
onPressEnter={(e) => handleTitleEdit(e.currentTarget.value)}
|
||||
onBlur={(e) => handleTitleEdit(e.currentTarget.value)}
|
||||
/>
|
||||
) : (
|
||||
<Space>
|
||||
<Typography.Text
|
||||
className="task-title-text"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{task.title}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
{...buttonProps}
|
||||
onClick={() => setIsEditing(true)}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Task Progress */}
|
||||
<div className="task-cell">
|
||||
<Badge
|
||||
count={`${task.progress || 0}%`}
|
||||
color={task.progress === 100 ? 'green' : 'blue'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Task Assignees */}
|
||||
<div className="task-cell">
|
||||
<Space>
|
||||
<UserOutlined />
|
||||
<Typography.Text>
|
||||
{task.assignee_names?.join(', ') || 'Unassigned'}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Start Date */}
|
||||
<div className="task-cell">
|
||||
<Tooltip
|
||||
{...taskManagementAntdConfig.tooltipDefaults}
|
||||
title="Start Date"
|
||||
>
|
||||
<DatePicker
|
||||
{...datePickerProps}
|
||||
value={startDateValue}
|
||||
onChange={(date) => handleDateChange(date, 'startDate')}
|
||||
placeholder="Start Date"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Due Date */}
|
||||
<div className="task-cell">
|
||||
<Tooltip
|
||||
{...taskManagementAntdConfig.tooltipDefaults}
|
||||
title="Due Date"
|
||||
>
|
||||
<DatePicker
|
||||
{...datePickerProps}
|
||||
value={dueDateValue}
|
||||
onChange={(date) => handleDateChange(date, 'dueDate')}
|
||||
placeholder="Due Date"
|
||||
disabledDate={(current) =>
|
||||
startDateValue ? current.isBefore(startDateValue, 'day') : false
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Task Status */}
|
||||
<div className="task-cell">
|
||||
<Select
|
||||
{...taskManagementAntdConfig.taskSelectDefaults}
|
||||
value={task.status}
|
||||
placeholder="Status"
|
||||
onChange={(value) => {
|
||||
if (onTaskUpdate) {
|
||||
onTaskUpdate(task.id, { status: value });
|
||||
taskMessage.success('Status updated');
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
{ label: 'To Do', value: 'todo' },
|
||||
{ label: 'In Progress', value: 'in_progress' },
|
||||
{ label: 'Done', value: 'done' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Task Priority */}
|
||||
<div className="task-cell">
|
||||
<Select
|
||||
{...taskManagementAntdConfig.taskSelectDefaults}
|
||||
value={task.priority}
|
||||
placeholder="Priority"
|
||||
onChange={(value) => {
|
||||
if (onTaskUpdate) {
|
||||
onTaskUpdate(task.id, { priority: value });
|
||||
taskMessage.success('Priority updated');
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
{ label: 'Low', value: 'low' },
|
||||
{ label: 'Medium', value: 'medium' },
|
||||
{ label: 'High', value: 'high' },
|
||||
{ label: 'Critical', value: 'critical' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Time Tracking */}
|
||||
<div className="task-cell">
|
||||
<Space>
|
||||
<ClockCircleOutlined />
|
||||
<Typography.Text>
|
||||
{task.timeTracking?.logged ? `${task.timeTracking.logged}h` : '0h'}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="task-cell">
|
||||
<Button
|
||||
{...taskManagementAntdConfig.taskButtonDefaults}
|
||||
icon={<MoreOutlined />}
|
||||
onClick={() => {
|
||||
// Handle more actions
|
||||
console.log('More actions clicked');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskRowExample;
|
||||
|
||||
/**
|
||||
* Migration Guide:
|
||||
*
|
||||
* 1. Replace direct antd imports with centralized imports:
|
||||
* - Change: import { DatePicker } from 'antd';
|
||||
* - To: import { DatePicker } from './antd-imports';
|
||||
*
|
||||
* 2. Use centralized configurations:
|
||||
* - Apply taskManagementAntdConfig.datePickerDefaults to all DatePickers
|
||||
* - Use taskMessage instead of direct message calls
|
||||
* - Apply consistent styling with taskManagementTheme
|
||||
*
|
||||
* 3. Benefits:
|
||||
* - Better tree-shaking (smaller bundle size)
|
||||
* - Consistent component props across all task management components
|
||||
* - Centralized theme management
|
||||
* - Type safety with proper TypeScript types
|
||||
* - Easy maintenance and updates
|
||||
*
|
||||
* 4. Performance optimizations included:
|
||||
* - Memoized date values to prevent unnecessary dayjs parsing
|
||||
* - Centralized configurations to prevent prop recreation
|
||||
* - Optimized message utilities
|
||||
*/
|
||||
@@ -2,17 +2,22 @@ import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react'
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Input, Typography } from 'antd';
|
||||
import type { InputRef } from 'antd';
|
||||
import {
|
||||
import {
|
||||
Input,
|
||||
Typography,
|
||||
DatePicker,
|
||||
dayjs,
|
||||
taskManagementAntdConfig,
|
||||
HolderOutlined,
|
||||
MessageOutlined,
|
||||
PaperClipOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
UserOutlined,
|
||||
type InputRef
|
||||
} from './antd-imports';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { RootState } from '@/app/store';
|
||||
import { AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLabel, CustomNumberLabel, LabelsSelector, Progress, Tag, Tooltip } from '@/components';
|
||||
import { AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLabel, CustomNumberLabel, LabelsSelector, Progress, Tooltip } from '@/components';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import TaskStatusDropdown from './task-status-dropdown';
|
||||
@@ -147,19 +152,20 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
const containerClasses = useMemo(() => {
|
||||
const baseClasses = 'border-b transition-all duration-300';
|
||||
const themeClasses = isDarkMode
|
||||
? 'border-gray-700 bg-gray-900 hover:bg-gray-800'
|
||||
: 'border-gray-200 bg-white hover:bg-gray-50';
|
||||
? 'border-gray-600 hover:bg-gray-800'
|
||||
: 'border-gray-300 hover:bg-gray-50';
|
||||
const backgroundClasses = isDarkMode ? 'bg-[#18181b]' : 'bg-white';
|
||||
const selectedClasses = isSelected
|
||||
? (isDarkMode ? 'bg-blue-900/20' : 'bg-blue-50')
|
||||
: '';
|
||||
const overlayClasses = isDragOverlay
|
||||
? `rounded shadow-lg border-2 ${isDarkMode ? 'bg-gray-900 border-gray-600 shadow-2xl' : 'bg-white border-gray-300 shadow-2xl'}`
|
||||
? `rounded shadow-lg border-2 ${isDarkMode ? 'border-gray-600 shadow-2xl' : 'border-gray-300 shadow-2xl'}`
|
||||
: '';
|
||||
return `${baseClasses} ${themeClasses} ${selectedClasses} ${overlayClasses}`;
|
||||
return `${baseClasses} ${themeClasses} ${backgroundClasses} ${selectedClasses} ${overlayClasses}`;
|
||||
}, [isDarkMode, isSelected, isDragOverlay]);
|
||||
|
||||
const fixedColumnsClasses = useMemo(() =>
|
||||
`flex sticky left-0 z-10 border-r-2 shadow-sm ${isDarkMode ? 'bg-gray-900 border-gray-700' : 'bg-white border-gray-200'}`,
|
||||
`flex sticky left-0 z-10 border-r-2 shadow-sm ${isDarkMode ? 'bg-gray-900 border-gray-600' : 'bg-white border-gray-300'}`,
|
||||
[isDarkMode]
|
||||
);
|
||||
|
||||
@@ -180,6 +186,23 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
const getStatusColor = useCallback((status: string) =>
|
||||
STATUS_COLORS[status as keyof typeof STATUS_COLORS] || '#d9d9d9', []);
|
||||
|
||||
// Memoize date values for performance optimization
|
||||
const startDateValue = useMemo(() =>
|
||||
task.startDate ? dayjs(task.startDate) : undefined,
|
||||
[task.startDate]
|
||||
);
|
||||
|
||||
const dueDateValue = useMemo(() =>
|
||||
task.dueDate ? dayjs(task.dueDate) : undefined,
|
||||
[task.dueDate]
|
||||
);
|
||||
|
||||
// Memoize DatePicker configuration
|
||||
const datePickerProps = useMemo(() => ({
|
||||
...taskManagementAntdConfig.datePickerDefaults,
|
||||
className: "w-full bg-transparent border-none shadow-none"
|
||||
}), []);
|
||||
|
||||
// Create adapter for LabelsSelector - memoized
|
||||
const taskAdapter = useMemo(() => ({
|
||||
id: task.id,
|
||||
@@ -229,6 +252,35 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
}
|
||||
}, [task.dueDate]);
|
||||
|
||||
// Memoize date formatting functions
|
||||
const formatDate = useCallback((dateString?: string) => {
|
||||
if (!dateString) return '';
|
||||
return dayjs(dateString).format('MMM DD, YYYY');
|
||||
}, []);
|
||||
|
||||
const formatDateTime = useCallback((dateString?: string) => {
|
||||
if (!dateString) return '';
|
||||
return dayjs(dateString).format('MMM DD, YYYY HH:mm');
|
||||
}, []);
|
||||
|
||||
// Handle date changes
|
||||
const handleDateChange = useCallback((date: dayjs.Dayjs | null, field: 'startDate' | 'dueDate') => {
|
||||
if (!connected || !socket) return;
|
||||
|
||||
const eventType = field === 'startDate' ? SocketEvents.TASK_START_DATE_CHANGE : SocketEvents.TASK_END_DATE_CHANGE;
|
||||
const dateField = field === 'startDate' ? 'start_date' : 'end_date';
|
||||
|
||||
socket.emit(
|
||||
eventType.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
[dateField]: date?.format('YYYY-MM-DD'),
|
||||
parent_task: null,
|
||||
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
})
|
||||
);
|
||||
}, [connected, socket, task.id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
@@ -238,18 +290,18 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
<div className="flex h-10 max-h-10 overflow-visible relative">
|
||||
{/* Fixed Columns */}
|
||||
<div
|
||||
className="fixed-columns-row"
|
||||
className="flex"
|
||||
style={{
|
||||
display: 'flex',
|
||||
background: isDarkMode ? '#1a1a1a' : '#fff',
|
||||
width: fixedColumns?.reduce((sum, col) => sum + col.width, 0) || 0,
|
||||
}}
|
||||
>
|
||||
{fixedColumns?.map(col => {
|
||||
{fixedColumns?.map((col, colIdx) => {
|
||||
const isLastFixed = colIdx === fixedColumns.length - 1;
|
||||
const borderClasses = `${isLastFixed ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
||||
switch (col.key) {
|
||||
case 'drag':
|
||||
return (
|
||||
<div key={col.key} className="w-10 flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div key={col.key} className={`w-10 flex items-center justify-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
@@ -263,7 +315,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
);
|
||||
case 'select':
|
||||
return (
|
||||
<div key={col.key} className="w-10 flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div key={col.key} className={`w-10 flex items-center justify-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={handleSelectChange}
|
||||
@@ -273,14 +325,16 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
);
|
||||
case 'key':
|
||||
return (
|
||||
<div key={col.key} className="w-20 flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<Tag
|
||||
backgroundColor={isDarkMode ? "#374151" : "#f0f0f0"}
|
||||
color={isDarkMode ? "#d1d5db" : "#666"}
|
||||
className="truncate whitespace-nowrap max-w-full"
|
||||
<div key={col.key} className={`w-20 flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded truncate whitespace-nowrap max-w-full ${
|
||||
isDarkMode
|
||||
? 'bg-gray-700 text-gray-300'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{task.task_key}
|
||||
</Tag>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'task':
|
||||
@@ -291,7 +345,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
return (
|
||||
<div
|
||||
key={col.key}
|
||||
className={`flex items-center px-2${editTaskName ? ' task-name-edit-active' : ''}`}
|
||||
className={`flex items-center px-2 ${borderClasses}${editTaskName ? ' task-name-edit-active' : ''}`}
|
||||
style={cellStyle}
|
||||
>
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
|
||||
@@ -300,7 +354,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
{editTaskName ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="task-name-input"
|
||||
className="task-name-input w-full bg-transparent border-none outline-none text-sm"
|
||||
value={taskName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTaskName(e.target.value)}
|
||||
onBlur={handleTaskNameSave}
|
||||
@@ -310,10 +364,6 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
width: '100%',
|
||||
color: isDarkMode ? '#ffffff' : '#262626'
|
||||
}}
|
||||
autoFocus
|
||||
@@ -339,12 +389,29 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
})}
|
||||
</div>
|
||||
{/* Scrollable Columns */}
|
||||
<div className="scrollable-columns-row overflow-visible" style={{ display: 'flex', minWidth: scrollableColumns?.reduce((sum, col) => sum + col.width, 0) || 0 }}>
|
||||
{scrollableColumns?.map(col => {
|
||||
<div className="overflow-visible" style={{ display: 'flex', minWidth: scrollableColumns?.reduce((sum, col) => sum + col.width, 0) || 0 }}>
|
||||
{scrollableColumns?.map((col, colIdx) => {
|
||||
const isLastScrollable = colIdx === scrollableColumns.length - 1;
|
||||
const borderClasses = `${isLastScrollable ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
||||
switch (col.key) {
|
||||
case 'description':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<Typography.Paragraph
|
||||
ellipsis={{
|
||||
expandable: false,
|
||||
rows: 1,
|
||||
tooltip: task.description,
|
||||
}}
|
||||
className={`w-full mb-0 text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}
|
||||
>
|
||||
{task.description || ''}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
);
|
||||
case 'progress':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center justify-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div key={col.key} className={`flex items-center justify-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
{task.progress !== undefined && task.progress >= 0 && (
|
||||
<Progress
|
||||
type="circle"
|
||||
@@ -360,7 +427,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
);
|
||||
case 'members':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<div className="flex items-center gap-2">
|
||||
{avatarGroupMembers.length > 0 && (
|
||||
<AvatarGroup
|
||||
@@ -380,7 +447,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
);
|
||||
case 'labels':
|
||||
return (
|
||||
<div key={col.key} className="max-w-[200px] flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div key={col.key} className={`max-w-[200px] flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<div className="flex items-center gap-1 flex-wrap h-full w-full overflow-visible relative">
|
||||
{task.labels?.map((label, index) => (
|
||||
label.end && label.names && label.name ? (
|
||||
@@ -405,9 +472,17 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'phase':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.phase || 'No Phase'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'status':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2 border-r overflow-visible" style={{ width: col.width }}>
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses} overflow-visible`} style={{ width: col.width }}>
|
||||
<TaskStatusDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
@@ -417,13 +492,13 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
);
|
||||
case 'priority':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: getPriorityColor(task.priority) }}
|
||||
/>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.priority}
|
||||
</span>
|
||||
</div>
|
||||
@@ -431,12 +506,12 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
);
|
||||
case 'timeTracking':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<div className="flex items-center gap-2 h-full overflow-hidden">
|
||||
{task.timeTracking?.logged && task.timeTracking.logged > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`} />
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`} />
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{typeof task.timeTracking.logged === 'number'
|
||||
? `${task.timeTracking.logged}h`
|
||||
: task.timeTracking.logged
|
||||
@@ -447,6 +522,79 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'estimation':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.timeTracking?.estimated ? `${task.timeTracking.estimated}h` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'startDate':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<DatePicker
|
||||
{...datePickerProps}
|
||||
value={startDateValue}
|
||||
onChange={(date) => handleDateChange(date, 'startDate')}
|
||||
placeholder="Start Date"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'dueDate':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<DatePicker
|
||||
{...datePickerProps}
|
||||
value={dueDateValue}
|
||||
onChange={(date) => handleDateChange(date, 'dueDate')}
|
||||
placeholder="Due Date"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'dueTime':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.dueDate ? dayjs(task.dueDate).format('HH:mm') : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'completedDate':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.completedAt ? formatDate(task.completedAt) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'createdDate':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.createdAt ? formatDate(task.createdAt) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'lastUpdated':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.updatedAt ? formatDateTime(task.updatedAt) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
case 'reporter':
|
||||
return (
|
||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<UserOutlined className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`} />
|
||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
{task.reporter || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -457,66 +605,33 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
// Simplified comparison for better performance
|
||||
return (
|
||||
const taskPropsEqual = (
|
||||
prevProps.task.id === nextProps.task.id &&
|
||||
prevProps.task.title === nextProps.task.title &&
|
||||
prevProps.task.progress === nextProps.task.progress &&
|
||||
prevProps.task.status === nextProps.task.status &&
|
||||
prevProps.task.priority === nextProps.task.priority &&
|
||||
prevProps.task.labels?.length === nextProps.task.labels?.length &&
|
||||
prevProps.task.assignee_names?.length === nextProps.task.assignee_names?.length &&
|
||||
prevProps.task.assignee_names?.length === nextProps.task.assignee_names?.length
|
||||
);
|
||||
|
||||
const otherPropsEqual = (
|
||||
prevProps.isSelected === nextProps.isSelected &&
|
||||
prevProps.isDragOverlay === nextProps.isDragOverlay &&
|
||||
prevProps.groupId === nextProps.groupId
|
||||
);
|
||||
|
||||
// Check column props - these are critical for re-rendering when columns change
|
||||
const columnPropsEqual = (
|
||||
prevProps.fixedColumns?.length === nextProps.fixedColumns?.length &&
|
||||
prevProps.scrollableColumns?.length === nextProps.scrollableColumns?.length &&
|
||||
JSON.stringify(prevProps.fixedColumns?.map(c => c.key)) === JSON.stringify(nextProps.fixedColumns?.map(c => c.key)) &&
|
||||
JSON.stringify(prevProps.scrollableColumns?.map(c => c.key)) === JSON.stringify(nextProps.scrollableColumns?.map(c => c.key))
|
||||
);
|
||||
|
||||
return taskPropsEqual && otherPropsEqual && columnPropsEqual;
|
||||
});
|
||||
|
||||
TaskRow.displayName = 'TaskRow';
|
||||
|
||||
// Add styles for better border visibility
|
||||
const taskRowStyles = `
|
||||
.task-row-container {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .task-row-container,
|
||||
[data-theme="dark"] .task-row-container {
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
.task-row-container:hover {
|
||||
border-bottom-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.dark .task-row-container:hover,
|
||||
[data-theme="dark"] .task-row-container:hover {
|
||||
border-bottom-color: #4b5563;
|
||||
}
|
||||
|
||||
.fixed-columns-row > div,
|
||||
.scrollable-columns-row > div {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .fixed-columns-row > div,
|
||||
.dark .scrollable-columns-row > div,
|
||||
[data-theme="dark"] .fixed-columns-row > div,
|
||||
[data-theme="dark"] .scrollable-columns-row > div {
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
`;
|
||||
|
||||
// Inject styles
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleId = 'task-row-styles';
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = taskRowStyles;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
export default TaskRow;
|
||||
export default TaskRow;
|
||||
@@ -39,14 +39,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
// Get field visibility from taskListFields slice
|
||||
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
||||
|
||||
// Debug logging
|
||||
useEffect(() => {
|
||||
console.log('VirtualizedTaskList Debug:', {
|
||||
taskListFields,
|
||||
fieldsLength: taskListFields?.length,
|
||||
fieldsState: taskListFields?.map(f => ({ key: f.key, visible: f.visible }))
|
||||
});
|
||||
}, [taskListFields]);
|
||||
|
||||
|
||||
// Get tasks for this group using memoization for performance
|
||||
const groupTasks = useMemo(() => {
|
||||
@@ -105,12 +98,22 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = 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
|
||||
@@ -145,6 +148,8 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
const scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0);
|
||||
const totalTableWidth = fixedWidth + scrollableWidth;
|
||||
|
||||
|
||||
|
||||
// Row renderer for virtualization (only task rows)
|
||||
const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
|
||||
const task: Task | undefined = groupTasks[index];
|
||||
|
||||
@@ -31,26 +31,17 @@ const LOCAL_STORAGE_KEY = 'worklenz.taskManagement.fields';
|
||||
|
||||
function loadFields(): TaskListField[] {
|
||||
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
console.log('Loading fields from localStorage:', stored);
|
||||
|
||||
// Temporarily force defaults to debug
|
||||
console.log('FORCING DEFAULT FIELDS FOR DEBUGGING');
|
||||
return DEFAULT_FIELDS;
|
||||
|
||||
/* Commented out for debugging
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
console.log('Parsed fields from localStorage:', parsed);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse stored fields, using defaults:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Using default fields:', DEFAULT_FIELDS);
|
||||
return DEFAULT_FIELDS;
|
||||
*/
|
||||
}
|
||||
|
||||
function saveFields(fields: TaskListField[]) {
|
||||
|
||||
@@ -177,7 +177,7 @@ const ProjectView = () => {
|
||||
onChange={handleTabChange}
|
||||
items={tabMenuItems}
|
||||
tabBarStyle={{ paddingInline: 0 }}
|
||||
destroyOnHidden={true}
|
||||
destroyInactiveTabPane
|
||||
/>
|
||||
|
||||
{portalElements}
|
||||
|
||||
@@ -12,7 +12,10 @@ export interface Task {
|
||||
assignees: string[];
|
||||
assignee_names?: InlineMember[];
|
||||
labels: Label[];
|
||||
dueDate?: string;
|
||||
startDate?: string; // Start date for the task
|
||||
dueDate?: string; // Due date for the task
|
||||
completedAt?: string; // When the task was completed
|
||||
reporter?: string; // Who reported/created the task
|
||||
timeTracking: {
|
||||
estimated?: number;
|
||||
logged: number;
|
||||
|
||||
Reference in New Issue
Block a user