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-table": "^8.20.6",
|
||||||
"@tanstack/react-virtual": "^3.11.2",
|
"@tanstack/react-virtual": "^3.11.2",
|
||||||
"@tinymce/tinymce-react": "^5.1.1",
|
"@tinymce/tinymce-react": "^5.1.1",
|
||||||
"antd": "^5.24.9",
|
"antd": "^5.26.2",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"chart.js": "^4.4.7",
|
"chart.js": "^4.4.7",
|
||||||
"chartjs-plugin-datalabels": "^2.2.0",
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
@@ -1924,9 +1924,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rc-component/trigger": {
|
"node_modules/@rc-component/trigger": {
|
||||||
"version": "2.2.6",
|
"version": "2.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.7.tgz",
|
||||||
"integrity": "sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==",
|
"integrity": "sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.2",
|
"@babel/runtime": "^7.23.2",
|
||||||
@@ -2864,9 +2864,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/antd": {
|
"node_modules/antd": {
|
||||||
"version": "5.24.9",
|
"version": "5.26.2",
|
||||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.24.9.tgz",
|
"resolved": "https://registry.npmjs.org/antd/-/antd-5.26.2.tgz",
|
||||||
"integrity": "sha512-liB+Y/JwD5/KSKbK1Z1EVAbWcoWYvWJ1s97AbbT+mOdigpJQuWwH7kG8IXNEljI7onvj0DdD43TXhSRLUu9AMA==",
|
"integrity": "sha512-C8dBgwSzXfUS5ousUN+mfcaGFhEOd9wuyhvmw0lQnU9gukpRoFe1B0UKzvr6Z50QgapIl+s03nYlQJUghKqVjQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "^7.2.1",
|
"@ant-design/colors": "^7.2.1",
|
||||||
@@ -2880,7 +2880,7 @@
|
|||||||
"@rc-component/mutate-observer": "^1.1.0",
|
"@rc-component/mutate-observer": "^1.1.0",
|
||||||
"@rc-component/qrcode": "~1.0.0",
|
"@rc-component/qrcode": "~1.0.0",
|
||||||
"@rc-component/tour": "~1.15.1",
|
"@rc-component/tour": "~1.15.1",
|
||||||
"@rc-component/trigger": "^2.2.6",
|
"@rc-component/trigger": "^2.2.7",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
@@ -2888,7 +2888,7 @@
|
|||||||
"rc-checkbox": "~3.5.0",
|
"rc-checkbox": "~3.5.0",
|
||||||
"rc-collapse": "~3.9.0",
|
"rc-collapse": "~3.9.0",
|
||||||
"rc-dialog": "~9.6.0",
|
"rc-dialog": "~9.6.0",
|
||||||
"rc-drawer": "~7.2.0",
|
"rc-drawer": "~7.3.0",
|
||||||
"rc-dropdown": "~4.2.1",
|
"rc-dropdown": "~4.2.1",
|
||||||
"rc-field-form": "~2.7.0",
|
"rc-field-form": "~2.7.0",
|
||||||
"rc-image": "~7.12.0",
|
"rc-image": "~7.12.0",
|
||||||
@@ -2908,13 +2908,13 @@
|
|||||||
"rc-slider": "~11.1.8",
|
"rc-slider": "~11.1.8",
|
||||||
"rc-steps": "~6.0.1",
|
"rc-steps": "~6.0.1",
|
||||||
"rc-switch": "~4.1.0",
|
"rc-switch": "~4.1.0",
|
||||||
"rc-table": "~7.50.5",
|
"rc-table": "~7.51.1",
|
||||||
"rc-tabs": "~15.6.1",
|
"rc-tabs": "~15.6.1",
|
||||||
"rc-textarea": "~1.10.0",
|
"rc-textarea": "~1.10.0",
|
||||||
"rc-tooltip": "~6.4.0",
|
"rc-tooltip": "~6.4.0",
|
||||||
"rc-tree": "~5.13.1",
|
"rc-tree": "~5.13.1",
|
||||||
"rc-tree-select": "~5.27.0",
|
"rc-tree-select": "~5.27.0",
|
||||||
"rc-upload": "~4.9.0",
|
"rc-upload": "~4.9.2",
|
||||||
"rc-util": "^5.44.4",
|
"rc-util": "^5.44.4",
|
||||||
"scroll-into-view-if-needed": "^3.1.0",
|
"scroll-into-view-if-needed": "^3.1.0",
|
||||||
"throttle-debounce": "^5.0.2"
|
"throttle-debounce": "^5.0.2"
|
||||||
@@ -5529,9 +5529,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rc-drawer": {
|
"node_modules/rc-drawer": {
|
||||||
"version": "7.2.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz",
|
||||||
"integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==",
|
"integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.9",
|
"@babel/runtime": "^7.23.9",
|
||||||
@@ -5942,9 +5942,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rc-table": {
|
"node_modules/rc-table": {
|
||||||
"version": "7.50.5",
|
"version": "7.51.1",
|
||||||
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.5.tgz",
|
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.51.1.tgz",
|
||||||
"integrity": "sha512-FDZu8aolhSYd3v9KOc3lZOVAU77wmRRu44R0Wfb8Oj1dXRUsloFaXMSl6f7yuWZUxArJTli7k8TEOX2mvhDl4A==",
|
"integrity": "sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.10.1",
|
"@babel/runtime": "^7.10.1",
|
||||||
@@ -6055,9 +6055,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rc-upload": {
|
"node_modules/rc-upload": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz",
|
||||||
"integrity": "sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA==",
|
"integrity": "sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"@tanstack/react-table": "^8.20.6",
|
"@tanstack/react-table": "^8.20.6",
|
||||||
"@tanstack/react-virtual": "^3.11.2",
|
"@tanstack/react-virtual": "^3.11.2",
|
||||||
"@tinymce/tinymce-react": "^5.1.1",
|
"@tinymce/tinymce-react": "^5.1.1",
|
||||||
"antd": "^5.24.9",
|
"antd": "^5.26.2",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"chart.js": "^4.4.7",
|
"chart.js": "^4.4.7",
|
||||||
"chartjs-plugin-datalabels": "^2.2.0",
|
"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 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,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
|
||||||
TagOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
CheckOutlined,
|
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
RetweetOutlined,
|
RetweetOutlined,
|
||||||
@@ -13,11 +15,11 @@ import {
|
|||||||
InboxOutlined,
|
InboxOutlined,
|
||||||
TagsOutlined,
|
TagsOutlined,
|
||||||
UsergroupAddOutlined,
|
UsergroupAddOutlined,
|
||||||
} from '@ant-design/icons';
|
type CheckboxChangeEvent,
|
||||||
|
type InputRef
|
||||||
|
} from './antd-imports';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { IGroupBy, fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
import { IGroupBy, fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
||||||
import { AppDispatch, RootState } from '@/app/store';
|
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
} from '@ant-design/icons';
|
} from './antd-imports';
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
import { AppDispatch } from '@/app/store';
|
import { AppDispatch } from '@/app/store';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
@@ -27,7 +27,13 @@ import { SocketEvents } from '@/shared/socket-events';
|
|||||||
import { colors } from '@/styles/colors';
|
import { colors } from '@/styles/colors';
|
||||||
import SingleAvatar from '@components/common/single-avatar/single-avatar';
|
import SingleAvatar from '@components/common/single-avatar/single-avatar';
|
||||||
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
|
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 { toggleField, TaskListField } from '@/features/task-management/taskListFields.slice';
|
||||||
|
|
||||||
// Import Redux actions
|
// Import Redux actions
|
||||||
@@ -40,50 +46,40 @@ import { ITaskPriority } from '@/types/tasks/taskPriority.types';
|
|||||||
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||||
import { IGroupBy } from '@/features/tasks/tasks.slice';
|
import { IGroupBy } from '@/features/tasks/tasks.slice';
|
||||||
|
|
||||||
// Memoized selectors to prevent unnecessary re-renders
|
// Optimized selectors with proper transformation logic
|
||||||
const selectPriorities = createSelector(
|
const selectFilterData = createSelector(
|
||||||
[(state: any) => state.priorityReducer.priorities],
|
[
|
||||||
(priorities) => priorities || []
|
(state: any) => state.priorityReducer.priorities,
|
||||||
);
|
(state: any) => state.taskReducer.priorities,
|
||||||
|
(state: any) => state.boardReducer.priorities,
|
||||||
const selectTaskPriorities = createSelector(
|
(state: any) => state.taskReducer.labels,
|
||||||
[(state: any) => state.taskReducer.priorities],
|
(state: any) => state.boardReducer.labels,
|
||||||
(priorities) => priorities || []
|
(state: any) => state.taskReducer.taskAssignees,
|
||||||
);
|
(state: any) => state.boardReducer.taskAssignees,
|
||||||
|
(state: any) => state.projectReducer.project,
|
||||||
const selectBoardPriorities = createSelector(
|
(state: any) => state.taskManagement.selectedPriorities,
|
||||||
[(state: any) => state.boardReducer.priorities],
|
],
|
||||||
(priorities) => priorities || []
|
(
|
||||||
);
|
priorities,
|
||||||
|
taskPriorities,
|
||||||
const selectTaskLabels = createSelector(
|
boardPriorities,
|
||||||
[(state: any) => state.taskReducer.labels],
|
taskLabels,
|
||||||
(labels) => labels || []
|
boardLabels,
|
||||||
);
|
taskAssignees,
|
||||||
|
boardAssignees,
|
||||||
const selectBoardLabels = createSelector(
|
project,
|
||||||
[(state: any) => state.boardReducer.labels],
|
selectedPriorities
|
||||||
(labels) => labels || []
|
) => ({
|
||||||
);
|
priorities: priorities || [],
|
||||||
|
taskPriorities: taskPriorities || [],
|
||||||
const selectTaskAssignees = createSelector(
|
boardPriorities: boardPriorities || [],
|
||||||
[(state: any) => state.taskReducer.taskAssignees],
|
taskLabels: taskLabels || [],
|
||||||
(assignees) => assignees || []
|
boardLabels: boardLabels || [],
|
||||||
);
|
taskAssignees: taskAssignees || [],
|
||||||
|
boardAssignees: boardAssignees || [],
|
||||||
const selectBoardAssignees = createSelector(
|
project,
|
||||||
[(state: any) => state.boardReducer.taskAssignees],
|
selectedPriorities: selectedPriorities || [],
|
||||||
(assignees) => assignees || []
|
})
|
||||||
);
|
|
||||||
|
|
||||||
const selectProject = createSelector(
|
|
||||||
[(state: any) => state.projectReducer.project],
|
|
||||||
(project) => project
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectSelectedPriorities = createSelector(
|
|
||||||
[(state: any) => state.taskManagement.selectedPriorities],
|
|
||||||
(selectedPriorities) => selectedPriorities || []
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@@ -118,50 +114,29 @@ const useFilterData = (): FilterSection[] => {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { projectView } = useTabSearchParam();
|
const { projectView } = useTabSearchParam();
|
||||||
|
|
||||||
// Use memoized selectors to prevent unnecessary re-renders
|
// Use optimized selector to get all filter data at once
|
||||||
const priorities = useAppSelector(selectPriorities);
|
const filterData = useAppSelector(selectFilterData);
|
||||||
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);
|
|
||||||
const currentGrouping = useAppSelector(selectCurrentGrouping);
|
const currentGrouping = useAppSelector(selectCurrentGrouping);
|
||||||
const selectedPriorities = useAppSelector(selectSelectedPriorities);
|
|
||||||
|
|
||||||
const tab = searchParams.get('tab');
|
const tab = searchParams.get('tab');
|
||||||
const currentProjectView = tab === 'tasks-list' ? 'list' : 'kanban';
|
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(() => {
|
return useMemo(() => {
|
||||||
const currentPriorities = currentProjectView === 'list' ? taskPriorities : boardPriorities;
|
const currentPriorities = currentProjectView === 'list' ? filterData.taskPriorities : filterData.boardPriorities;
|
||||||
const currentLabels = currentProjectView === 'list' ? taskLabels : boardLabels;
|
const currentLabels = currentProjectView === 'list' ? filterData.taskLabels : filterData.boardLabels;
|
||||||
const currentAssignees = currentProjectView === 'list' ? taskAssignees : boardAssignees;
|
const currentAssignees = currentProjectView === 'list' ? filterData.taskAssignees : filterData.boardAssignees;
|
||||||
const groupByValue = currentGrouping || 'status';
|
const groupByValue = currentGrouping || 'status';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'priority',
|
id: 'priority',
|
||||||
label: 'Priority',
|
label: 'Priority',
|
||||||
options: priorities.map((p: any) => ({
|
options: filterData.priorities.map((p: any) => ({
|
||||||
value: p.id,
|
value: p.id,
|
||||||
label: p.name,
|
label: p.name,
|
||||||
color: p.color_code,
|
color: p.color_code,
|
||||||
})),
|
})),
|
||||||
selectedValues: selectedPriorities,
|
selectedValues: filterData.selectedPriorities,
|
||||||
multiSelect: true,
|
multiSelect: true,
|
||||||
searchable: false,
|
searchable: false,
|
||||||
icon: FlagOutlined,
|
icon: FlagOutlined,
|
||||||
@@ -206,25 +181,15 @@ const useFilterData = (): FilterSection[] => {
|
|||||||
options: [
|
options: [
|
||||||
{ id: 'status', label: t('statusText'), value: 'status' },
|
{ id: 'status', label: t('statusText'), value: 'status' },
|
||||||
{ id: 'priority', label: t('priorityText'), value: 'priority' },
|
{ 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,
|
filterData,
|
||||||
taskPriorities,
|
|
||||||
boardPriorities,
|
|
||||||
taskLabels,
|
|
||||||
boardLabels,
|
|
||||||
taskAssignees,
|
|
||||||
boardAssignees,
|
|
||||||
taskGroupBy,
|
|
||||||
boardGroupBy,
|
|
||||||
project,
|
|
||||||
currentProjectView,
|
currentProjectView,
|
||||||
t,
|
t,
|
||||||
currentGrouping,
|
currentGrouping
|
||||||
selectedPriorities
|
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 { useDroppable } from '@dnd-kit/core';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Button, Typography } from 'antd';
|
import {
|
||||||
import { PlusOutlined, RightOutlined, DownOutlined } from '@ant-design/icons';
|
Button,
|
||||||
|
Typography,
|
||||||
|
taskManagementAntdConfig,
|
||||||
|
PlusOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
DownOutlined
|
||||||
|
} from './antd-imports';
|
||||||
import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types';
|
import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types';
|
||||||
import { taskManagementSelectors } from '@/features/task-management/task-management.slice';
|
import { taskManagementSelectors } from '@/features/task-management/task-management.slice';
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
@@ -72,6 +78,8 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
|||||||
// Get field visibility from taskListFields slice
|
// Get field visibility from taskListFields slice
|
||||||
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Define all possible columns
|
// Define all possible columns
|
||||||
const allFixedColumns = [
|
const allFixedColumns = [
|
||||||
{ key: 'drag', label: '', width: 40, alwaysVisible: true },
|
{ key: 'drag', label: '', width: 40, alwaysVisible: true },
|
||||||
@@ -81,12 +89,22 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const allScrollableColumns = [
|
const allScrollableColumns = [
|
||||||
|
{ key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' },
|
||||||
{ key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' },
|
{ key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' },
|
||||||
{ key: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' },
|
{ key: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' },
|
||||||
{ key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' },
|
{ 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: 'status', label: 'Status', width: 100, fieldKey: 'STATUS' },
|
||||||
{ key: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' },
|
{ key: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' },
|
||||||
{ key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' },
|
{ 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
|
// Filter columns based on field visibility
|
||||||
@@ -150,6 +168,8 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
|||||||
return { isAllSelected, isIndeterminate };
|
return { isAllSelected, isIndeterminate };
|
||||||
}, [groupTasks, selectedTaskIds]);
|
}, [groupTasks, selectedTaskIds]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Get group color based on grouping type - memoized
|
// Get group color based on grouping type - memoized
|
||||||
const groupColor = useMemo(() => {
|
const groupColor = useMemo(() => {
|
||||||
if (group.color) return group.color;
|
if (group.color) return group.color;
|
||||||
@@ -218,8 +238,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
|||||||
style={{ backgroundColor: groupColor }}
|
style={{ backgroundColor: groupColor }}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
{...taskManagementAntdConfig.taskButtonDefaults}
|
||||||
size="small"
|
|
||||||
icon={isCollapsed ? <RightOutlined /> : <DownOutlined />}
|
icon={isCollapsed ? <RightOutlined /> : <DownOutlined />}
|
||||||
onClick={handleToggleCollapse}
|
onClick={handleToggleCollapse}
|
||||||
className="task-group-header-button"
|
className="task-group-header-button"
|
||||||
@@ -290,6 +309,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
|||||||
<br />
|
<br />
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
|
{...taskManagementAntdConfig.taskButtonDefaults}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={handleAddTask}
|
onClick={handleAddTask}
|
||||||
className="mt-2"
|
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 { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Input, Typography } from 'antd';
|
|
||||||
import type { InputRef } from 'antd';
|
|
||||||
import {
|
import {
|
||||||
|
Input,
|
||||||
|
Typography,
|
||||||
|
DatePicker,
|
||||||
|
dayjs,
|
||||||
|
taskManagementAntdConfig,
|
||||||
HolderOutlined,
|
HolderOutlined,
|
||||||
MessageOutlined,
|
MessageOutlined,
|
||||||
PaperClipOutlined,
|
PaperClipOutlined,
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
UserOutlined,
|
||||||
|
type InputRef
|
||||||
|
} from './antd-imports';
|
||||||
import { Task } from '@/types/task-management.types';
|
import { Task } from '@/types/task-management.types';
|
||||||
import { RootState } from '@/app/store';
|
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 { useSocket } from '@/socket/socketContext';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import TaskStatusDropdown from './task-status-dropdown';
|
import TaskStatusDropdown from './task-status-dropdown';
|
||||||
@@ -147,19 +152,20 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
const containerClasses = useMemo(() => {
|
const containerClasses = useMemo(() => {
|
||||||
const baseClasses = 'border-b transition-all duration-300';
|
const baseClasses = 'border-b transition-all duration-300';
|
||||||
const themeClasses = isDarkMode
|
const themeClasses = isDarkMode
|
||||||
? 'border-gray-700 bg-gray-900 hover:bg-gray-800'
|
? 'border-gray-600 hover:bg-gray-800'
|
||||||
: 'border-gray-200 bg-white hover:bg-gray-50';
|
: 'border-gray-300 hover:bg-gray-50';
|
||||||
|
const backgroundClasses = isDarkMode ? 'bg-[#18181b]' : 'bg-white';
|
||||||
const selectedClasses = isSelected
|
const selectedClasses = isSelected
|
||||||
? (isDarkMode ? 'bg-blue-900/20' : 'bg-blue-50')
|
? (isDarkMode ? 'bg-blue-900/20' : 'bg-blue-50')
|
||||||
: '';
|
: '';
|
||||||
const overlayClasses = isDragOverlay
|
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]);
|
}, [isDarkMode, isSelected, isDragOverlay]);
|
||||||
|
|
||||||
const fixedColumnsClasses = useMemo(() =>
|
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]
|
[isDarkMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -180,6 +186,23 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
const getStatusColor = useCallback((status: string) =>
|
const getStatusColor = useCallback((status: string) =>
|
||||||
STATUS_COLORS[status as keyof typeof STATUS_COLORS] || '#d9d9d9', []);
|
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
|
// Create adapter for LabelsSelector - memoized
|
||||||
const taskAdapter = useMemo(() => ({
|
const taskAdapter = useMemo(() => ({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
@@ -229,6 +252,35 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
}
|
}
|
||||||
}, [task.dueDate]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
@@ -238,18 +290,18 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
<div className="flex h-10 max-h-10 overflow-visible relative">
|
<div className="flex h-10 max-h-10 overflow-visible relative">
|
||||||
{/* Fixed Columns */}
|
{/* Fixed Columns */}
|
||||||
<div
|
<div
|
||||||
className="fixed-columns-row"
|
className="flex"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
|
||||||
background: isDarkMode ? '#1a1a1a' : '#fff',
|
|
||||||
width: fixedColumns?.reduce((sum, col) => sum + col.width, 0) || 0,
|
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) {
|
switch (col.key) {
|
||||||
case 'drag':
|
case 'drag':
|
||||||
return (
|
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
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -263,7 +315,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
case 'select':
|
case 'select':
|
||||||
return (
|
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
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={handleSelectChange}
|
onChange={handleSelectChange}
|
||||||
@@ -273,14 +325,16 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
case 'key':
|
case 'key':
|
||||||
return (
|
return (
|
||||||
<div key={col.key} className="w-20 flex items-center px-2 border-r" style={{ width: col.width }}>
|
<div key={col.key} className={`w-20 flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||||
<Tag
|
<span
|
||||||
backgroundColor={isDarkMode ? "#374151" : "#f0f0f0"}
|
className={`px-2 py-1 text-xs font-medium rounded truncate whitespace-nowrap max-w-full ${
|
||||||
color={isDarkMode ? "#d1d5db" : "#666"}
|
isDarkMode
|
||||||
className="truncate whitespace-nowrap max-w-full"
|
? 'bg-gray-700 text-gray-300'
|
||||||
|
: 'bg-gray-100 text-gray-600'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{task.task_key}
|
{task.task_key}
|
||||||
</Tag>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'task':
|
case 'task':
|
||||||
@@ -291,7 +345,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={col.key}
|
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}
|
style={cellStyle}
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
|
<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 ? (
|
{editTaskName ? (
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="task-name-input"
|
className="task-name-input w-full bg-transparent border-none outline-none text-sm"
|
||||||
value={taskName}
|
value={taskName}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTaskName(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTaskName(e.target.value)}
|
||||||
onBlur={handleTaskNameSave}
|
onBlur={handleTaskNameSave}
|
||||||
@@ -310,10 +364,6 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
background: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
outline: 'none',
|
|
||||||
width: '100%',
|
|
||||||
color: isDarkMode ? '#ffffff' : '#262626'
|
color: isDarkMode ? '#ffffff' : '#262626'
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -339,12 +389,29 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* Scrollable Columns */}
|
{/* Scrollable Columns */}
|
||||||
<div className="scrollable-columns-row overflow-visible" style={{ display: 'flex', minWidth: scrollableColumns?.reduce((sum, col) => sum + col.width, 0) || 0 }}>
|
<div className="overflow-visible" style={{ display: 'flex', minWidth: scrollableColumns?.reduce((sum, col) => sum + col.width, 0) || 0 }}>
|
||||||
{scrollableColumns?.map(col => {
|
{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) {
|
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':
|
case 'progress':
|
||||||
return (
|
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 && (
|
{task.progress !== undefined && task.progress >= 0 && (
|
||||||
<Progress
|
<Progress
|
||||||
type="circle"
|
type="circle"
|
||||||
@@ -360,7 +427,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
case 'members':
|
case 'members':
|
||||||
return (
|
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="flex items-center gap-2">
|
||||||
{avatarGroupMembers.length > 0 && (
|
{avatarGroupMembers.length > 0 && (
|
||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
@@ -380,7 +447,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
case 'labels':
|
case 'labels':
|
||||||
return (
|
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">
|
<div className="flex items-center gap-1 flex-wrap h-full w-full overflow-visible relative">
|
||||||
{task.labels?.map((label, index) => (
|
{task.labels?.map((label, index) => (
|
||||||
label.end && label.names && label.name ? (
|
label.end && label.names && label.name ? (
|
||||||
@@ -405,9 +472,17 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
</div>
|
</div>
|
||||||
</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':
|
case 'status':
|
||||||
return (
|
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
|
<TaskStatusDropdown
|
||||||
task={task}
|
task={task}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@@ -417,13 +492,13 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
case 'priority':
|
case 'priority':
|
||||||
return (
|
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="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 rounded-full"
|
className="w-2 h-2 rounded-full"
|
||||||
style={{ backgroundColor: getPriorityColor(task.priority) }}
|
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}
|
{task.priority}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -431,12 +506,12 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
case 'timeTracking':
|
case 'timeTracking':
|
||||||
return (
|
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">
|
<div className="flex items-center gap-2 h-full overflow-hidden">
|
||||||
{task.timeTracking?.logged && task.timeTracking.logged > 0 && (
|
{task.timeTracking?.logged && task.timeTracking.logged > 0 && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`} />
|
<ClockCircleOutlined className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`} />
|
||||||
<span className={`text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-600'}`}>
|
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||||
{typeof task.timeTracking.logged === 'number'
|
{typeof task.timeTracking.logged === 'number'
|
||||||
? `${task.timeTracking.logged}h`
|
? `${task.timeTracking.logged}h`
|
||||||
: task.timeTracking.logged
|
: task.timeTracking.logged
|
||||||
@@ -447,6 +522,79 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
</div>
|
</div>
|
||||||
</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:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -457,66 +605,33 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
// Simplified comparison for better performance
|
// Simplified comparison for better performance
|
||||||
return (
|
const taskPropsEqual = (
|
||||||
prevProps.task.id === nextProps.task.id &&
|
prevProps.task.id === nextProps.task.id &&
|
||||||
prevProps.task.title === nextProps.task.title &&
|
prevProps.task.title === nextProps.task.title &&
|
||||||
prevProps.task.progress === nextProps.task.progress &&
|
prevProps.task.progress === nextProps.task.progress &&
|
||||||
prevProps.task.status === nextProps.task.status &&
|
prevProps.task.status === nextProps.task.status &&
|
||||||
prevProps.task.priority === nextProps.task.priority &&
|
prevProps.task.priority === nextProps.task.priority &&
|
||||||
prevProps.task.labels?.length === nextProps.task.labels?.length &&
|
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.isSelected === nextProps.isSelected &&
|
||||||
prevProps.isDragOverlay === nextProps.isDragOverlay &&
|
prevProps.isDragOverlay === nextProps.isDragOverlay &&
|
||||||
prevProps.groupId === nextProps.groupId
|
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';
|
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
|
// Get field visibility from taskListFields slice
|
||||||
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
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
|
// Get tasks for this group using memoization for performance
|
||||||
const groupTasks = useMemo(() => {
|
const groupTasks = useMemo(() => {
|
||||||
@@ -105,12 +98,22 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const allScrollableColumns = [
|
const allScrollableColumns = [
|
||||||
|
{ key: 'description', label: 'Description', width: 200, fieldKey: 'DESCRIPTION' },
|
||||||
{ key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' },
|
{ key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' },
|
||||||
{ key: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' },
|
{ key: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' },
|
||||||
{ key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' },
|
{ 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: 'status', label: 'Status', width: 100, fieldKey: 'STATUS' },
|
||||||
{ key: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' },
|
{ key: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' },
|
||||||
{ key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' },
|
{ 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
|
// 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 scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0);
|
||||||
const totalTableWidth = fixedWidth + scrollableWidth;
|
const totalTableWidth = fixedWidth + scrollableWidth;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Row renderer for virtualization (only task rows)
|
// Row renderer for virtualization (only task rows)
|
||||||
const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
|
const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
|
||||||
const task: Task | undefined = groupTasks[index];
|
const task: Task | undefined = groupTasks[index];
|
||||||
|
|||||||
@@ -31,26 +31,17 @@ const LOCAL_STORAGE_KEY = 'worklenz.taskManagement.fields';
|
|||||||
|
|
||||||
function loadFields(): TaskListField[] {
|
function loadFields(): TaskListField[] {
|
||||||
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
|
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) {
|
if (stored) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored);
|
const parsed = JSON.parse(stored);
|
||||||
console.log('Parsed fields from localStorage:', parsed);
|
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to parse stored fields, using defaults:', error);
|
console.warn('Failed to parse stored fields, using defaults:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Using default fields:', DEFAULT_FIELDS);
|
|
||||||
return DEFAULT_FIELDS;
|
return DEFAULT_FIELDS;
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveFields(fields: TaskListField[]) {
|
function saveFields(fields: TaskListField[]) {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const ProjectView = () => {
|
|||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
items={tabMenuItems}
|
items={tabMenuItems}
|
||||||
tabBarStyle={{ paddingInline: 0 }}
|
tabBarStyle={{ paddingInline: 0 }}
|
||||||
destroyOnHidden={true}
|
destroyInactiveTabPane
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{portalElements}
|
{portalElements}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ export interface Task {
|
|||||||
assignees: string[];
|
assignees: string[];
|
||||||
assignee_names?: InlineMember[];
|
assignee_names?: InlineMember[];
|
||||||
labels: Label[];
|
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: {
|
timeTracking: {
|
||||||
estimated?: number;
|
estimated?: number;
|
||||||
logged: number;
|
logged: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user