From e26f16bbc22956c3ab628859718022ae36d6d2ed Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Thu, 3 Jul 2025 10:34:06 +0530 Subject: [PATCH] feat(routes): implement lazy loading and suspense fallback for route components - Refactored account setup, admin center, reporting, and settings routes to utilize React's lazy loading for improved performance. - Wrapped route components in Suspense with a fallback UI to enhance user experience during loading states. - Removed unused drag-and-drop CSS file to clean up the codebase. --- .../src/app/routes/account-setup-routes.tsx | 10 ++++- .../src/app/routes/admin-center-routes.tsx | 8 +++- .../src/app/routes/reporting-routes.tsx | 8 +++- .../src/app/routes/settings-routes.tsx | 8 +++- .../task-management/drag-drop-optimized.css | 40 ------------------- .../task-management/task-list-board.tsx | 2 + .../task-management/task-management.slice.ts | 19 ++++++++- .../src/lib/reporting/reporting-constants.ts | 16 ++++---- .../src/lib/settings/settings-constants.ts | 28 ++++++------- .../admin-center/admin-center-constants.ts | 12 +++--- .../task-list-task-cell.tsx | 2 +- .../task-list-table-wrapper.tsx | 1 + .../task-list-table/task-list-table.tsx | 29 ++++++++++++-- 13 files changed, 104 insertions(+), 79 deletions(-) diff --git a/worklenz-frontend/src/app/routes/account-setup-routes.tsx b/worklenz-frontend/src/app/routes/account-setup-routes.tsx index b7f2fcaf..6df71a4d 100644 --- a/worklenz-frontend/src/app/routes/account-setup-routes.tsx +++ b/worklenz-frontend/src/app/routes/account-setup-routes.tsx @@ -1,9 +1,15 @@ import { RouteObject } from 'react-router-dom'; -import AccountSetup from '@/pages/account-setup/account-setup'; +import { lazy, Suspense } from 'react'; +import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback'; +const AccountSetup = lazy(() => import('@/pages/account-setup/account-setup')); const accountSetupRoute: RouteObject = { path: '/worklenz/setup', - element: , + element: ( + }> + + + ), }; export default accountSetupRoute; diff --git a/worklenz-frontend/src/app/routes/admin-center-routes.tsx b/worklenz-frontend/src/app/routes/admin-center-routes.tsx index 2e670a7f..67ad5b26 100644 --- a/worklenz-frontend/src/app/routes/admin-center-routes.tsx +++ b/worklenz-frontend/src/app/routes/admin-center-routes.tsx @@ -1,8 +1,10 @@ import { RouteObject } from 'react-router-dom'; +import { Suspense } from 'react'; import AdminCenterLayout from '@/layouts/admin-center-layout'; import { adminCenterItems } from '@/pages/admin-center/admin-center-constants'; import { Navigate } from 'react-router-dom'; import { useAuthService } from '@/hooks/useAuth'; +import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback'; const AdminCenterGuard = ({ children }: { children: React.ReactNode }) => { const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin(); @@ -24,7 +26,11 @@ const adminCenterRoutes: RouteObject[] = [ ), children: adminCenterItems.map(item => ({ path: item.endpoint, - element: item.element, + element: ( + }> + {item.element} + + ), })), }, ]; diff --git a/worklenz-frontend/src/app/routes/reporting-routes.tsx b/worklenz-frontend/src/app/routes/reporting-routes.tsx index 01f83e9b..2e62de18 100644 --- a/worklenz-frontend/src/app/routes/reporting-routes.tsx +++ b/worklenz-frontend/src/app/routes/reporting-routes.tsx @@ -1,6 +1,8 @@ import { RouteObject } from 'react-router-dom'; +import { Suspense } from 'react'; import ReportingLayout from '@/layouts/ReportingLayout'; import { ReportingMenuItems, reportingsItems } from '@/lib/reporting/reporting-constants'; +import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback'; // function to flatten nested menu items const flattenItems = (items: ReportingMenuItems[]): ReportingMenuItems[] => { @@ -20,7 +22,11 @@ const reportingRoutes: RouteObject[] = [ element: , children: flattenedItems.map(item => ({ path: item.endpoint, - element: item.element, + element: ( + }> + {item.element} + + ), })), }, ]; diff --git a/worklenz-frontend/src/app/routes/settings-routes.tsx b/worklenz-frontend/src/app/routes/settings-routes.tsx index 9999841b..78f59f88 100644 --- a/worklenz-frontend/src/app/routes/settings-routes.tsx +++ b/worklenz-frontend/src/app/routes/settings-routes.tsx @@ -1,8 +1,10 @@ import { RouteObject } from 'react-router-dom'; import { Navigate } from 'react-router-dom'; +import { Suspense } from 'react'; import SettingsLayout from '@/layouts/SettingsLayout'; import { settingsItems } from '@/lib/settings/settings-constants'; import { useAuthService } from '@/hooks/useAuth'; +import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback'; const SettingsGuard = ({ children, @@ -26,7 +28,11 @@ const settingsRoutes: RouteObject[] = [ element: , children: settingsItems.map(item => ({ path: item.endpoint, - element: {item.element}, + element: ( + }> + {item.element} + + ), })), }, ]; diff --git a/worklenz-frontend/src/components/task-management/drag-drop-optimized.css b/worklenz-frontend/src/components/task-management/drag-drop-optimized.css index 6f21e39c..e69de29b 100644 --- a/worklenz-frontend/src/components/task-management/drag-drop-optimized.css +++ b/worklenz-frontend/src/components/task-management/drag-drop-optimized.css @@ -1,40 +0,0 @@ -/* MINIMAL DRAG AND DROP CSS - SHOW ONLY TASK NAME */ - -/* Basic drag handle styling */ -.drag-handle-optimized { - cursor: grab; - opacity: 0.6; - transition: opacity 0.2s ease; -} - -.drag-handle-optimized:hover { - opacity: 1; -} - -.drag-handle-optimized:active { - cursor: grabbing; -} - -/* Simple drag overlay - just show task name */ -[data-dnd-overlay] { - background: white; - border: 1px solid #d9d9d9; - border-radius: 4px; - padding: 8px 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - pointer-events: none; - z-index: 9999; -} - -/* Dark mode support for drag overlay */ -.dark [data-dnd-overlay], -[data-theme="dark"] [data-dnd-overlay] { - background: #1f1f1f; - border-color: #404040; - color: white; -} - -/* Hide drag handle during drag */ -[data-dnd-dragging="true"] .drag-handle-optimized { - opacity: 0; -} diff --git a/worklenz-frontend/src/components/task-management/task-list-board.tsx b/worklenz-frontend/src/components/task-management/task-list-board.tsx index ca89af5e..8c82609f 100644 --- a/worklenz-frontend/src/components/task-management/task-list-board.tsx +++ b/worklenz-frontend/src/components/task-management/task-list-board.tsx @@ -27,6 +27,8 @@ import { fetchTasksV3, selectTaskGroupsV3, selectCurrentGroupingV3, + fetchSubTasks, + toggleTaskExpansion, } from '@/features/task-management/task-management.slice'; import { selectTaskGroups, diff --git a/worklenz-frontend/src/features/task-management/task-management.slice.ts b/worklenz-frontend/src/features/task-management/task-management.slice.ts index 20493847..29199214 100644 --- a/worklenz-frontend/src/features/task-management/task-management.slice.ts +++ b/worklenz-frontend/src/features/task-management/task-management.slice.ts @@ -343,6 +343,14 @@ export const moveTaskToGroupWithAPI = createAsyncThunk( } ); +// Add action to update task with subtasks +export const updateTaskWithSubtasks = createAsyncThunk( + 'taskManagement/updateTaskWithSubtasks', + async ({ taskId, subtasks }: { taskId: string; subtasks: any[] }, { getState }) => { + return { taskId, subtasks }; + } +); + const taskManagementSlice = createSlice({ name: 'taskManagement', initialState: tasksAdapter.getInitialState(initialState), @@ -627,8 +635,7 @@ const taskManagementSlice = createSlice({ return tasksAdapter.getInitialState(initialState); }, toggleTaskExpansion: (state, action: PayloadAction) => { - const taskId = action.payload; - const task = state.entities[taskId]; + const task = state.entities[action.payload]; if (task) { task.show_sub_tasks = !task.show_sub_tasks; } @@ -700,6 +707,14 @@ const taskManagementSlice = createSlice({ }) .addCase(refreshTaskProgress.rejected, (state, action) => { state.error = (action.payload as string) || 'Failed to refresh task progress'; + }) + .addCase(updateTaskWithSubtasks.fulfilled, (state, action) => { + const { taskId, subtasks } = action.payload; + const task = state.entities[taskId]; + if (task) { + task.sub_tasks = subtasks; + task.show_sub_tasks = true; + } }); }, }); diff --git a/worklenz-frontend/src/lib/reporting/reporting-constants.ts b/worklenz-frontend/src/lib/reporting/reporting-constants.ts index 25d23cbd..dfb975f9 100644 --- a/worklenz-frontend/src/lib/reporting/reporting-constants.ts +++ b/worklenz-frontend/src/lib/reporting/reporting-constants.ts @@ -1,11 +1,11 @@ -import React, { ReactNode } from 'react'; -import OverviewReports from '@/pages/reporting/overview-reports/overview-reports'; -import ProjectsReports from '@/pages/reporting/projects-reports/projects-reports'; -import MembersReports from '@/pages/reporting/members-reports/members-reports'; -import OverviewTimeReports from '@/pages/reporting/timeReports/overview-time-reports'; -import ProjectsTimeReports from '@/pages/reporting/timeReports/projects-time-reports'; -import MembersTimeReports from '@/pages/reporting/timeReports/members-time-reports'; -import EstimatedVsActualTimeReports from '@/pages/reporting/timeReports/estimated-vs-actual-time-reports'; +import React, { ReactNode, lazy } from 'react'; +const OverviewReports = lazy(() => import('@/pages/reporting/overview-reports/overview-reports')); +const ProjectsReports = lazy(() => import('@/pages/reporting/projects-reports/projects-reports')); +const MembersReports = lazy(() => import('@/pages/reporting/members-reports/members-reports')); +const OverviewTimeReports = lazy(() => import('@/pages/reporting/timeReports/overview-time-reports')); +const ProjectsTimeReports = lazy(() => import('@/pages/reporting/timeReports/projects-time-reports')); +const MembersTimeReports = lazy(() => import('@/pages/reporting/timeReports/members-time-reports')); +const EstimatedVsActualTimeReports = lazy(() => import('@/pages/reporting/timeReports/estimated-vs-actual-time-reports')); // Type definition for a menu item export type ReportingMenuItems = { diff --git a/worklenz-frontend/src/lib/settings/settings-constants.ts b/worklenz-frontend/src/lib/settings/settings-constants.ts index 7f9beb2a..8823bd7e 100644 --- a/worklenz-frontend/src/lib/settings/settings-constants.ts +++ b/worklenz-frontend/src/lib/settings/settings-constants.ts @@ -13,20 +13,20 @@ import { UserSwitchOutlined, BulbOutlined, } from '@ant-design/icons'; -import React, { ReactNode } from 'react'; -import ProfileSettings from '../../pages/settings/profile/profile-settings'; -import NotificationsSettings from '../../pages/settings/notifications/notifications-settings'; -import ClientsSettings from '../../pages/settings/clients/clients-settings'; -import JobTitlesSettings from '@/pages/settings/job-titles/job-titles-settings'; -import LabelsSettings from '../../pages/settings/labels/labels-settings'; -import CategoriesSettings from '../../pages/settings/categories/categories-settings'; -import ProjectTemplatesSettings from '@/pages/settings/project-templates/project-templates-settings'; -import TaskTemplatesSettings from '@/pages/settings/task-templates/task-templates-settings'; -import TeamMembersSettings from '@/pages/settings/team-members/team-members-settings'; -import TeamsSettings from '../../pages/settings/teams/teams-settings'; -import ChangePassword from '@/pages/settings/change-password/change-password'; -import LanguageAndRegionSettings from '@/pages/settings/language-and-region/language-and-region-settings'; -import AppearanceSettings from '@/pages/settings/appearance/appearance-settings'; +import React, { ReactNode, lazy } from 'react'; +const ProfileSettings = lazy(() => import('../../pages/settings/profile/profile-settings')); +const NotificationsSettings = lazy(() => import('../../pages/settings/notifications/notifications-settings')); +const ClientsSettings = lazy(() => import('../../pages/settings/clients/clients-settings')); +const JobTitlesSettings = lazy(() => import('@/pages/settings/job-titles/job-titles-settings')); +const LabelsSettings = lazy(() => import('../../pages/settings/labels/labels-settings')); +const CategoriesSettings = lazy(() => import('../../pages/settings/categories/categories-settings')); +const ProjectTemplatesSettings = lazy(() => import('@/pages/settings/project-templates/project-templates-settings')); +const TaskTemplatesSettings = lazy(() => import('@/pages/settings/task-templates/task-templates-settings')); +const TeamMembersSettings = lazy(() => import('@/pages/settings/team-members/team-members-settings')); +const TeamsSettings = lazy(() => import('../../pages/settings/teams/teams-settings')); +const ChangePassword = lazy(() => import('@/pages/settings/change-password/change-password')); +const LanguageAndRegionSettings = lazy(() => import('@/pages/settings/language-and-region/language-and-region-settings')); +const AppearanceSettings = lazy(() => import('@/pages/settings/appearance/appearance-settings')); // type of menu item in settings sidebar type SettingMenuItems = { diff --git a/worklenz-frontend/src/pages/admin-center/admin-center-constants.ts b/worklenz-frontend/src/pages/admin-center/admin-center-constants.ts index 77161db2..1cee4eb8 100644 --- a/worklenz-frontend/src/pages/admin-center/admin-center-constants.ts +++ b/worklenz-frontend/src/pages/admin-center/admin-center-constants.ts @@ -5,12 +5,12 @@ import { TeamOutlined, UserOutlined, } from '@ant-design/icons'; -import React, { ReactNode } from 'react'; -import Overview from './overview/overview'; -import Users from './users/users'; -import Teams from './teams/teams'; -import Billing from './billing/billing'; -import Projects from './projects/projects'; +import React, { ReactNode, lazy } from 'react'; +const Overview = lazy(() => import('./overview/overview')); +const Users = lazy(() => import('./users/users')); +const Teams = lazy(() => import('./teams/teams')); +const Billing = lazy(() => import('./billing/billing')); +const Projects = lazy(() => import('./projects/projects')); // type of a menu item in admin center sidebar type AdminCenterMenuItems = { diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-task-cell/task-list-task-cell.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-task-cell/task-list-task-cell.tsx index 553843bf..293efc05 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-task-cell/task-list-task-cell.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-cells/task-list-task-cell/task-list-task-cell.tsx @@ -20,7 +20,7 @@ import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/tas import { useState, useRef, useEffect } from 'react'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; -import { fetchSubTasks } from '@/features/tasks/tasks.slice'; +import { fetchSubTasks } from '@/features/task-management/task-management.slice'; type TaskListTaskCellProps = { task: IProjectTask; diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-wrapper/task-list-table-wrapper.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-wrapper/task-list-table-wrapper.tsx index 784b4b76..bee5c22a 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-wrapper/task-list-table-wrapper.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table-wrapper/task-list-table-wrapper.tsx @@ -266,6 +266,7 @@ const TaskListTableWrapper = ({ tableId={tableId} activeId={activeId} groupBy={groupBy} + isOver={isOver} /> diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table.tsx index a9cec576..5f250577 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/task-list-table.tsx @@ -32,7 +32,7 @@ import { sortableKeyboardCoordinates, } from '@dnd-kit/sortable'; import { createPortal } from 'react-dom'; -import { DragEndEvent } from '@dnd-kit/core'; +import { DragOverEvent } from '@dnd-kit/core'; import { List, Card, Avatar, Dropdown, Empty, Divider, Button } from 'antd'; import dayjs from 'dayjs'; @@ -90,6 +90,7 @@ interface TaskListTableProps { tableId: string; activeId?: string | null; groupBy?: string; + isOver?: boolean; // Add this line } interface DraggableRowProps { @@ -1291,6 +1292,7 @@ const TaskListTable: React.FC = ({ taskList, tableId, active // Add drag state const [dragActiveId, setDragActiveId] = useState(null); + const [placeholderIndex, setPlaceholderIndex] = useState(null); // Configure sensors for drag and drop const sensors = useSensors( @@ -1640,6 +1642,7 @@ const TaskListTable: React.FC = ({ taskList, tableId, active const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; setDragActiveId(null); + setPlaceholderIndex(null); // Reset placeholder index if (!over || !active || active.id === over.id) { return; @@ -1794,6 +1797,7 @@ const TaskListTable: React.FC = ({ taskList, tableId, active sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd} + onDragOver={handleDragOver} // Add this line autoScroll={false} // Disable auto-scroll animations > = ({ taskList, tableId, active {displayTasks && displayTasks.length > 0 ? ( displayTasks .filter(task => task?.id) // Filter out tasks without valid IDs - .map(task => { + .map((task, index) => { const updatedTask = findTaskInGroups(task.id || '') || task; + const isDraggingCurrent = dragActiveId === updatedTask.id; return ( - {renderTaskRow(updatedTask)} + {placeholderIndex === index && ( + + +
+ Drop task here +
+ + + )} + {!isDraggingCurrent && renderTaskRow(updatedTask)} {updatedTask.show_sub_tasks && ( <> {updatedTask?.sub_tasks?.map(subtask => @@ -1910,6 +1924,15 @@ const TaskListTable: React.FC = ({ taskList, tableId, active )} + {placeholderIndex === displayTasks.length && ( + + +
+ Drop task here +
+ + + )}