expand sub tasks
This commit is contained in:
@@ -15,7 +15,7 @@ class ReduxPerformanceMonitor {
|
||||
|
||||
logMetric(metric: PerformanceMetrics) {
|
||||
this.metrics.push(metric);
|
||||
|
||||
|
||||
// Keep only recent metrics
|
||||
if (this.metrics.length > this.maxMetrics) {
|
||||
this.metrics = this.metrics.slice(-this.maxMetrics);
|
||||
@@ -49,14 +49,14 @@ class ReduxPerformanceMonitor {
|
||||
export const performanceMonitor = new ReduxPerformanceMonitor();
|
||||
|
||||
// Redux middleware for performance monitoring
|
||||
export const performanceMiddleware: Middleware = (store) => (next) => (action: any) => {
|
||||
export const performanceMiddleware: Middleware = store => next => (action: any) => {
|
||||
const start = performance.now();
|
||||
|
||||
|
||||
const result = next(action);
|
||||
|
||||
|
||||
const end = performance.now();
|
||||
const duration = end - start;
|
||||
|
||||
|
||||
// Calculate approximate state size (in development only)
|
||||
let stateSize = 0;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@@ -101,7 +101,7 @@ export function analyzeReduxPerformance() {
|
||||
|
||||
// Count action frequencies
|
||||
metrics.forEach(m => {
|
||||
analysis.mostFrequentActions[m.actionType] =
|
||||
analysis.mostFrequentActions[m.actionType] =
|
||||
(analysis.mostFrequentActions[m.actionType] || 0) + 1;
|
||||
});
|
||||
|
||||
@@ -109,14 +109,15 @@ export function analyzeReduxPerformance() {
|
||||
if (analysis.slowActions > analysis.totalActions * 0.1) {
|
||||
analysis.recommendations.push('Consider optimizing selectors with createSelector');
|
||||
}
|
||||
|
||||
if (analysis.largestStateSize > 1000000) { // 1MB
|
||||
|
||||
if (analysis.largestStateSize > 1000000) {
|
||||
// 1MB
|
||||
analysis.recommendations.push('State size is large - consider normalizing data');
|
||||
}
|
||||
|
||||
|
||||
if (analysis.averageActionTime > 20) {
|
||||
analysis.recommendations.push('Average action time is high - check for expensive reducers');
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +62,12 @@ export const AdminGuard = memo(({ children }: GuardProps) => {
|
||||
const guardResult = useMemo(() => {
|
||||
try {
|
||||
// Defensive checks to ensure authService and its methods exist
|
||||
if (!authService ||
|
||||
typeof authService.isAuthenticated !== 'function' ||
|
||||
typeof authService.isOwnerOrAdmin !== 'function' ||
|
||||
typeof authService.getCurrentSession !== 'function') {
|
||||
if (
|
||||
!authService ||
|
||||
typeof authService.isAuthenticated !== 'function' ||
|
||||
typeof authService.isOwnerOrAdmin !== 'function' ||
|
||||
typeof authService.getCurrentSession !== 'function'
|
||||
) {
|
||||
return null; // Don't redirect if auth service is not ready
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@ export const AdminGuard = memo(({ children }: GuardProps) => {
|
||||
|
||||
const currentSession = authService.getCurrentSession();
|
||||
const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
|
||||
|
||||
|
||||
if (!authService.isOwnerOrAdmin() || isFreePlan) {
|
||||
return { redirect: '/worklenz/unauthorized' };
|
||||
}
|
||||
@@ -103,9 +105,11 @@ export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
|
||||
const shouldRedirect = useMemo(() => {
|
||||
try {
|
||||
// Defensive checks to ensure authService and its methods exist
|
||||
if (!authService ||
|
||||
typeof authService.isAuthenticated !== 'function' ||
|
||||
typeof authService.getCurrentSession !== 'function') {
|
||||
if (
|
||||
!authService ||
|
||||
typeof authService.isAuthenticated !== 'function' ||
|
||||
typeof authService.getCurrentSession !== 'function'
|
||||
) {
|
||||
return false; // Don't redirect if auth service is not ready
|
||||
}
|
||||
|
||||
@@ -120,37 +124,40 @@ export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
|
||||
const currentSession = authService.getCurrentSession();
|
||||
|
||||
// Check if trial is expired more than 7 days or if is_expired flag is set
|
||||
const isLicenseExpiredMoreThan7Days = () => {
|
||||
const isLicenseExpiredMoreThan7Days = () => {
|
||||
// Quick bail if no session data is available
|
||||
if (!currentSession) return false;
|
||||
|
||||
|
||||
// Check is_expired flag first
|
||||
if (currentSession.is_expired) {
|
||||
if (currentSession.is_expired) {
|
||||
// If no trial_expire_date exists but is_expired is true, defer to backend check
|
||||
if (!currentSession.trial_expire_date) return true;
|
||||
|
||||
|
||||
// If there is a trial_expire_date, check if it's more than 7 days past
|
||||
const today = new Date();
|
||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||
const diffTime = today.getTime() - expiryDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
|
||||
// Redirect if more than 7 days past expiration
|
||||
return diffDays > 7;
|
||||
}
|
||||
|
||||
|
||||
// If not marked as expired but has trial_expire_date, do a date check
|
||||
if (currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL && currentSession.trial_expire_date) {
|
||||
if (
|
||||
currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL &&
|
||||
currentSession.trial_expire_date
|
||||
) {
|
||||
const today = new Date();
|
||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||
|
||||
const diffTime = today.getTime() - expiryDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
|
||||
// If expired more than 7 days, redirect
|
||||
return diffDays > 7;
|
||||
}
|
||||
|
||||
|
||||
// No expiration data found
|
||||
return false;
|
||||
};
|
||||
@@ -227,30 +234,34 @@ const wrapRoutes = (
|
||||
// Optimized static license expired component
|
||||
const StaticLicenseExpired = memo(() => {
|
||||
return (
|
||||
<div style={{
|
||||
marginTop: 65,
|
||||
minHeight: '90vh',
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'white',
|
||||
padding: '30px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
textAlign: 'center',
|
||||
maxWidth: '600px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 65,
|
||||
minHeight: '90vh',
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'white',
|
||||
padding: '30px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
textAlign: 'center',
|
||||
maxWidth: '600px',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: '24px', color: '#faad14', marginBottom: '16px' }}>
|
||||
Your Worklenz trial has expired!
|
||||
</h1>
|
||||
<p style={{ fontSize: '16px', color: '#555', marginBottom: '24px' }}>
|
||||
Please upgrade now to continue using Worklenz.
|
||||
</p>
|
||||
<button
|
||||
<button
|
||||
style={{
|
||||
backgroundColor: '#1890ff',
|
||||
color: 'white',
|
||||
@@ -258,9 +269,9 @@ const StaticLicenseExpired = memo(() => {
|
||||
padding: '8px 16px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => window.location.href = '/worklenz/admin-center/billing'}
|
||||
onClick={() => (window.location.href = '/worklenz/admin-center/billing')}
|
||||
>
|
||||
Upgrade now
|
||||
</button>
|
||||
@@ -272,11 +283,7 @@ const StaticLicenseExpired = memo(() => {
|
||||
StaticLicenseExpired.displayName = 'StaticLicenseExpired';
|
||||
|
||||
// Create route arrays (moved outside of useMemo to avoid hook violations)
|
||||
const publicRoutes = [
|
||||
...rootRoutes,
|
||||
...authRoutes,
|
||||
notFoundRoute
|
||||
];
|
||||
const publicRoutes = [...rootRoutes, ...authRoutes, notFoundRoute];
|
||||
|
||||
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
|
||||
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
|
||||
@@ -305,37 +312,35 @@ const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
||||
const licenseCheckedMainRoutes = withLicenseExpiryCheck(protectedMainRoutes);
|
||||
|
||||
// Create optimized router with future flags for better performance
|
||||
const router = createBrowserRouter([
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
element: (
|
||||
<ErrorBoundary>
|
||||
<AuthenticatedLayout />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
errorElement: (
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<NotFoundPage />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
),
|
||||
children: [...licenseCheckedMainRoutes, ...adminRoutes, ...setupRoutes, licenseExpiredRoute],
|
||||
},
|
||||
...publicRoutes,
|
||||
],
|
||||
{
|
||||
element: (
|
||||
<ErrorBoundary>
|
||||
<AuthenticatedLayout />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
errorElement: (
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<NotFoundPage />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
),
|
||||
children: [
|
||||
...licenseCheckedMainRoutes,
|
||||
...adminRoutes,
|
||||
...setupRoutes,
|
||||
licenseExpiredRoute,
|
||||
],
|
||||
},
|
||||
...publicRoutes,
|
||||
], {
|
||||
// Enable React Router future features for better performance
|
||||
future: {
|
||||
v7_relativeSplatPath: true,
|
||||
v7_fetcherPersist: true,
|
||||
v7_normalizeFormMethod: true,
|
||||
v7_partialHydration: true,
|
||||
v7_skipActionErrorRevalidation: true
|
||||
// Enable React Router future features for better performance
|
||||
future: {
|
||||
v7_relativeSplatPath: true,
|
||||
v7_fetcherPersist: true,
|
||||
v7_normalizeFormMethod: true,
|
||||
v7_partialHydration: true,
|
||||
v7_skipActionErrorRevalidation: true,
|
||||
},
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -11,7 +11,9 @@ import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallba
|
||||
const HomePage = lazy(() => import('@/pages/home/home-page'));
|
||||
const ProjectList = lazy(() => import('@/pages/projects/project-list'));
|
||||
const Schedule = lazy(() => import('@/pages/schedule/schedule'));
|
||||
const ProjectTemplateEditView = lazy(() => import('@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView'));
|
||||
const ProjectTemplateEditView = lazy(
|
||||
() => import('@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView')
|
||||
);
|
||||
const LicenseExpired = lazy(() => import('@/pages/license-expired/license-expired'));
|
||||
const ProjectView = lazy(() => import('@/pages/projects/projectView/project-view'));
|
||||
const Unauthorized = lazy(() => import('@/pages/unauthorized/unauthorized'));
|
||||
@@ -23,9 +25,11 @@ const AdminGuard = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
try {
|
||||
// Defensive checks to ensure authService and its methods exist
|
||||
if (!authService ||
|
||||
typeof authService.isAuthenticated !== 'function' ||
|
||||
typeof authService.isOwnerOrAdmin !== 'function') {
|
||||
if (
|
||||
!authService ||
|
||||
typeof authService.isAuthenticated !== 'function' ||
|
||||
typeof authService.isOwnerOrAdmin !== 'function'
|
||||
) {
|
||||
// If auth service is not ready, render children (don't block)
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -52,21 +56,21 @@ const mainRoutes: RouteObject[] = [
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="home" replace /> },
|
||||
{
|
||||
path: 'home',
|
||||
{
|
||||
path: 'home',
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<HomePage />
|
||||
</Suspense>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
{
|
||||
path: 'projects',
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<ProjectList />
|
||||
</Suspense>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'schedule',
|
||||
@@ -76,15 +80,15 @@ const mainRoutes: RouteObject[] = [
|
||||
<Schedule />
|
||||
</AdminGuard>
|
||||
</Suspense>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: `projects/:projectId`,
|
||||
{
|
||||
path: `projects/:projectId`,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<ProjectView />
|
||||
</Suspense>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: `settings/project-templates/edit/:templateId/:templateName`,
|
||||
@@ -94,13 +98,13 @@ const mainRoutes: RouteObject[] = [
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'unauthorized',
|
||||
{
|
||||
path: 'unauthorized',
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<Unauthorized />
|
||||
</Suspense>
|
||||
)
|
||||
),
|
||||
},
|
||||
...settingsRoutes,
|
||||
...adminCenterRoutes,
|
||||
@@ -113,15 +117,15 @@ export const licenseExpiredRoute: RouteObject = {
|
||||
path: '/worklenz',
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{
|
||||
path: 'license-expired',
|
||||
{
|
||||
path: 'license-expired',
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<LicenseExpired />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
]
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default mainRoutes;
|
||||
|
||||
@@ -4,7 +4,13 @@ import SettingsLayout from '@/layouts/SettingsLayout';
|
||||
import { settingsItems } from '@/lib/settings/settings-constants';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
|
||||
const SettingsGuard = ({ children, adminRequired }: { children: React.ReactNode; adminRequired: boolean }) => {
|
||||
const SettingsGuard = ({
|
||||
children,
|
||||
adminRequired,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
adminRequired: boolean;
|
||||
}) => {
|
||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||
|
||||
if (adminRequired && !isOwnerOrAdmin) {
|
||||
@@ -20,11 +26,7 @@ const settingsRoutes: RouteObject[] = [
|
||||
element: <SettingsLayout />,
|
||||
children: settingsItems.map(item => ({
|
||||
path: item.endpoint,
|
||||
element: (
|
||||
<SettingsGuard adminRequired={!!item.adminOnly}>
|
||||
{item.element}
|
||||
</SettingsGuard>
|
||||
),
|
||||
element: <SettingsGuard adminRequired={!!item.adminOnly}>{item.element}</SettingsGuard>,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,10 +7,7 @@ import { RootState } from './store';
|
||||
// Auth selectors
|
||||
export const selectAuth = (state: RootState) => state.auth;
|
||||
export const selectUser = (state: RootState) => state.userReducer;
|
||||
export const selectIsAuthenticated = createSelector(
|
||||
[selectAuth],
|
||||
(auth) => !!auth.user
|
||||
);
|
||||
export const selectIsAuthenticated = createSelector([selectAuth], auth => !!auth.user);
|
||||
|
||||
// Project selectors
|
||||
export const selectProjects = (state: RootState) => state.projectsReducer;
|
||||
@@ -69,13 +66,10 @@ export const selectGroupByFilter = (state: RootState) => state.groupByFilterDrop
|
||||
// Memoized computed selectors for common use cases
|
||||
export const selectHasActiveProject = createSelector(
|
||||
[selectCurrentProject],
|
||||
(project) => !!project && Object.keys(project).length > 0
|
||||
project => !!project && Object.keys(project).length > 0
|
||||
);
|
||||
|
||||
export const selectIsLoading = createSelector(
|
||||
[selectTasks, selectProjects],
|
||||
(tasks, projects) => {
|
||||
// Check if any major feature is loading
|
||||
return (tasks as any)?.loading || (projects as any)?.loading;
|
||||
}
|
||||
);
|
||||
export const selectIsLoading = createSelector([selectTasks, selectProjects], (tasks, projects) => {
|
||||
// Check if any major feature is loading
|
||||
return (tasks as any)?.loading || (projects as any)?.loading;
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@ export const store = configureStore({
|
||||
taskListCustomColumnsReducer: taskListCustomColumnsReducer,
|
||||
boardReducer: boardReducer,
|
||||
projectDrawerReducer: projectDrawerReducer,
|
||||
|
||||
|
||||
projectViewReducer: projectViewReducer,
|
||||
|
||||
// Project Lookups
|
||||
|
||||
Reference in New Issue
Block a user