Your Worklenz trial has expired!
Please upgrade now to continue using Worklenz.
-
@@ -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: (
+
+
+
+ ),
+ errorElement: (
+
+ }>
+
+
+
+ ),
+ children: [...licenseCheckedMainRoutes, ...adminRoutes, ...setupRoutes, licenseExpiredRoute],
+ },
+ ...publicRoutes,
+ ],
{
- element: (
-
-
-
- ),
- errorElement: (
-
- }>
-
-
-
- ),
- 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;
diff --git a/worklenz-frontend/src/app/routes/main-routes.tsx b/worklenz-frontend/src/app/routes/main-routes.tsx
index 225fd9a7..8ec8cb9a 100644
--- a/worklenz-frontend/src/app/routes/main-routes.tsx
+++ b/worklenz-frontend/src/app/routes/main-routes.tsx
@@ -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:
,
children: [
{ index: true, element:
},
- {
- path: 'home',
+ {
+ path: 'home',
element: (
}>
- )
+ ),
},
- {
- path: 'projects',
+ {
+ path: 'projects',
element: (
}>
- )
+ ),
},
{
path: 'schedule',
@@ -76,15 +80,15 @@ const mainRoutes: RouteObject[] = [
- )
+ ),
},
- {
- path: `projects/:projectId`,
+ {
+ path: `projects/:projectId`,
element: (
}>
- )
+ ),
},
{
path: `settings/project-templates/edit/:templateId/:templateName`,
@@ -94,13 +98,13 @@ const mainRoutes: RouteObject[] = [
),
},
- {
- path: 'unauthorized',
+ {
+ path: 'unauthorized',
element: (
}>
- )
+ ),
},
...settingsRoutes,
...adminCenterRoutes,
@@ -113,15 +117,15 @@ export const licenseExpiredRoute: RouteObject = {
path: '/worklenz',
element:
,
children: [
- {
- path: 'license-expired',
+ {
+ path: 'license-expired',
element: (
}>
- )
- }
- ]
+ ),
+ },
+ ],
};
export default mainRoutes;
diff --git a/worklenz-frontend/src/app/routes/settings-routes.tsx b/worklenz-frontend/src/app/routes/settings-routes.tsx
index 39468efb..9999841b 100644
--- a/worklenz-frontend/src/app/routes/settings-routes.tsx
+++ b/worklenz-frontend/src/app/routes/settings-routes.tsx
@@ -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:
,
children: settingsItems.map(item => ({
path: item.endpoint,
- element: (
-
- {item.element}
-
- ),
+ element:
{item.element},
})),
},
];
diff --git a/worklenz-frontend/src/app/selectors.ts b/worklenz-frontend/src/app/selectors.ts
index 29cbd3be..7796fb5d 100644
--- a/worklenz-frontend/src/app/selectors.ts
+++ b/worklenz-frontend/src/app/selectors.ts
@@ -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;
- }
-);
\ No newline at end of file
+export const selectIsLoading = createSelector([selectTasks, selectProjects], (tasks, projects) => {
+ // Check if any major feature is loading
+ return (tasks as any)?.loading || (projects as any)?.loading;
+});
diff --git a/worklenz-frontend/src/app/store.ts b/worklenz-frontend/src/app/store.ts
index 573333d7..262f654b 100644
--- a/worklenz-frontend/src/app/store.ts
+++ b/worklenz-frontend/src/app/store.ts
@@ -122,7 +122,7 @@ export const store = configureStore({
taskListCustomColumnsReducer: taskListCustomColumnsReducer,
boardReducer: boardReducer,
projectDrawerReducer: projectDrawerReducer,
-
+
projectViewReducer: projectViewReducer,
// Project Lookups
diff --git a/worklenz-frontend/src/components/AssigneeSelector.tsx b/worklenz-frontend/src/components/AssigneeSelector.tsx
index 50cabbab..650b4b6f 100644
--- a/worklenz-frontend/src/components/AssigneeSelector.tsx
+++ b/worklenz-frontend/src/components/AssigneeSelector.tsx
@@ -22,10 +22,10 @@ interface AssigneeSelectorProps {
isDarkMode?: boolean;
}
-const AssigneeSelector: React.FC
= ({
- task,
- groupId = null,
- isDarkMode = false
+const AssigneeSelector: React.FC = ({
+ task,
+ groupId = null,
+ isDarkMode = false,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
@@ -63,8 +63,12 @@ const AssigneeSelector: React.FC = ({
// Close dropdown when clicking outside and handle scroll
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
- if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) &&
- buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
+ if (
+ dropdownRef.current &&
+ !dropdownRef.current.contains(event.target as Node) &&
+ buttonRef.current &&
+ !buttonRef.current.contains(event.target as Node)
+ ) {
setIsOpen(false);
}
};
@@ -74,10 +78,12 @@ const AssigneeSelector: React.FC = ({
// Check if the button is still visible in the viewport
if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
- const isVisible = rect.top >= 0 && rect.left >= 0 &&
- rect.bottom <= window.innerHeight &&
- rect.right <= window.innerWidth;
-
+ const isVisible =
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= window.innerHeight &&
+ rect.right <= window.innerWidth;
+
if (isVisible) {
updateDropdownPosition();
} else {
@@ -98,7 +104,7 @@ const AssigneeSelector: React.FC = ({
document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('scroll', handleScroll, true);
window.addEventListener('resize', handleResize);
-
+
return () => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('scroll', handleScroll, true);
@@ -113,10 +119,10 @@ const AssigneeSelector: React.FC = ({
const handleDropdownToggle = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
-
+
if (!isOpen) {
updateDropdownPosition();
-
+
// Prepare team members data when opening
const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
const membersData = (members?.data || []).map(member => ({
@@ -125,7 +131,7 @@ const AssigneeSelector: React.FC = ({
}));
const sortedMembers = sortTeamMembers(membersData);
setTeamMembers({ data: sortedMembers });
-
+
setIsOpen(true);
// Focus search input after opening
setTimeout(() => {
@@ -160,11 +166,9 @@ const AssigneeSelector: React.FC = ({
// Update local team members state for dropdown UI
setTeamMembers(prev => ({
...prev,
- data: (prev.data || []).map(member =>
- member.id === memberId
- ? { ...member, selected: checked }
- : member
- )
+ data: (prev.data || []).map(member =>
+ member.id === memberId ? { ...member, selected: checked } : member
+ ),
}));
const body = {
@@ -178,12 +182,9 @@ const AssigneeSelector: React.FC = ({
// Emit socket event - the socket handler will update Redux with proper types
socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body));
- socket?.once(
- SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(),
- (data: any) => {
- dispatch(updateEnhancedKanbanTaskAssignees(data));
- }
- );
+ socket?.once(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), (data: any) => {
+ dispatch(updateEnhancedKanbanTaskAssignees(data));
+ });
// Remove from pending changes after a short delay (optimistic)
setTimeout(() => {
@@ -198,9 +199,10 @@ const AssigneeSelector: React.FC = ({
const checkMemberSelected = (memberId: string) => {
if (!memberId) return false;
// Use optimistic assignees if available, otherwise fall back to task assignees
- const assignees = optimisticAssignees.length > 0
- ? optimisticAssignees
- : task?.assignees?.map(assignee => assignee.team_member_id) || [];
+ const assignees =
+ optimisticAssignees.length > 0
+ ? optimisticAssignees
+ : task?.assignees?.map(assignee => assignee.team_member_id) || [];
return assignees.includes(memberId);
};
@@ -217,149 +219,159 @@ const AssigneeSelector: React.FC = ({
className={`
w-5 h-5 rounded-full border border-dashed flex items-center justify-center
transition-colors duration-200
- ${isOpen
- ? isDarkMode
- ? 'border-blue-500 bg-blue-900/20 text-blue-400'
- : 'border-blue-500 bg-blue-50 text-blue-600'
- : isDarkMode
- ? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
- : 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
+ ${
+ isOpen
+ ? isDarkMode
+ ? 'border-blue-500 bg-blue-900/20 text-blue-400'
+ : 'border-blue-500 bg-blue-50 text-blue-600'
+ : isDarkMode
+ ? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400'
+ : 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
}
`}
>
- {isOpen && createPortal(
- e.stopPropagation()}
- className={`
+ {isOpen &&
+ createPortal(
+
e.stopPropagation()}
+ className={`
fixed z-9999 w-72 rounded-md shadow-lg border
- ${isDarkMode
- ? 'bg-gray-800 border-gray-600'
- : 'bg-white border-gray-200'
- }
+ ${isDarkMode ? 'bg-gray-800 border-gray-600' : 'bg-white border-gray-200'}
`}
- style={{
- top: dropdownPosition.top,
- left: dropdownPosition.left,
- }}
- >
- {/* Header */}
-
- {/* Members List */}
-
- {filteredMembers && filteredMembers.length > 0 ? (
- filteredMembers.map((member) => (
-
+ {filteredMembers && filteredMembers.length > 0 ? (
+ filteredMembers.map(member => (
+
{
- if (!member.pending_invitation) {
- const isSelected = checkMemberSelected(member.id || '');
- handleMemberToggle(member.id || '', !isSelected);
- }
- }}
- style={{
- // Add visual feedback for immediate response
- transition: 'all 0.15s ease-in-out',
- }}
- >
-
-
e.stopPropagation()}>
- handleMemberToggle(member.id || '', checked)}
- disabled={member.pending_invitation || pendingChanges.has(member.id || '')}
- isDarkMode={isDarkMode}
- />
-
- {pendingChanges.has(member.id || '') && (
-
- )}
-
-
-
-
-
-
- {member.name}
-
-
- {member.email}
- {member.pending_invitation && (
-
(Pending)
+ onClick={() => {
+ if (!member.pending_invitation) {
+ const isSelected = checkMemberSelected(member.id || '');
+ handleMemberToggle(member.id || '', !isSelected);
+ }
+ }}
+ style={{
+ // Add visual feedback for immediate response
+ transition: 'all 0.15s ease-in-out',
+ }}
+ >
+
+
e.stopPropagation()}>
+ handleMemberToggle(member.id || '', checked)}
+ disabled={
+ member.pending_invitation || pendingChanges.has(member.id || '')
+ }
+ isDarkMode={isDarkMode}
+ />
+
+ {pendingChanges.has(member.id || '') && (
+
)}
-
-
- ))
- ) : (
-
- )}
-
- {/* Footer */}
-
-
+
+
+
+ {member.name}
+
+
+ {member.email}
+ {member.pending_invitation && (
+ (Pending)
+ )}
+
+
+
+ ))
+ ) : (
+
+ )}
+
+
+ {/* Footer */}
+
+
-
-
,
- document.body
- )}
+ onClick={handleInviteProjectMemberDrawer}
+ >
+
+ Invite member
+
+
+
,
+ document.body
+ )}
>
);
};
-export default AssigneeSelector;
\ No newline at end of file
+export default AssigneeSelector;
diff --git a/worklenz-frontend/src/components/Avatar.tsx b/worklenz-frontend/src/components/Avatar.tsx
index 413a4e3d..59da1650 100644
--- a/worklenz-frontend/src/components/Avatar.tsx
+++ b/worklenz-frontend/src/components/Avatar.tsx
@@ -11,47 +11,63 @@ interface AvatarProps {
style?: React.CSSProperties;
}
-const Avatar: React.FC = ({
- name = '',
- size = 'default',
- isDarkMode = false,
+const Avatar: React.FC = ({
+ name = '',
+ size = 'default',
+ isDarkMode = false,
className = '',
src,
backgroundColor,
onClick,
- style = {}
+ style = {},
}) => {
// Handle both numeric and string sizes
const getSize = () => {
if (typeof size === 'number') {
return { width: size, height: size, fontSize: `${size * 0.4}px` };
}
-
+
const sizeMap = {
small: { width: 24, height: 24, fontSize: '10px' },
default: { width: 32, height: 32, fontSize: '14px' },
- large: { width: 48, height: 48, fontSize: '18px' }
+ large: { width: 48, height: 48, fontSize: '18px' },
};
-
+
return sizeMap[size];
};
const sizeStyle = getSize();
-
+
const lightColors = [
- '#f56565', '#4299e1', '#48bb78', '#ed8936', '#9f7aea',
- '#ed64a6', '#667eea', '#38b2ac', '#f6ad55', '#4fd1c7'
+ '#f56565',
+ '#4299e1',
+ '#48bb78',
+ '#ed8936',
+ '#9f7aea',
+ '#ed64a6',
+ '#667eea',
+ '#38b2ac',
+ '#f6ad55',
+ '#4fd1c7',
];
-
+
const darkColors = [
- '#e53e3e', '#3182ce', '#38a169', '#dd6b20', '#805ad5',
- '#d53f8c', '#5a67d8', '#319795', '#d69e2e', '#319795'
+ '#e53e3e',
+ '#3182ce',
+ '#38a169',
+ '#dd6b20',
+ '#805ad5',
+ '#d53f8c',
+ '#5a67d8',
+ '#319795',
+ '#d69e2e',
+ '#319795',
];
-
+
const colors = isDarkMode ? darkColors : lightColors;
const colorIndex = name.charCodeAt(0) % colors.length;
const defaultBgColor = backgroundColor || colors[colorIndex];
-
+
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onClick?.(e);
@@ -60,7 +76,7 @@ const Avatar: React.FC = ({
const avatarStyle = {
...sizeStyle,
backgroundColor: defaultBgColor,
- ...style
+ ...style,
};
if (src) {
@@ -74,9 +90,9 @@ const Avatar: React.FC = ({
/>
);
}
-
+
return (
- = ({
);
};
-export default Avatar;
\ No newline at end of file
+export default Avatar;
diff --git a/worklenz-frontend/src/components/AvatarGroup.tsx b/worklenz-frontend/src/components/AvatarGroup.tsx
index a0eaf410..04e4b57a 100644
--- a/worklenz-frontend/src/components/AvatarGroup.tsx
+++ b/worklenz-frontend/src/components/AvatarGroup.tsx
@@ -20,42 +20,49 @@ interface AvatarGroupProps {
onClick?: (e: React.MouseEvent) => void;
}
-const AvatarGroup: React.FC
= ({
- members,
- maxCount,
- size = 28,
+const AvatarGroup: React.FC = ({
+ members,
+ maxCount,
+ size = 28,
isDarkMode = false,
className = '',
- onClick
+ onClick,
}) => {
- const stopPropagation = useCallback((e: React.MouseEvent) => {
- e.stopPropagation();
- onClick?.(e);
- }, [onClick]);
+ const stopPropagation = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation();
+ onClick?.(e);
+ },
+ [onClick]
+ );
- const renderAvatar = useCallback((member: Member, index: number) => {
- const memberName = member.end && member.names ? member.names.join(', ') : member.name || '';
- const displayName = member.end && member.names ? member.name : member.name?.charAt(0).toUpperCase();
-
- return (
-
- {
+ const memberName = member.end && member.names ? member.names.join(', ') : member.name || '';
+ const displayName =
+ member.end && member.names ? member.name : member.name?.charAt(0).toUpperCase();
+
+ return (
+
-
- );
- }, [stopPropagation, size, isDarkMode]);
+ >
+
+
+ );
+ },
+ [stopPropagation, size, isDarkMode]
+ );
const visibleMembers = useMemo(() => {
return maxCount ? members.slice(0, maxCount) : members;
@@ -73,13 +80,13 @@ const AvatarGroup: React.FC = ({
if (typeof size === 'number') {
return { width: size, height: size, fontSize: `${size * 0.4}px` };
}
-
+
const sizeMap = {
small: { width: 24, height: 24, fontSize: '10px' },
default: { width: 32, height: 32, fontSize: '14px' },
- large: { width: 48, height: 48, fontSize: '18px' }
+ large: { width: 48, height: 48, fontSize: '18px' },
};
-
+
return sizeMap[size];
};
@@ -87,15 +94,10 @@ const AvatarGroup: React.FC = ({
{avatarElements}
{remainingCount > 0 && (
-
-
+
= ({
);
};
-export default AvatarGroup;
\ No newline at end of file
+export default AvatarGroup;
diff --git a/worklenz-frontend/src/components/Button.tsx b/worklenz-frontend/src/components/Button.tsx
index 51d79d32..8d9ce9d1 100644
--- a/worklenz-frontend/src/components/Button.tsx
+++ b/worklenz-frontend/src/components/Button.tsx
@@ -12,25 +12,25 @@ interface ButtonProps {
type?: 'button' | 'submit' | 'reset';
}
-const Button: React.FC
> = ({
- children,
- onClick,
- variant = 'default',
- size = 'default',
- className = '',
- icon,
- isDarkMode = false,
+const Button: React.FC> = ({
+ children,
+ onClick,
+ variant = 'default',
+ size = 'default',
+ className = '',
+ icon,
+ isDarkMode = false,
disabled = false,
type = 'button',
- ...props
+ ...props
}) => {
const baseClasses = `inline-flex items-center justify-center font-medium transition-colors duration-200 focus:outline-none focus:ring-2 ${isDarkMode ? 'focus:ring-blue-400' : 'focus:ring-blue-500'} focus:ring-offset-2 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`;
-
+
const variantClasses = {
- text: isDarkMode
+ text: isDarkMode
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700/50'
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-100',
- default: isDarkMode
+ default: isDarkMode
? 'bg-gray-800 border border-gray-600 text-gray-200 hover:bg-gray-700'
: 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50',
primary: isDarkMode
@@ -38,15 +38,15 @@ const Button: React.FC
- {icon && {icon}}
+ {icon && {icon}}
{children}
);
};
-export default Button;
\ No newline at end of file
+export default Button;
diff --git a/worklenz-frontend/src/components/Checkbox.tsx b/worklenz-frontend/src/components/Checkbox.tsx
index 4ed89018..f663c3f7 100644
--- a/worklenz-frontend/src/components/Checkbox.tsx
+++ b/worklenz-frontend/src/components/Checkbox.tsx
@@ -9,36 +9,48 @@ interface CheckboxProps {
indeterminate?: boolean;
}
-const Checkbox: React.FC = ({
- checked,
- onChange,
- isDarkMode = false,
+const Checkbox: React.FC = ({
+ checked,
+ onChange,
+ isDarkMode = false,
className = '',
disabled = false,
- indeterminate = false
+ indeterminate = false,
}) => {
return (
- ,
+ document.body
+ )}
>
);
};
-export default LabelsSelector;
\ No newline at end of file
+export default LabelsSelector;
diff --git a/worklenz-frontend/src/components/Progress.tsx b/worklenz-frontend/src/components/Progress.tsx
index be89433a..94ccf95a 100644
--- a/worklenz-frontend/src/components/Progress.tsx
+++ b/worklenz-frontend/src/components/Progress.tsx
@@ -11,15 +11,15 @@ interface ProgressProps {
className?: string;
}
-const Progress: React.FC
= ({
- percent,
- type = 'line',
- size = 24,
- strokeColor = '#1890ff',
- strokeWidth = 2,
+const Progress: React.FC = ({
+ percent,
+ type = 'line',
+ size = 24,
+ strokeColor = '#1890ff',
+ strokeWidth = 2,
showInfo = true,
isDarkMode = false,
- className = ''
+ className = '',
}) => {
// Ensure percent is between 0 and 100
const normalizedPercent = Math.min(Math.max(percent, 0), 100);
@@ -29,7 +29,7 @@ const Progress: React.FC = ({
const circumference = radius * 2 * Math.PI;
const strokeDasharray = circumference;
const strokeDashoffset = circumference - (normalizedPercent / 100) * circumference;
-
+
return (
{showInfo && (
-
+
{normalizedPercent}%
)}
);
}
-
+
return (
-
+
{showInfo && (
@@ -81,4 +85,4 @@ const Progress: React.FC
= ({
);
};
-export default Progress;
\ No newline at end of file
+export default Progress;
diff --git a/worklenz-frontend/src/components/Tag.tsx b/worklenz-frontend/src/components/Tag.tsx
index 519091c4..e6e7e966 100644
--- a/worklenz-frontend/src/components/Tag.tsx
+++ b/worklenz-frontend/src/components/Tag.tsx
@@ -10,30 +10,30 @@ interface TagProps {
isDarkMode?: boolean;
}
-const Tag: React.FC = ({
- children,
- color = 'white',
- backgroundColor = '#1890ff',
+const Tag: React.FC = ({
+ children,
+ color = 'white',
+ backgroundColor = '#1890ff',
className = '',
size = 'default',
variant = 'default',
- isDarkMode = false
+ isDarkMode = false,
}) => {
const sizeClasses = {
small: 'px-1 py-0.5 text-xs',
- default: 'px-2 py-1 text-xs'
+ default: 'px-2 py-1 text-xs',
};
const baseClasses = `inline-flex items-center font-medium rounded-sm ${sizeClasses[size]}`;
-
+
if (variant === 'outlined') {
return (
{children}
@@ -42,13 +42,10 @@ const Tag: React.FC = ({
}
return (
-
+
{children}
);
};
-export default Tag;
\ No newline at end of file
+export default Tag;
diff --git a/worklenz-frontend/src/components/TawkTo.tsx b/worklenz-frontend/src/components/TawkTo.tsx
index d07b5a71..c447a050 100644
--- a/worklenz-frontend/src/components/TawkTo.tsx
+++ b/worklenz-frontend/src/components/TawkTo.tsx
@@ -20,7 +20,7 @@ const TawkTo: React.FC = ({ propertyId, widgetId }) => {
s1.async = true;
s1.src = `https://embed.tawk.to/${propertyId}/${widgetId}`;
s1.setAttribute('crossorigin', '*');
-
+
const s0 = document.getElementsByTagName('script')[0];
s0.parentNode?.insertBefore(s1, s0);
@@ -31,13 +31,13 @@ const TawkTo: React.FC = ({ propertyId, widgetId }) => {
if (tawkScript && tawkScript.parentNode) {
tawkScript.parentNode.removeChild(tawkScript);
}
-
+
// Remove the tawk.to iframe
const tawkIframe = document.getElementById('tawk-iframe');
if (tawkIframe) {
tawkIframe.remove();
}
-
+
// Reset Tawk globals
delete window.Tawk_API;
delete window.Tawk_LoadStart;
@@ -47,4 +47,4 @@ const TawkTo: React.FC = ({ propertyId, widgetId }) => {
return null;
};
-export default TawkTo;
\ No newline at end of file
+export default TawkTo;
diff --git a/worklenz-frontend/src/components/Tooltip.tsx b/worklenz-frontend/src/components/Tooltip.tsx
index a0074b8a..e8f71a8a 100644
--- a/worklenz-frontend/src/components/Tooltip.tsx
+++ b/worklenz-frontend/src/components/Tooltip.tsx
@@ -8,28 +8,30 @@ interface TooltipProps {
className?: string;
}
-const Tooltip: React.FC = ({
- title,
- children,
- isDarkMode = false,
+const Tooltip: React.FC = ({
+ title,
+ children,
+ isDarkMode = false,
placement = 'top',
- className = ''
+ className = '',
}) => {
const placementClasses = {
top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2',
bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2',
left: 'right-full top-1/2 transform -translate-y-1/2 mr-2',
- right: 'left-full top-1/2 transform -translate-y-1/2 ml-2'
+ right: 'left-full top-1/2 transform -translate-y-1/2 ml-2',
};
return (
{children}
-
);
};
-export default Tooltip;
\ No newline at end of file
+export default Tooltip;
diff --git a/worklenz-frontend/src/components/account-setup/tasks-step.tsx b/worklenz-frontend/src/components/account-setup/tasks-step.tsx
index 472654fc..129e4409 100644
--- a/worklenz-frontend/src/components/account-setup/tasks-step.tsx
+++ b/worklenz-frontend/src/components/account-setup/tasks-step.tsx
@@ -39,7 +39,9 @@ export const TasksStep: React.FC
= ({ onEnter, styles, isDarkMode }) => {
const updateTask = (id: number, value: string) => {
const sanitizedValue = sanitizeInput(value);
- dispatch(setTasks(tasks.map(task => (task.id === id ? { ...task, value: sanitizedValue } : task))));
+ dispatch(
+ setTasks(tasks.map(task => (task.id === id ? { ...task, value: sanitizedValue } : task)))
+ );
};
const handleKeyPress = (e: React.KeyboardEvent) => {
diff --git a/worklenz-frontend/src/components/admin-center/billing/account-storage/account-storage.tsx b/worklenz-frontend/src/components/admin-center/billing/account-storage/account-storage.tsx
index 6af50210..11a3282e 100644
--- a/worklenz-frontend/src/components/admin-center/billing/account-storage/account-storage.tsx
+++ b/worklenz-frontend/src/components/admin-center/billing/account-storage/account-storage.tsx
@@ -18,7 +18,9 @@ const AccountStorage = ({ themeMode }: IAccountStorageProps) => {
const dispatch = useAppDispatch();
const [subscriptionType, setSubscriptionType] = useState(SUBSCRIPTION_STATUS.TRIALING);
- const { loadingBillingInfo, billingInfo, storageInfo } = useAppSelector(state => state.adminCenterReducer);
+ const { loadingBillingInfo, billingInfo, storageInfo } = useAppSelector(
+ state => state.adminCenterReducer
+ );
const formatBytes = useMemo(
() =>
diff --git a/worklenz-frontend/src/components/admin-center/billing/current-bill.tsx b/worklenz-frontend/src/components/admin-center/billing/current-bill.tsx
index 72188293..86252375 100644
--- a/worklenz-frontend/src/components/admin-center/billing/current-bill.tsx
+++ b/worklenz-frontend/src/components/admin-center/billing/current-bill.tsx
@@ -10,7 +10,10 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useMediaQuery } from 'react-responsive';
import { useTranslation } from 'react-i18next';
-import { fetchBillingInfo, fetchFreePlanSettings } from '@/features/admin-center/admin-center.slice';
+import {
+ fetchBillingInfo,
+ fetchFreePlanSettings,
+} from '@/features/admin-center/admin-center.slice';
import CurrentPlanDetails from './current-plan-details/current-plan-details';
import AccountStorage from './account-storage/account-storage';
@@ -68,10 +71,7 @@ const CurrentBill: React.FC = () => {
- {t('invoices')}}
- style={{ marginTop: '16px' }}
- >
+ {t('invoices')}} style={{ marginTop: '16px' }}>
@@ -92,7 +92,8 @@ const CurrentBill: React.FC = () => {
) : (
renderMobileView()
)}
- {currentSession?.subscription_type === ISUBSCRIPTION_TYPE.PADDLE && renderChargesAndInvoices()}
+ {currentSession?.subscription_type === ISUBSCRIPTION_TYPE.PADDLE &&
+ renderChargesAndInvoices()}
);
};
diff --git a/worklenz-frontend/src/components/admin-center/billing/current-plan-details/current-plan-details.tsx b/worklenz-frontend/src/components/admin-center/billing/current-plan-details/current-plan-details.tsx
index 8111bc56..77de144e 100644
--- a/worklenz-frontend/src/components/admin-center/billing/current-plan-details/current-plan-details.tsx
+++ b/worklenz-frontend/src/components/admin-center/billing/current-plan-details/current-plan-details.tsx
@@ -7,7 +7,20 @@ import {
} from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
import logger from '@/utils/errorLogger';
-import { Button, Card, Flex, Modal, Space, Tooltip, Typography, Statistic, Select, Form, Row, Col } from 'antd/es';
+import {
+ Button,
+ Card,
+ Flex,
+ Modal,
+ Space,
+ Tooltip,
+ Typography,
+ Statistic,
+ Select,
+ Form,
+ Row,
+ Col,
+} from 'antd/es';
import RedeemCodeDrawer from '../drawers/redeem-code-drawer/redeem-code-drawer';
import {
fetchBillingInfo,
@@ -44,8 +57,9 @@ const CurrentPlanDetails = () => {
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
type SeatOption = { label: string; value: number | string };
- const seatCountOptions: SeatOption[] = [1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90]
- .map(value => ({ label: value.toString(), value }));
+ const seatCountOptions: SeatOption[] = [
+ 1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90,
+ ].map(value => ({ label: value.toString(), value }));
seatCountOptions.push({ label: '100+', value: '100+' });
const handleSubscriptionAction = async (action: 'pause' | 'resume') => {
@@ -127,8 +141,10 @@ const CurrentPlanDetails = () => {
const shouldShowAddSeats = () => {
if (!billingInfo) return false;
- return billingInfo.subscription_type === ISUBSCRIPTION_TYPE.PADDLE &&
- billingInfo.status === SUBSCRIPTION_STATUS.ACTIVE;
+ return (
+ billingInfo.subscription_type === ISUBSCRIPTION_TYPE.PADDLE &&
+ billingInfo.status === SUBSCRIPTION_STATUS.ACTIVE
+ );
};
const renderExtra = () => {
@@ -199,13 +215,13 @@ const CurrentPlanDetails = () => {
const getExpirationMessage = (expireDate: string) => {
const today = new Date();
today.setHours(0, 0, 0, 0); // Set to start of day for comparison
-
+
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
-
+
const expDate = new Date(expireDate);
expDate.setHours(0, 0, 0, 0); // Set to start of day for comparison
-
+
if (expDate.getTime() === today.getTime()) {
return t('expirestoday', 'today');
} else if (expDate.getTime() === tomorrow.getTime()) {
@@ -230,14 +246,13 @@ const CurrentPlanDetails = () => {
- {isExpired
+ {isExpired
? t('trialExpired', {
- trial_expire_string: getExpirationMessage(trialExpireDate)
+ trial_expire_string: getExpirationMessage(trialExpireDate),
})
: t('trialInProgress', {
- trial_expire_string: getExpirationMessage(trialExpireDate)
- })
- }
+ trial_expire_string: getExpirationMessage(trialExpireDate),
+ })}
@@ -268,25 +283,24 @@ const CurrentPlanDetails = () => {
{billingInfo?.billing_type === 'year'
? billingInfo.unit_price_per_month
: billingInfo?.unit_price}
-
{t('perMonthPerUser')}
-
+
{shouldShowAddSeats() && billingInfo?.total_seats && (
-
- }
+ }
onClick={handleAddMoreSeats}
style={{ backgroundColor: '#1890ff', borderColor: '#1890ff' }}
>
@@ -294,9 +308,9 @@ const CurrentPlanDetails = () => {
-
@@ -308,16 +322,24 @@ const CurrentPlanDetails = () => {
};
const renderCreditSubscriptionInfo = () => {
- return
- {t('creditPlan','Credit Plan')}
-
+ return (
+
+ {t('creditPlan', 'Credit Plan')}
+
+ );
};
const renderCustomSubscriptionInfo = () => {
- return
- {t('customPlan','Custom Plan')}
- {t('planValidTill','Your plan is valid till {{date}}',{date: billingInfo?.valid_till_date})}
-
+ return (
+
+ {t('customPlan', 'Custom Plan')}
+
+ {t('planValidTill', 'Your plan is valid till {{date}}', {
+ date: billingInfo?.valid_till_date,
+ })}
+
+
+ );
};
return (
@@ -326,7 +348,6 @@ const CurrentPlanDetails = () => {
title={
{
>
- {billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.LIFE_TIME_DEAL && renderLtdDetails()}
+ {billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.LIFE_TIME_DEAL &&
+ renderLtdDetails()}
{billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.TRIAL && renderTrialDetails()}
{billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.FREE && renderFreePlan()}
- {billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.PADDLE && renderPaddleSubscriptionInfo()}
- {billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.CREDIT && renderCreditSubscriptionInfo()}
- {billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.CUSTOM && renderCustomSubscriptionInfo()}
+ {billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.PADDLE &&
+ renderPaddleSubscriptionInfo()}
+ {billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.CREDIT &&
+ renderCreditSubscriptionInfo()}
+ {billingInfo?.subscription_type === ISUBSCRIPTION_TYPE.CUSTOM &&
+ renderCustomSubscriptionInfo()}
{shouldShowRedeemButton() && (
@@ -370,7 +395,7 @@ const CurrentPlanDetails = () => {
>
{browserTimeZone === 'Asia/Colombo' ? : }
-
+
{
centered
>
-
- {t('purchaseSeatsText','To continue, you\'ll need to purchase additional seats.')}
+
+ {t('purchaseSeatsText', "To continue, you'll need to purchase additional seats.")}
-
+
- {t('currentSeatsText','You currently have {{seats}} seats available.',{seats: billingInfo?.total_seats})}
+ {t('currentSeatsText', 'You currently have {{seats}} seats available.', {
+ seats: billingInfo?.total_seats,
+ })}
-
+
- {t('selectSeatsText','Please select the number of additional seats to purchase.')}
+ {t('selectSeatsText', 'Please select the number of additional seats to purchase.')}
-
+
*
Seats:
@@ -402,28 +431,25 @@ const CurrentPlanDetails = () => {
style={{ width: '300px' }}
/>
-
+
{selectedSeatCount.toString() !== '100+' ? (
-
- {t('purchase','Purchase')}
+ {t('purchase', 'Purchase')}
) : (
-
- {t('contactSales','Contact sales')}
+
+ {t('contactSales', 'Contact sales')}
)}
diff --git a/worklenz-frontend/src/components/admin-center/billing/drawers/upgrade-plans/upgrade-plans.tsx b/worklenz-frontend/src/components/admin-center/billing/drawers/upgrade-plans/upgrade-plans.tsx
index f390896a..8cccc24c 100644
--- a/worklenz-frontend/src/components/admin-center/billing/drawers/upgrade-plans/upgrade-plans.tsx
+++ b/worklenz-frontend/src/components/admin-center/billing/drawers/upgrade-plans/upgrade-plans.tsx
@@ -1,5 +1,17 @@
import { useEffect, useState } from 'react';
-import { Button, Card, Col, Flex, Form, Row, Select, Tag, Tooltip, Typography, message } from 'antd/es';
+import {
+ Button,
+ Card,
+ Col,
+ Flex,
+ Form,
+ Row,
+ Select,
+ Tag,
+ Tooltip,
+ Typography,
+ message,
+} from 'antd/es';
import { useTranslation } from 'react-i18next';
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
@@ -106,7 +118,7 @@ const UpgradePlans = () => {
const handlePaddleCallback = (data: any) => {
console.log('Paddle event:', data);
-
+
switch (data.event) {
case 'Checkout.Loaded':
setSwitchingToPaddlePlan(false);
@@ -144,13 +156,13 @@ const UpgradePlans = () => {
const initializePaddle = (data: IUpgradeSubscriptionPlanResponse) => {
setPaddleLoading(true);
setPaddleError(null);
-
+
// Check if Paddle is already loaded
if (window.Paddle) {
configurePaddle(data);
return;
}
-
+
const script = document.createElement('script');
script.src = 'https://cdn.paddle.com/paddle/paddle.js';
script.type = 'text/javascript';
@@ -159,7 +171,7 @@ const UpgradePlans = () => {
script.onload = () => {
configurePaddle(data);
};
-
+
script.onerror = () => {
setPaddleLoading(false);
setPaddleError('Failed to load Paddle checkout');
@@ -169,7 +181,7 @@ const UpgradePlans = () => {
document.getElementsByTagName('head')[0].appendChild(script);
};
-
+
const configurePaddle = (data: IUpgradeSubscriptionPlanResponse) => {
try {
if (data.sandbox) Paddle.Environment.set('sandbox');
@@ -193,7 +205,7 @@ const UpgradePlans = () => {
setSwitchingToPaddlePlan(true);
setPaddleLoading(true);
setPaddleError(null);
-
+
if (billingInfo?.trial_in_progress && billingInfo.status === SUBSCRIPTION_STATUS.TRIALING) {
const res = await billingApiService.upgradeToPaidPlan(planId, selectedSeatCount);
if (res.done) {
@@ -264,7 +276,6 @@ const UpgradePlans = () => {
const isSelected = (cardIndex: IPaddlePlans) =>
selectedPlan === cardIndex ? { border: '2px solid #1890ff' } : {};
-
const cardStyles = {
title: {
color: themeMode === 'dark' ? '#ffffffd9' : '#000000d9',
@@ -363,7 +374,6 @@ const UpgradePlans = () => {
title={{t('freePlan')}}
onClick={() => setSelectedCard(paddlePlans.FREE)}
>
-
$ 0.00
@@ -389,7 +399,6 @@ const UpgradePlans = () => {
{t('annualPlan')}{' '}
@@ -401,7 +410,6 @@ const UpgradePlans = () => {
onClick={() => setSelectedCard(paddlePlans.ANNUAL)}
>
-
$ {plans.annual_price}
seat / month
@@ -442,7 +450,6 @@ const UpgradePlans = () => {
hoverable
title={{t('monthlyPlan')}}
onClick={() => setSelectedCard(paddlePlans.MONTHLY)}
-
>
@@ -501,7 +508,9 @@ const UpgradePlans = () => {
onClick={continueWithPaddlePlan}
disabled={billingInfo?.plan_id === plans.annual_plan_id}
>
- {billingInfo?.status === SUBSCRIPTION_STATUS.ACTIVE ? t('changeToPlan', {plan: t('annualPlan')}) : t('continueWith', {plan: t('annualPlan')})}
+ {billingInfo?.status === SUBSCRIPTION_STATUS.ACTIVE
+ ? t('changeToPlan', { plan: t('annualPlan') })
+ : t('continueWith', { plan: t('annualPlan') })}
)}
{selectedPlan === paddlePlans.MONTHLY && (
@@ -512,7 +521,9 @@ const UpgradePlans = () => {
onClick={continueWithPaddlePlan}
disabled={billingInfo?.plan_id === plans.monthly_plan_id}
>
- {billingInfo?.status === SUBSCRIPTION_STATUS.ACTIVE ? t('changeToPlan', {plan: t('monthlyPlan')}) : t('continueWith', {plan: t('monthlyPlan')})}
+ {billingInfo?.status === SUBSCRIPTION_STATUS.ACTIVE
+ ? t('changeToPlan', { plan: t('monthlyPlan') })
+ : t('continueWith', { plan: t('monthlyPlan') })}
)}
diff --git a/worklenz-frontend/src/components/admin-center/configuration/configuration.tsx b/worklenz-frontend/src/components/admin-center/configuration/configuration.tsx
index a9a24e24..afa5b51a 100644
--- a/worklenz-frontend/src/components/admin-center/configuration/configuration.tsx
+++ b/worklenz-frontend/src/components/admin-center/configuration/configuration.tsx
@@ -39,7 +39,7 @@ const Configuration: React.FC = () => {
}, []);
const handleSave = async (values: any) => {
- try {
+ try {
setLoading(true);
const res = await adminCenterApiService.updateBillingConfiguration(values);
if (res.done) {
@@ -75,11 +75,7 @@ const Configuration: React.FC = () => {
}
style={{ marginTop: '16px' }}
>
-
))
) : (
@@ -201,7 +200,7 @@ const BoardAssigneeSelector = ({ task, groupId = null }: BoardAssigneeSelectorPr
type="dashed"
shape="circle"
size="small"
- onClick={(e) => e.stopPropagation()}
+ onClick={e => e.stopPropagation()}
icon={
{
e.stopPropagation()}
+ onClick={e => e.stopPropagation()}
style={{
borderRadius: 4,
cursor: 'pointer',
diff --git a/worklenz-frontend/src/components/board/custom-due-date-picker.tsx b/worklenz-frontend/src/components/board/custom-due-date-picker.tsx
index 3335fd7e..5f5a25b0 100644
--- a/worklenz-frontend/src/components/board/custom-due-date-picker.tsx
+++ b/worklenz-frontend/src/components/board/custom-due-date-picker.tsx
@@ -86,7 +86,7 @@ const CustomDueDatePicker = ({
width: 26,
height: 26,
}}
- onClick={(e) => {
+ onClick={e => {
e.stopPropagation(); // Keep this as a backup
setIsDatePickerOpen(true);
}}
@@ -98,4 +98,4 @@ const CustomDueDatePicker = ({
);
};
-export default CustomDueDatePicker;
\ No newline at end of file
+export default CustomDueDatePicker;
diff --git a/worklenz-frontend/src/components/board/taskCard/priority-section/priority-section.tsx b/worklenz-frontend/src/components/board/taskCard/priority-section/priority-section.tsx
index bf12b447..deb1cc7c 100644
--- a/worklenz-frontend/src/components/board/taskCard/priority-section/priority-section.tsx
+++ b/worklenz-frontend/src/components/board/taskCard/priority-section/priority-section.tsx
@@ -31,7 +31,8 @@ const PrioritySection = ({ task }: PrioritySectionProps) => {
const iconProps = {
style: {
- color: themeMode === 'dark' ? selectedPriority.color_code_dark : selectedPriority.color_code,
+ color:
+ themeMode === 'dark' ? selectedPriority.color_code_dark : selectedPriority.color_code,
marginRight: '0.25rem',
},
};
@@ -40,9 +41,19 @@ const PrioritySection = ({ task }: PrioritySectionProps) => {
case 'Low':
return ;
case 'Medium':
- return ;
+ return (
+
+ );
case 'High':
- return ;
+ return (
+
+ );
default:
return null;
}
@@ -50,11 +61,7 @@ const PrioritySection = ({ task }: PrioritySectionProps) => {
if (!task.priority || !selectedPriority) return null;
- return (
-
- {priorityIcon}
-
- );
+ return {priorityIcon};
};
export default PrioritySection;
diff --git a/worklenz-frontend/src/components/charts/chart-loader.tsx b/worklenz-frontend/src/components/charts/chart-loader.tsx
index e4790cde..142b8e2d 100644
--- a/worklenz-frontend/src/components/charts/chart-loader.tsx
+++ b/worklenz-frontend/src/components/charts/chart-loader.tsx
@@ -2,8 +2,12 @@ import React, { Suspense } from 'react';
import { Spin } from 'antd';
// Lazy load chart components to reduce initial bundle size
-const LazyBar = React.lazy(() => import('react-chartjs-2').then(module => ({ default: module.Bar })));
-const LazyDoughnut = React.lazy(() => import('react-chartjs-2').then(module => ({ default: module.Doughnut })));
+const LazyBar = React.lazy(() =>
+ import('react-chartjs-2').then(module => ({ default: module.Bar }))
+);
+const LazyDoughnut = React.lazy(() =>
+ import('react-chartjs-2').then(module => ({ default: module.Doughnut }))
+);
interface ChartLoaderProps {
type: 'bar' | 'doughnut';
@@ -14,7 +18,7 @@ interface ChartLoaderProps {
const ChartLoader: React.FC = ({ type, ...props }) => {
const ChartComponent = type === 'bar' ? LazyBar : LazyDoughnut;
-
+
return (
}>
@@ -22,4 +26,4 @@ const ChartLoader: React.FC = ({ type, ...props }) => {
);
};
-export default ChartLoader;
\ No newline at end of file
+export default ChartLoader;
diff --git a/worklenz-frontend/src/components/collapsible/collapsible.tsx b/worklenz-frontend/src/components/collapsible/collapsible.tsx
index 8a1f3457..e85e55fb 100644
--- a/worklenz-frontend/src/components/collapsible/collapsible.tsx
+++ b/worklenz-frontend/src/components/collapsible/collapsible.tsx
@@ -15,7 +15,9 @@ const Collapsible = ({ isOpen, children, className = '', color }: CollapsiblePro
marginTop: '6px',
}}
className={`transition-all duration-300 ease-in-out ${
- isOpen ? 'max-h-[2000px] opacity-100 overflow-x-scroll' : 'max-h-0 opacity-0 overflow-hidden'
+ isOpen
+ ? 'max-h-[2000px] opacity-100 overflow-x-scroll'
+ : 'max-h-0 opacity-0 overflow-hidden'
} ${className}`}
>
{children}
diff --git a/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx b/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx
index 2aba8828..b71e4d65 100644
--- a/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx
+++ b/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx
@@ -1,7 +1,10 @@
import { AutoComplete, Button, Drawer, Flex, Form, message, Select, Spin, Typography } from 'antd';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
-import { toggleInviteMemberDrawer, triggerTeamMembersRefresh } from '../../../features/settings/member/memberSlice';
+import {
+ toggleInviteMemberDrawer,
+ triggerTeamMembersRefresh,
+} from '../../../features/settings/member/memberSlice';
import { useTranslation } from 'react-i18next';
import { useState, useEffect, useCallback } from 'react';
import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service';
@@ -177,4 +180,4 @@ const InviteTeamMembers = () => {
);
};
-export default InviteTeamMembers;
\ No newline at end of file
+export default InviteTeamMembers;
diff --git a/worklenz-frontend/src/components/common/template-drawer/template-drawer.css b/worklenz-frontend/src/components/common/template-drawer/template-drawer.css
index 7783762c..f8be1fe5 100644
--- a/worklenz-frontend/src/components/common/template-drawer/template-drawer.css
+++ b/worklenz-frontend/src/components/common/template-drawer/template-drawer.css
@@ -91,4 +91,3 @@
.custom-template-list .selected-custom-template:hover {
background-color: var(--color-paleBlue);
}
-
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoard.css b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoard.css
index 1640d576..5a023bce 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoard.css
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoard.css
@@ -40,4 +40,4 @@
justify-content: center;
align-items: center;
min-height: 200px;
-}
\ No newline at end of file
+}
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoard.tsx
index 5c275cb9..b048744e 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoard.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoard.tsx
@@ -16,10 +16,7 @@ import {
pointerWithin,
rectIntersection,
} from '@dnd-kit/core';
-import {
- SortableContext,
- horizontalListSortingStrategy,
-} from '@dnd-kit/sortable';
+import { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';
import { RootState } from '@/app/store';
import {
fetchEnhancedKanbanGroups,
@@ -49,7 +46,9 @@ import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import { useAuthService } from '@/hooks/useAuth';
// Import the TaskListFilters component
-const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
+const TaskListFilters = React.lazy(
+ () => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters')
+);
interface EnhancedKanbanBoardProps {
projectId: string;
className?: string;
@@ -57,24 +56,22 @@ interface EnhancedKanbanBoardProps {
const EnhancedKanbanBoard: React.FC = ({ projectId, className = '' }) => {
const dispatch = useDispatch();
- const {
- taskGroups,
- loadingGroups,
- error,
- dragState,
- performanceMetrics
- } = useSelector((state: RootState) => state.enhancedKanbanReducer);
+ const { taskGroups, loadingGroups, error, dragState, performanceMetrics } = useSelector(
+ (state: RootState) => state.enhancedKanbanReducer
+ );
const { socket } = useSocket();
const authService = useAuthService();
const teamId = authService.getCurrentSession()?.team_id;
const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy);
const project = useAppSelector((state: RootState) => state.projectReducer.project);
- const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
+ const { statusCategories, status: existingStatuses } = useAppSelector(
+ state => state.taskStatusReducer
+ );
const themeMode = useAppSelector(state => state.themeReducer.mode);
// Load filter data
useFilterDataLoader();
-
+
// Set up socket event handlers for real-time updates
useTaskSocketHandlers();
@@ -106,22 +103,18 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
}, [dispatch, projectId]);
// Get all task IDs for sortable context
- const allTaskIds = useMemo(() =>
- taskGroups.flatMap(group => group.tasks.map(task => task.id!)),
- [taskGroups]
- );
- const allGroupIds = useMemo(() =>
- taskGroups.map(group => group.id),
+ const allTaskIds = useMemo(
+ () => taskGroups.flatMap(group => group.tasks.map(task => task.id!)),
[taskGroups]
);
+ const allGroupIds = useMemo(() => taskGroups.map(group => group.id), [taskGroups]);
// Enhanced collision detection
const collisionDetectionStrategy = (args: any) => {
// First, let's see if we're colliding with any droppable areas
const pointerIntersections = pointerWithin(args);
- const intersections = pointerIntersections.length > 0
- ? pointerIntersections
- : rectIntersection(args);
+ const intersections =
+ pointerIntersections.length > 0 ? pointerIntersections : rectIntersection(args);
let overId = getFirstCollision(intersections, 'id');
@@ -162,11 +155,13 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
setActiveGroup(foundGroup);
setActiveTask(null);
- dispatch(setDragState({
- activeTaskId: null,
- activeGroupId: activeId,
- isDragging: true,
- }));
+ dispatch(
+ setDragState({
+ activeTaskId: null,
+ activeGroupId: activeId,
+ isDragging: true,
+ })
+ );
} else {
// Dragging a task
let foundTask = null;
@@ -184,11 +179,13 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
setActiveTask(foundTask);
setActiveGroup(null);
- dispatch(setDragState({
- activeTaskId: activeId,
- activeGroupId: foundGroup?.id || null,
- isDragging: true,
- }));
+ dispatch(
+ setDragState({
+ activeTaskId: activeId,
+ activeGroupId: foundGroup?.id || null,
+ isDragging: true,
+ })
+ );
}
};
@@ -220,12 +217,14 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
setOverId(null);
// Reset Redux drag state
- dispatch(setDragState({
- activeTaskId: null,
- activeGroupId: null,
- overId: null,
- isDragging: false,
- }));
+ dispatch(
+ setDragState({
+ activeTaskId: null,
+ activeGroupId: null,
+ overId: null,
+ isDragging: false,
+ })
+ );
if (!over) return;
@@ -258,7 +257,7 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
// Call API to update status order
try {
const requestBody: ITaskStatusCreateRequest = {
- status_order: columnOrder
+ status_order: columnOrder,
};
const response = await statusApiService.updateStatusOrder(requestBody, projectId);
@@ -267,7 +266,13 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
const revertedGroups = [...reorderedGroups];
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
revertedGroups.splice(fromIndex, 0, movedBackGroup);
- dispatch(reorderGroups({ fromIndex: toIndex, toIndex: fromIndex, reorderedGroups: revertedGroups }));
+ dispatch(
+ reorderGroups({
+ fromIndex: toIndex,
+ toIndex: fromIndex,
+ reorderedGroups: revertedGroups,
+ })
+ );
alertService.error('Failed to update column order', 'Please try again');
}
} catch (error) {
@@ -275,7 +280,13 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
const revertedGroups = [...reorderedGroups];
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
revertedGroups.splice(fromIndex, 0, movedBackGroup);
- dispatch(reorderGroups({ fromIndex: toIndex, toIndex: fromIndex, reorderedGroups: revertedGroups }));
+ dispatch(
+ reorderGroups({
+ fromIndex: toIndex,
+ toIndex: fromIndex,
+ reorderedGroups: revertedGroups,
+ })
+ );
alertService.error('Failed to update column order', 'Please try again');
logger.error('Failed to update column order', error);
}
@@ -338,24 +349,28 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
}
// Synchronous UI update
- dispatch(reorderTasks({
- activeGroupId: sourceGroup.id,
- overGroupId: targetGroup.id,
- fromIndex: sourceIndex,
- toIndex: targetIndex,
- task: movedTask,
- updatedSourceTasks,
- updatedTargetTasks,
- }));
- dispatch(reorderEnhancedKanbanTasks({
- activeGroupId: sourceGroup.id,
- overGroupId: targetGroup.id,
- fromIndex: sourceIndex,
- toIndex: targetIndex,
- task: movedTask,
- updatedSourceTasks,
- updatedTargetTasks,
- }) as any);
+ dispatch(
+ reorderTasks({
+ activeGroupId: sourceGroup.id,
+ overGroupId: targetGroup.id,
+ fromIndex: sourceIndex,
+ toIndex: targetIndex,
+ task: movedTask,
+ updatedSourceTasks,
+ updatedTargetTasks,
+ })
+ );
+ dispatch(
+ reorderEnhancedKanbanTasks({
+ activeGroupId: sourceGroup.id,
+ overGroupId: targetGroup.id,
+ fromIndex: sourceIndex,
+ toIndex: targetIndex,
+ task: movedTask,
+ updatedSourceTasks,
+ updatedTargetTasks,
+ }) as any
+ );
// --- Socket emit for task sort order ---
if (socket && projectId && movedTask) {
@@ -368,7 +383,10 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
toSortOrder = -1;
toLastIndex = true;
} else if (targetGroup.tasks[targetIndex]) {
- toSortOrder = typeof targetGroup.tasks[targetIndex].sort_order === 'number' ? targetGroup.tasks[targetIndex].sort_order! : -1;
+ toSortOrder =
+ typeof targetGroup.tasks[targetIndex].sort_order === 'number'
+ ? targetGroup.tasks[targetIndex].sort_order!
+ : -1;
toLastIndex = false;
} else if (targetGroup.tasks.length > 0) {
const lastSortOrder = targetGroup.tasks[targetGroup.tasks.length - 1].sort_order;
@@ -490,4 +508,4 @@ const EnhancedKanbanBoard: React.FC = ({ projectId, cl
);
};
-export default EnhancedKanbanBoard;
\ No newline at end of file
+export default EnhancedKanbanBoard;
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSection.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSection.tsx
index f414efe1..97f70bd0 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSection.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSection.tsx
@@ -7,7 +7,10 @@ import { nanoid } from '@reduxjs/toolkit';
import { useAppSelector } from '@/hooks/useAppSelector';
import { themeWiseColor } from '@/utils/themeWiseColor';
import { useAppDispatch } from '@/hooks/useAppDispatch';
-import { IGroupBy, fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
+import {
+ IGroupBy,
+ fetchEnhancedKanbanGroups,
+} from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
import { createStatus, fetchStatuses } from '@/features/taskAttributes/taskStatusSlice';
import { ALPHA_CHANNEL } from '@/shared/constants';
@@ -19,10 +22,12 @@ import useIsProjectManager from '@/hooks/useIsProjectManager';
const EnhancedKanbanCreateSection: React.FC = () => {
const { t } = useTranslation('kanban-board');
- const themeMode = useAppSelector((state) => state.themeReducer.mode);
- const { projectId } = useAppSelector((state) => state.projectReducer);
- const groupBy = useAppSelector((state) => state.enhancedKanbanReducer.groupBy);
- const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const { projectId } = useAppSelector(state => state.projectReducer);
+ const groupBy = useAppSelector(state => state.enhancedKanbanReducer.groupBy);
+ const { statusCategories, status: existingStatuses } = useAppSelector(
+ state => state.taskStatusReducer
+ );
const dispatch = useAppDispatch();
const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
@@ -36,20 +41,20 @@ const EnhancedKanbanCreateSection: React.FC = () => {
const getUniqueSectionName = (baseName: string): string => {
// Check if the base name already exists
const existingNames = existingStatuses.map(status => status.name?.toLowerCase());
-
+
if (!existingNames.includes(baseName.toLowerCase())) {
return baseName;
}
-
+
// If the base name exists, add a number suffix
let counter = 1;
let newName = `${baseName.trim()} (${counter})`;
-
+
while (existingNames.includes(newName.toLowerCase())) {
counter++;
newName = `${baseName.trim()} (${counter})`;
}
-
+
return newName;
};
@@ -57,14 +62,14 @@ const EnhancedKanbanCreateSection: React.FC = () => {
const sectionId = nanoid();
const baseNameSection = 'Untitled section';
const sectionName = getUniqueSectionName(baseNameSection);
-
+
if (groupBy === IGroupBy.STATUS && projectId) {
// Find the "To do" category
- const todoCategory = statusCategories.find(category =>
- category.name?.toLowerCase() === 'to do' ||
- category.name?.toLowerCase() === 'todo'
+ const todoCategory = statusCategories.find(
+ category =>
+ category.name?.toLowerCase() === 'to do' || category.name?.toLowerCase() === 'todo'
);
-
+
if (todoCategory && todoCategory.id) {
// Create a new status
const body = {
@@ -72,11 +77,13 @@ const EnhancedKanbanCreateSection: React.FC = () => {
project_id: projectId,
category_id: todoCategory.id,
};
-
+
try {
// Create the status
- const response = await dispatch(createStatus({ body, currentProjectId: projectId })).unwrap();
-
+ const response = await dispatch(
+ createStatus({ body, currentProjectId: projectId })
+ ).unwrap();
+
if (response.done && response.body) {
// Refresh the board to show the new section
dispatch(fetchEnhancedKanbanGroups(projectId));
@@ -87,7 +94,7 @@ const EnhancedKanbanCreateSection: React.FC = () => {
logger.error('Failed to create status:', error);
}
}
- }
+ }
if (groupBy === IGroupBy.PHASE && projectId) {
const body = {
@@ -95,7 +102,7 @@ const EnhancedKanbanCreateSection: React.FC = () => {
project_id: projectId,
};
- try {
+ try {
const response = await phasesApiService.addPhaseOption(projectId);
if (response.done && response.body) {
dispatch(fetchEnhancedKanbanGroups(projectId));
@@ -147,4 +154,4 @@ const EnhancedKanbanCreateSection: React.FC = () => {
);
};
-export default React.memo(EnhancedKanbanCreateSection);
\ No newline at end of file
+export default React.memo(EnhancedKanbanCreateSection);
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSubtaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSubtaskCard.tsx
index 5cd0dabc..b8abd977 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSubtaskCard.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSubtaskCard.tsx
@@ -85,22 +85,27 @@ const EnhancedKanbanCreateSubtaskCard = ({
}, 0);
if (task.parent_task_id) {
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
- socket?.once(SocketEvents.GET_TASK_PROGRESS.toString(), (data: {
- id: string;
- complete_ratio: number;
- completed_count: number;
- total_tasks_count: number;
- parent_task: string;
- }) => {
- if (!data.parent_task) data.parent_task = task.parent_task_id || '';
- dispatch(updateEnhancedKanbanTaskProgress({
- id: task.id || '',
- complete_ratio: data.complete_ratio,
- completed_count: data.completed_count,
- total_tasks_count: data.total_tasks_count,
- parent_task: data.parent_task,
- }));
- });
+ socket?.once(
+ SocketEvents.GET_TASK_PROGRESS.toString(),
+ (data: {
+ id: string;
+ complete_ratio: number;
+ completed_count: number;
+ total_tasks_count: number;
+ parent_task: string;
+ }) => {
+ if (!data.parent_task) data.parent_task = task.parent_task_id || '';
+ dispatch(
+ updateEnhancedKanbanTaskProgress({
+ id: task.id || '',
+ complete_ratio: data.complete_ratio,
+ completed_count: data.completed_count,
+ total_tasks_count: data.total_tasks_count,
+ parent_task: data.parent_task,
+ })
+ );
+ }
+ );
}
});
} catch (error) {
@@ -143,7 +148,7 @@ const EnhancedKanbanCreateSubtaskCard = ({
cursor: 'pointer',
overflow: 'hidden',
}}
- // className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
+ // className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
onBlur={handleCancelNewCard}
>
// Real-time socket event handler
const eventHandler = (task: IProjectTask) => {
- dispatch(addTaskToGroup({ sectionId, task: { ...task, id: task.id || nanoid(), name: task.name || newTaskName.trim() } }));
+ dispatch(
+ addTaskToGroup({
+ sectionId,
+ task: { ...task, id: task.id || nanoid(), name: task.name || newTaskName.trim() },
+ })
+ );
socket?.off(SocketEvents.QUICK_TASK.toString(), eventHandler);
resetForNextTask();
};
@@ -159,4 +164,4 @@ const EnhancedKanbanCreateTaskCard: React.FC
);
};
-export default EnhancedKanbanCreateTaskCard;
\ No newline at end of file
+export default EnhancedKanbanCreateTaskCard;
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.css b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.css
index 4498387b..c7ed1d3f 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.css
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.css
@@ -127,7 +127,8 @@
}
@keyframes dropPulse {
- 0%, 100% {
+ 0%,
+ 100% {
opacity: 0.6;
transform: scaleX(0.8);
}
@@ -205,7 +206,7 @@
min-width: 240px;
max-width: 280px;
}
-
+
.enhanced-kanban-group-tasks {
max-height: 400px;
}
@@ -216,7 +217,7 @@
min-width: 200px;
max-width: 240px;
}
-
+
.enhanced-kanban-group-tasks {
max-height: 300px;
}
@@ -239,4 +240,4 @@
max-width: 220px;
display: inline-block;
vertical-align: middle;
-}
\ No newline at end of file
+}
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.tsx
index 5d7aa3f2..86b98521 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanGroup.tsx
@@ -1,6 +1,11 @@
import React, { useMemo, useRef, useEffect, useState } from 'react';
import { useDroppable } from '@dnd-kit/core';
-import { SortableContext, verticalListSortingStrategy, useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable';
+import {
+ SortableContext,
+ verticalListSortingStrategy,
+ useSortable,
+ defaultAnimateLayoutChanges,
+} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { ITaskListGroup } from '@/types/tasks/taskList.types';
import EnhancedKanbanTaskCard from './EnhancedKanbanTaskCard';
@@ -11,7 +16,14 @@ import { Badge, Flex, InputRef, MenuProps, Popconfirm } from 'antd';
import { themeWiseColor } from '@/utils/themeWiseColor';
import useIsProjectManager from '@/hooks/useIsProjectManager';
import { useAuthService } from '@/hooks/useAuth';
-import { DeleteOutlined, ExclamationCircleFilled, EditOutlined, LoadingOutlined, RetweetOutlined, MoreOutlined } from '@ant-design/icons/lib/icons';
+import {
+ DeleteOutlined,
+ ExclamationCircleFilled,
+ EditOutlined,
+ LoadingOutlined,
+ RetweetOutlined,
+ MoreOutlined,
+} from '@ant-design/icons/lib/icons';
import { colors } from '@/styles/colors';
import { Input } from 'antd';
import { Tooltip } from 'antd';
@@ -29,8 +41,14 @@ import { evt_project_board_column_setting_click } from '@/shared/worklenz-analyt
import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service';
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
-import { deleteStatusToggleDrawer, seletedStatusCategory } from '@/features/projects/status/DeleteStatusSlice';
-import { fetchEnhancedKanbanGroups, IGroupBy } from '@/features/enhanced-kanban/enhanced-kanban.slice';
+import {
+ deleteStatusToggleDrawer,
+ seletedStatusCategory,
+} from '@/features/projects/status/DeleteStatusSlice';
+import {
+ fetchEnhancedKanbanGroups,
+ IGroupBy,
+} from '@/features/enhanced-kanban/enhanced-kanban.slice';
import EnhancedKanbanCreateTaskCard from './EnhancedKanbanCreateTaskCard';
interface EnhancedKanbanGroupProps {
@@ -42,493 +60,512 @@ interface EnhancedKanbanGroupProps {
// Performance threshold for virtualization
const VIRTUALIZATION_THRESHOLD = 50;
-const EnhancedKanbanGroup: React.FC = React.memo(({
- group,
- activeTaskId,
- overId
-}) => {
- const [isHover, setIsHover] = useState(false);
- const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
- const [isEditable, setIsEditable] = useState(false);
- const isProjectManager = useIsProjectManager();
- const [isLoading, setIsLoading] = useState(false);
- const [name, setName] = useState(group.name);
- const inputRef = useRef(null);
- const [editName, setEdit] = useState(group.name);
- const [isEllipsisActive, setIsEllipsisActive] = useState(false);
- const themeMode = useAppSelector(state => state.themeReducer.mode);
- const dispatch = useAppDispatch();
- const { projectId } = useAppSelector(state => state.projectReducer);
- const { groupBy } = useAppSelector(state => state.enhancedKanbanReducer);
- const { statusCategories, status } = useAppSelector(state => state.taskStatusReducer);
- const { trackMixpanelEvent } = useMixpanelTracking();
- const [showNewCardTop, setShowNewCardTop] = useState(false);
- const [showNewCardBottom, setShowNewCardBottom] = useState(false);
- const { t } = useTranslation('kanban-board');
+const EnhancedKanbanGroup: React.FC = React.memo(
+ ({ group, activeTaskId, overId }) => {
+ const [isHover, setIsHover] = useState(false);
+ const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
+ const [isEditable, setIsEditable] = useState(false);
+ const isProjectManager = useIsProjectManager();
+ const [isLoading, setIsLoading] = useState(false);
+ const [name, setName] = useState(group.name);
+ const inputRef = useRef(null);
+ const [editName, setEdit] = useState(group.name);
+ const [isEllipsisActive, setIsEllipsisActive] = useState(false);
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const dispatch = useAppDispatch();
+ const { projectId } = useAppSelector(state => state.projectReducer);
+ const { groupBy } = useAppSelector(state => state.enhancedKanbanReducer);
+ const { statusCategories, status } = useAppSelector(state => state.taskStatusReducer);
+ const { trackMixpanelEvent } = useMixpanelTracking();
+ const [showNewCardTop, setShowNewCardTop] = useState(false);
+ const [showNewCardBottom, setShowNewCardBottom] = useState(false);
+ const { t } = useTranslation('kanban-board');
- const { setNodeRef: setDroppableRef, isOver } = useDroppable({
- id: group.id,
- data: {
- type: 'group',
- group,
- },
- });
+ const { setNodeRef: setDroppableRef, isOver } = useDroppable({
+ id: group.id,
+ data: {
+ type: 'group',
+ group,
+ },
+ });
- // Add sortable functionality for group header
- const {
- attributes,
- listeners,
- setNodeRef: setSortableRef,
- transform,
- transition,
- isDragging: isGroupDragging,
- } = useSortable({
- id: group.id,
- data: {
- type: 'group',
- group,
- },
- animateLayoutChanges: defaultAnimateLayoutChanges,
- });
+ // Add sortable functionality for group header
+ const {
+ attributes,
+ listeners,
+ setNodeRef: setSortableRef,
+ transform,
+ transition,
+ isDragging: isGroupDragging,
+ } = useSortable({
+ id: group.id,
+ data: {
+ type: 'group',
+ group,
+ },
+ animateLayoutChanges: defaultAnimateLayoutChanges,
+ });
- const groupRef = useRef(null);
- const [groupHeight, setGroupHeight] = useState(400);
+ const groupRef = useRef(null);
+ const [groupHeight, setGroupHeight] = useState(400);
- // Get task IDs for sortable context
- const taskIds = group.tasks.map(task => task.id!);
+ // Get task IDs for sortable context
+ const taskIds = group.tasks.map(task => task.id!);
- // Check if this group is the target for dropping
- const isTargetGroup = overId === group.id;
- const isDraggingOver = isOver || isTargetGroup;
+ // Check if this group is the target for dropping
+ const isTargetGroup = overId === group.id;
+ const isDraggingOver = isOver || isTargetGroup;
- // Determine if virtualization should be used
- const shouldVirtualize = useMemo(() => {
- return group.tasks.length > VIRTUALIZATION_THRESHOLD;
- }, [group.tasks.length]);
+ // Determine if virtualization should be used
+ const shouldVirtualize = useMemo(() => {
+ return group.tasks.length > VIRTUALIZATION_THRESHOLD;
+ }, [group.tasks.length]);
- // Calculate optimal height for virtualization
- useEffect(() => {
- if (groupRef.current) {
- const containerHeight = Math.min(
- Math.max(group.tasks.length * 80, 200), // Minimum 200px, scale with tasks
- 600 // Maximum 600px
- );
- setGroupHeight(containerHeight);
- }
- }, [group.tasks.length]);
+ // Calculate optimal height for virtualization
+ useEffect(() => {
+ if (groupRef.current) {
+ const containerHeight = Math.min(
+ Math.max(group.tasks.length * 80, 200), // Minimum 200px, scale with tasks
+ 600 // Maximum 600px
+ );
+ setGroupHeight(containerHeight);
+ }
+ }, [group.tasks.length]);
- // Memoize task rendering to prevent unnecessary re-renders
- const renderTask = useMemo(() => (task: any, index: number) => (
-
- ), [activeTaskId, overId]);
+ // Memoize task rendering to prevent unnecessary re-renders
+ const renderTask = useMemo(
+ () => (task: any, index: number) => (
+
+ ),
+ [activeTaskId, overId]
+ );
- // Performance optimization: Only render drop indicators when needed
- const shouldShowDropIndicators = isDraggingOver && !shouldVirtualize;
+ // Performance optimization: Only render drop indicators when needed
+ const shouldShowDropIndicators = isDraggingOver && !shouldVirtualize;
- // Combine refs for the main container
- const setRefs = (el: HTMLElement | null) => {
- setDroppableRef(el);
- setSortableRef(el);
- };
-
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- opacity: isGroupDragging ? 0.5 : 1,
- };
- const getUniqueSectionName = (baseName: string): string => {
- // Check if the base name already exists
- const existingNames = status.map(status => status.name?.toLowerCase());
-
- if (!existingNames.includes(baseName.toLowerCase())) {
- return baseName;
- }
-
- // If the base name exists, add a number suffix
- let counter = 1;
- let newName = `${baseName.trim()} (${counter})`;
-
- while (existingNames.includes(newName.toLowerCase())) {
- counter++;
- newName = `${baseName.trim()} (${counter})`;
- }
-
- return newName;
- };
- const updateStatus = async (category = group.category_id ?? null) => {
- if (!category || !projectId || !group.id) return;
- const sectionName = getUniqueSectionName(name);
- const body: ITaskStatusUpdateModel = {
- name: sectionName,
- project_id: projectId,
- category_id: category,
+ // Combine refs for the main container
+ const setRefs = (el: HTMLElement | null) => {
+ setDroppableRef(el);
+ setSortableRef(el);
};
- const res = await statusApiService.updateStatus(group.id, body, projectId);
- if (res.done) {
- dispatch(fetchEnhancedKanbanGroups(projectId));
- dispatch(fetchStatuses(projectId));
- setName(sectionName);
- } else {
- setName(editName);
- logger.error('Error updating status', res.message);
- }
- };
- // Get the appropriate background color based on theme
- const headerBackgroundColor = useMemo(() => {
- if (themeMode === 'dark') {
- return group.color_code_dark || group.color_code || '#1e1e1e';
- }
- return group.color_code || '#f5f5f5';
- }, [themeMode, group.color_code, group.color_code_dark]);
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isGroupDragging ? 0.5 : 1,
+ };
+ const getUniqueSectionName = (baseName: string): string => {
+ // Check if the base name already exists
+ const existingNames = status.map(status => status.name?.toLowerCase());
- const handleChange = async (e: React.ChangeEvent) => {
- const taskName = e.target.value;
- setName(taskName);
- };
+ if (!existingNames.includes(baseName.toLowerCase())) {
+ return baseName;
+ }
- const handleBlur = async () => {
- if (name === 'Untitled section') {
- dispatch(fetchEnhancedKanbanGroups(projectId ?? ''));
- }
- setIsEditable(false);
+ // If the base name exists, add a number suffix
+ let counter = 1;
+ let newName = `${baseName.trim()} (${counter})`;
- if (!projectId || !group.id) return;
+ while (existingNames.includes(newName.toLowerCase())) {
+ counter++;
+ newName = `${baseName.trim()} (${counter})`;
+ }
- if (groupBy === IGroupBy.STATUS) {
- await updateStatus();
- }
-
- if (groupBy === IGroupBy.PHASE) {
- const body = {
- id: group.id,
- name: name,
+ return newName;
+ };
+ const updateStatus = async (category = group.category_id ?? null) => {
+ if (!category || !projectId || !group.id) return;
+ const sectionName = getUniqueSectionName(name);
+ const body: ITaskStatusUpdateModel = {
+ name: sectionName,
+ project_id: projectId,
+ category_id: category,
};
-
- const res = await phasesApiService.updateNameOfPhase(group.id, body as ITaskPhase, projectId);
+ const res = await statusApiService.updateStatus(group.id, body, projectId);
if (res.done) {
- trackMixpanelEvent(evt_project_board_column_setting_click, { Rename: 'Phase' });
dispatch(fetchEnhancedKanbanGroups(projectId));
+ dispatch(fetchStatuses(projectId));
+ setName(sectionName);
+ } else {
+ setName(editName);
+ logger.error('Error updating status', res.message);
}
- }
- };
+ };
- const handlePressEnter = () => {
- setShowNewCardTop(true);
- setShowNewCardBottom(false);
- handleBlur();
- };
- const handleDeleteSection = async () => {
- if (!projectId || !group.id) return;
+ // Get the appropriate background color based on theme
+ const headerBackgroundColor = useMemo(() => {
+ if (themeMode === 'dark') {
+ return group.color_code_dark || group.color_code || '#1e1e1e';
+ }
+ return group.color_code || '#f5f5f5';
+ }, [themeMode, group.color_code, group.color_code_dark]);
+
+ const handleChange = async (e: React.ChangeEvent) => {
+ const taskName = e.target.value;
+ setName(taskName);
+ };
+
+ const handleBlur = async () => {
+ if (name === 'Untitled section') {
+ dispatch(fetchEnhancedKanbanGroups(projectId ?? ''));
+ }
+ setIsEditable(false);
+
+ if (!projectId || !group.id) return;
- try {
if (groupBy === IGroupBy.STATUS) {
- const replacingStatusId = '';
- const res = await statusApiService.deleteStatus(group.id, projectId, replacingStatusId);
- if (res.message === 'At least one status should exists under each category.') return
- if (res.done) {
- dispatch(fetchEnhancedKanbanGroups(projectId));
- } else {
- dispatch(seletedStatusCategory({ id: group.id, name: name, category_id: group.category_id ?? '', message: res.message ?? '' }));
- dispatch(deleteStatusToggleDrawer());
- }
- } else if (groupBy === IGroupBy.PHASE) {
- const res = await phasesApiService.deletePhaseOption(group.id, projectId);
+ await updateStatus();
+ }
+
+ if (groupBy === IGroupBy.PHASE) {
+ const body = {
+ id: group.id,
+ name: name,
+ };
+
+ const res = await phasesApiService.updateNameOfPhase(
+ group.id,
+ body as ITaskPhase,
+ projectId
+ );
if (res.done) {
+ trackMixpanelEvent(evt_project_board_column_setting_click, { Rename: 'Phase' });
dispatch(fetchEnhancedKanbanGroups(projectId));
}
}
- } catch (error) {
- logger.error('Error deleting section', error);
- }
- };
- const items: MenuProps['items'] = [
- {
- key: '1',
- label: (
- setIsEditable(true)}
- >
- {t('rename')}
-
- ),
- },
- groupBy === IGroupBy.STATUS && {
- key: '2',
- icon: ,
- label: 'Change category',
- children: statusCategories?.map(status => ({
- key: status.id,
+ };
+
+ const handlePressEnter = () => {
+ setShowNewCardTop(true);
+ setShowNewCardBottom(false);
+ handleBlur();
+ };
+ const handleDeleteSection = async () => {
+ if (!projectId || !group.id) return;
+
+ try {
+ if (groupBy === IGroupBy.STATUS) {
+ const replacingStatusId = '';
+ const res = await statusApiService.deleteStatus(group.id, projectId, replacingStatusId);
+ if (res.message === 'At least one status should exists under each category.') return;
+ if (res.done) {
+ dispatch(fetchEnhancedKanbanGroups(projectId));
+ } else {
+ dispatch(
+ seletedStatusCategory({
+ id: group.id,
+ name: name,
+ category_id: group.category_id ?? '',
+ message: res.message ?? '',
+ })
+ );
+ dispatch(deleteStatusToggleDrawer());
+ }
+ } else if (groupBy === IGroupBy.PHASE) {
+ const res = await phasesApiService.deletePhaseOption(group.id, projectId);
+ if (res.done) {
+ dispatch(fetchEnhancedKanbanGroups(projectId));
+ }
+ }
+ } catch (error) {
+ logger.error('Error deleting section', error);
+ }
+ };
+ const items: MenuProps['items'] = [
+ {
+ key: '1',
label: (
- status.id && updateStatus(status.id)}
- style={group.category_id === status.id ? { fontWeight: 700 } : {}}
+ setIsEditable(true)}
>
-
- {status.name}
-
+ {t('rename')}
+
),
- })),
- },
- groupBy !== IGroupBy.PRIORITY && {
- key: '3',
- label: (
- }
- okText={t('deleteConfirmationOk')}
- cancelText={t('deleteConfirmationCancel')}
- onConfirm={handleDeleteSection}
- >
-
-
- {t('delete')}
-
-
- ),
- },
- ].filter(Boolean) as MenuProps['items'];
-
-
- return (
-
- {/* section header */}
-
- {/*
({group.tasks.length}) */}
-
setIsHover(true)}
- onMouseLeave={() => setIsHover(false)}
- >
- {
- e.stopPropagation();
- if ((isProjectManager || isOwnerOrAdmin) && group.name !== 'Unmapped') setIsEditable(true);
- }}
- onMouseDown={(e) => {
- e.stopPropagation();
- }}
+ },
+ groupBy === IGroupBy.STATUS && {
+ key: '2',
+ icon: ,
+ label: 'Change category',
+ children: statusCategories?.map(status => ({
+ key: status.id,
+ label: (
+ status.id && updateStatus(status.id)}
+ style={group.category_id === status.id ? { fontWeight: 700 } : {}}
+ >
+
+ {status.name}
+
+ ),
+ })),
+ },
+ groupBy !== IGroupBy.PRIORITY && {
+ key: '3',
+ label: (
+ }
+ okText={t('deleteConfirmationOk')}
+ cancelText={t('deleteConfirmationCancel')}
+ onConfirm={handleDeleteSection}
>
+
+
+ {t('delete')}
+
+
+ ),
+ },
+ ].filter(Boolean) as MenuProps['items'];
- {isLoading && }
- {isEditable ? (
- {
- e.stopPropagation();
- }}
- onKeyDown={(e) => {
- e.stopPropagation();
- }}
- onClick={(e) => {
- e.stopPropagation();
- }}
- />
- ) : (
-
- setIsEllipsisActive(ellipsed),
- }}
- style={{
- minWidth: 185,
- textTransform: 'capitalize',
- color: themeMode === 'dark' ? '#383838' : '',
- display: 'inline-block',
- overflow: 'hidden',
- userSelect: 'text',
- }}
- onMouseDown={(e) => {
- e.stopPropagation();
- e.preventDefault();
- }}
- onMouseUp={(e) => {
- e.stopPropagation();
- }}
- onClick={(e) => {
- e.stopPropagation();
- }}
- >
- {name} ({group.tasks.length})
-
-
- )}
-
-
-
-
{
- setShowNewCardTop(true);
- setShowNewCardBottom(false);
+ return (
+
+ {/* section header */}
+
-
- {/*
{group.name}
*/}
+ style={{
+ minWidth: 185,
+ textTransform: 'capitalize',
+ color: themeMode === 'dark' ? '#383838' : '',
+ display: 'inline-block',
+ overflow: 'hidden',
+ userSelect: 'text',
+ }}
+ onMouseDown={e => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ onMouseUp={e => {
+ e.stopPropagation();
+ }}
+ onClick={e => {
+ e.stopPropagation();
+ }}
+ >
+ {name} ({group.tasks.length})
+
+
+ )}
+
- {/* {shouldVirtualize && (
+
+
{
+ setShowNewCardTop(true);
+ setShowNewCardBottom(false);
+ }}
+ >
+
+
+
+ {(isOwnerOrAdmin || isProjectManager) && name !== 'Unmapped' && (
+
+
+
+
+
+ )}
+
+
+ {/*
{group.name}
*/}
+
+ {/* {shouldVirtualize && (
⚡
)} */}
-
+
-
- {/* Create card at top */}
- {showNewCardTop && (isOwnerOrAdmin || isProjectManager) && (
-
- )}
- {group.tasks.length === 0 && isDraggingOver && (
-
- )}
-
- {shouldVirtualize ? (
- // Use virtualization for large task lists
-
-
+ {/* Create card at top */}
+ {showNewCardTop && (isOwnerOrAdmin || isProjectManager) && (
+
-
- ) : (
- // Use standard rendering for smaller lists
-
- {group.tasks.map((task, index) => (
-
- {/* Drop indicator before the card if this is the drop target */}
- {overId === task.id && (
-
- )}
-
- {/* Drop indicator at the end if dropping at the end of the group */}
- {index === group.tasks.length - 1 && overId === group.id && (
-
- )}
-
- ))}
-
- )}
- {/* Create card at bottom */}
- {showNewCardBottom && (isOwnerOrAdmin || isProjectManager) && (
-
- )}
- {/* Footer Add Task Button */}
- {(isOwnerOrAdmin || isProjectManager) && !showNewCardTop && !showNewCardBottom && (
-
}
- onClick={() => {
- setShowNewCardBottom(true);
- setShowNewCardTop(false);
- }}
- >
- {t('addTask')}
-
- )}
-
-
- );
-});
+ )}
+ {group.tasks.length === 0 && isDraggingOver && (
+
+ )}
-export default EnhancedKanbanGroup;
\ No newline at end of file
+ {shouldVirtualize ? (
+ // Use virtualization for large task lists
+
+
+
+ ) : (
+ // Use standard rendering for smaller lists
+
+ {group.tasks.map((task, index) => (
+
+ {/* Drop indicator before the card if this is the drop target */}
+ {overId === task.id && (
+
+ )}
+
+ {/* Drop indicator at the end if dropping at the end of the group */}
+ {index === group.tasks.length - 1 && overId === group.id && (
+
+ )}
+
+ ))}
+
+ )}
+ {/* Create card at bottom */}
+ {showNewCardBottom && (isOwnerOrAdmin || isProjectManager) && (
+
+ )}
+ {/* Footer Add Task Button */}
+ {(isOwnerOrAdmin || isProjectManager) && !showNewCardTop && !showNewCardBottom && (
+
}
+ onClick={() => {
+ setShowNewCardBottom(true);
+ setShowNewCardTop(false);
+ }}
+ >
+ {t('addTask')}
+
+ )}
+
+
+ );
+ }
+);
+
+export default EnhancedKanbanGroup;
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.css b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.css
index 0425f61d..e0a6dbf1 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.css
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.css
@@ -47,7 +47,7 @@
}
.enhanced-kanban-task-card.drop-target::before {
- content: '';
+ content: "";
position: absolute;
top: -2px;
left: -2px;
@@ -60,7 +60,8 @@
}
@keyframes dropTargetPulse {
- 0%, 100% {
+ 0%,
+ 100% {
opacity: 0.3;
transform: scale(1);
}
@@ -117,4 +118,4 @@
font-size: 12px;
color: var(--ant-color-text-tertiary);
margin-top: 4px;
-}
\ No newline at end of file
+}
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx
index 7fcf062b..8b483107 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx
@@ -19,7 +19,10 @@ import { ForkOutlined } from '@ant-design/icons';
import { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
-import { fetchBoardSubTasks, toggleTaskExpansion } from '@/features/enhanced-kanban/enhanced-kanban.slice';
+import {
+ fetchBoardSubTasks,
+ toggleTaskExpansion,
+} from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { Divider } from 'antd';
import { List } from 'antd';
import { Skeleton } from 'antd';
@@ -46,227 +49,233 @@ const PRIORITY_COLORS = {
low: '#52c41a',
} as const;
-const EnhancedKanbanTaskCard: React.FC = React.memo(({
- task,
- sectionId,
- isActive = false,
- isDragOverlay = false,
- isDropTarget = false
-}) => {
- const dispatch = useAppDispatch();
- const { t } = useTranslation('kanban-board');
- const themeMode = useAppSelector(state => state.themeReducer.mode);
- const [showNewSubtaskCard, setShowNewSubtaskCard] = useState(false);
- const [dueDate, setDueDate] = useState(
- task?.end_date ? dayjs(task?.end_date) : null
- );
+const EnhancedKanbanTaskCard: React.FC = React.memo(
+ ({ task, sectionId, isActive = false, isDragOverlay = false, isDropTarget = false }) => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation('kanban-board');
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const [showNewSubtaskCard, setShowNewSubtaskCard] = useState(false);
+ const [dueDate, setDueDate] = useState(
+ task?.end_date ? dayjs(task?.end_date) : null
+ );
- const projectId = useAppSelector(state => state.projectReducer.projectId);
- const {
- attributes,
- listeners,
- setNodeRef,
- transform,
- transition,
- isDragging,
- } = useSortable({
- id: task.id!,
- data: {
- type: 'task',
- task,
- },
- disabled: isDragOverlay,
- animateLayoutChanges: defaultAnimateLayoutChanges,
- });
+ const projectId = useAppSelector(state => state.projectReducer.projectId);
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
+ id: task.id!,
+ data: {
+ type: 'task',
+ task,
+ },
+ disabled: isDragOverlay,
+ animateLayoutChanges: defaultAnimateLayoutChanges,
+ });
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- opacity: isDragging ? 0.5 : 1,
- backgroundColor: themeMode === 'dark' ? '#292929' : '#fafafa',
- };
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ backgroundColor: themeMode === 'dark' ? '#292929' : '#fafafa',
+ };
- const handleCardClick = useCallback((e: React.MouseEvent, id: string) => {
- // Prevent the event from propagating to parent elements
- e.stopPropagation();
+ const handleCardClick = useCallback(
+ (e: React.MouseEvent, id: string) => {
+ // Prevent the event from propagating to parent elements
+ e.stopPropagation();
- // Don't handle click if we're dragging
- if (isDragging) return;
- dispatch(setSelectedTaskId(id));
- dispatch(setShowTaskDrawer(true));
- }, [dispatch, isDragging]);
+ // Don't handle click if we're dragging
+ if (isDragging) return;
+ dispatch(setSelectedTaskId(id));
+ dispatch(setShowTaskDrawer(true));
+ },
+ [dispatch, isDragging]
+ );
- const renderLabels = useMemo(() => {
- if (!task?.labels?.length) return null;
+ const renderLabels = useMemo(() => {
+ if (!task?.labels?.length) return null;
+
+ return (
+ <>
+ {task.labels.slice(0, 2).map((label: any) => (
+
+
+ {label.name}
+
+
+ ))}
+ {task.labels.length > 2 && + {task.labels.length - 2}}
+ >
+ );
+ }, [task.labels, themeMode]);
+
+ const handleSubTaskExpand = useCallback(() => {
+ if (task && task.id && projectId) {
+ // Check if subtasks are already loaded and we have subtask data
+ if (task.sub_tasks && task.sub_tasks.length > 0 && task.sub_tasks_count > 0) {
+ // If subtasks are already loaded, just toggle visibility
+ dispatch(toggleTaskExpansion(task.id));
+ } else if (task.sub_tasks_count > 0) {
+ // If we have a subtask count but no loaded subtasks, fetch them
+ dispatch(toggleTaskExpansion(task.id));
+ dispatch(fetchBoardSubTasks({ taskId: task.id, projectId }));
+ } else {
+ // If no subtasks exist, just toggle visibility (will show empty state)
+ dispatch(toggleTaskExpansion(task.id));
+ }
+ }
+ }, [task, projectId, dispatch]);
+
+ const handleSubtaskButtonClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation();
+ handleSubTaskExpand();
+ },
+ [handleSubTaskExpand]
+ );
+
+ const handleAddSubtaskClick = useCallback((e: React.MouseEvent) => {
+ e.stopPropagation();
+ setShowNewSubtaskCard(true);
+ }, []);
return (
- <>
- {task.labels.slice(0, 2).map((label: any) => (
-
-
- {label.name}
-
-
- ))}
- {task.labels.length > 2 && + {task.labels.length - 2}}
- >
- );
- }, [task.labels, themeMode]);
+
+
handleCardClick(e, task.id || '')}>
+
+ {renderLabels}
-
-
- const handleSubTaskExpand = useCallback(() => {
- if (task && task.id && projectId) {
- // Check if subtasks are already loaded and we have subtask data
- if (task.sub_tasks && task.sub_tasks.length > 0 && task.sub_tasks_count > 0) {
- // If subtasks are already loaded, just toggle visibility
- dispatch(toggleTaskExpansion(task.id));
- } else if (task.sub_tasks_count > 0) {
- // If we have a subtask count but no loaded subtasks, fetch them
- dispatch(toggleTaskExpansion(task.id));
- dispatch(fetchBoardSubTasks({ taskId: task.id, projectId }));
- } else {
- // If no subtasks exist, just toggle visibility (will show empty state)
- dispatch(toggleTaskExpansion(task.id));
- }
- }
- }, [task, projectId, dispatch]);
-
- const handleSubtaskButtonClick = useCallback((e: React.MouseEvent) => {
- e.stopPropagation();
- handleSubTaskExpand();
- }, [handleSubTaskExpand]);
-
- const handleAddSubtaskClick = useCallback((e: React.MouseEvent) => {
- e.stopPropagation();
- setShowNewSubtaskCard(true);
- }, []);
-
- return (
-
-
handleCardClick(e, task.id || '')}>
-
-
- {renderLabels}
-
-
-
-
-
-
- {/* Action Icons */}
-
-
- {task.name}
-
-
-
-
-
-
+
+
-
-
- {/* Subtask Section */}
-
-
-
- {task.sub_tasks_count}
- {task.show_sub_tasks ? : }
-
-
+ {/* Action Icons */}
+
+
+ {task.name}
+
-
-
- {task.show_sub_tasks && (
-
-
-
- {task.sub_tasks_loading && (
-
-
-
- )}
+
+
+
+
+
+
+
- {!task.sub_tasks_loading && task?.sub_tasks && task.sub_tasks.length > 0 &&
- task.sub_tasks.map((subtask: any) => (
-
- ))}
-
- {!task.sub_tasks_loading && (!task?.sub_tasks || task.sub_tasks.length === 0) && task.sub_tasks_count === 0 && (
-
-
- {t('noSubtasks', 'No subtasks')}
-
-
- )}
-
- {showNewSubtaskCard && (
-
- )}
-
+ {/* Subtask Section */}
}
- onClick={handleAddSubtaskClick}
+ type="text"
>
- {t('addSubtask', 'Add Subtask')}
+
+
+ {task.sub_tasks_count}
+ {task.show_sub_tasks ? : }
+
- )}
-
-
-
- );
-});
+
+
+ {task.show_sub_tasks && (
+
+
+
+ {task.sub_tasks_loading && (
+
+
+
+ )}
-export default EnhancedKanbanTaskCard;
\ No newline at end of file
+ {!task.sub_tasks_loading &&
+ task?.sub_tasks &&
+ task.sub_tasks.length > 0 &&
+ task.sub_tasks.map((subtask: any) => (
+
+ ))}
+
+ {!task.sub_tasks_loading &&
+ (!task?.sub_tasks || task.sub_tasks.length === 0) &&
+ task.sub_tasks_count === 0 && (
+
+
+ {t('noSubtasks', 'No subtasks')}
+
+
+ )}
+
+ {showNewSubtaskCard && (
+
+ )}
+
+ }
+ onClick={handleAddSubtaskClick}
+ >
+ {t('addSubtask', 'Add Subtask')}
+
+
+ )}
+
+
+
+ );
+ }
+);
+
+export default EnhancedKanbanTaskCard;
diff --git a/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.css b/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.css
index 94432801..f8dd177a 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.css
+++ b/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.css
@@ -93,9 +93,9 @@
width: 100%;
margin-bottom: 16px;
}
-
+
.performance-metrics {
grid-template-columns: 1fr;
gap: 8px;
}
-}
\ No newline at end of file
+}
diff --git a/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.tsx b/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.tsx
index fdf2e7c9..1d203c9c 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/PerformanceMonitor.tsx
@@ -21,11 +21,16 @@ const PerformanceMonitor: React.FC = () => {
const getStatusColor = (status: string) => {
switch (status) {
- case 'critical': return 'red';
- case 'warning': return 'orange';
- case 'good': return 'blue';
- case 'excellent': return 'green';
- default: return 'default';
+ case 'critical':
+ return 'red';
+ case 'warning':
+ return 'orange';
+ case 'good':
+ return 'blue';
+ case 'excellent':
+ return 'green';
+ default:
+ return 'default';
}
};
@@ -33,15 +38,15 @@ const PerformanceMonitor: React.FC = () => {
const statusColor = getStatusColor(status);
return (
-
Performance Monitor
-
@@ -56,7 +61,7 @@ const PerformanceMonitor: React.FC = () => {
valueStyle={{ fontSize: '16px' }}
/>
-
+
{
valueStyle={{ fontSize: '16px' }}
/>
-
+
{
valueStyle={{ fontSize: '16px' }}
/>
-
+
Virtualization:
-
-
+
{performanceMetrics.totalTasks > 500 && (
Performance Tips:
@@ -100,4 +105,4 @@ const PerformanceMonitor: React.FC = () => {
);
};
-export default React.memo(PerformanceMonitor);
\ No newline at end of file
+export default React.memo(PerformanceMonitor);
diff --git a/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.css b/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.css
index 8a751bd7..478ac4ac 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.css
+++ b/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.css
@@ -57,4 +57,4 @@
.virtualized-task-list::-webkit-scrollbar-thumb:hover {
background: var(--ant-color-text-tertiary);
-}
\ No newline at end of file
+}
diff --git a/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.tsx b/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.tsx
index 41fe4fd3..c6486b62 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/VirtualizedTaskList.tsx
@@ -22,45 +22,54 @@ const VirtualizedTaskList: React.FC
= ({
onTaskRender,
}) => {
// Memoize task data to prevent unnecessary re-renders
- const taskData = useMemo(() => ({
- tasks,
- activeTaskId,
- overId,
- onTaskRender,
- }), [tasks, activeTaskId, overId, onTaskRender]);
+ const taskData = useMemo(
+ () => ({
+ tasks,
+ activeTaskId,
+ overId,
+ onTaskRender,
+ }),
+ [tasks, activeTaskId, overId, onTaskRender]
+ );
// Row renderer for virtualized list
- const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
- const task = tasks[index];
- if (!task) return null;
+ const Row = useCallback(
+ ({ index, style }: { index: number; style: React.CSSProperties }) => {
+ const task = tasks[index];
+ if (!task) return null;
- // Call onTaskRender callback if provided
- onTaskRender?.(task, index);
+ // Call onTaskRender callback if provided
+ onTaskRender?.(task, index);
- return (
+ return (
- );
- }, [tasks, activeTaskId, overId, onTaskRender]);
+ );
+ },
+ [tasks, activeTaskId, overId, onTaskRender]
+ );
// Memoize the list component to prevent unnecessary re-renders
- const VirtualizedList = useMemo(() => (
-
- {Row}
-
- ), [height, tasks.length, itemHeight, taskData, Row]);
+ const VirtualizedList = useMemo(
+ () => (
+
+ {Row}
+
+ ),
+ [height, tasks.length, itemHeight, taskData, Row]
+ );
if (tasks.length === 0) {
return (
@@ -73,4 +82,4 @@ const VirtualizedTaskList: React.FC = ({
return VirtualizedList;
};
-export default React.memo(VirtualizedTaskList);
\ No newline at end of file
+export default React.memo(VirtualizedTaskList);
diff --git a/worklenz-frontend/src/components/home-tasks/statusDropdown/home-tasks-status-dropdown.tsx b/worklenz-frontend/src/components/home-tasks/statusDropdown/home-tasks-status-dropdown.tsx
index f3595e99..424d3388 100644
--- a/worklenz-frontend/src/components/home-tasks/statusDropdown/home-tasks-status-dropdown.tsx
+++ b/worklenz-frontend/src/components/home-tasks/statusDropdown/home-tasks-status-dropdown.tsx
@@ -20,9 +20,7 @@ const HomeTasksStatusDropdown = ({ task, teamId }: HomeTasksStatusDropdownProps)
const { t } = useTranslation('task-list-table');
const { socket, connected } = useSocket();
const { homeTasksConfig } = useAppSelector(state => state.homePageReducer);
- const {
- refetch
- } = useGetMyTasksQuery(homeTasksConfig, {
+ const { refetch } = useGetMyTasksQuery(homeTasksConfig, {
skip: false, // Ensure this query runs
});
diff --git a/worklenz-frontend/src/components/home-tasks/taskDatePicker/home-tasks-date-picker.tsx b/worklenz-frontend/src/components/home-tasks/taskDatePicker/home-tasks-date-picker.tsx
index 857458ff..37f4d5a3 100644
--- a/worklenz-frontend/src/components/home-tasks/taskDatePicker/home-tasks-date-picker.tsx
+++ b/worklenz-frontend/src/components/home-tasks/taskDatePicker/home-tasks-date-picker.tsx
@@ -1,110 +1,111 @@
-import { useSocket } from "@/socket/socketContext";
-import { IProjectTask } from "@/types/project/projectTasksViewModel.types";
-import { DatePicker } from "antd";
+import { useSocket } from '@/socket/socketContext';
+import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
+import { DatePicker } from 'antd';
import dayjs from 'dayjs';
import calendar from 'dayjs/plugin/calendar';
import { SocketEvents } from '@/shared/socket-events';
import type { Dayjs } from 'dayjs';
-import { useTranslation } from "react-i18next";
-import { useEffect, useState, useMemo } from "react";
-import { useAppSelector } from "@/hooks/useAppSelector";
-import { useGetMyTasksQuery } from "@/api/home-page/home-page.api.service";
-import { getUserSession } from "@/utils/session-helper";
+import { useTranslation } from 'react-i18next';
+import { useEffect, useState, useMemo } from 'react';
+import { useAppSelector } from '@/hooks/useAppSelector';
+import { useGetMyTasksQuery } from '@/api/home-page/home-page.api.service';
+import { getUserSession } from '@/utils/session-helper';
// Extend dayjs with the calendar plugin
dayjs.extend(calendar);
type HomeTasksDatePickerProps = {
- record: IProjectTask;
+ record: IProjectTask;
};
const HomeTasksDatePicker = ({ record }: HomeTasksDatePickerProps) => {
- const { socket, connected } = useSocket();
- const { t } = useTranslation('home');
- const { homeTasksConfig } = useAppSelector(state => state.homePageReducer);
- const { refetch } = useGetMyTasksQuery(homeTasksConfig, {
- skip: false
+ const { socket, connected } = useSocket();
+ const { t } = useTranslation('home');
+ const { homeTasksConfig } = useAppSelector(state => state.homePageReducer);
+ const { refetch } = useGetMyTasksQuery(homeTasksConfig, {
+ skip: false,
+ });
+
+ // Use useMemo to avoid re-renders when record.end_date is the same
+ const initialDate = useMemo(
+ () => (record.end_date ? dayjs(record.end_date) : null),
+ [record.end_date]
+ );
+
+ const [selectedDate, setSelectedDate] = useState(initialDate);
+
+ // Update selected date when record changes
+ useEffect(() => {
+ setSelectedDate(initialDate);
+ }, [initialDate]);
+
+ const handleChangeReceived = (value: any) => {
+ refetch();
+ };
+
+ useEffect(() => {
+ socket?.on(SocketEvents.TASK_END_DATE_CHANGE.toString(), handleChangeReceived);
+ socket?.on(SocketEvents.TASK_STATUS_CHANGE.toString(), handleChangeReceived);
+ return () => {
+ socket?.removeListener(SocketEvents.TASK_END_DATE_CHANGE.toString(), handleChangeReceived);
+ socket?.removeListener(SocketEvents.TASK_STATUS_CHANGE.toString(), handleChangeReceived);
+ };
+ }, [connected]);
+
+ const handleEndDateChanged = (value: Dayjs | null, task: IProjectTask) => {
+ setSelectedDate(value);
+ if (!task.id) return;
+
+ const body = {
+ task_id: task.id,
+ end_date: value?.format('YYYY-MM-DD'),
+ parent_task: task.parent_task_id,
+ time_zone: getUserSession()?.timezone_name
+ ? getUserSession()?.timezone_name
+ : Intl.DateTimeFormat().resolvedOptions().timeZone,
+ };
+ socket?.emit(SocketEvents.TASK_END_DATE_CHANGE.toString(), JSON.stringify(body));
+ };
+
+ // Function to dynamically format the date based on the calendar rules
+ const getFormattedDate = (date: Dayjs | null) => {
+ if (!date) return '';
+
+ return date.calendar(null, {
+ sameDay: '[Today]',
+ nextDay: '[Tomorrow]',
+ nextWeek: 'MMM DD',
+ lastDay: '[Yesterday]',
+ lastWeek: 'MMM DD',
+ sameElse: date.year() === dayjs().year() ? 'MMM DD' : 'MMM DD, YYYY',
});
+ };
- // Use useMemo to avoid re-renders when record.end_date is the same
- const initialDate = useMemo(() =>
- record.end_date ? dayjs(record.end_date) : null
- , [record.end_date]);
-
- const [selectedDate, setSelectedDate] = useState(initialDate);
-
- // Update selected date when record changes
- useEffect(() => {
- setSelectedDate(initialDate);
- }, [initialDate]);
-
- const handleChangeReceived = (value: any) => {
- refetch();
- };
-
- useEffect(() => {
- socket?.on(SocketEvents.TASK_END_DATE_CHANGE.toString(), handleChangeReceived);
- socket?.on(SocketEvents.TASK_STATUS_CHANGE.toString(), handleChangeReceived);
- return () => {
- socket?.removeListener(SocketEvents.TASK_END_DATE_CHANGE.toString(), handleChangeReceived);
- socket?.removeListener(SocketEvents.TASK_STATUS_CHANGE.toString(), handleChangeReceived);
- };
- }, [connected]);
-
- const handleEndDateChanged = (value: Dayjs | null, task: IProjectTask) => {
- setSelectedDate(value);
- if (!task.id) return;
-
- const body = {
- task_id: task.id,
- end_date: value?.format('YYYY-MM-DD'),
- parent_task: task.parent_task_id,
- time_zone: getUserSession()?.timezone_name
- ? getUserSession()?.timezone_name
- : Intl.DateTimeFormat().resolvedOptions().timeZone,
- };
- socket?.emit(SocketEvents.TASK_END_DATE_CHANGE.toString(), JSON.stringify(body));
- };
-
- // Function to dynamically format the date based on the calendar rules
- const getFormattedDate = (date: Dayjs | null) => {
- if (!date) return '';
-
- return date.calendar(null, {
- sameDay: '[Today]',
- nextDay: '[Tomorrow]',
- nextWeek: 'MMM DD',
- lastDay: '[Yesterday]',
- lastWeek: 'MMM DD',
- sameElse: date.year() === dayjs().year() ? 'MMM DD' : 'MMM DD, YYYY',
- });
- };
-
- return (
- current.isBefore(dayjs(record.start_date)) : undefined
- }
- placeholder={t('tasks.dueDatePlaceholder')}
- value={selectedDate}
- onChange={value => handleEndDateChanged(value || null, record || null)}
- format={(value) => getFormattedDate(value)} // Dynamically format the displayed value
- style={{
- color: selectedDate
- ? selectedDate.isSame(dayjs(), 'day') || selectedDate.isSame(dayjs().add(1, 'day'), 'day')
- ? '#52c41a'
- : selectedDate.isAfter(dayjs().add(1, 'day'), 'day')
- ? undefined
- : '#ff4d4f'
- : undefined,
- width: '125px', // Ensure the input takes full width
- }}
- inputReadOnly // Prevent manual input to avoid overflow issues
- variant={'borderless'} // Make the DatePicker borderless
- suffixIcon={null}
- />
- );
+ return (
+ current.isBefore(dayjs(record.start_date)) : undefined
+ }
+ placeholder={t('tasks.dueDatePlaceholder')}
+ value={selectedDate}
+ onChange={value => handleEndDateChanged(value || null, record || null)}
+ format={value => getFormattedDate(value)} // Dynamically format the displayed value
+ style={{
+ color: selectedDate
+ ? selectedDate.isSame(dayjs(), 'day') || selectedDate.isSame(dayjs().add(1, 'day'), 'day')
+ ? '#52c41a'
+ : selectedDate.isAfter(dayjs().add(1, 'day'), 'day')
+ ? undefined
+ : '#ff4d4f'
+ : undefined,
+ width: '125px', // Ensure the input takes full width
+ }}
+ inputReadOnly // Prevent manual input to avoid overflow issues
+ variant={'borderless'} // Make the DatePicker borderless
+ suffixIcon={null}
+ />
+ );
};
-export default HomeTasksDatePicker;
\ No newline at end of file
+export default HomeTasksDatePicker;
diff --git a/worklenz-frontend/src/components/index.ts b/worklenz-frontend/src/components/index.ts
index dcec2980..57cd8bcd 100644
--- a/worklenz-frontend/src/components/index.ts
+++ b/worklenz-frontend/src/components/index.ts
@@ -9,4 +9,4 @@ export { default as CustomNumberLabel } from './CustomNumberLabel';
export { default as LabelsSelector } from './LabelsSelector';
export { default as Progress } from './Progress';
export { default as Tag } from './Tag';
-export { default as Tooltip } from './Tooltip';
\ No newline at end of file
+export { default as Tooltip } from './Tooltip';
diff --git a/worklenz-frontend/src/components/kanban-board-management-v2/SortableKanbanGroup.tsx b/worklenz-frontend/src/components/kanban-board-management-v2/SortableKanbanGroup.tsx
index d1f41391..1fe1c94a 100644
--- a/worklenz-frontend/src/components/kanban-board-management-v2/SortableKanbanGroup.tsx
+++ b/worklenz-frontend/src/components/kanban-board-management-v2/SortableKanbanGroup.tsx
@@ -6,47 +6,40 @@ import { ITaskListGroup } from '@/types/tasks/taskList.types';
import { IGroupBy } from '@/features/tasks/tasks.slice';
interface SortableKanbanGroupProps {
- group: ITaskListGroup;
- projectId: string;
- currentGrouping: IGroupBy;
- selectedTaskIds: string[];
- onAddTask?: (groupId: string) => void;
- onToggleCollapse?: (groupId: string) => void;
- onSelectTask?: (taskId: string, selected: boolean) => void;
- onToggleSubtasks?: (taskId: string) => void;
- activeTaskId?: string | null;
+ group: ITaskListGroup;
+ projectId: string;
+ currentGrouping: IGroupBy;
+ selectedTaskIds: string[];
+ onAddTask?: (groupId: string) => void;
+ onToggleCollapse?: (groupId: string) => void;
+ onSelectTask?: (taskId: string, selected: boolean) => void;
+ onToggleSubtasks?: (taskId: string) => void;
+ activeTaskId?: string | null;
}
-const SortableKanbanGroup: React.FC = (props) => {
- const { group, activeTaskId } = props;
- const {
- setNodeRef,
- attributes,
- listeners,
- transform,
- transition,
- isDragging,
- } = useSortable({
- id: group.id,
- data: { type: 'group', groupId: group.id },
- });
+const SortableKanbanGroup: React.FC = props => {
+ const { group, activeTaskId } = props;
+ const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
+ id: group.id,
+ data: { type: 'group', groupId: group.id },
+ });
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- opacity: isDragging ? 0.5 : 1,
- zIndex: isDragging ? 10 : undefined,
- };
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ zIndex: isDragging ? 10 : undefined,
+ };
- return (
-
-
-
- );
+ return (
+
+
+
+ );
};
-export default SortableKanbanGroup;
\ No newline at end of file
+export default SortableKanbanGroup;
diff --git a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanGroup.tsx b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanGroup.tsx
index 6b5fe9f1..3409237e 100644
--- a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanGroup.tsx
+++ b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanGroup.tsx
@@ -10,130 +10,122 @@ import KanbanTaskCard from './kanbanTaskCard';
const { Text } = Typography;
interface TaskGroupProps {
- group: ITaskListGroup;
- projectId: string;
- currentGrouping: IGroupBy;
- selectedTaskIds: string[];
- onAddTask?: (groupId: string) => void;
- onToggleCollapse?: (groupId: string) => void;
- onSelectTask?: (taskId: string, selected: boolean) => void;
- onToggleSubtasks?: (taskId: string) => void;
- dragHandleProps?: any;
- activeTaskId?: string | null;
+ group: ITaskListGroup;
+ projectId: string;
+ currentGrouping: IGroupBy;
+ selectedTaskIds: string[];
+ onAddTask?: (groupId: string) => void;
+ onToggleCollapse?: (groupId: string) => void;
+ onSelectTask?: (taskId: string, selected: boolean) => void;
+ onToggleSubtasks?: (taskId: string) => void;
+ dragHandleProps?: any;
+ activeTaskId?: string | null;
}
const KanbanGroup: React.FC = ({
- group,
- projectId,
- currentGrouping,
- selectedTaskIds,
- onAddTask,
- onToggleCollapse,
- onSelectTask,
- onToggleSubtasks,
- dragHandleProps,
- activeTaskId,
+ group,
+ projectId,
+ currentGrouping,
+ selectedTaskIds,
+ onAddTask,
+ onToggleCollapse,
+ onSelectTask,
+ onToggleSubtasks,
+ dragHandleProps,
+ activeTaskId,
}) => {
- const [isCollapsed, setIsCollapsed] = useState(false);
- const { setNodeRef, isOver } = useDroppable({
- id: group.id,
- data: {
- type: 'group',
- groupId: group.id,
- },
- });
+ const [isCollapsed, setIsCollapsed] = useState(false);
+ const { setNodeRef, isOver } = useDroppable({
+ id: group.id,
+ data: {
+ type: 'group',
+ groupId: group.id,
+ },
+ });
- // Get task IDs for sortable context
- const taskIds = group.tasks.map(task => task.id!);
+ // Get task IDs for sortable context
+ const taskIds = group.tasks.map(task => task.id!);
- // Get group color based on grouping type
- const getGroupColor = () => {
- if (group.color_code) return group.color_code;
- switch (currentGrouping) {
- case 'status':
- return group.id === 'todo' ? '#faad14' : group.id === 'doing' ? '#1890ff' : '#52c41a';
- case 'priority':
- return group.id === 'critical'
- ? '#ff4d4f'
- : group.id === 'high'
- ? '#fa8c16'
- : group.id === 'medium'
- ? '#faad14'
- : '#52c41a';
- case 'phase':
- return '#722ed1';
- default:
- return '#d9d9d9';
- }
- };
+ // Get group color based on grouping type
+ const getGroupColor = () => {
+ if (group.color_code) return group.color_code;
+ switch (currentGrouping) {
+ case 'status':
+ return group.id === 'todo' ? '#faad14' : group.id === 'doing' ? '#1890ff' : '#52c41a';
+ case 'priority':
+ return group.id === 'critical'
+ ? '#ff4d4f'
+ : group.id === 'high'
+ ? '#fa8c16'
+ : group.id === 'medium'
+ ? '#faad14'
+ : '#52c41a';
+ case 'phase':
+ return '#722ed1';
+ default:
+ return '#d9d9d9';
+ }
+ };
- const handleAddTask = () => {
- onAddTask?.(group.id);
- };
+ const handleAddTask = () => {
+ onAddTask?.(group.id);
+ };
- return (
-
- {/* Group Header */}
-
- {/* Drag handle for column */}
-
}
- className="kanban-group-drag-handle"
- style={{ marginRight: 8, cursor: 'grab', opacity: 0.7 }}
- {...(dragHandleProps || {})}
+ return (
+
+ {/* Group Header */}
+
+ {/* Drag handle for column */}
+ }
+ className="kanban-group-drag-handle"
+ style={{ marginRight: 8, cursor: 'grab', opacity: 0.7 }}
+ {...(dragHandleProps || {})}
+ />
+
+ {group.name} ({group.tasks.length})
+
+
+
+ {/* Tasks as Cards */}
+
+
+ {group.tasks.length === 0 ? (
+
+ No tasks in this group
+
+ ) : (
+ group.tasks.map((task, index) =>
+ task.id === activeTaskId ? (
+
+ ) : (
+
-
- {group.name} ({group.tasks.length})
-
-
+ )
+ )
+ )}
+
+
- {/* Tasks as Cards */}
-
-
- {group.tasks.length === 0 ? (
-
- No tasks in this group
-
- ) : (
- group.tasks.map((task, index) => (
- task.id === activeTaskId ? (
-
- ) : (
-
- )
- ))
- )}
-
-
+ {/* Add Task Button */}
+
+ } block onClick={handleAddTask}>
+ Add Task
+
+
- {/* Add Task Button */}
-
- }
- block
- onClick={handleAddTask}
- >
- Add Task
-
-
-
-
-
- );
+
+ );
};
export default KanbanGroup;
diff --git a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskCard.tsx b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskCard.tsx
index db4ff780..f3862cbd 100644
--- a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskCard.tsx
+++ b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskCard.tsx
@@ -36,14 +36,7 @@ const KanbanTaskCard: React.FC = ({
onSelect,
onToggleSubtasks,
}) => {
- const {
- attributes,
- listeners,
- setNodeRef,
- transform,
- transition,
- isDragging,
- } = useSortable({
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: task.id!,
data: {
type: 'task',
@@ -93,7 +86,10 @@ const KanbanTaskCard: React.FC = ({
{...attributes}
{...listeners}
/>
-
+
{task.name}
{task.sub_tasks_count && task.sub_tasks_count > 0 && (
@@ -112,15 +108,23 @@ const KanbanTaskCard: React.FC = ({
{/* Task Key and Status */}
{task.task_key && (
-
{task.task_key}
+
+ {task.task_key}
+
)}
{task.status_name && (
-
+
{task.status_name}
)}
{task.priority_name && (
-
+
{task.priority_name}
)}
@@ -139,7 +143,11 @@ const KanbanTaskCard: React.FC = ({
/>
)}
{dueDate && (
-
+
{dueDate.text}
@@ -149,7 +157,7 @@ const KanbanTaskCard: React.FC = ({
{task.assignees && task.assignees.length > 0 && (
- {task.assignees.map((assignee) => (
+ {task.assignees.map(assignee => (
{assignee.name?.charAt(0)?.toUpperCase()}
@@ -158,11 +166,16 @@ const KanbanTaskCard: React.FC = ({
)}
{task.labels && task.labels.length > 0 && (
- {task.labels.slice(0, 2).map((label) => (
+ {task.labels.slice(0, 2).map(label => (
{label.name}
@@ -198,7 +211,7 @@ const KanbanTaskCard: React.FC
= ({
{/* Subtasks */}
{task.show_sub_tasks && task.sub_tasks && task.sub_tasks.length > 0 && (
- {task.sub_tasks.map((subtask) => (
+ {task.sub_tasks.map(subtask => (
= ({
);
};
-export default KanbanTaskCard;
\ No newline at end of file
+export default KanbanTaskCard;
diff --git a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskListBoard.tsx b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskListBoard.tsx
index a6361a54..314f0f72 100644
--- a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskListBoard.tsx
+++ b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskListBoard.tsx
@@ -1,30 +1,25 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
- DndContext,
- DragOverlay,
- DragStartEvent,
- DragEndEvent,
- DragOverEvent,
- closestCorners,
- KeyboardSensor,
- PointerSensor,
- useSensor,
- useSensors,
+ DndContext,
+ DragOverlay,
+ DragStartEvent,
+ DragEndEvent,
+ DragOverEvent,
+ closestCorners,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
} from '@dnd-kit/core';
import {
- horizontalListSortingStrategy,
- SortableContext,
- sortableKeyboardCoordinates,
+ horizontalListSortingStrategy,
+ SortableContext,
+ sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { Card, Spin, Empty, Flex } from 'antd';
import { RootState } from '@/app/store';
-import {
- IGroupBy,
- setGroup,
- fetchTaskGroups,
- reorderTasks,
-} from '@/features/tasks/tasks.slice';
+import { IGroupBy, setGroup, fetchTaskGroups, reorderTasks } from '@/features/tasks/tasks.slice';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { AppDispatch } from '@/app/store';
import BoardSectionCard from '@/pages/projects/projectView/board/board-section/board-section-card/board-section-card';
@@ -38,269 +33,261 @@ import KanbanGroup from './kanbanGroup';
import KanbanTaskCard from './kanbanTaskCard';
import SortableKanbanGroup from './SortableKanbanGroup';
-
// Import the TaskListFilters component
-const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
+const TaskListFilters = React.lazy(
+ () => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters')
+);
interface TaskListBoardProps {
- projectId: string;
- className?: string;
+ projectId: string;
+ className?: string;
}
interface DragState {
- activeTask: IProjectTask | null;
- activeGroupId: string | null;
+ activeTask: IProjectTask | null;
+ activeGroupId: string | null;
}
const KanbanTaskListBoard: React.FC = ({ projectId, className = '' }) => {
- const dispatch = useDispatch();
- const [dragState, setDragState] = useState({
- activeTask: null,
- activeGroupId: null,
- });
- // New state for active/over ids
- const [activeTaskId, setActiveTaskId] = useState(null);
- const [overId, setOverId] = useState(null);
+ const dispatch = useDispatch();
+ const [dragState, setDragState] = useState({
+ activeTask: null,
+ activeGroupId: null,
+ });
+ // New state for active/over ids
+ const [activeTaskId, setActiveTaskId] = useState(null);
+ const [overId, setOverId] = useState(null);
- // Redux selectors
+ // Redux selectors
- const { taskGroups, groupBy, loadingGroups, error, search, archived } = useSelector((state: RootState) => state.boardReducer);
+ const { taskGroups, groupBy, loadingGroups, error, search, archived } = useSelector(
+ (state: RootState) => state.boardReducer
+ );
- // Selection state
- const [selectedTaskIds, setSelectedTaskIds] = useState([]);
+ // Selection state
+ const [selectedTaskIds, setSelectedTaskIds] = useState([]);
- // Drag and Drop sensors
- const sensors = useSensors(
- useSensor(PointerSensor, {
- activationConstraint: {
- distance: 8,
- },
- }),
- useSensor(KeyboardSensor, {
- coordinateGetter: sortableKeyboardCoordinates,
- })
- );
- const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
- const isProjectManager = useIsProjectManager();
+ // Drag and Drop sensors
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ }),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ );
+ const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
+ const isProjectManager = useIsProjectManager();
- // Fetch task groups when component mounts or dependencies change
- useEffect(() => {
- if (projectId) {
- dispatch(fetchTaskGroups(projectId));
- }
- }, [dispatch, projectId, groupBy, search, archived]);
-
- // Memoized calculations
- const allTaskIds = useMemo(() => {
- return taskGroups.flatMap(group => group.tasks.map(task => task.id!));
- }, [taskGroups]);
-
- const totalTasksCount = useMemo(() => {
- return taskGroups.reduce((sum, group) => sum + group.tasks.length, 0);
- }, [taskGroups]);
-
- const hasSelection = selectedTaskIds.length > 0;
-
- // // Handlers
- // const handleGroupingChange = (newGroupBy: IGroupBy) => {
- // dispatch(setGroup(newGroupBy));
- // };
-
- const handleDragStart = (event: DragStartEvent) => {
- const { active } = event;
- const taskId = active.id as string;
- setActiveTaskId(taskId);
- setOverId(null);
- // Find the task and its group
- let activeTask: IProjectTask | null = null;
- let activeGroupId: string | null = null;
- for (const group of taskGroups) {
- const task = group.tasks.find(t => t.id === taskId);
- if (task) {
- activeTask = task;
- activeGroupId = group.id;
- break;
- }
- }
- setDragState({
- activeTask,
- activeGroupId,
- });
- };
-
- const handleDragOver = (event: DragOverEvent) => {
- setOverId(event.over?.id as string || null);
- };
-
- const handleDragEnd = (event: DragEndEvent) => {
- const { active, over } = event;
- setActiveTaskId(null);
- setOverId(null);
- setDragState({
- activeTask: null,
- activeGroupId: null,
- });
- if (!over || !dragState.activeTask || !dragState.activeGroupId) {
- return;
- }
- const activeTaskId = active.id as string;
- const overIdVal = over.id as string;
- // Find the group and index for drop
- let targetGroupId = overIdVal;
- let targetIndex = -1;
- let isOverTask = false;
- // Check if over is a group or a task
- const overGroup = taskGroups.find(g => g.id === overIdVal);
- if (!overGroup) {
- // Dropping on a task, find which group it belongs to
- for (const group of taskGroups) {
- const taskIndex = group.tasks.findIndex(t => t.id === overIdVal);
- if (taskIndex !== -1) {
- targetGroupId = group.id;
- targetIndex = taskIndex;
- isOverTask = true;
- break;
- }
- }
- }
- const sourceGroup = taskGroups.find(g => g.id === dragState.activeGroupId);
- const targetGroup = taskGroups.find(g => g.id === targetGroupId);
- if (!sourceGroup || !targetGroup) return;
- const sourceIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
- if (sourceIndex === -1) return;
- // Calculate new positions
- let finalTargetIndex = targetIndex;
- if (!isOverTask || finalTargetIndex === -1) {
- finalTargetIndex = targetGroup.tasks.length;
- }
- // If moving within the same group and after itself, adjust index
- if (sourceGroup.id === targetGroup.id && sourceIndex < finalTargetIndex) {
- finalTargetIndex--;
- }
- // Create updated task arrays
- const updatedSourceTasks = [...sourceGroup.tasks];
- const [movedTask] = updatedSourceTasks.splice(sourceIndex, 1);
- let updatedTargetTasks: IProjectTask[];
- if (sourceGroup.id === targetGroup.id) {
- updatedTargetTasks = updatedSourceTasks;
- updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
- } else {
- updatedTargetTasks = [...targetGroup.tasks];
- updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
- }
- // Dispatch the reorder action
- dispatch(reorderTasks({
- activeGroupId: sourceGroup.id,
- overGroupId: targetGroup.id,
- fromIndex: sourceIndex,
- toIndex: finalTargetIndex,
- task: movedTask,
- updatedSourceTasks,
- updatedTargetTasks,
- }));
- };
-
-
-
- const handleSelectTask = (taskId: string, selected: boolean) => {
- setSelectedTaskIds(prev => {
- if (selected) {
- return [...prev, taskId];
- } else {
- return prev.filter(id => id !== taskId);
- }
- });
- };
-
- const handleToggleSubtasks = (taskId: string) => {
- // Implementation for toggling subtasks
- console.log('Toggle subtasks for task:', taskId);
- };
-
- if (error) {
- return (
-
-
-
- );
+ // Fetch task groups when component mounts or dependencies change
+ useEffect(() => {
+ if (projectId) {
+ dispatch(fetchTaskGroups(projectId));
}
+ }, [dispatch, projectId, groupBy, search, archived]);
+ // Memoized calculations
+ const allTaskIds = useMemo(() => {
+ return taskGroups.flatMap(group => group.tasks.map(task => task.id!));
+ }, [taskGroups]);
+
+ const totalTasksCount = useMemo(() => {
+ return taskGroups.reduce((sum, group) => sum + group.tasks.length, 0);
+ }, [taskGroups]);
+
+ const hasSelection = selectedTaskIds.length > 0;
+
+ // // Handlers
+ // const handleGroupingChange = (newGroupBy: IGroupBy) => {
+ // dispatch(setGroup(newGroupBy));
+ // };
+
+ const handleDragStart = (event: DragStartEvent) => {
+ const { active } = event;
+ const taskId = active.id as string;
+ setActiveTaskId(taskId);
+ setOverId(null);
+ // Find the task and its group
+ let activeTask: IProjectTask | null = null;
+ let activeGroupId: string | null = null;
+ for (const group of taskGroups) {
+ const task = group.tasks.find(t => t.id === taskId);
+ if (task) {
+ activeTask = task;
+ activeGroupId = group.id;
+ break;
+ }
+ }
+ setDragState({
+ activeTask,
+ activeGroupId,
+ });
+ };
+
+ const handleDragOver = (event: DragOverEvent) => {
+ setOverId((event.over?.id as string) || null);
+ };
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event;
+ setActiveTaskId(null);
+ setOverId(null);
+ setDragState({
+ activeTask: null,
+ activeGroupId: null,
+ });
+ if (!over || !dragState.activeTask || !dragState.activeGroupId) {
+ return;
+ }
+ const activeTaskId = active.id as string;
+ const overIdVal = over.id as string;
+ // Find the group and index for drop
+ let targetGroupId = overIdVal;
+ let targetIndex = -1;
+ let isOverTask = false;
+ // Check if over is a group or a task
+ const overGroup = taskGroups.find(g => g.id === overIdVal);
+ if (!overGroup) {
+ // Dropping on a task, find which group it belongs to
+ for (const group of taskGroups) {
+ const taskIndex = group.tasks.findIndex(t => t.id === overIdVal);
+ if (taskIndex !== -1) {
+ targetGroupId = group.id;
+ targetIndex = taskIndex;
+ isOverTask = true;
+ break;
+ }
+ }
+ }
+ const sourceGroup = taskGroups.find(g => g.id === dragState.activeGroupId);
+ const targetGroup = taskGroups.find(g => g.id === targetGroupId);
+ if (!sourceGroup || !targetGroup) return;
+ const sourceIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
+ if (sourceIndex === -1) return;
+ // Calculate new positions
+ let finalTargetIndex = targetIndex;
+ if (!isOverTask || finalTargetIndex === -1) {
+ finalTargetIndex = targetGroup.tasks.length;
+ }
+ // If moving within the same group and after itself, adjust index
+ if (sourceGroup.id === targetGroup.id && sourceIndex < finalTargetIndex) {
+ finalTargetIndex--;
+ }
+ // Create updated task arrays
+ const updatedSourceTasks = [...sourceGroup.tasks];
+ const [movedTask] = updatedSourceTasks.splice(sourceIndex, 1);
+ let updatedTargetTasks: IProjectTask[];
+ if (sourceGroup.id === targetGroup.id) {
+ updatedTargetTasks = updatedSourceTasks;
+ updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
+ } else {
+ updatedTargetTasks = [...targetGroup.tasks];
+ updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
+ }
+ // Dispatch the reorder action
+ dispatch(
+ reorderTasks({
+ activeGroupId: sourceGroup.id,
+ overGroupId: targetGroup.id,
+ fromIndex: sourceIndex,
+ toIndex: finalTargetIndex,
+ task: movedTask,
+ updatedSourceTasks,
+ updatedTargetTasks,
+ })
+ );
+ };
+
+ const handleSelectTask = (taskId: string, selected: boolean) => {
+ setSelectedTaskIds(prev => {
+ if (selected) {
+ return [...prev, taskId];
+ } else {
+ return prev.filter(id => id !== taskId);
+ }
+ });
+ };
+
+ const handleToggleSubtasks = (taskId: string) => {
+ // Implementation for toggling subtasks
+ console.log('Toggle subtasks for task:', taskId);
+ };
+
+ if (error) {
return (
-
- {/* Task Filters */}
-
- Loading filters...
}>
-
-
-
+
+
+
+ );
+ }
+ return (
+
+ {/* Task Filters */}
+
+ Loading filters...
}>
+
+
+
- {/* Task Groups Container */}
-
- {loadingGroups ? (
-
-
-
-
-
- ) : taskGroups.length === 0 ? (
-
-
-
- ) : (
-
- g.id)}
- strategy={horizontalListSortingStrategy}
- >
-
- {taskGroups.map((group) => (
-
- ))}
-
-
-
- {dragState.activeTask ? (
-
- ) : null}
-
-
- )}
+ {/* Task Groups Container */}
+
+ {loadingGroups ? (
+
+
+
+
+ ) : taskGroups.length === 0 ? (
+
+
+
+ ) : (
+
+ g.id)}
+ strategy={horizontalListSortingStrategy}
+ >
+
+ {taskGroups.map(group => (
+
+ ))}
+
+
+
+ {dragState.activeTask ? (
+
+ ) : null}
+
+
+ )}
+
-
-
- );
+
+ );
};
-export default KanbanTaskListBoard;
\ No newline at end of file
+export default KanbanTaskListBoard;
diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/invitation-item.tsx b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/invitation-item.tsx
index ba7e6cbe..894b722f 100644
--- a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/invitation-item.tsx
+++ b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/invitation-item.tsx
@@ -83,7 +83,10 @@ const InvitationItem: React.FC = ({ item, isUnreadNotificat
You have been invited to work with {item.team_name}.
{isUnreadNotifications && (
-
+
acceptInvite(true)}
disabled={inProgress()}
diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notfication-drawer.tsx b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notfication-drawer.tsx
index daa46c6b..4f6d7201 100644
--- a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notfication-drawer.tsx
+++ b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notfication-drawer.tsx
@@ -164,15 +164,15 @@ const NotificationDrawer = () => {
await handleVerifyAuth();
}
if (notification.project && notification.task_id) {
- navigate(`${notification.url}${toQueryString({task: notification.params?.task, tab: notification.params?.tab})}`);
+ navigate(
+ `${notification.url}${toQueryString({ task: notification.params?.task, tab: notification.params?.tab })}`
+ );
}
-
} catch (error) {
console.error('Error navigating to URL:', error);
} finally {
setIsLoading(false);
}
-
}
};
diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-template.tsx b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-template.tsx
index 1dee76ef..17b7fe91 100644
--- a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-template.tsx
+++ b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-template.tsx
@@ -27,9 +27,9 @@ const NotificationTemplate: React.FC = ({
const goToUrl = async (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
-
+
console.log('goToUrl triggered', { url: item.url, teamId: item.team_id });
-
+
if (item.url) {
dispatch(toggleDrawer());
@@ -92,4 +92,4 @@ const NotificationTemplate: React.FC = ({
);
};
-export default NotificationTemplate;
\ No newline at end of file
+export default NotificationTemplate;
diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.css b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.css
index 3df65b2b..5f73f54a 100644
--- a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.css
+++ b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.css
@@ -4,4 +4,4 @@
.notification-content.clickable:hover {
background-color: rgba(0, 0, 0, 0.02);
-}
\ No newline at end of file
+}
diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.tsx b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.tsx
index bf22936a..05575285 100644
--- a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.tsx
+++ b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.tsx
@@ -5,7 +5,11 @@ import { toQueryString } from '@/utils/toQueryString';
import { BankOutlined } from '@ant-design/icons';
import './push-notification-template.css';
-const PushNotificationTemplate = ({ notification: notificationData }: { notification: IWorklenzNotification }) => {
+const PushNotificationTemplate = ({
+ notification: notificationData,
+}: {
+ notification: IWorklenzNotification;
+}) => {
const handleClick = async () => {
if (notificationData.url) {
let url = notificationData.url;
@@ -23,23 +27,25 @@ const PushNotificationTemplate = ({ notification: notificationData }: { notifica
};
return (
-
-
+
{notificationData.team && (
<>
@@ -48,14 +54,14 @@ const PushNotificationTemplate = ({ notification: notificationData }: { notifica
)}
{!notificationData.team && 'Worklenz'}
-
);
@@ -66,10 +72,10 @@ let isProcessing = false;
const processNotificationQueue = () => {
if (isProcessing || notificationQueue.length === 0) return;
-
+
isProcessing = true;
const notificationData = notificationQueue.shift();
-
+
if (notificationData) {
notification.info({
message: null,
@@ -81,12 +87,12 @@ const processNotificationQueue = () => {
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
padding: '12px 16px',
minWidth: '300px',
- maxWidth: '400px'
+ maxWidth: '400px',
},
onClose: () => {
isProcessing = false;
processNotificationQueue();
- }
+ },
});
} else {
isProcessing = false;
diff --git a/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx b/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx
index 2deafb5d..1e919118 100644
--- a/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx
+++ b/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx
@@ -1,48 +1,48 @@
import React, { useMemo } from 'react';
-import {
- Card,
- Col,
- Empty,
- Row,
- Skeleton,
- Typography,
- Progress,
- Tooltip,
- Badge,
+import {
+ Card,
+ Col,
+ Empty,
+ Row,
+ Skeleton,
+ Typography,
+ Progress,
+ Tooltip,
+ Badge,
Space,
Avatar,
theme,
- Divider
+ Divider,
} from 'antd';
-import {
- ClockCircleOutlined,
- TeamOutlined,
+import {
+ ClockCircleOutlined,
+ TeamOutlined,
CheckCircleOutlined,
ProjectOutlined,
UserOutlined,
SettingOutlined,
InboxOutlined,
- MoreOutlined
+ MoreOutlined,
} from '@ant-design/icons';
import { ProjectGroupListProps } from '@/types/project/project.types';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { themeWiseColor } from '@/utils/themeWiseColor';
-import {
- fetchProjectData,
- setProjectId,
- toggleProjectDrawer
+import {
+ fetchProjectData,
+ setProjectId,
+ toggleProjectDrawer,
} from '@/features/project/project-drawer.slice';
-import {
- toggleArchiveProject,
- toggleArchiveProjectForAll
+import {
+ toggleArchiveProject,
+ toggleArchiveProjectForAll,
} from '@/features/projects/projectsSlice';
import { useAuthService } from '@/hooks/useAuth';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
-import {
- evt_projects_settings_click,
- evt_projects_archive,
- evt_projects_archive_all
+import {
+ evt_projects_settings_click,
+ evt_projects_archive,
+ evt_projects_archive_all,
} from '@/shared/worklenz-analytics-events';
import logger from '@/utils/errorLogger';
@@ -53,7 +53,7 @@ const ProjectGroupList: React.FC
= ({
navigate,
onProjectSelect,
loading,
- t
+ t,
}) => {
// Preload project view components on hover for smoother navigation
const handleProjectHover = React.useCallback((project_id: string) => {
@@ -62,7 +62,7 @@ const ProjectGroupList: React.FC = ({
import('@/pages/projects/projectView/project-view').catch(() => {
// Silently fail if preload doesn't work
});
-
+
// Also preload critical task management components
import('@/components/task-management/task-list-board').catch(() => {
// Silently fail if preload doesn't work
@@ -83,16 +83,16 @@ const ProjectGroupList: React.FC = ({
// Enhanced color processing for better contrast
const processColor = (color: string | undefined, fallback?: string) => {
if (!color) return fallback || token.colorPrimary;
-
+
if (color.startsWith('#')) {
if (themeMode === 'dark') {
const hex = color.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
-
+
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
-
+
if (brightness < 100) {
const factor = 1.5;
const newR = Math.min(255, Math.floor(r * factor));
@@ -105,9 +105,9 @@ const ProjectGroupList: React.FC = ({
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
-
+
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
-
+
if (brightness > 200) {
const factor = 0.7;
const newR = Math.floor(r * factor);
@@ -117,7 +117,7 @@ const ProjectGroupList: React.FC = ({
}
}
}
-
+
return color;
};
@@ -130,7 +130,11 @@ const ProjectGroupList: React.FC = ({
dispatch(toggleProjectDrawer());
};
- const handleArchiveClick = async (e: React.MouseEvent, projectId: string, isArchived: boolean) => {
+ const handleArchiveClick = async (
+ e: React.MouseEvent,
+ projectId: string,
+ isArchived: boolean
+ ) => {
e.stopPropagation();
try {
if (isOwnerOrAdmin) {
@@ -146,184 +150,187 @@ const ProjectGroupList: React.FC = ({
};
// Memoized styles for better performance
- const styles = useMemo(() => ({
- container: {
- padding: '0',
- background: 'transparent',
- },
- groupSection: {
- marginBottom: '24px',
- background: 'transparent',
- },
- groupHeader: {
- background: getThemeAwareColor(
- `linear-gradient(135deg, ${token.colorFillAlter} 0%, ${token.colorFillTertiary} 100%)`,
- `linear-gradient(135deg, ${token.colorFillQuaternary} 0%, ${token.colorFillSecondary} 100%)`
- ),
- borderRadius: token.borderRadius,
- padding: '12px 16px',
- marginBottom: '12px',
- border: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
- boxShadow: getThemeAwareColor(
- '0 1px 4px rgba(0, 0, 0, 0.06)',
- '0 1px 4px rgba(0, 0, 0, 0.15)'
- ),
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
- },
- groupTitle: {
- margin: 0,
- color: getThemeAwareColor(token.colorText, token.colorTextBase),
- fontSize: '16px',
- fontWeight: 600,
- letterSpacing: '-0.01em',
- },
- groupMeta: {
- color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
- fontSize: '12px',
- marginTop: '2px',
- },
- projectCard: {
- height: '100%',
- borderRadius: token.borderRadius,
- border: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
- boxShadow: getThemeAwareColor(
- '0 1px 4px rgba(0, 0, 0, 0.04)',
- '0 1px 4px rgba(0, 0, 0, 0.12)'
- ),
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
- cursor: 'pointer',
- overflow: 'hidden',
- background: getThemeAwareColor(token.colorBgContainer, token.colorBgElevated),
- },
- projectCardHover: {
- transform: 'translateY(-2px)',
- boxShadow: getThemeAwareColor(
- '0 4px 12px rgba(0, 0, 0, 0.08)',
- '0 4px 12px rgba(0, 0, 0, 0.20)'
- ),
- borderColor: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
- },
- statusBar: {
- height: '3px',
- background: 'linear-gradient(90deg, transparent 0%, currentColor 100%)',
- borderRadius: '0 0 2px 2px',
- },
- projectContent: {
- padding: '12px',
- height: '100%',
- display: 'flex',
- flexDirection: 'column' as const,
- minHeight: '200px', // Ensure minimum height for consistent card sizes
- },
- projectTitle: {
- margin: '0 0 6px 0',
- color: getThemeAwareColor(token.colorText, token.colorTextBase),
- fontSize: '14px',
- fontWeight: 600,
- lineHeight: 1.3,
- },
- clientName: {
- color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
- fontSize: '12px',
- marginBottom: '8px',
- display: 'flex',
- alignItems: 'center',
- gap: '4px',
- },
- progressSection: {
- marginBottom: '10px',
- // Remove flex: 1 to prevent it from taking all available space
- },
- progressLabel: {
- fontSize: '10px',
- color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary),
- marginBottom: '4px',
- fontWeight: 500,
- textTransform: 'uppercase' as const,
- letterSpacing: '0.3px',
- },
- metaGrid: {
- display: 'grid',
- gridTemplateColumns: 'repeat(2, 1fr)',
- gap: '8px',
- marginTop: 'auto', // This pushes the meta section to the bottom
- paddingTop: '10px',
- borderTop: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
- flexShrink: 0, // Prevent the meta section from shrinking
- },
- metaItem: {
- display: 'flex',
- flexDirection: 'row' as const,
- alignItems: 'center',
- gap: '8px',
- padding: '8px 12px',
- borderRadius: token.borderRadiusSM,
- background: getThemeAwareColor(token.colorFillAlter, token.colorFillQuaternary),
- transition: 'all 0.2s ease',
- },
- metaContent: {
- display: 'flex',
- flexDirection: 'column' as const,
- gap: '1px',
- flex: 1,
- },
- metaIcon: {
- fontSize: '12px',
- color: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
- },
- metaValue: {
- fontSize: '11px',
- fontWeight: 600,
- color: getThemeAwareColor(token.colorText, token.colorTextBase),
- lineHeight: 1,
- },
- metaLabel: {
- fontSize: '9px',
- color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary),
- lineHeight: 1,
- textTransform: 'uppercase' as const,
- letterSpacing: '0.2px',
- },
- actionButtons: {
- position: 'absolute' as const,
- top: '8px',
- right: '8px',
- display: 'flex',
- gap: '4px',
- opacity: 0,
- transition: 'opacity 0.2s ease',
- },
- actionButton: {
- width: '24px',
- height: '24px',
- borderRadius: '4px',
- border: 'none',
- background: getThemeAwareColor('rgba(255,255,255,0.9)', 'rgba(0,0,0,0.7)'),
- color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
- cursor: 'pointer',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- fontSize: '12px',
- transition: 'all 0.2s ease',
- backdropFilter: 'blur(4px)',
- '&:hover': {
- background: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
- color: getThemeAwareColor('#fff', token.colorTextLightSolid),
- transform: 'scale(1.1)',
- }
- },
- emptyState: {
- padding: '60px 20px',
- textAlign: 'center' as const,
- background: getThemeAwareColor(token.colorFillAlter, token.colorFillQuaternary),
- borderRadius: token.borderRadiusLG,
- border: `2px dashed ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
- },
- loadingContainer: {
- padding: '40px 20px',
- }
- }), [token, themeMode, getThemeAwareColor]);
+ const styles = useMemo(
+ () => ({
+ container: {
+ padding: '0',
+ background: 'transparent',
+ },
+ groupSection: {
+ marginBottom: '24px',
+ background: 'transparent',
+ },
+ groupHeader: {
+ background: getThemeAwareColor(
+ `linear-gradient(135deg, ${token.colorFillAlter} 0%, ${token.colorFillTertiary} 100%)`,
+ `linear-gradient(135deg, ${token.colorFillQuaternary} 0%, ${token.colorFillSecondary} 100%)`
+ ),
+ borderRadius: token.borderRadius,
+ padding: '12px 16px',
+ marginBottom: '12px',
+ border: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
+ boxShadow: getThemeAwareColor(
+ '0 1px 4px rgba(0, 0, 0, 0.06)',
+ '0 1px 4px rgba(0, 0, 0, 0.15)'
+ ),
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
+ },
+ groupTitle: {
+ margin: 0,
+ color: getThemeAwareColor(token.colorText, token.colorTextBase),
+ fontSize: '16px',
+ fontWeight: 600,
+ letterSpacing: '-0.01em',
+ },
+ groupMeta: {
+ color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
+ fontSize: '12px',
+ marginTop: '2px',
+ },
+ projectCard: {
+ height: '100%',
+ borderRadius: token.borderRadius,
+ border: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
+ boxShadow: getThemeAwareColor(
+ '0 1px 4px rgba(0, 0, 0, 0.04)',
+ '0 1px 4px rgba(0, 0, 0, 0.12)'
+ ),
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
+ cursor: 'pointer',
+ overflow: 'hidden',
+ background: getThemeAwareColor(token.colorBgContainer, token.colorBgElevated),
+ },
+ projectCardHover: {
+ transform: 'translateY(-2px)',
+ boxShadow: getThemeAwareColor(
+ '0 4px 12px rgba(0, 0, 0, 0.08)',
+ '0 4px 12px rgba(0, 0, 0, 0.20)'
+ ),
+ borderColor: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
+ },
+ statusBar: {
+ height: '3px',
+ background: 'linear-gradient(90deg, transparent 0%, currentColor 100%)',
+ borderRadius: '0 0 2px 2px',
+ },
+ projectContent: {
+ padding: '12px',
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column' as const,
+ minHeight: '200px', // Ensure minimum height for consistent card sizes
+ },
+ projectTitle: {
+ margin: '0 0 6px 0',
+ color: getThemeAwareColor(token.colorText, token.colorTextBase),
+ fontSize: '14px',
+ fontWeight: 600,
+ lineHeight: 1.3,
+ },
+ clientName: {
+ color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
+ fontSize: '12px',
+ marginBottom: '8px',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '4px',
+ },
+ progressSection: {
+ marginBottom: '10px',
+ // Remove flex: 1 to prevent it from taking all available space
+ },
+ progressLabel: {
+ fontSize: '10px',
+ color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary),
+ marginBottom: '4px',
+ fontWeight: 500,
+ textTransform: 'uppercase' as const,
+ letterSpacing: '0.3px',
+ },
+ metaGrid: {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(2, 1fr)',
+ gap: '8px',
+ marginTop: 'auto', // This pushes the meta section to the bottom
+ paddingTop: '10px',
+ borderTop: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
+ flexShrink: 0, // Prevent the meta section from shrinking
+ },
+ metaItem: {
+ display: 'flex',
+ flexDirection: 'row' as const,
+ alignItems: 'center',
+ gap: '8px',
+ padding: '8px 12px',
+ borderRadius: token.borderRadiusSM,
+ background: getThemeAwareColor(token.colorFillAlter, token.colorFillQuaternary),
+ transition: 'all 0.2s ease',
+ },
+ metaContent: {
+ display: 'flex',
+ flexDirection: 'column' as const,
+ gap: '1px',
+ flex: 1,
+ },
+ metaIcon: {
+ fontSize: '12px',
+ color: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
+ },
+ metaValue: {
+ fontSize: '11px',
+ fontWeight: 600,
+ color: getThemeAwareColor(token.colorText, token.colorTextBase),
+ lineHeight: 1,
+ },
+ metaLabel: {
+ fontSize: '9px',
+ color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary),
+ lineHeight: 1,
+ textTransform: 'uppercase' as const,
+ letterSpacing: '0.2px',
+ },
+ actionButtons: {
+ position: 'absolute' as const,
+ top: '8px',
+ right: '8px',
+ display: 'flex',
+ gap: '4px',
+ opacity: 0,
+ transition: 'opacity 0.2s ease',
+ },
+ actionButton: {
+ width: '24px',
+ height: '24px',
+ borderRadius: '4px',
+ border: 'none',
+ background: getThemeAwareColor('rgba(255,255,255,0.9)', 'rgba(0,0,0,0.7)'),
+ color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
+ cursor: 'pointer',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ fontSize: '12px',
+ transition: 'all 0.2s ease',
+ backdropFilter: 'blur(4px)',
+ '&:hover': {
+ background: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
+ color: getThemeAwareColor('#fff', token.colorTextLightSolid),
+ transform: 'scale(1.1)',
+ },
+ },
+ emptyState: {
+ padding: '60px 20px',
+ textAlign: 'center' as const,
+ background: getThemeAwareColor(token.colorFillAlter, token.colorFillQuaternary),
+ borderRadius: token.borderRadiusLG,
+ border: `2px dashed ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
+ },
+ loadingContainer: {
+ padding: '40px 20px',
+ },
+ }),
+ [token, themeMode, getThemeAwareColor]
+ );
// Early return for loading state
if (loading) {
@@ -338,7 +345,7 @@ const ProjectGroupList: React.FC = ({
if (groups.length === 0) {
return (
-
}
description={
@@ -356,19 +363,19 @@ const ProjectGroupList: React.FC
= ({
);
}
- const renderProjectCard = (project: any) => {
- const projectColor = processColor(project.color_code, token.colorPrimary);
- const statusColor = processColor(project.status_color, token.colorPrimary);
- const progress = project.progress || 0;
- const completedTasks = project.completed_tasks_count || 0;
- const totalTasks = project.all_tasks_count || 0;
- const membersCount = project.members_count || 0;
+ const renderProjectCard = (project: any) => {
+ const projectColor = processColor(project.color_code, token.colorPrimary);
+ const statusColor = processColor(project.status_color, token.colorPrimary);
+ const progress = project.progress || 0;
+ const completedTasks = project.completed_tasks_count || 0;
+ const totalTasks = project.all_tasks_count || 0;
+ const membersCount = project.members_count || 0;
return (
{
+ onMouseEnter={e => {
Object.assign(e.currentTarget.style, styles.projectCardHover);
const actionButtons = e.currentTarget.querySelector('.action-buttons') as HTMLElement;
if (actionButtons) {
@@ -377,7 +384,7 @@ const ProjectGroupList: React.FC = ({
// Preload components for smoother navigation
handleProjectHover(project.id);
}}
- onMouseLeave={(e) => {
+ onMouseLeave={e => {
Object.assign(e.currentTarget.style, styles.projectCard);
const actionButtons = e.currentTarget.querySelector('.action-buttons') as HTMLElement;
if (actionButtons) {
@@ -392,15 +399,15 @@ const ProjectGroupList: React.FC = ({
handleSettingsClick(e, project.id)}
- onMouseEnter={(e) => {
+ onClick={e => handleSettingsClick(e, project.id)}
+ onMouseEnter={e => {
Object.assign(e.currentTarget.style, {
background: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
color: getThemeAwareColor('#fff', token.colorTextLightSolid),
transform: 'scale(1.1)',
});
}}
- onMouseLeave={(e) => {
+ onMouseLeave={e => {
Object.assign(e.currentTarget.style, {
background: getThemeAwareColor('rgba(255,255,255,0.9)', 'rgba(0,0,0,0.7)'),
color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
@@ -414,15 +421,15 @@ const ProjectGroupList: React.FC = ({
handleArchiveClick(e, project.id, project.archived)}
- onMouseEnter={(e) => {
+ onClick={e => handleArchiveClick(e, project.id, project.archived)}
+ onMouseEnter={e => {
Object.assign(e.currentTarget.style, {
background: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
color: getThemeAwareColor('#fff', token.colorTextLightSolid),
transform: 'scale(1.1)',
});
}}
- onMouseLeave={(e) => {
+ onMouseLeave={e => {
Object.assign(e.currentTarget.style, {
background: getThemeAwareColor('rgba(255,255,255,0.9)', 'rgba(0,0,0,0.7)'),
color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
@@ -434,20 +441,24 @@ const ProjectGroupList: React.FC = ({
- {/* Project color indicator bar */}
-
-
+ {/* Project color indicator bar */}
+
+
{/* Project title */}
-
+
{project.name}
-
+
{/* Client name */}
{project.client_name && (
@@ -457,45 +468,47 @@ const ProjectGroupList: React.FC
= ({
)}
-
+
{/* Progress section */}
-
- Progress
-
-
-
- {progress}%
-
+
Progress
+
+
+ {progress}%
+
-
+
{/* Meta information grid */}
- {completedTasks}/{totalTasks}
+
+ {completedTasks}/{totalTasks}
+
Tasks
-
+
@@ -521,14 +534,16 @@ const ProjectGroupList: React.FC
= ({
{group.groupColor && (
-
+
)}
@@ -539,10 +554,10 @@ const ProjectGroupList: React.FC = ({
-
- = ({
height: '24px',
lineHeight: '22px',
borderRadius: '12px',
- border: `2px solid ${getThemeAwareColor(token.colorBgContainer, token.colorBgElevated)}`
+ border: `2px solid ${getThemeAwareColor(token.colorBgContainer, token.colorBgElevated)}`,
}}
/>
-
+
{/* Projects grid */}
-
- {group.projects.map(renderProjectCard)}
-
-
+ {group.projects.map(renderProjectCard)}
+
{/* Add spacing between groups except for the last one */}
{groupIndex < groups.length - 1 && (
-
+
)}
))}
@@ -576,4 +591,4 @@ const ProjectGroupList: React.FC
= ({
);
};
-export default ProjectGroupList;
\ No newline at end of file
+export default ProjectGroupList;
diff --git a/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx b/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx
index 3d2222d0..57c14e36 100644
--- a/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx
+++ b/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx
@@ -1,6 +1,10 @@
import { useGetProjectsQuery } from '@/api/projects/projects.v1.api.service';
import { AppDispatch } from '@/app/store';
-import { fetchProjectData, setProjectId, toggleProjectDrawer } from '@/features/project/project-drawer.slice';
+import {
+ fetchProjectData,
+ setProjectId,
+ toggleProjectDrawer,
+} from '@/features/project/project-drawer.slice';
import {
toggleArchiveProjectForAll,
toggleArchiveProject,
@@ -12,7 +16,11 @@ import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import logger from '@/utils/errorLogger';
import { SettingOutlined, InboxOutlined } from '@ant-design/icons';
import { Tooltip, Button, Popconfirm, Space } from 'antd';
-import { evt_projects_archive, evt_projects_archive_all, evt_projects_settings_click } from '@/shared/worklenz-analytics-events';
+import {
+ evt_projects_archive,
+ evt_projects_archive_all,
+ evt_projects_settings_click,
+} from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
interface ActionButtonsProps {
@@ -71,7 +79,9 @@ export const ActionButtons: React.FC = ({
icon={}
/>
-
+
= ({ record, t }) => {
if (!record.category_name) return '-';
-
- const { requestParams } = useAppSelector(
- state => state.projectsReducer
- );
+
+ const { requestParams } = useAppSelector(state => state.projectsReducer);
const dispatch = useAppDispatch();
const newParams: Partial = {};
const filterByCategory = (categoryId: string | undefined) => {
diff --git a/worklenz-frontend/src/components/project-list/project-list-table/project-list-favorite/project-rate-cell.tsx b/worklenz-frontend/src/components/project-list/project-list-table/project-list-favorite/project-rate-cell.tsx
index 51858075..c7e9ff89 100644
--- a/worklenz-frontend/src/components/project-list/project-list-table/project-list-favorite/project-rate-cell.tsx
+++ b/worklenz-frontend/src/components/project-list/project-list-table/project-list-favorite/project-rate-cell.tsx
@@ -37,7 +37,8 @@ export const ProjectRateCell: React.FC<{
);
useEffect(() => {
- setIsFavorite(record.favorite);}, [record.favorite]);
+ setIsFavorite(record.favorite);
+ }, [record.favorite]);
return (
@@ -48,7 +49,7 @@ export const ProjectRateCell: React.FC<{
style={{ backgroundColor: colors.transparent }}
shape="circle"
icon={}
- onClick={(e) => {
+ onClick={e => {
e.stopPropagation();
handleFavorite();
}}
@@ -56,4 +57,4 @@ export const ProjectRateCell: React.FC<{
);
-};
\ No newline at end of file
+};
diff --git a/worklenz-frontend/src/components/project-task-filters/delete-status-drawer/delete-status-drawer.tsx b/worklenz-frontend/src/components/project-task-filters/delete-status-drawer/delete-status-drawer.tsx
index 48a5a635..d66439e5 100644
--- a/worklenz-frontend/src/components/project-task-filters/delete-status-drawer/delete-status-drawer.tsx
+++ b/worklenz-frontend/src/components/project-task-filters/delete-status-drawer/delete-status-drawer.tsx
@@ -12,10 +12,7 @@ import { deleteStatusToggleDrawer } from '@/features/projects/status/DeleteStatu
import { Drawer, Alert, Card, Select, Button, Typography, Badge } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import { useSelector } from 'react-redux';
-import {
- deleteSection,
- IGroupBy,
-} from '@features/board/board-slice';
+import { deleteSection, IGroupBy } from '@features/board/board-slice';
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service';
import logger from '@/utils/errorLogger';
@@ -24,124 +21,123 @@ const { Title, Text } = Typography;
const { Option } = Select;
const DeleteStatusDrawer: React.FC = () => {
- const [currentStatus, setCurrentStatus] = useState('');
- const [deletingStatus, setDeletingStatus] = useState(false);
- const dispatch = useAppDispatch();
- const { trackMixpanelEvent } = useMixpanelTracking();
- const { projectView } = useTabSearchParam();
- const [form] = Form.useForm();
- const { t } = useTranslation('task-list-filters');
- const { editableSectionId, groupBy } = useAppSelector(state => state.boardReducer);
- const isDelteStatusDrawerOpen = useAppSelector(
- state => state.deleteStatusReducer.isDeleteStatusDrawerOpen
- );
- const { isDeleteStatusDrawerOpen, status: selectedForDelete } = useAppSelector(
- (state) => state.deleteStatusReducer
- );
- const { status, statusCategories } = useAppSelector(state => state.taskStatusReducer);
- const { projectId } = useAppSelector(state => state.projectReducer);
- const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const [currentStatus, setCurrentStatus] = useState('');
+ const [deletingStatus, setDeletingStatus] = useState(false);
+ const dispatch = useAppDispatch();
+ const { trackMixpanelEvent } = useMixpanelTracking();
+ const { projectView } = useTabSearchParam();
+ const [form] = Form.useForm();
+ const { t } = useTranslation('task-list-filters');
+ const { editableSectionId, groupBy } = useAppSelector(state => state.boardReducer);
+ const isDelteStatusDrawerOpen = useAppSelector(
+ state => state.deleteStatusReducer.isDeleteStatusDrawerOpen
+ );
+ const { isDeleteStatusDrawerOpen, status: selectedForDelete } = useAppSelector(
+ state => state.deleteStatusReducer
+ );
+ const { status, statusCategories } = useAppSelector(state => state.taskStatusReducer);
+ const { projectId } = useAppSelector(state => state.projectReducer);
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
- const refreshTasks = useCallback(() => {
- if (!projectId) return;
- const fetchAction = projectView === 'list' ? fetchTaskGroups : fetchEnhancedKanbanGroups;
- dispatch(fetchAction(projectId) as any);
- }, [projectId, projectView, dispatch]);
+ const refreshTasks = useCallback(() => {
+ if (!projectId) return;
+ const fetchAction = projectView === 'list' ? fetchTaskGroups : fetchEnhancedKanbanGroups;
+ dispatch(fetchAction(projectId) as any);
+ }, [projectId, projectView, dispatch]);
- const handleDrawerOpenChange = () => {
- if (status.length === 0) {
- dispatch(fetchStatusesCategories());
+ const handleDrawerOpenChange = () => {
+ if (status.length === 0) {
+ dispatch(fetchStatusesCategories());
+ }
+ };
+
+ const setReplacingStatus = (value: string) => {
+ setCurrentStatus(value);
+ };
+ const moveAndDelete = async () => {
+ const groupId = selectedForDelete?.id;
+ if (!projectId || !currentStatus || !groupId) return;
+ setDeletingStatus(true);
+ try {
+ if (groupBy === IGroupBy.STATUS) {
+ const replacingStatusId = currentStatus;
+ if (!replacingStatusId) return;
+ const res = await statusApiService.deleteStatus(groupId, projectId, replacingStatusId);
+ if (res.done) {
+ dispatch(deleteSection({ sectionId: groupId }));
+ dispatch(deleteStatusToggleDrawer());
+ dispatch(fetchStatuses(projectId));
+ refreshTasks();
+ dispatch(fetchStatusesCategories());
+ } else {
+ console.error('Error deleting status', res);
}
- };
-
- const setReplacingStatus = (value: string) => {
- setCurrentStatus(value);
- };
- const moveAndDelete = async () => {
- const groupId = selectedForDelete?.id;
- if (!projectId || !currentStatus || !groupId) return;
- setDeletingStatus(true);
- try {
- if (groupBy === IGroupBy.STATUS) {
- const replacingStatusId = currentStatus;
- if (!replacingStatusId) return;
- const res = await statusApiService.deleteStatus(groupId, projectId, replacingStatusId);
- if (res.done) {
- dispatch(deleteSection({ sectionId: groupId }));
- dispatch(deleteStatusToggleDrawer());
- dispatch(fetchStatuses(projectId));
- refreshTasks();
- dispatch(fetchStatusesCategories());
- } else {
- console.error('Error deleting status', res);
- }
- } else if (groupBy === IGroupBy.PHASE) {
- const res = await phasesApiService.deletePhaseOption(groupId, projectId);
- if (res.done) {
- dispatch(deleteSection({ sectionId: groupId }));
- }
- }
-
- } catch (error) {
- logger.error('Error deleting section', error);
- } finally {
- setDeletingStatus(false);
+ } else if (groupBy === IGroupBy.PHASE) {
+ const res = await phasesApiService.deletePhaseOption(groupId, projectId);
+ if (res.done) {
+ dispatch(deleteSection({ sectionId: groupId }));
}
- };
- useEffect(() => {
- setCurrentStatus(status[0]?.id || '');
- }, [isDelteStatusDrawerOpen]);
+ }
+ } catch (error) {
+ logger.error('Error deleting section', error);
+ } finally {
+ setDeletingStatus(false);
+ }
+ };
+ useEffect(() => {
+ setCurrentStatus(status[0]?.id || '');
+ }, [isDelteStatusDrawerOpen]);
- return (
- dispatch(deleteStatusToggleDrawer())}
- open={isDelteStatusDrawerOpen}
- afterOpenChange={handleDrawerOpenChange}
+ return (
+ dispatch(deleteStatusToggleDrawer())}
+ open={isDelteStatusDrawerOpen}
+ afterOpenChange={handleDrawerOpenChange}
+ >
+
+
+
+ {selectedForDelete?.name}
+
+
+
+
+
- );
+ Done
+
+
+
+ );
};
-export default DeleteStatusDrawer;
\ No newline at end of file
+export default DeleteStatusDrawer;
diff --git a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx
index d5ed3660..34b317a4 100644
--- a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx
+++ b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx
@@ -37,7 +37,7 @@ const ColumnConfigurationModal: React.FC = ({
}, [currentConfig, open]);
const handleToggleColumn = (key: string) => {
- const newConfig = config.map(col =>
+ const newConfig = config.map(col =>
col.key === key ? { ...col, showInDropdown: !col.showInDropdown } : col
);
setConfig(newConfig);
@@ -76,14 +76,17 @@ const ColumnConfigurationModal: React.FC = ({
setHasChanges(false);
};
- const groupedColumns = config.reduce((groups, column) => {
- const category = column.category || 'other';
- if (!groups[category]) {
- groups[category] = [];
- }
- groups[category].push(column);
- return groups;
- }, {} as Record);
+ const groupedColumns = config.reduce(
+ (groups, column) => {
+ const category = column.category || 'other';
+ if (!groups[category]) {
+ groups[category] = [];
+ }
+ groups[category].push(column);
+ return groups;
+ },
+ {} as Record
+ );
const categoryLabels: Record = {
basic: 'Basic Information',
@@ -117,8 +120,8 @@ const ColumnConfigurationModal: React.FC = ({
>
- Configure which columns appear in the "Show Fields" dropdown and their order.
- Use the up/down arrows to reorder columns.
+ Configure which columns appear in the "Show Fields" dropdown and their order. Use the
+ up/down arrows to reorder columns.
@@ -127,7 +130,7 @@ const ColumnConfigurationModal: React.FC = ({