feat(gantt-integration): add SVAR Gantt chart component and related features
- Integrated SVAR Gantt chart into the project view for enhanced task visualization. - Implemented lazy loading for improved performance and reduced initial bundle size. - Added custom styles for light and dark themes to match Worklenz branding. - Updated Redux state management to support Gantt-specific data handling. - Introduced a ResourcePreloader component to preload critical chunks for better navigation performance. - Enhanced project view to support Gantt as a new project view option. - Documented Gantt integration details and usage in README.md.
This commit is contained in:
@@ -10,6 +10,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||||
rel="stylesheet" />
|
rel="stylesheet" />
|
||||||
|
<!-- SVAR Gantt Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.svar.dev/fonts/wxi/wx-icons.css" />
|
||||||
<title>Worklenz</title>
|
<title>Worklenz</title>
|
||||||
|
|
||||||
<!-- Environment configuration -->
|
<!-- Environment configuration -->
|
||||||
|
|||||||
13
worklenz-frontend/package-lock.json
generated
13
worklenz-frontend/package-lock.json
generated
@@ -49,7 +49,8 @@
|
|||||||
"react-window": "^1.8.11",
|
"react-window": "^1.8.11",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tinymce": "^7.7.2",
|
"tinymce": "^7.7.2",
|
||||||
"web-vitals": "^4.2.4"
|
"web-vitals": "^4.2.4",
|
||||||
|
"wx-react-gantt": "^1.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
@@ -7738,6 +7739,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wx-react-gantt": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/wx-react-gantt/-/wx-react-gantt-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-Ua1hrMXfXENjhTVFBDf9D2mTXsw8BEHUDUUgnjDyQ4iXDEd5ueZGoUiCpBSX85XCDC8zIlm2f0KfuVSYNLuMRA==",
|
||||||
|
"license": "GPLv3",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xmlhttprequest-ssl": {
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
|||||||
@@ -52,7 +52,8 @@
|
|||||||
"react-window": "^1.8.11",
|
"react-window": "^1.8.11",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tinymce": "^7.7.2",
|
"tinymce": "^7.7.2",
|
||||||
"web-vitals": "^4.2.4"
|
"web-vitals": "^4.2.4",
|
||||||
|
"wx-react-gantt": "^1.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import i18next from 'i18next';
|
|||||||
// Components
|
// Components
|
||||||
import ThemeWrapper from './features/theme/ThemeWrapper';
|
import ThemeWrapper from './features/theme/ThemeWrapper';
|
||||||
import PreferenceSelector from './components/PreferenceSelector';
|
import PreferenceSelector from './components/PreferenceSelector';
|
||||||
|
import ResourcePreloader from './components/resource-preloader/resource-preloader';
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
import router from './app/routes';
|
import router from './app/routes';
|
||||||
@@ -47,6 +48,7 @@ const App: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<RouterProvider router={router} future={{ v7_startTransition: true }} />
|
<RouterProvider router={router} future={{ v7_startTransition: true }} />
|
||||||
|
<ResourcePreloader />
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResourcePreloader component to preload critical chunks for better performance
|
||||||
|
* This helps reduce loading times when users navigate to different project view tabs
|
||||||
|
*/
|
||||||
|
const ResourcePreloader = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
// Preload critical project view chunks after initial page load
|
||||||
|
const preloadCriticalChunks = () => {
|
||||||
|
// Only preload in production and if the user is likely to use project views
|
||||||
|
if (import.meta.env.DEV) return;
|
||||||
|
|
||||||
|
// Check if user is on a project-related page or dashboard
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
const isProjectRelated = currentPath.includes('/projects') ||
|
||||||
|
currentPath.includes('/worklenz') ||
|
||||||
|
currentPath === '/';
|
||||||
|
|
||||||
|
if (!isProjectRelated) return;
|
||||||
|
|
||||||
|
// Preload the most commonly used project view components
|
||||||
|
const criticalImports = [
|
||||||
|
() => import('@/pages/projects/projectView/taskList/project-view-task-list'),
|
||||||
|
() => import('@/pages/projects/projectView/board/project-view-board'),
|
||||||
|
() => import('@/components/project-task-filters/filter-dropdowns/group-by-filter-dropdown'),
|
||||||
|
() => import('@/components/project-task-filters/filter-dropdowns/search-dropdown'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Preload with a small delay to not interfere with initial page load
|
||||||
|
setTimeout(() => {
|
||||||
|
criticalImports.forEach(importFn => {
|
||||||
|
importFn().catch(error => {
|
||||||
|
// Silently handle preload failures - they're not critical
|
||||||
|
console.debug('Preload failed:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 2000); // 2 second delay after initial load
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start preloading when component mounts
|
||||||
|
preloadCriticalChunks();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// This component doesn't render anything
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResourcePreloader;
|
||||||
@@ -27,7 +27,7 @@ interface TaskListState {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
importTaskTemplateDrawerOpen: boolean;
|
importTaskTemplateDrawerOpen: boolean;
|
||||||
createTaskTemplateDrawerOpen: boolean;
|
createTaskTemplateDrawerOpen: boolean;
|
||||||
projectView: 'list' | 'kanban';
|
projectView: 'list' | 'kanban' | 'gantt';
|
||||||
refreshTimestamp: string | null;
|
refreshTimestamp: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +35,9 @@ const initialState: TaskListState = {
|
|||||||
projectId: null,
|
projectId: null,
|
||||||
project: null,
|
project: null,
|
||||||
projectLoading: false,
|
projectLoading: false,
|
||||||
activeMembers: [],
|
|
||||||
columns: [],
|
columns: [],
|
||||||
members: [],
|
members: [],
|
||||||
|
activeMembers: [],
|
||||||
labels: [],
|
labels: [],
|
||||||
statuses: [],
|
statuses: [],
|
||||||
priorities: [],
|
priorities: [],
|
||||||
@@ -173,7 +173,7 @@ const projectSlice = createSlice({
|
|||||||
setRefreshTimestamp: (state) => {
|
setRefreshTimestamp: (state) => {
|
||||||
state.refreshTimestamp = new Date().getTime().toString();
|
state.refreshTimestamp = new Date().getTime().toString();
|
||||||
},
|
},
|
||||||
setProjectView: (state, action: PayloadAction<'list' | 'kanban'>) => {
|
setProjectView: (state, action: PayloadAction<'list' | 'kanban' | 'gantt'>) => {
|
||||||
state.projectView = action.payload;
|
state.projectView = action.payload;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, lazy } from 'react';
|
||||||
import ProjectViewInsights from '@/pages/projects/projectView/insights/project-view-insights';
|
|
||||||
import ProjectViewFiles from '@/pages/projects/projectView/files/project-view-files';
|
// Lazy load all project view components for better code splitting
|
||||||
import ProjectViewMembers from '@/pages/projects/projectView/members/project-view-members';
|
const ProjectViewTaskList = lazy(() => import('@/pages/projects/projectView/taskList/project-view-task-list'));
|
||||||
import ProjectViewUpdates from '@/pages/projects/projectView/updates/ProjectViewUpdates';
|
const ProjectViewBoard = lazy(() => import('@/pages/projects/projectView/board/project-view-board'));
|
||||||
import ProjectViewTaskList from '@/pages/projects/projectView/taskList/project-view-task-list';
|
const ProjectViewGantt = lazy(() => import('@/pages/projects/projectView/gantt/project-view-gantt'));
|
||||||
import ProjectViewBoard from '@/pages/projects/projectView/board/project-view-board';
|
const ProjectViewInsights = lazy(() => import('@/pages/projects/projectView/insights/project-view-insights'));
|
||||||
import ProjectViewFinance from '@/pages/projects/projectView/finance/project-view-finance';
|
const ProjectViewFiles = lazy(() => import('@/pages/projects/projectView/files/project-view-files'));
|
||||||
|
const ProjectViewMembers = lazy(() => import('@/pages/projects/projectView/members/project-view-members'));
|
||||||
|
const ProjectViewUpdates = lazy(() => import('@/pages/projects/projectView/updates/ProjectViewUpdates'));
|
||||||
|
const ProjectViewFinance = lazy(() => import('@/pages/projects/projectView/finance/project-view-finance'));
|
||||||
|
|
||||||
// type of a tab items
|
// type of a tab items
|
||||||
type TabItems = {
|
type TabItems = {
|
||||||
@@ -32,6 +35,12 @@ export const tabItems: TabItems[] = [
|
|||||||
isPinned: true,
|
isPinned: true,
|
||||||
element: React.createElement(ProjectViewBoard),
|
element: React.createElement(ProjectViewBoard),
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// index: 2,
|
||||||
|
// key: 'gantt',
|
||||||
|
// label: 'Gantt Chart',
|
||||||
|
// element: React.createElement(ProjectViewGantt),
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
index: 4,
|
index: 4,
|
||||||
key: 'project-insights-member-overview',
|
key: 'project-insights-member-overview',
|
||||||
|
|||||||
181
worklenz-frontend/src/pages/projects/projectView/gantt/README.md
Normal file
181
worklenz-frontend/src/pages/projects/projectView/gantt/README.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# SVAR Gantt Integration for Worklenz
|
||||||
|
|
||||||
|
This directory contains the integration of SVAR Gantt chart component into the Worklenz project management system.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
SVAR Gantt is a modern React Gantt chart component that provides interactive project timeline visualization. This integration allows users to view their project tasks in a Gantt chart format within the Worklenz project view.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### `project-view-gantt.tsx`
|
||||||
|
Main component that handles:
|
||||||
|
- Task data transformation from Worklenz format to SVAR Gantt format
|
||||||
|
- Redux state management integration
|
||||||
|
- Dark/light theme support
|
||||||
|
- Loading states and error handling
|
||||||
|
- Event handling for task interactions
|
||||||
|
|
||||||
|
### `gantt-custom-styles.css`
|
||||||
|
Custom CSS that provides:
|
||||||
|
- Theme-aware styling (light/dark mode)
|
||||||
|
- Worklenz brand color integration
|
||||||
|
- Responsive design adjustments
|
||||||
|
- Smooth theme transitions
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### ✅ Implemented
|
||||||
|
- **Task Visualization**: Displays tasks and subtasks as Gantt bars
|
||||||
|
- **Group Support**: Shows task groups as summary tasks
|
||||||
|
- **Progress Tracking**: Visual progress indicators on task bars
|
||||||
|
- **Dark Mode**: Full dark/light theme compatibility
|
||||||
|
- **Responsive Design**: Works on desktop and mobile devices
|
||||||
|
- **Redux Integration**: Uses existing Worklenz Redux state management
|
||||||
|
- **Filtering**: Integrates with existing task filters
|
||||||
|
- **Loading States**: Proper skeleton loading while data loads
|
||||||
|
|
||||||
|
### 🚧 Planned Enhancements
|
||||||
|
- **Task Editing**: Click to open task drawer for editing
|
||||||
|
- **Drag & Drop**: Update task dates by dragging bars
|
||||||
|
- **Dependency Links**: Show task dependencies as links
|
||||||
|
- **Real-time Updates**: Live updates when tasks change
|
||||||
|
- **Export Features**: Export Gantt chart as PDF/image
|
||||||
|
- **Critical Path**: Highlight critical path in projects
|
||||||
|
|
||||||
|
## Data Transformation
|
||||||
|
|
||||||
|
The component transforms Worklenz task data structure to SVAR Gantt format:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Worklenz Task Group → SVAR Gantt Summary Task
|
||||||
|
{
|
||||||
|
id: `group-${group.id}`,
|
||||||
|
text: group.name,
|
||||||
|
start: groupStartDate,
|
||||||
|
end: groupEndDate,
|
||||||
|
type: 'summary',
|
||||||
|
parent: 0,
|
||||||
|
progress: group.done_progress
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worklenz Task → SVAR Gantt Task
|
||||||
|
{
|
||||||
|
id: task.id,
|
||||||
|
text: task.name,
|
||||||
|
start: task.start_date,
|
||||||
|
end: task.end_date,
|
||||||
|
type: task.sub_tasks_count > 0 ? 'summary' : 'task',
|
||||||
|
parent: `group-${group.id}`,
|
||||||
|
progress: task.progress / 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Theme Integration
|
||||||
|
|
||||||
|
The component uses Worklenz's theme system:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Theme detection from Redux
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
const isDarkMode = themeMode === 'dark';
|
||||||
|
|
||||||
|
// Dynamic CSS classes
|
||||||
|
<div className={`gantt-container ${isDarkMode ? 'gantt-dark-mode' : 'gantt-light-mode'}`}>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Variables
|
||||||
|
|
||||||
|
**Light Mode:**
|
||||||
|
- Task bars: `#1890ff` (Worklenz primary blue)
|
||||||
|
- Summary tasks: `#52c41a` (Worklenz green)
|
||||||
|
- Backgrounds: `#ffffff`, `#fafafa`
|
||||||
|
- Borders: `#d9d9d9`
|
||||||
|
|
||||||
|
**Dark Mode:**
|
||||||
|
- Task bars: `#4096ff` (Lighter blue for contrast)
|
||||||
|
- Summary tasks: `#73d13d` (Lighter green for contrast)
|
||||||
|
- Backgrounds: `#1f1f1f`, `#262626`
|
||||||
|
- Borders: `#424242`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Gantt Settings
|
||||||
|
```typescript
|
||||||
|
const ganttConfig = {
|
||||||
|
scales: [
|
||||||
|
{ unit: 'month', step: 1, format: 'MMMM yyyy' },
|
||||||
|
{ unit: 'day', step: 1, format: 'd' }
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
{ id: 'text', header: 'Task Name', width: 200 },
|
||||||
|
{ id: 'start', header: 'Start Date', width: 100 },
|
||||||
|
{ id: 'end', header: 'End Date', width: 100 },
|
||||||
|
{ id: 'progress', header: 'Progress', width: 80 }
|
||||||
|
],
|
||||||
|
taskHeight: 32,
|
||||||
|
rowHeight: 40
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `wx-react-gantt`: SVAR Gantt React component
|
||||||
|
- `wx-react-gantt/dist/gantt.css`: Base SVAR Gantt styles
|
||||||
|
- External CDN for icons: `https://cdn.svar.dev/fonts/wxi/wx-icons.css`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The Gantt tab is automatically available in the project view tabs. Users can:
|
||||||
|
1. Navigate to a project
|
||||||
|
2. Click the "Gantt" tab
|
||||||
|
3. View tasks in timeline format
|
||||||
|
4. Use existing filters to refine the view
|
||||||
|
5. Toggle between light/dark themes
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- **Lazy Loading**: Component is lazy-loaded to reduce initial bundle size
|
||||||
|
- **Data Memoization**: Task transformation is memoized to prevent unnecessary recalculations
|
||||||
|
- **Batch Loading**: Initial data fetching is batched for efficiency
|
||||||
|
- **Conditional Rendering**: Only renders when tab is active (destroyInactiveTabPane)
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
Compatible with all modern browsers that support:
|
||||||
|
- ES6+ JavaScript features
|
||||||
|
- CSS Grid and Flexbox
|
||||||
|
- CSS Custom Properties (variables)
|
||||||
|
- React 18+
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Styles not loading**: Ensure CSS import order is correct
|
||||||
|
2. **Theme not updating**: Check Redux theme state
|
||||||
|
3. **Tasks not displaying**: Verify task data has required fields (name, dates)
|
||||||
|
4. **Performance issues**: Check if too many tasks are being rendered
|
||||||
|
|
||||||
|
### Debug Tips
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Enable console logging for debugging
|
||||||
|
console.log('Gantt data:', ganttData);
|
||||||
|
console.log('Theme mode:', themeMode);
|
||||||
|
console.log('Task groups:', taskGroups);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
When modifying the Gantt integration:
|
||||||
|
|
||||||
|
1. Test both light and dark themes
|
||||||
|
2. Verify responsive behavior
|
||||||
|
3. Check performance with large datasets
|
||||||
|
4. Update this README if adding new features
|
||||||
|
5. Follow Worklenz coding standards and patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more information about SVAR Gantt, visit: https://svar.dev/react/gantt/
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
/* Custom styling for SVAR Gantt to match Worklenz theme */
|
||||||
|
|
||||||
|
/* Light Mode (Default) */
|
||||||
|
.gantt-container.gantt-light-mode {
|
||||||
|
--wx-gantt-border: 1px solid #d9d9d9;
|
||||||
|
--wx-gantt-icon-color: #666;
|
||||||
|
|
||||||
|
/* Task bars */
|
||||||
|
--wx-gantt-task-color: #1890ff;
|
||||||
|
--wx-gantt-task-font-color: #fff;
|
||||||
|
--wx-gantt-task-fill-color: #096dd9;
|
||||||
|
--wx-gantt-task-border-color: #096dd9;
|
||||||
|
|
||||||
|
/* Project/Summary tasks */
|
||||||
|
--wx-gantt-project-color: #52c41a;
|
||||||
|
--wx-gantt-project-font-color: #ffffff;
|
||||||
|
--wx-gantt-project-fill-color: #389e0d;
|
||||||
|
--wx-gantt-project-border-color: #389e0d;
|
||||||
|
|
||||||
|
/* Milestone */
|
||||||
|
--wx-gantt-milestone-color: #fa8c16;
|
||||||
|
|
||||||
|
/* Grid styling */
|
||||||
|
--wx-grid-header-font-color: #262626;
|
||||||
|
--wx-grid-body-font-color: #262626;
|
||||||
|
--wx-grid-body-row-border: 1px solid #f0f0f0;
|
||||||
|
--wx-grid-header-background: #fafafa;
|
||||||
|
--wx-grid-body-background: #ffffff;
|
||||||
|
|
||||||
|
/* Timescale */
|
||||||
|
--wx-timescale-font-color: #262626;
|
||||||
|
--wx-timescale-border: 1px solid #d9d9d9;
|
||||||
|
--wx-timescale-background: #fafafa;
|
||||||
|
|
||||||
|
/* Selection and hover */
|
||||||
|
--wx-gantt-select-color: #e6f7ff;
|
||||||
|
--wx-gantt-link-color: #d9d9d9;
|
||||||
|
--wx-gantt-hover-color: #f5f5f5;
|
||||||
|
|
||||||
|
/* Progress */
|
||||||
|
--wx-gantt-progress-border-color: #d9d9d9;
|
||||||
|
|
||||||
|
/* Weekend/holiday */
|
||||||
|
--wx-gantt-holiday-background: #fafafa;
|
||||||
|
--wx-gantt-holiday-color: #9fa1ae;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode */
|
||||||
|
.gantt-container.gantt-dark-mode {
|
||||||
|
--wx-gantt-border: 1px solid #424242;
|
||||||
|
--wx-gantt-icon-color: #bfbfbf;
|
||||||
|
|
||||||
|
/* Task bars - Adjusted for dark theme */
|
||||||
|
--wx-gantt-task-color: #4096ff;
|
||||||
|
--wx-gantt-task-font-color: #ffffff;
|
||||||
|
--wx-gantt-task-fill-color: #1668dc;
|
||||||
|
--wx-gantt-task-border-color: #1668dc;
|
||||||
|
|
||||||
|
/* Project/Summary tasks - Adjusted for dark theme */
|
||||||
|
--wx-gantt-project-color: #73d13d;
|
||||||
|
--wx-gantt-project-font-color: #000000;
|
||||||
|
--wx-gantt-project-fill-color: #52c41a;
|
||||||
|
--wx-gantt-project-border-color: #52c41a;
|
||||||
|
|
||||||
|
/* Milestone - Adjusted for dark theme */
|
||||||
|
--wx-gantt-milestone-color: #ffa940;
|
||||||
|
|
||||||
|
/* Grid styling - Dark theme */
|
||||||
|
--wx-grid-header-font-color: #ffffff;
|
||||||
|
--wx-grid-body-font-color: #ffffff;
|
||||||
|
--wx-grid-body-row-border: 1px solid #424242;
|
||||||
|
--wx-grid-header-background: #262626;
|
||||||
|
--wx-grid-body-background: #1f1f1f;
|
||||||
|
|
||||||
|
/* Timescale - Dark theme */
|
||||||
|
--wx-timescale-font-color: #ffffff;
|
||||||
|
--wx-timescale-border: 1px solid #424242;
|
||||||
|
--wx-timescale-background: #262626;
|
||||||
|
|
||||||
|
/* Selection and hover - Dark theme */
|
||||||
|
--wx-gantt-select-color: #111b26;
|
||||||
|
--wx-gantt-link-color: #595959;
|
||||||
|
--wx-gantt-hover-color: #2c2c2c;
|
||||||
|
|
||||||
|
/* Progress - Dark theme */
|
||||||
|
--wx-gantt-progress-border-color: #595959;
|
||||||
|
|
||||||
|
/* Weekend/holiday - Dark theme */
|
||||||
|
--wx-gantt-holiday-background: #262626;
|
||||||
|
--wx-gantt-holiday-color: #8c8c8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force dark mode styles with higher specificity */
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt,
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt * {
|
||||||
|
background-color: #1f1f1f !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-dark-mode .wx-grid-header,
|
||||||
|
.gantt-container.gantt-dark-mode .wx-timescale {
|
||||||
|
background-color: #262626 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: #424242 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-dark-mode .wx-grid-body {
|
||||||
|
background-color: #1f1f1f !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-dark-mode .wx-grid-cell {
|
||||||
|
border-color: #424242 !important;
|
||||||
|
background-color: #1f1f1f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt-task {
|
||||||
|
background-color: #4096ff !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt-project {
|
||||||
|
background-color: #73d13d !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common styles for both themes */
|
||||||
|
.gantt-container .wx-gantt {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Task text styling */
|
||||||
|
.gantt-container .wx-gantt-task-text {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary task styling */
|
||||||
|
.gantt-container .wx-gantt-project-text {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid header styling */
|
||||||
|
.gantt-container .wx-grid-header {
|
||||||
|
background-color: var(--wx-grid-header-background);
|
||||||
|
border-bottom: 2px solid var(--wx-gantt-border);
|
||||||
|
color: var(--wx-grid-header-font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid body styling */
|
||||||
|
.gantt-container .wx-grid-body {
|
||||||
|
background-color: var(--wx-grid-body-background);
|
||||||
|
color: var(--wx-grid-body-font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timescale header styling */
|
||||||
|
.gantt-container .wx-timescale {
|
||||||
|
background-color: var(--wx-timescale-background);
|
||||||
|
color: var(--wx-timescale-font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Task row hover effect */
|
||||||
|
.gantt-container .wx-grid-row:hover {
|
||||||
|
background-color: var(--wx-gantt-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom progress bar styling */
|
||||||
|
.gantt-container .wx-gantt-progress {
|
||||||
|
opacity: 0.8;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Weekend/holiday highlighting */
|
||||||
|
.gantt-container .wx-gantt-holiday {
|
||||||
|
background-color: var(--wx-gantt-holiday-background);
|
||||||
|
color: var(--wx-gantt-holiday-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling - Light mode */
|
||||||
|
.gantt-container.gantt-light-mode .wx-gantt-scroll {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #d9d9d9 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-light-mode .wx-gantt-scroll::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-light-mode .wx-gantt-scroll::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-light-mode .wx-gantt-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-light-mode .wx-gantt-scroll::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling - Dark mode */
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt-scroll {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #595959 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt-scroll::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt-scroll::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #595959;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt-scroll::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode specific overrides for SVAR Gantt */
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt {
|
||||||
|
background-color: #1f1f1f;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode task selection */
|
||||||
|
.gantt-container.gantt-dark-mode .wx-gantt-task:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode grid lines */
|
||||||
|
.gantt-container.gantt-dark-mode .wx-grid-cell {
|
||||||
|
border-color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode tooltip styling */
|
||||||
|
.gantt-container.gantt-dark-mode .wx-tooltip {
|
||||||
|
background-color: #434343;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #595959;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.gantt-container {
|
||||||
|
--wx-gantt-bar-font: 500 12px 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container .wx-grid-header,
|
||||||
|
.gantt-container .wx-timescale {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme transition for smooth mode switching */
|
||||||
|
.gantt-container * {
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
|
import { Flex, Skeleton, Empty, message } from 'antd';
|
||||||
|
// @ts-ignore - wx-react-gantt doesn't have TypeScript definitions
|
||||||
|
import { Gantt, Willow, WillowDark } from 'wx-react-gantt';
|
||||||
|
import 'wx-react-gantt/dist/gantt.css';
|
||||||
|
// import './gantt-custom-styles.css'; // Temporarily disabled
|
||||||
|
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
||||||
|
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
||||||
|
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
|
||||||
|
import { fetchTaskAssignees } from '@/features/tasks/tasks.slice';
|
||||||
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||||
|
import TaskListFilters from '../taskList/task-list-filters/task-list-filters';
|
||||||
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
|
import { colors } from '@/styles/colors';
|
||||||
|
|
||||||
|
// Transform Worklenz task data to SVAR Gantt format
|
||||||
|
const transformTasksToGanttData = (taskGroups: ITaskListGroup[]) => {
|
||||||
|
const tasks: any[] = [];
|
||||||
|
const links: any[] = [];
|
||||||
|
|
||||||
|
taskGroups.forEach((group, groupIndex) => {
|
||||||
|
// Add group as a summary task
|
||||||
|
if (group.tasks.length > 0) {
|
||||||
|
const groupStartDate = group.start_date ? new Date(group.start_date) : new Date();
|
||||||
|
const groupEndDate = group.end_date ? new Date(group.end_date) : new Date();
|
||||||
|
|
||||||
|
tasks.push({
|
||||||
|
id: `group-${group.id}`,
|
||||||
|
text: group.name,
|
||||||
|
start: groupStartDate,
|
||||||
|
end: groupEndDate,
|
||||||
|
type: 'summary',
|
||||||
|
open: true,
|
||||||
|
parent: 0,
|
||||||
|
progress: Math.round((group.done_progress || 0) * 100) / 100,
|
||||||
|
details: `Status: ${group.name}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add individual tasks
|
||||||
|
group.tasks.forEach((task: IProjectTask, taskIndex: number) => {
|
||||||
|
const startDate = task.start_date ? new Date(task.start_date) : new Date();
|
||||||
|
const endDate = task.end_date ? new Date(task.end_date) : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // Default to 1 week from now
|
||||||
|
|
||||||
|
// Handle task type
|
||||||
|
let taskType = 'task';
|
||||||
|
if (task.sub_tasks_count && task.sub_tasks_count > 0) {
|
||||||
|
taskType = 'summary';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ganttTask = {
|
||||||
|
id: task.id,
|
||||||
|
text: task.name || 'Untitled Task',
|
||||||
|
start: startDate,
|
||||||
|
end: endDate,
|
||||||
|
type: taskType,
|
||||||
|
parent: `group-${group.id}`,
|
||||||
|
progress: (task.progress || 0) / 100,
|
||||||
|
details: task.description || '',
|
||||||
|
priority: task.priority_name || 'Normal',
|
||||||
|
status: task.status || 'New',
|
||||||
|
assignees: task.names?.map(member => member.name).join(', ') || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
tasks.push(ganttTask);
|
||||||
|
|
||||||
|
// Add subtasks if they exist
|
||||||
|
if (task.sub_tasks && task.sub_tasks.length > 0) {
|
||||||
|
task.sub_tasks.forEach((subTask: IProjectTask) => {
|
||||||
|
const subStartDate = subTask.start_date ? new Date(subTask.start_date) : startDate;
|
||||||
|
const subEndDate = subTask.end_date ? new Date(subTask.end_date) : endDate;
|
||||||
|
|
||||||
|
tasks.push({
|
||||||
|
id: subTask.id,
|
||||||
|
text: subTask.name || 'Untitled Subtask',
|
||||||
|
start: subStartDate,
|
||||||
|
end: subEndDate,
|
||||||
|
type: 'task',
|
||||||
|
parent: task.id,
|
||||||
|
progress: (subTask.progress || 0) / 100,
|
||||||
|
details: subTask.description || '',
|
||||||
|
priority: subTask.priority_name || 'Normal',
|
||||||
|
status: subTask.status || 'New',
|
||||||
|
assignees: subTask.names?.map(member => member.name).join(', ') || '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { tasks, links };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectViewGantt = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { projectView } = useTabSearchParam();
|
||||||
|
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||||
|
|
||||||
|
// Redux selectors
|
||||||
|
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||||
|
const taskGroups = useAppSelector(state => state.taskReducer.taskGroups);
|
||||||
|
const loadingGroups = useAppSelector(state => state.taskReducer.loadingGroups);
|
||||||
|
const groupBy = useAppSelector(state => state.taskReducer.groupBy);
|
||||||
|
const archived = useAppSelector(state => state.taskReducer.archived);
|
||||||
|
const fields = useAppSelector(state => state.taskReducer.fields);
|
||||||
|
const search = useAppSelector(state => state.taskReducer.search);
|
||||||
|
|
||||||
|
const statusCategories = useAppSelector(state => state.taskStatusReducer.statusCategories);
|
||||||
|
const loadingStatusCategories = useAppSelector(state => state.taskStatusReducer.loading);
|
||||||
|
|
||||||
|
const loadingPhases = useAppSelector(state => state.phaseReducer.loadingPhases);
|
||||||
|
|
||||||
|
// Get theme mode from Worklenz theme system
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
const isDarkMode = themeMode === 'dark';
|
||||||
|
|
||||||
|
// Debug theme detection
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Theme mode detected:', themeMode, 'isDarkMode:', isDarkMode);
|
||||||
|
}, [themeMode, isDarkMode]);
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
const isLoading = useMemo(() =>
|
||||||
|
loadingGroups || loadingPhases || loadingStatusCategories || !initialLoadComplete,
|
||||||
|
[loadingGroups, loadingPhases, loadingStatusCategories, initialLoadComplete]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Empty state check
|
||||||
|
const isEmptyState = useMemo(() =>
|
||||||
|
taskGroups && taskGroups.length === 0 && !isLoading,
|
||||||
|
[taskGroups, isLoading]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transform data for SVAR Gantt
|
||||||
|
const ganttData = useMemo(() => {
|
||||||
|
if (!taskGroups || taskGroups.length === 0) {
|
||||||
|
return { tasks: [], links: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with hardcoded data first to isolate the issue
|
||||||
|
const testData = {
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
text: "Test Project",
|
||||||
|
start: new Date(2024, 0, 1),
|
||||||
|
end: new Date(2024, 0, 15),
|
||||||
|
type: "summary",
|
||||||
|
open: true,
|
||||||
|
parent: 0,
|
||||||
|
progress: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
text: "Test Task 1",
|
||||||
|
start: new Date(2024, 0, 2),
|
||||||
|
end: new Date(2024, 0, 8),
|
||||||
|
type: "task",
|
||||||
|
parent: 1,
|
||||||
|
progress: 0.3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
text: "Test Task 2",
|
||||||
|
start: new Date(2024, 0, 9),
|
||||||
|
end: new Date(2024, 0, 14),
|
||||||
|
type: "task",
|
||||||
|
parent: 1,
|
||||||
|
progress: 0.7,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
links: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Using test data for debugging:', testData);
|
||||||
|
return testData;
|
||||||
|
|
||||||
|
// Original transformation (commented out for testing)
|
||||||
|
// const result = transformTasksToGanttData(taskGroups);
|
||||||
|
// console.log('Gantt data - tasks count:', result.tasks.length);
|
||||||
|
// if (result.tasks.length > 0) {
|
||||||
|
// console.log('First task:', result.tasks[0]);
|
||||||
|
// console.log('Sample dates:', result.tasks[0]?.start, result.tasks[0]?.end);
|
||||||
|
// }
|
||||||
|
// return result;
|
||||||
|
}, [taskGroups]);
|
||||||
|
|
||||||
|
// Calculate date range for the Gantt chart
|
||||||
|
const dateRange = useMemo(() => {
|
||||||
|
// Fixed range for testing
|
||||||
|
return {
|
||||||
|
start: new Date(2023, 11, 1), // December 1, 2023
|
||||||
|
end: new Date(2024, 1, 29), // February 29, 2024
|
||||||
|
};
|
||||||
|
|
||||||
|
// Original dynamic calculation (commented out for testing)
|
||||||
|
// if (ganttData.tasks.length === 0) {
|
||||||
|
// const now = new Date();
|
||||||
|
// return {
|
||||||
|
// start: new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
||||||
|
// end: new Date(now.getFullYear(), now.getMonth() + 2, 0),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const dates = ganttData.tasks.map(task => [task.start, task.end]).flat();
|
||||||
|
// const minDate = new Date(Math.min(...dates.map(d => d.getTime())));
|
||||||
|
// const maxDate = new Date(Math.max(...dates.map(d => d.getTime())));
|
||||||
|
|
||||||
|
// // Add some padding
|
||||||
|
// const startDate = new Date(minDate);
|
||||||
|
// startDate.setDate(startDate.getDate() - 7);
|
||||||
|
// const endDate = new Date(maxDate);
|
||||||
|
// endDate.setDate(endDate.getDate() + 7);
|
||||||
|
|
||||||
|
// return { start: startDate, end: endDate };
|
||||||
|
}, [ganttData.tasks]);
|
||||||
|
|
||||||
|
// Batch initial data fetching
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchInitialData = async () => {
|
||||||
|
if (!projectId || !groupBy || initialLoadComplete) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.allSettled([
|
||||||
|
dispatch(fetchPhasesByProjectId(projectId)),
|
||||||
|
dispatch(fetchStatusesCategories()),
|
||||||
|
dispatch(fetchTaskAssignees(projectId)),
|
||||||
|
]);
|
||||||
|
setInitialLoadComplete(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching initial data:', error);
|
||||||
|
setInitialLoadComplete(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchInitialData();
|
||||||
|
}, [projectId, groupBy, dispatch, initialLoadComplete]);
|
||||||
|
|
||||||
|
// Fetch task groups
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTasks = async () => {
|
||||||
|
if (!projectId || !groupBy || !initialLoadComplete) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch(fetchTaskGroups(projectId));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching task groups:', error);
|
||||||
|
message.error('Failed to load tasks for Gantt chart');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTasks();
|
||||||
|
}, [projectId, groupBy, dispatch, fields, search, archived, initialLoadComplete]);
|
||||||
|
|
||||||
|
// Gantt configuration
|
||||||
|
const ganttConfig = useMemo(() => ({
|
||||||
|
// Time scale configuration
|
||||||
|
scales: [
|
||||||
|
{ unit: 'month', step: 1, format: 'MMMM yyyy' },
|
||||||
|
{ unit: 'day', step: 1, format: 'd' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Columns configuration
|
||||||
|
columns: [
|
||||||
|
{ id: 'text', header: 'Task Name', width: 200 },
|
||||||
|
{ id: 'start', header: 'Start Date', width: 100 },
|
||||||
|
{ id: 'end', header: 'End Date', width: 100 },
|
||||||
|
{ id: 'progress', header: 'Progress', width: 80 },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
onTaskClick: (task: any) => {
|
||||||
|
console.log('Task clicked:', task);
|
||||||
|
// TODO: Open task drawer
|
||||||
|
},
|
||||||
|
|
||||||
|
onTaskUpdate: (task: any) => {
|
||||||
|
console.log('Task updated:', task);
|
||||||
|
// TODO: Update task via API
|
||||||
|
},
|
||||||
|
|
||||||
|
// Style configuration
|
||||||
|
taskHeight: 32,
|
||||||
|
rowHeight: 40,
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
if (isEmptyState) {
|
||||||
|
return (
|
||||||
|
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||||
|
<TaskListFilters position="list" />
|
||||||
|
<Empty description="No tasks found for Gantt chart" />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||||
|
<TaskListFilters position="list" />
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.wx-gantt {
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
|
.wx-gantt-task {
|
||||||
|
background-color: #3983eb !important;
|
||||||
|
border: 1px solid #1f6bd9 !important;
|
||||||
|
}
|
||||||
|
.wx-gantt-project {
|
||||||
|
background-color: #00ba94 !important;
|
||||||
|
border: 1px solid #099f81 !important;
|
||||||
|
}
|
||||||
|
${isDarkMode ? `
|
||||||
|
.wx-gantt-task {
|
||||||
|
background-color: #37a9ef !important;
|
||||||
|
border: 1px solid #098cdc !important;
|
||||||
|
}
|
||||||
|
` : ''}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
<Skeleton active loading={isLoading} className='mt-4 p-4'>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '600px',
|
||||||
|
width: '100%',
|
||||||
|
border: `1px solid ${isDarkMode ? '#424242' : '#d9d9d9'}`,
|
||||||
|
borderRadius: '6px',
|
||||||
|
backgroundColor: isDarkMode ? colors.darkGray : colors.white,
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isDarkMode ? (
|
||||||
|
<WillowDark>
|
||||||
|
<Gantt
|
||||||
|
tasks={ganttData.tasks}
|
||||||
|
links={ganttData.links}
|
||||||
|
start={new Date(2024, 0, 1)}
|
||||||
|
end={new Date(2024, 0, 31)}
|
||||||
|
scales={[
|
||||||
|
{ unit: 'day', step: 1, format: 'd' }
|
||||||
|
]}
|
||||||
|
columns={[
|
||||||
|
{ id: 'text', header: 'Task Name', width: 200 }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</WillowDark>
|
||||||
|
) : (
|
||||||
|
<Willow>
|
||||||
|
<Gantt
|
||||||
|
tasks={ganttData.tasks}
|
||||||
|
links={ganttData.links}
|
||||||
|
start={new Date(2024, 0, 1)}
|
||||||
|
end={new Date(2024, 0, 31)}
|
||||||
|
scales={[
|
||||||
|
{ unit: 'day', step: 1, format: 'd' }
|
||||||
|
]}
|
||||||
|
columns={[
|
||||||
|
{ id: 'text', header: 'Task Name', width: 200 }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Willow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Skeleton>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectViewGantt;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
import React, { useEffect, useState, useMemo, useCallback, Suspense } from 'react';
|
||||||
import { PushpinFilled, PushpinOutlined } from '@ant-design/icons';
|
import { PushpinFilled, PushpinOutlined } from '@ant-design/icons';
|
||||||
import { Button, ConfigProvider, Flex, Tabs } from 'antd';
|
import { Button, ConfigProvider, Flex, Tabs, Spin } from 'antd';
|
||||||
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
@@ -30,6 +30,18 @@ const ProjectMemberDrawer = React.lazy(
|
|||||||
);
|
);
|
||||||
const TaskDrawer = React.lazy(() => import('@components/task-drawer/task-drawer'));
|
const TaskDrawer = React.lazy(() => import('@components/task-drawer/task-drawer'));
|
||||||
|
|
||||||
|
// Loading component for lazy-loaded tabs
|
||||||
|
const TabLoadingFallback = () => (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '400px'
|
||||||
|
}}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const ProjectView = () => {
|
const ProjectView = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -76,7 +88,11 @@ const ProjectView = () => {
|
|||||||
const pinToDefaultTab = useCallback(async (itemKey: string) => {
|
const pinToDefaultTab = useCallback(async (itemKey: string) => {
|
||||||
if (!itemKey || !projectId) return;
|
if (!itemKey || !projectId) return;
|
||||||
|
|
||||||
const defaultView = itemKey === 'tasks-list' ? 'TASK_LIST' : 'BOARD';
|
let defaultView = 'TASK_LIST';
|
||||||
|
if (itemKey === 'board') {
|
||||||
|
defaultView = 'BOARD';
|
||||||
|
}
|
||||||
|
|
||||||
const res = await projectsApiService.updateDefaultTab({
|
const res = await projectsApiService.updateDefaultTab({
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
default_view: defaultView,
|
default_view: defaultView,
|
||||||
@@ -104,7 +120,13 @@ const ProjectView = () => {
|
|||||||
|
|
||||||
const handleTabChange = useCallback((key: string) => {
|
const handleTabChange = useCallback((key: string) => {
|
||||||
setActiveTab(key);
|
setActiveTab(key);
|
||||||
dispatch(setProjectView(key === 'board' ? 'kanban' : 'list'));
|
let projectView: 'list' | 'kanban' | 'gantt' = 'list';
|
||||||
|
if (key === 'board') {
|
||||||
|
projectView = 'kanban';
|
||||||
|
} else if (key === 'gantt') {
|
||||||
|
projectView = 'gantt';
|
||||||
|
}
|
||||||
|
dispatch(setProjectView(projectView));
|
||||||
navigate({
|
navigate({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: new URLSearchParams({
|
search: new URLSearchParams({
|
||||||
@@ -119,7 +141,7 @@ const ProjectView = () => {
|
|||||||
label: (
|
label: (
|
||||||
<Flex align="center" >
|
<Flex align="center" >
|
||||||
{item.label}
|
{item.label}
|
||||||
{item.key === 'tasks-list' || item.key === 'board' ? (
|
{(item.key === 'tasks-list' || item.key === 'board') ? (
|
||||||
<ConfigProvider wave={{ disabled: true }}>
|
<ConfigProvider wave={{ disabled: true }}>
|
||||||
<Button
|
<Button
|
||||||
className="borderless-icon-btn"
|
className="borderless-icon-btn"
|
||||||
@@ -152,7 +174,11 @@ const ProjectView = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
),
|
),
|
||||||
children: item.element,
|
children: (
|
||||||
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
|
{item.element}
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
})), [pinnedTab, pinToDefaultTab]);
|
})), [pinnedTab, pinToDefaultTab]);
|
||||||
|
|
||||||
const portalElements = useMemo(() => (
|
const portalElements = useMemo(() => (
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { UserConfig } from 'vite'; // Import type for better auto-completion
|
|
||||||
|
|
||||||
export default defineConfig(async ({ command }: { command: 'build' | 'serve' }) => {
|
|
||||||
const tsconfigPaths = (await import('vite-tsconfig-paths')).default;
|
|
||||||
|
|
||||||
|
export default defineConfig(({ command }) => {
|
||||||
return {
|
return {
|
||||||
// **Plugins**
|
// **Plugins**
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -40,6 +38,9 @@ export default defineConfig(async ({ command }: { command: 'build' | 'serve' })
|
|||||||
assetsDir: 'assets', // Consider a more specific directory for better organization, e.g., 'build/assets'
|
assetsDir: 'assets', // Consider a more specific directory for better organization, e.g., 'build/assets'
|
||||||
cssCodeSplit: true,
|
cssCodeSplit: true,
|
||||||
|
|
||||||
|
// **Chunk Size Optimization**
|
||||||
|
chunkSizeWarningLimit: 1000, // Increase limit for vendor chunks but keep warning for others
|
||||||
|
|
||||||
// **Sourcemaps**
|
// **Sourcemaps**
|
||||||
sourcemap: command === 'serve' ? 'inline' : true, // Adjust sourcemap strategy based on command
|
sourcemap: command === 'serve' ? 'inline' : true, // Adjust sourcemap strategy based on command
|
||||||
|
|
||||||
@@ -49,22 +50,89 @@ export default defineConfig(async ({ command }: { command: 'build' | 'serve' })
|
|||||||
compress: {
|
compress: {
|
||||||
drop_console: command === 'build',
|
drop_console: command === 'build',
|
||||||
drop_debugger: command === 'build',
|
drop_debugger: command === 'build',
|
||||||
|
// Remove unused code more aggressively
|
||||||
|
unused: true,
|
||||||
|
dead_code: true,
|
||||||
},
|
},
|
||||||
// **Additional Optimization**
|
// **Additional Optimization**
|
||||||
format: {
|
format: {
|
||||||
comments: command === 'serve', // Preserve comments during development
|
comments: command === 'serve', // Preserve comments during development
|
||||||
},
|
},
|
||||||
|
mangle: {
|
||||||
|
// Mangle private properties for smaller bundles
|
||||||
|
properties: {
|
||||||
|
regex: /^_/,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// **Rollup Options**
|
// **Rollup Options**
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
// **Chunking Strategy**
|
// **Enhanced Chunking Strategy**
|
||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
if (['react', 'react-dom', 'react-router-dom'].includes(id)) return 'vendor';
|
// Core React dependencies
|
||||||
if (id.includes('antd')) return 'antd';
|
if (['react', 'react-dom'].includes(id)) return 'react-vendor';
|
||||||
if (id.includes('i18next')) return 'i18n';
|
|
||||||
// Add more conditions as needed
|
// Router and navigation
|
||||||
|
if (id.includes('react-router-dom') || id.includes('react-router')) return 'router';
|
||||||
|
|
||||||
|
// UI Library
|
||||||
|
if (id.includes('antd')) return 'antd-ui';
|
||||||
|
|
||||||
|
// Internationalization
|
||||||
|
if (id.includes('i18next') || id.includes('react-i18next')) return 'i18n';
|
||||||
|
|
||||||
|
// Redux and state management
|
||||||
|
if (id.includes('@reduxjs/toolkit') || id.includes('redux') || id.includes('react-redux')) return 'redux';
|
||||||
|
|
||||||
|
// Date and time utilities
|
||||||
|
if (id.includes('moment') || id.includes('dayjs') || id.includes('date-fns')) return 'date-utils';
|
||||||
|
|
||||||
|
// Drag and drop
|
||||||
|
if (id.includes('@dnd-kit')) return 'dnd-kit';
|
||||||
|
|
||||||
|
// Charts and visualization
|
||||||
|
if (id.includes('chart') || id.includes('echarts') || id.includes('highcharts') || id.includes('recharts')) return 'charts';
|
||||||
|
|
||||||
|
// Text editor
|
||||||
|
if (id.includes('tinymce') || id.includes('quill') || id.includes('editor')) return 'editors';
|
||||||
|
|
||||||
|
// Project view components - split into separate chunks for better lazy loading
|
||||||
|
if (id.includes('/pages/projects/projectView/taskList/')) return 'project-task-list';
|
||||||
|
if (id.includes('/pages/projects/projectView/board/')) return 'project-board';
|
||||||
|
if (id.includes('/pages/projects/projectView/insights/')) return 'project-insights';
|
||||||
|
if (id.includes('/pages/projects/projectView/finance/')) return 'project-finance';
|
||||||
|
if (id.includes('/pages/projects/projectView/members/')) return 'project-members';
|
||||||
|
if (id.includes('/pages/projects/projectView/files/')) return 'project-files';
|
||||||
|
if (id.includes('/pages/projects/projectView/updates/')) return 'project-updates';
|
||||||
|
|
||||||
|
// Task-related components
|
||||||
|
if (id.includes('/components/task-') || id.includes('/features/tasks/')) return 'task-components';
|
||||||
|
|
||||||
|
// Filter components
|
||||||
|
if (id.includes('/components/project-task-filters/') || id.includes('filter-dropdown')) return 'filter-components';
|
||||||
|
|
||||||
|
// Other project components
|
||||||
|
if (id.includes('/pages/projects/') && !id.includes('/projectView/')) return 'project-pages';
|
||||||
|
|
||||||
|
// Settings and admin
|
||||||
|
if (id.includes('/pages/settings/') || id.includes('/pages/admin-center/')) return 'settings-admin';
|
||||||
|
|
||||||
|
// Reporting
|
||||||
|
if (id.includes('/pages/reporting/') || id.includes('/features/reporting/')) return 'reporting';
|
||||||
|
|
||||||
|
// Schedule components
|
||||||
|
if (id.includes('/components/schedule') || id.includes('/features/schedule')) return 'schedule';
|
||||||
|
|
||||||
|
// Common utilities
|
||||||
|
if (id.includes('/utils/') || id.includes('/shared/') || id.includes('/hooks/')) return 'common-utils';
|
||||||
|
|
||||||
|
// API and services
|
||||||
|
if (id.includes('/api/') || id.includes('/services/')) return 'api-services';
|
||||||
|
|
||||||
|
// Other vendor libraries
|
||||||
|
if (id.includes('node_modules')) return 'vendor';
|
||||||
},
|
},
|
||||||
// **File Naming Strategies**
|
// **File Naming Strategies**
|
||||||
chunkFileNames: 'assets/js/[name]-[hash].js',
|
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||||
|
|||||||
Reference in New Issue
Block a user