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.
This commit is contained in:
@@ -1,9 +1,15 @@
|
|||||||
import { RouteObject } from 'react-router-dom';
|
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 = {
|
const accountSetupRoute: RouteObject = {
|
||||||
path: '/worklenz/setup',
|
path: '/worklenz/setup',
|
||||||
element: <AccountSetup />,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<AccountSetup />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default accountSetupRoute;
|
export default accountSetupRoute;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { RouteObject } from 'react-router-dom';
|
import { RouteObject } from 'react-router-dom';
|
||||||
|
import { Suspense } from 'react';
|
||||||
import AdminCenterLayout from '@/layouts/admin-center-layout';
|
import AdminCenterLayout from '@/layouts/admin-center-layout';
|
||||||
import { adminCenterItems } from '@/pages/admin-center/admin-center-constants';
|
import { adminCenterItems } from '@/pages/admin-center/admin-center-constants';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||||
|
|
||||||
const AdminCenterGuard = ({ children }: { children: React.ReactNode }) => {
|
const AdminCenterGuard = ({ children }: { children: React.ReactNode }) => {
|
||||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||||
@@ -24,7 +26,11 @@ const adminCenterRoutes: RouteObject[] = [
|
|||||||
),
|
),
|
||||||
children: adminCenterItems.map(item => ({
|
children: adminCenterItems.map(item => ({
|
||||||
path: item.endpoint,
|
path: item.endpoint,
|
||||||
element: item.element,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
{item.element}
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { RouteObject } from 'react-router-dom';
|
import { RouteObject } from 'react-router-dom';
|
||||||
|
import { Suspense } from 'react';
|
||||||
import ReportingLayout from '@/layouts/ReportingLayout';
|
import ReportingLayout from '@/layouts/ReportingLayout';
|
||||||
import { ReportingMenuItems, reportingsItems } from '@/lib/reporting/reporting-constants';
|
import { ReportingMenuItems, reportingsItems } from '@/lib/reporting/reporting-constants';
|
||||||
|
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||||
|
|
||||||
// function to flatten nested menu items
|
// function to flatten nested menu items
|
||||||
const flattenItems = (items: ReportingMenuItems[]): ReportingMenuItems[] => {
|
const flattenItems = (items: ReportingMenuItems[]): ReportingMenuItems[] => {
|
||||||
@@ -20,7 +22,11 @@ const reportingRoutes: RouteObject[] = [
|
|||||||
element: <ReportingLayout />,
|
element: <ReportingLayout />,
|
||||||
children: flattenedItems.map(item => ({
|
children: flattenedItems.map(item => ({
|
||||||
path: item.endpoint,
|
path: item.endpoint,
|
||||||
element: item.element,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
{item.element}
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { RouteObject } from 'react-router-dom';
|
import { RouteObject } from 'react-router-dom';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import { Suspense } from 'react';
|
||||||
import SettingsLayout from '@/layouts/SettingsLayout';
|
import SettingsLayout from '@/layouts/SettingsLayout';
|
||||||
import { settingsItems } from '@/lib/settings/settings-constants';
|
import { settingsItems } from '@/lib/settings/settings-constants';
|
||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||||
|
|
||||||
const SettingsGuard = ({
|
const SettingsGuard = ({
|
||||||
children,
|
children,
|
||||||
@@ -26,7 +28,11 @@ const settingsRoutes: RouteObject[] = [
|
|||||||
element: <SettingsLayout />,
|
element: <SettingsLayout />,
|
||||||
children: settingsItems.map(item => ({
|
children: settingsItems.map(item => ({
|
||||||
path: item.endpoint,
|
path: item.endpoint,
|
||||||
element: <SettingsGuard adminRequired={!!item.adminOnly}>{item.element}</SettingsGuard>,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<SettingsGuard adminRequired={!!item.adminOnly}>{item.element}</SettingsGuard>
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import {
|
|||||||
fetchTasksV3,
|
fetchTasksV3,
|
||||||
selectTaskGroupsV3,
|
selectTaskGroupsV3,
|
||||||
selectCurrentGroupingV3,
|
selectCurrentGroupingV3,
|
||||||
|
fetchSubTasks,
|
||||||
|
toggleTaskExpansion,
|
||||||
} from '@/features/task-management/task-management.slice';
|
} from '@/features/task-management/task-management.slice';
|
||||||
import {
|
import {
|
||||||
selectTaskGroups,
|
selectTaskGroups,
|
||||||
|
|||||||
@@ -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({
|
const taskManagementSlice = createSlice({
|
||||||
name: 'taskManagement',
|
name: 'taskManagement',
|
||||||
initialState: tasksAdapter.getInitialState(initialState),
|
initialState: tasksAdapter.getInitialState(initialState),
|
||||||
@@ -627,8 +635,7 @@ const taskManagementSlice = createSlice({
|
|||||||
return tasksAdapter.getInitialState(initialState);
|
return tasksAdapter.getInitialState(initialState);
|
||||||
},
|
},
|
||||||
toggleTaskExpansion: (state, action: PayloadAction<string>) => {
|
toggleTaskExpansion: (state, action: PayloadAction<string>) => {
|
||||||
const taskId = action.payload;
|
const task = state.entities[action.payload];
|
||||||
const task = state.entities[taskId];
|
|
||||||
if (task) {
|
if (task) {
|
||||||
task.show_sub_tasks = !task.show_sub_tasks;
|
task.show_sub_tasks = !task.show_sub_tasks;
|
||||||
}
|
}
|
||||||
@@ -700,6 +707,14 @@ const taskManagementSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(refreshTaskProgress.rejected, (state, action) => {
|
.addCase(refreshTaskProgress.rejected, (state, action) => {
|
||||||
state.error = (action.payload as string) || 'Failed to refresh task progress';
|
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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, lazy } from 'react';
|
||||||
import OverviewReports from '@/pages/reporting/overview-reports/overview-reports';
|
const OverviewReports = lazy(() => import('@/pages/reporting/overview-reports/overview-reports'));
|
||||||
import ProjectsReports from '@/pages/reporting/projects-reports/projects-reports';
|
const ProjectsReports = lazy(() => import('@/pages/reporting/projects-reports/projects-reports'));
|
||||||
import MembersReports from '@/pages/reporting/members-reports/members-reports';
|
const MembersReports = lazy(() => import('@/pages/reporting/members-reports/members-reports'));
|
||||||
import OverviewTimeReports from '@/pages/reporting/timeReports/overview-time-reports';
|
const OverviewTimeReports = lazy(() => import('@/pages/reporting/timeReports/overview-time-reports'));
|
||||||
import ProjectsTimeReports from '@/pages/reporting/timeReports/projects-time-reports';
|
const ProjectsTimeReports = lazy(() => import('@/pages/reporting/timeReports/projects-time-reports'));
|
||||||
import MembersTimeReports from '@/pages/reporting/timeReports/members-time-reports';
|
const MembersTimeReports = lazy(() => import('@/pages/reporting/timeReports/members-time-reports'));
|
||||||
import EstimatedVsActualTimeReports from '@/pages/reporting/timeReports/estimated-vs-actual-time-reports';
|
const EstimatedVsActualTimeReports = lazy(() => import('@/pages/reporting/timeReports/estimated-vs-actual-time-reports'));
|
||||||
|
|
||||||
// Type definition for a menu item
|
// Type definition for a menu item
|
||||||
export type ReportingMenuItems = {
|
export type ReportingMenuItems = {
|
||||||
|
|||||||
@@ -13,20 +13,20 @@ import {
|
|||||||
UserSwitchOutlined,
|
UserSwitchOutlined,
|
||||||
BulbOutlined,
|
BulbOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, lazy } from 'react';
|
||||||
import ProfileSettings from '../../pages/settings/profile/profile-settings';
|
const ProfileSettings = lazy(() => import('../../pages/settings/profile/profile-settings'));
|
||||||
import NotificationsSettings from '../../pages/settings/notifications/notifications-settings';
|
const NotificationsSettings = lazy(() => import('../../pages/settings/notifications/notifications-settings'));
|
||||||
import ClientsSettings from '../../pages/settings/clients/clients-settings';
|
const ClientsSettings = lazy(() => import('../../pages/settings/clients/clients-settings'));
|
||||||
import JobTitlesSettings from '@/pages/settings/job-titles/job-titles-settings';
|
const JobTitlesSettings = lazy(() => import('@/pages/settings/job-titles/job-titles-settings'));
|
||||||
import LabelsSettings from '../../pages/settings/labels/labels-settings';
|
const LabelsSettings = lazy(() => import('../../pages/settings/labels/labels-settings'));
|
||||||
import CategoriesSettings from '../../pages/settings/categories/categories-settings';
|
const CategoriesSettings = lazy(() => import('../../pages/settings/categories/categories-settings'));
|
||||||
import ProjectTemplatesSettings from '@/pages/settings/project-templates/project-templates-settings';
|
const ProjectTemplatesSettings = lazy(() => import('@/pages/settings/project-templates/project-templates-settings'));
|
||||||
import TaskTemplatesSettings from '@/pages/settings/task-templates/task-templates-settings';
|
const TaskTemplatesSettings = lazy(() => import('@/pages/settings/task-templates/task-templates-settings'));
|
||||||
import TeamMembersSettings from '@/pages/settings/team-members/team-members-settings';
|
const TeamMembersSettings = lazy(() => import('@/pages/settings/team-members/team-members-settings'));
|
||||||
import TeamsSettings from '../../pages/settings/teams/teams-settings';
|
const TeamsSettings = lazy(() => import('../../pages/settings/teams/teams-settings'));
|
||||||
import ChangePassword from '@/pages/settings/change-password/change-password';
|
const ChangePassword = lazy(() => import('@/pages/settings/change-password/change-password'));
|
||||||
import LanguageAndRegionSettings from '@/pages/settings/language-and-region/language-and-region-settings';
|
const LanguageAndRegionSettings = lazy(() => import('@/pages/settings/language-and-region/language-and-region-settings'));
|
||||||
import AppearanceSettings from '@/pages/settings/appearance/appearance-settings';
|
const AppearanceSettings = lazy(() => import('@/pages/settings/appearance/appearance-settings'));
|
||||||
|
|
||||||
// type of menu item in settings sidebar
|
// type of menu item in settings sidebar
|
||||||
type SettingMenuItems = {
|
type SettingMenuItems = {
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import {
|
|||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, lazy } from 'react';
|
||||||
import Overview from './overview/overview';
|
const Overview = lazy(() => import('./overview/overview'));
|
||||||
import Users from './users/users';
|
const Users = lazy(() => import('./users/users'));
|
||||||
import Teams from './teams/teams';
|
const Teams = lazy(() => import('./teams/teams'));
|
||||||
import Billing from './billing/billing';
|
const Billing = lazy(() => import('./billing/billing'));
|
||||||
import Projects from './projects/projects';
|
const Projects = lazy(() => import('./projects/projects'));
|
||||||
|
|
||||||
// type of a menu item in admin center sidebar
|
// type of a menu item in admin center sidebar
|
||||||
type AdminCenterMenuItems = {
|
type AdminCenterMenuItems = {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/tas
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import { fetchSubTasks } from '@/features/tasks/tasks.slice';
|
import { fetchSubTasks } from '@/features/task-management/task-management.slice';
|
||||||
|
|
||||||
type TaskListTaskCellProps = {
|
type TaskListTaskCellProps = {
|
||||||
task: IProjectTask;
|
task: IProjectTask;
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ const TaskListTableWrapper = ({
|
|||||||
tableId={tableId}
|
tableId={tableId}
|
||||||
activeId={activeId}
|
activeId={activeId}
|
||||||
groupBy={groupBy}
|
groupBy={groupBy}
|
||||||
|
isOver={isOver}
|
||||||
/>
|
/>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
sortableKeyboardCoordinates,
|
sortableKeyboardCoordinates,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { createPortal } from 'react-dom';
|
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 { List, Card, Avatar, Dropdown, Empty, Divider, Button } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@@ -90,6 +90,7 @@ interface TaskListTableProps {
|
|||||||
tableId: string;
|
tableId: string;
|
||||||
activeId?: string | null;
|
activeId?: string | null;
|
||||||
groupBy?: string;
|
groupBy?: string;
|
||||||
|
isOver?: boolean; // Add this line
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DraggableRowProps {
|
interface DraggableRowProps {
|
||||||
@@ -1291,6 +1292,7 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
|||||||
|
|
||||||
// Add drag state
|
// Add drag state
|
||||||
const [dragActiveId, setDragActiveId] = useState<string | null>(null);
|
const [dragActiveId, setDragActiveId] = useState<string | null>(null);
|
||||||
|
const [placeholderIndex, setPlaceholderIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
// Configure sensors for drag and drop
|
// Configure sensors for drag and drop
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
@@ -1640,6 +1642,7 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
|||||||
const handleDragEnd = (event: DragEndEvent) => {
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
setDragActiveId(null);
|
setDragActiveId(null);
|
||||||
|
setPlaceholderIndex(null); // Reset placeholder index
|
||||||
|
|
||||||
if (!over || !active || active.id === over.id) {
|
if (!over || !active || active.id === over.id) {
|
||||||
return;
|
return;
|
||||||
@@ -1794,6 +1797,7 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
|||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragOver={handleDragOver} // Add this line
|
||||||
autoScroll={false} // Disable auto-scroll animations
|
autoScroll={false} // Disable auto-scroll animations
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
@@ -1858,12 +1862,22 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
|||||||
{displayTasks && displayTasks.length > 0 ? (
|
{displayTasks && displayTasks.length > 0 ? (
|
||||||
displayTasks
|
displayTasks
|
||||||
.filter(task => task?.id) // Filter out tasks without valid IDs
|
.filter(task => task?.id) // Filter out tasks without valid IDs
|
||||||
.map(task => {
|
.map((task, index) => {
|
||||||
const updatedTask = findTaskInGroups(task.id || '') || task;
|
const updatedTask = findTaskInGroups(task.id || '') || task;
|
||||||
|
const isDraggingCurrent = dragActiveId === updatedTask.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={updatedTask.id}>
|
<React.Fragment key={updatedTask.id}>
|
||||||
{renderTaskRow(updatedTask)}
|
{placeholderIndex === index && (
|
||||||
|
<tr className="placeholder-row">
|
||||||
|
<td colSpan={visibleColumns.length + 2}>
|
||||||
|
<div className="h-10 border-2 border-dashed border-blue-400 rounded-md flex items-center justify-center text-blue-500">
|
||||||
|
Drop task here
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{!isDraggingCurrent && renderTaskRow(updatedTask)}
|
||||||
{updatedTask.show_sub_tasks && (
|
{updatedTask.show_sub_tasks && (
|
||||||
<>
|
<>
|
||||||
{updatedTask?.sub_tasks?.map(subtask =>
|
{updatedTask?.sub_tasks?.map(subtask =>
|
||||||
@@ -1910,6 +1924,15 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
{placeholderIndex === displayTasks.length && (
|
||||||
|
<tr className="placeholder-row">
|
||||||
|
<td colSpan={visibleColumns.length + 2}>
|
||||||
|
<div className="h-10 border-2 border-dashed border-blue-400 rounded-md flex items-center justify-center text-blue-500">
|
||||||
|
Drop task here
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user