Merge branch 'release-v2.1.4' into feature/task-activities-by-user

This commit is contained in:
Chamika J
2025-07-29 08:46:05 +05:30
committed by GitHub
626 changed files with 17159 additions and 3505 deletions

View File

@@ -1,6 +1,6 @@
import React from 'react';
import notFoundImg from '../../assets/images/not-found-img.png';
import { Button, Flex, Layout, Typography } from 'antd';
import { Button, Flex, Layout, Typography } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
const NotFoundPage = () => {

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { AdvancedGanttDemo } from '../components/advanced-gantt';
const GanttDemoPage: React.FC = () => {
return <AdvancedGanttDemo />;
};
export default GanttDemoPage;

View File

@@ -1,5 +1,9 @@
/* Account Setup Page Styles */
/* Steps styling - using Ant Design theme tokens */
.ant-steps-item-finish .ant-steps-item-icon {
border-color: #1890ff !important;
border-color: var(--ant-color-primary) !important;
background-color: var(--ant-color-primary) !important;
}
.ant-steps-item-icon {
@@ -9,37 +13,53 @@
font-size: 16px !important;
line-height: 32px !important;
text-align: center !important;
border: 1px solid rgba(0, 0, 0, 0.25) !important;
border: 1px solid var(--ant-color-border) !important;
border-radius: 32px !important;
transition:
background-color 0.3s,
border-color 0.3s !important;
transition: all 0.3s !important;
background-color: var(--ant-color-bg-container) !important;
color: var(--ant-color-text) !important;
}
.ant-steps-item-wait .ant-steps-item-icon {
cursor: not-allowed;
opacity: 0.6;
}
.dark-mode .ant-steps-item-wait .ant-steps-item-icon {
cursor: not-allowed;
.ant-steps-item-process .ant-steps-item-icon {
border-color: var(--ant-color-primary) !important;
background-color: var(--ant-color-primary) !important;
color: var(--ant-color-white) !important;
}
.progress-steps .ant-steps-item.ant-steps-item-process .ant-steps-item-title::after {
background-color: #1677ff !important;
background-color: var(--ant-color-primary) !important;
width: 60px !important;
}
.ant-steps-item-title {
color: var(--ant-color-text) !important;
}
.ant-steps-item-description {
color: var(--ant-color-text-secondary) !important;
}
/* Responsive layout */
@media (max-width: 1000px) {
.progress-steps {
width: 400px !important;
}
.step {
width: 400px !important;
}
.progress-steps,
.step,
.step-content {
width: 400px !important;
width: 90% !important;
max-width: 500px !important;
}
}
@media (max-width: 768px) {
.progress-steps,
.step,
.step-content {
width: 95% !important;
max-width: 400px !important;
}
}
@@ -53,98 +73,93 @@
}
.step-content {
width: 200px !important;
width: 100% !important;
max-width: 300px !important;
padding: 0 1rem !important;
}
}
@media (max-width: 1000px) {
.organization-name-form {
width: 400px !important;
/* Tasks step specific styles */
.tasks-step .task-item-card {
transition: all 0.3s ease;
}
.tasks-step .task-item-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.tasks-step .task-input {
font-size: 16px;
}
.tasks-step .task-input:focus {
box-shadow: none !important;
}
/* Project step specific styles */
.project-step .ant-input-affix-wrapper {
transition: all 0.3s ease;
}
.project-step .ant-input-affix-wrapper:focus-within {
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
}
.project-suggestion-button {
transition: all 0.2s ease;
cursor: pointer;
}
.project-suggestion-button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.project-suggestion-button:active {
transform: translateY(0);
}
/* Organization step specific styles */
.organization-step .ant-input-affix-wrapper {
transition: all 0.3s ease;
}
.organization-step .ant-input-affix-wrapper:focus-within {
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
border-color: #1890ff;
}
.organization-suggestion-button {
transition: all 0.2s ease;
cursor: pointer;
}
.organization-suggestion-button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.organization-suggestion-button:active {
transform: translateY(0);
}
/* Survey step animations */
.survey-page-transition {
animation: fadeInUp 0.4s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 500px) {
.organization-name-form {
width: 200px !important;
}
}
.vert-text {
max-width: 40px;
background-color: #fff;
position: relative;
z-index: 99;
margin-left: auto;
margin-right: auto;
text-align: center;
justify-content: center;
margin-top: 2rem;
}
.vert-text-dark {
max-width: 40px;
background-color: #141414;
position: relative;
z-index: 99;
margin-left: auto;
margin-right: auto;
text-align: center;
justify-content: center;
margin-top: 2rem;
}
.vert-line {
position: absolute;
left: 0;
right: 0;
width: 100%;
content: "";
height: 2px;
background-color: #00000047;
bottom: 0;
top: 0;
margin-bottom: auto;
margin-top: auto;
}
.vert-line-dark {
position: absolute;
left: 0;
right: 0;
width: 100%;
content: "";
height: 2px;
background-color: white;
bottom: 0;
top: 0;
margin-bottom: auto;
margin-top: auto;
}
@media (max-width: 1000px) {
.first-project-form {
width: 400px !important;
}
}
@media (max-width: 500px) {
.first-project-form {
width: 200px !important;
}
}
.custom-close-button:hover {
background-color: transparent !important;
}
@media (max-width: 1000px) {
.create-first-task-form {
width: 400px !important;
}
}
@media (max-width: 500px) {
.create-first-task-form {
width: 200px !important;
}
}
/* Theme transitions */
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}

View File

@@ -1,14 +1,16 @@
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Space, Steps, Button, Typography } from 'antd/es';
import { Space, Steps, Button, Typography, theme, Dropdown, MenuProps } from '@/shared/antd-imports';
import { GlobalOutlined, MoonOutlined, SunOutlined } from '@/shared/antd-imports';
import logger from '@/utils/errorLogger';
import { setCurrentStep } from '@/features/account-setup/account-setup.slice';
import { setCurrentStep, setSurveySubStep } from '@/features/account-setup/account-setup.slice';
import { OrganizationStep } from '@/components/account-setup/organization-step';
import { ProjectStep } from '@/components/account-setup/project-step';
import { TasksStep } from '@/components/account-setup/tasks-step';
import { SurveyStep } from '@/components/account-setup/survey-step';
import MembersStep from '@/components/account-setup/members-step';
import {
evt_account_setup_complete,
@@ -26,34 +28,121 @@ import { validateEmail } from '@/utils/validateEmail';
import { sanitizeInput } from '@/utils/sanitizeInput';
import logo from '@/assets/images/worklenz-light-mode.png';
import logoDark from '@/assets/images/worklenz-dark-mode.png';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import './account-setup.css';
import { IAccountSetupRequest } from '@/types/project-templates/project-templates.types';
import { profileSettingsApiService } from '@/api/settings/profile/profile-settings.api.service';
import { projectTemplatesApiService } from '@/api/project-templates/project-templates.api.service';
import { surveyApiService } from '@/api/survey/survey.api.service';
import { ISurveySubmissionRequest, ISurveyAnswer } from '@/types/account-setup/survey.types';
import { setLanguage } from '@/features/i18n/localesSlice';
import { ILanguageType, Language } from '@/features/i18n/localesSlice';
import { toggleTheme } from '@/features/theme/themeSlice';
const { Title } = Typography;
// Simplified styles for form components using Ant Design theme tokens
const getAccountSetupStyles = (token: any) => ({
form: {
width: '100%',
maxWidth: '600px',
},
label: {
color: token.colorText,
fontWeight: 500,
},
buttonContainer: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: '1rem',
},
drawerFooter: {
display: 'flex',
justifyContent: 'right',
padding: '10px 16px',
},
});
const AccountSetup: React.FC = () => {
const dispatch = useDispatch();
const { t } = useTranslation('account-setup');
const dispatch = useAppDispatch();
const { t, i18n } = useTranslation('account-setup');
useDocumentTitle(t('setupYourAccount', 'Account Setup'));
const navigate = useNavigate();
const { trackMixpanelEvent } = useMixpanelTracking();
const { token } = theme.useToken();
const { currentStep, organizationName, projectName, templateId, tasks, teamMembers } =
const { currentStep, organizationName, projectName, templateId, tasks, teamMembers, surveyData, surveySubStep } =
useSelector((state: RootState) => state.accountSetupReducer);
const lng = useSelector((state: RootState) => state.localesReducer.lng);
const userDetails = getUserSession();
const themeMode = useSelector((state: RootState) => state.themeReducer.mode);
const [surveyId, setSurveyId] = React.useState<string | null>(null);
const [isSkipping, setIsSkipping] = React.useState(false);
const isDarkMode = themeMode === 'dark';
const organizationNamePlaceholder = userDetails?.name ? `e.g. ${userDetails?.name}'s Team` : '';
// Helper to extract organization name from email or fallback to user name
function getOrganizationNamePlaceholder(userDetails: { email?: string; name?: string } | null): string {
if (!userDetails) return '';
const email = userDetails.email || '';
const name = userDetails.name || '';
if (email) {
const match = email.match(/^([^@]+)@([^@]+)$/);
if (match) {
const domain = match[2].toLowerCase();
// List of common public email providers
const publicProviders = [
'gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'icloud.com', 'aol.com', 'protonmail.com', 'zoho.com', 'gmx.com', 'mail.com', 'yandex.com', 'msn.com', 'live.com', 'me.com', 'comcast.net', 'rediffmail.com', 'ymail.com', 'rocketmail.com', 'inbox.com', 'mail.ru', 'qq.com', 'naver.com', '163.com', '126.com', 'sina.com', 'yeah.net', 'googlemail.com', 'fastmail.com', 'hushmail.com', 'tutanota.com', 'pm.me', 'mailbox.org', 'proton.me'
];
if (!publicProviders.includes(domain)) {
// Use the first part of the domain (before the first dot)
const org = domain.split('.')[0];
if (org && org.length > 1) {
return `e.g. ${org.charAt(0).toUpperCase() + org.slice(1)} Team`;
}
}
}
}
// Fallback to user name
return name ? `e.g. ${name}'s Team` : '';
}
const organizationNamePlaceholder = getOrganizationNamePlaceholder(userDetails);
// Helper to extract organization name from email or fallback to user name
function getOrganizationNameInitialValue(userDetails: { email?: string; name?: string } | null): string {
if (!userDetails) return '';
const email = userDetails.email || '';
const name = userDetails.name || '';
if (email) {
const match = email.match(/^([^@]+)@([^@]+)$/);
if (match) {
const domain = match[2].toLowerCase();
const publicProviders = [
'gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'icloud.com', 'aol.com', 'protonmail.com', 'zoho.com', 'gmx.com', 'mail.com', 'yandex.com', 'msn.com', 'live.com', 'me.com', 'comcast.net', 'rediffmail.com', 'ymail.com', 'rocketmail.com', 'inbox.com', 'mail.ru', 'qq.com', 'naver.com', '163.com', '126.com', 'sina.com', 'yeah.net', 'googlemail.com', 'fastmail.com', 'hushmail.com', 'tutanota.com', 'pm.me', 'mailbox.org', 'proton.me'
];
if (!publicProviders.includes(domain)) {
const org = domain.split('.')[0];
if (org && org.length > 1) {
return org.charAt(0).toUpperCase() + org.slice(1);
}
}
}
}
return name || '';
}
const organizationNameInitialValue = getOrganizationNameInitialValue(userDetails);
const styles = getAccountSetupStyles(token);
useEffect(() => {
trackMixpanelEvent(evt_account_setup_visit);
const verifyAuthStatus = async () => {
try {
const response = (await dispatch(verifyAuthentication()).unwrap())
.payload as IAuthorizeResponse;
const response = await dispatch(verifyAuthentication()).unwrap() as IAuthorizeResponse;
if (response?.authenticated) {
setSession(response.user);
dispatch(setUser(response.user));
@@ -65,88 +154,25 @@ const AccountSetup: React.FC = () => {
logger.error('Failed to verify authentication status', error);
}
};
const loadSurvey = async () => {
try {
const response = await surveyApiService.getAccountSetupSurvey();
if (response.done && response.body) {
setSurveyId(response.body.id);
} else {
logger.error('Survey not found or inactive (warn replaced with error)');
}
} catch (error) {
logger.error('Failed to load survey', error);
// Continue without survey - don't block account setup
}
};
void verifyAuthStatus();
void loadSurvey();
}, [dispatch, navigate, trackMixpanelEvent]);
const calculateHeight = () => {
if (currentStep === 2) {
return tasks.length * 105;
}
if (currentStep === 3) {
return teamMembers.length * 105;
}
return 'min-content';
};
const styles = {
form: {
width: '600px',
paddingBottom: '1rem',
marginTop: '3rem',
height: '100%',
overflow: 'hidden',
},
label: {
color: isDarkMode ? '' : '#00000073',
fontWeight: 500,
},
buttonContainer: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: '1rem',
},
drawerFooter: {
display: 'flex',
justifyContent: 'right',
padding: '10px 16px',
},
container: {
height: '100vh',
width: '100vw',
display: 'flex',
flexDirection: 'column' as const,
alignItems: 'center',
padding: '3rem 0',
backgroundColor: isDarkMode ? 'black' : '#FAFAFA',
},
contentContainer: {
backgroundColor: isDarkMode ? '#141414' : 'white',
marginTop: '1.5rem',
paddingTop: '3rem',
margin: '1.5rem auto 0',
width: '100%',
maxWidth: '66.66667%',
minHeight: 'fit-content',
display: 'flex',
flexDirection: 'column' as const,
},
space: {
display: 'flex',
flexDirection: 'column' as const,
alignItems: 'center',
gap: '0',
flexGrow: 1,
width: '100%',
minHeight: 'fit-content',
},
steps: {
margin: '1rem 0',
width: '600px',
},
stepContent: {
flexGrow: 1,
width: '600px',
minHeight: calculateHeight(),
overflow: 'visible',
},
actionButtons: {
flexGrow: 1,
width: '600px',
marginBottom: '1rem',
},
};
const completeAccountSetup = async (skip = false) => {
try {
@@ -159,10 +185,29 @@ const AccountSetup: React.FC = () => {
: teamMembers
.map(teamMember => sanitizeInput(teamMember.value.trim()))
.filter(email => validateEmail(email)),
survey_data: {
organization_type: surveyData.organization_type,
user_role: surveyData.user_role,
main_use_cases: surveyData.main_use_cases,
previous_tools: surveyData.previous_tools,
how_heard_about: surveyData.how_heard_about,
},
};
const res = await profileSettingsApiService.setupAccount(model);
if (res.done && res.body.id) {
trackMixpanelEvent(skip ? evt_account_setup_skip_invite : evt_account_setup_complete);
// Refresh user session to update setup_completed status
try {
const authResponse = await dispatch(verifyAuthentication()).unwrap() as IAuthorizeResponse;
if (authResponse?.authenticated && authResponse?.user) {
setSession(authResponse.user);
dispatch(setUser(authResponse.user));
}
} catch (error) {
logger.error('Failed to refresh user session after setup completion', error);
}
navigate(`/worklenz/projects/${res.body.id}?tab=tasks-list&pinned_tab=tasks-list`);
}
} catch (error) {
@@ -170,6 +215,59 @@ const AccountSetup: React.FC = () => {
}
};
const handleSkipMembers = async () => {
try {
setIsSkipping(true);
// Bypass all validation and complete setup without team members
await completeAccountSetup(true);
} catch (error) {
logger.error('Failed to skip members and complete setup', error);
} finally {
setIsSkipping(false);
}
};
const completeAccountSetupWithTemplate = async () => {
try {
await saveSurveyData(); // Save survey data first
const model: IAccountSetupRequest = {
team_name: sanitizeInput(organizationName),
project_name: null, // No project name when using template
template_id: templateId,
tasks: [],
team_members: [],
survey_data: {
organization_type: surveyData.organization_type,
user_role: surveyData.user_role,
main_use_cases: surveyData.main_use_cases,
previous_tools: surveyData.previous_tools,
how_heard_about: surveyData.how_heard_about,
},
};
const res = await projectTemplatesApiService.setupAccount(model);
if (res.done && res.body.id) {
trackMixpanelEvent(evt_account_setup_complete);
// Refresh user session to update setup_completed status
try {
const authResponse = await dispatch(verifyAuthentication()).unwrap() as IAuthorizeResponse;
if (authResponse?.authenticated && authResponse?.user) {
setSession(authResponse.user);
dispatch(setUser(authResponse.user));
}
} catch (error) {
logger.error('Failed to refresh user session after template setup completion', error);
}
navigate(`/worklenz/projects/${res.body.id}?tab=tasks-list&pinned_tab=tasks-list`);
}
} catch (error) {
logger.error('completeAccountSetupWithTemplate', error);
}
};
const steps = [
{
title: '',
@@ -178,6 +276,20 @@ const AccountSetup: React.FC = () => {
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
styles={styles}
organizationNamePlaceholder={organizationNamePlaceholder}
organizationNameInitialValue={organizationNameInitialValue}
isDarkMode={isDarkMode}
token={token}
/>
),
},
{
title: '',
content: (
<SurveyStep
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
styles={styles}
isDarkMode={isDarkMode}
token={token}
/>
),
},
@@ -188,6 +300,7 @@ const AccountSetup: React.FC = () => {
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
styles={styles}
isDarkMode={isDarkMode}
token={token}
/>
),
},
@@ -198,12 +311,13 @@ const AccountSetup: React.FC = () => {
onEnter={() => dispatch(setCurrentStep(currentStep + 1))}
styles={styles}
isDarkMode={isDarkMode}
token={token}
/>
),
},
{
title: '',
content: <MembersStep isDarkMode={isDarkMode} styles={styles} />,
content: <MembersStep isDarkMode={isDarkMode} styles={styles} token={token} />,
},
];
@@ -212,10 +326,21 @@ const AccountSetup: React.FC = () => {
case 0:
return !organizationName?.trim();
case 1:
return !projectName?.trim() && !templateId;
// Survey step - check current sub-step requirements
if (surveySubStep === 0) {
return !(surveyData.organization_type && surveyData.user_role);
} else if (surveySubStep === 1) {
return !(surveyData.main_use_cases && surveyData.main_use_cases.length > 0);
} else if (surveySubStep === 2) {
return !surveyData.how_heard_about;
}
return false;
case 2:
return tasks.length === 0 || tasks.every(task => !task.value?.trim());
// Project step - either project name OR template must be provided
return !projectName?.trim() && !templateId;
case 3:
return tasks.length === 0 || tasks.every(task => !task.value?.trim());
case 4:
return (
teamMembers.length > 0 && !teamMembers.some(member => validateEmail(member.value?.trim()))
);
@@ -224,58 +349,267 @@ const AccountSetup: React.FC = () => {
}
};
const nextStep = () => {
if (currentStep === 3) {
const saveSurveyData = async () => {
if (!surveyId || !surveyData) {
logger.error('Skipping survey save - no survey ID or data (info replaced with error)');
return;
}
try {
const answers: ISurveyAnswer[] = [];
// Get the survey questions to map data properly
const surveyResponse = await surveyApiService.getAccountSetupSurvey();
if (!surveyResponse.done || !surveyResponse.body?.questions) {
logger.error('Could not retrieve survey questions for data mapping (warn replaced with error)');
return;
}
const questions = surveyResponse.body.questions;
// Map survey data to answers based on question keys
questions.forEach(question => {
switch (question.question_key) {
case 'organization_type':
if (surveyData.organization_type) {
answers.push({
question_id: question.id,
answer_text: surveyData.organization_type
});
}
break;
case 'user_role':
if (surveyData.user_role) {
answers.push({
question_id: question.id,
answer_text: surveyData.user_role
});
}
break;
case 'main_use_cases':
if (surveyData.main_use_cases && surveyData.main_use_cases.length > 0) {
answers.push({
question_id: question.id,
answer_json: surveyData.main_use_cases
});
}
break;
case 'previous_tools':
if (surveyData.previous_tools) {
answers.push({
question_id: question.id,
answer_text: surveyData.previous_tools
});
}
break;
case 'how_heard_about':
if (surveyData.how_heard_about) {
answers.push({
question_id: question.id,
answer_text: surveyData.how_heard_about
});
}
break;
}
});
if (answers.length > 0) {
const submissionData: ISurveySubmissionRequest = {
survey_id: surveyId,
answers
};
const result = await surveyApiService.submitSurveyResponse(submissionData);
if (result.done) {
logger.error('Survey data saved successfully (info replaced with error)');
} else {
logger.error('Survey submission returned unsuccessful response (warn replaced with error)');
}
} else {
logger.error('No survey answers to save (info replaced with error)');
}
} catch (error) {
logger.error('Failed to save survey data', error);
// Don't block account setup flow if survey fails
}
};
const nextStep = async () => {
if (currentStep === 1) {
// Handle survey sub-step navigation
if (surveySubStep < 2) {
// Move to next survey sub-step
dispatch(setSurveySubStep(surveySubStep + 1));
} else {
// Survey completed, save data and move to next main step
await saveSurveyData();
dispatch(setCurrentStep(currentStep + 1));
dispatch(setSurveySubStep(0)); // Reset for next time
}
} else if (currentStep === 2) {
// Project step - check if template is selected
if (templateId) {
// Template selected, complete account setup with template
await completeAccountSetupWithTemplate();
} else {
// No template, proceed to tasks step
dispatch(setCurrentStep(currentStep + 1));
}
} else if (currentStep === 4) {
// Complete setup after members step
completeAccountSetup();
} else {
dispatch(setCurrentStep(currentStep + 1));
}
};
// Language switcher functionality
const languages = [
{ key: Language.EN, label: 'English', flag: '🇺🇸' },
{ key: Language.ES, label: 'Español', flag: '🇪🇸' },
{ key: Language.PT, label: 'Português', flag: '🇵🇹' },
{ key: Language.DE, label: 'Deutsch', flag: '🇩🇪' },
{ key: Language.ALB, label: 'Shqip', flag: '🇦🇱' },
{ key: Language.ZH_CN, label: '简体中文', flag: '🇨🇳' }
];
const handleLanguageChange = (languageKey: ILanguageType) => {
dispatch(setLanguage(languageKey));
i18n.changeLanguage(languageKey);
};
const handleThemeToggle = () => {
dispatch(toggleTheme());
};
const languageMenuItems: MenuProps['items'] = languages.map(lang => ({
key: lang.key,
label: (
<div className="flex items-center space-x-2">
<span>{lang.flag}</span>
<span>{lang.label}</span>
</div>
),
onClick: () => handleLanguageChange(lang.key as ILanguageType)
}));
const currentLanguage = languages.find(lang => lang.key === lng) || languages[0];
return (
<div style={styles.container}>
<div>
<div
className="account-setup-container min-h-screen w-full flex flex-col items-center py-8 px-4 relative"
style={{ backgroundColor: token.colorBgLayout }}
>
{/* Controls - Top Right */}
<div className="absolute top-6 right-6 flex items-center space-x-3">
{/* Theme Switcher */}
<Button
type="text"
size="small"
icon={isDarkMode ? <SunOutlined /> : <MoonOutlined />}
onClick={handleThemeToggle}
className="flex items-center"
style={{ color: token?.colorTextTertiary }}
title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
/>
{/* Language Switcher */}
<Dropdown
menu={{ items: languageMenuItems }}
placement="bottomRight"
trigger={['click']}
>
<Button
type="text"
size="small"
icon={<GlobalOutlined />}
className="flex items-center space-x-2"
style={{ color: token?.colorTextTertiary }}
>
<span>{currentLanguage.flag}</span>
<span>{currentLanguage.label}</span>
</Button>
</Dropdown>
</div>
{/* Logo */}
<div className="mb-4">
<img src={isDarkMode ? logoDark : logo} alt="Logo" width={235} height={50} />
</div>
<Title level={5} style={{ textAlign: 'center', margin: '4px 0 24px' }}>
{/* Title */}
<Title
level={3}
className="text-center mb-6 font-semibold"
style={{ color: token.colorText }}
>
{t('setupYourAccount')}
</Title>
<div style={styles.contentContainer}>
<Space className={isDarkMode ? 'dark-mode' : ''} style={styles.space} direction="vertical">
<Steps
className={isContinueDisabled() ? 'step' : 'progress-steps'}
current={currentStep}
items={steps}
style={styles.steps}
/>
<div className="step-content" style={styles.stepContent}>
{steps[currentStep].content}
{/* Content Container */}
<div
className="w-full max-w-4xl rounded-lg shadow-lg mt-6 p-8"
style={{
backgroundColor: token.colorBgContainer,
borderColor: token.colorBorder,
border: `1px solid ${token.colorBorder}`,
boxShadow: token.boxShadowTertiary
}}
>
<div className="flex flex-col items-center space-y-6 w-full">
{/* Steps */}
<div className="w-full max-w-2xl">
<Steps
className={`${isContinueDisabled() ? 'step' : 'progress-steps'} ${isDarkMode ? 'dark-mode' : 'light-mode'}`}
current={currentStep}
items={steps}
/>
</div>
<div style={styles.actionButtons} className="setup-action-buttons">
<div
style={{
display: 'flex',
justifyContent: currentStep !== 0 ? 'space-between' : 'flex-end',
}}
>
{/* Step Content */}
<div className="w-full max-w-2xl flex flex-col items-center min-h-fit">
<div className="step-content w-full">
{steps[currentStep].content}
</div>
</div>
{/* Action Buttons */}
<div className="w-full max-w-2xl mt-8">
<div className={`flex ${
currentStep !== 0 ? 'justify-between' : 'justify-end'
} items-center`}>
{currentStep !== 0 && (
<div>
<div className="flex flex-col space-y-2">
<Button
style={{ padding: 0 }}
type="link"
className="my-7"
onClick={() => dispatch(setCurrentStep(currentStep - 1))}
className="p-0 font-medium"
style={{ color: token.colorTextSecondary }}
onClick={() => {
if (currentStep === 1 && surveySubStep > 0) {
// Go back within survey sub-steps
dispatch(setSurveySubStep(surveySubStep - 1));
} else {
// Go back to previous main step
dispatch(setCurrentStep(currentStep - 1));
if (currentStep === 2) {
// When going back to survey from next step, go to last sub-step
dispatch(setSurveySubStep(2));
}
}
}}
>
{t('goBack')}
</Button>
{currentStep === 3 && (
{currentStep === 4 && (
<Button
style={{ color: isDarkMode ? '' : '#00000073', fontWeight: 500 }}
type="link"
className="my-7"
onClick={() => completeAccountSetup(true)}
className="p-0 font-medium"
style={{ color: token.colorTextTertiary }}
onClick={handleSkipMembers}
loading={isSkipping}
disabled={isSkipping}
>
{t('skipForNow')}
{isSkipping ? t('skipping') : t('skipForNow')}
</Button>
)}
</div>
@@ -284,14 +618,13 @@ const AccountSetup: React.FC = () => {
type="primary"
htmlType="submit"
disabled={isContinueDisabled()}
className="mt-7 mb-7"
onClick={nextStep}
>
{t('continue')}
</Button>
</div>
</div>
</Space>
</div>
</div>
</div>
);

View File

@@ -4,7 +4,7 @@ import {
ProfileOutlined,
TeamOutlined,
UserOutlined,
} from '@ant-design/icons';
} from '@/shared/antd-imports';
import React, { ReactNode, lazy } from 'react';
const Overview = lazy(() => import('./overview/overview'));
const Users = lazy(() => import('./users/users'));

View File

@@ -1,32 +1,39 @@
import { PageHeader } from '@ant-design/pro-components';
import { Tabs, TabsProps } from 'antd';
import React from 'react';
import { Tabs, TabsProps } from '@/shared/antd-imports';
import React, { useMemo } from 'react';
import CurrentBill from '@/components/admin-center/billing/current-bill';
import Configuration from '@/components/admin-center/configuration/configuration';
import { useTranslation } from 'react-i18next';
const Billing: React.FC = () => {
const Billing: React.FC = React.memo(() => {
const { t } = useTranslation('admin-center/current-bill');
const items: TabsProps['items'] = [
{
key: '1',
label: t('currentBill'),
children: <CurrentBill />,
},
{
key: '2',
label: t('configuration'),
children: <Configuration />,
},
];
const items: TabsProps['items'] = useMemo(
() => [
{
key: '1',
label: t('currentBill'),
children: <CurrentBill />,
},
{
key: '2',
label: t('configuration'),
children: <Configuration />,
},
],
[t]
);
const pageHeaderStyle = useMemo(() => ({ padding: '16px 0' }), []);
return (
<div style={{ width: '100%' }}>
<PageHeader title={<span>{t('title')}</span>} style={{ padding: '16px 0' }} />
<Tabs defaultActiveKey="1" items={items} destroyInactiveTabPane />
<PageHeader title={<span>{t('title')}</span>} style={pageHeaderStyle} />
<Tabs defaultActiveKey="1" items={items} destroyOnHidden />
</div>
);
};
});
Billing.displayName = 'Billing';
export default Billing;

View File

@@ -1,6 +1,6 @@
import { EditOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons';
import { EditOutlined, MailOutlined, PhoneOutlined } from '@/shared/antd-imports';
import { PageHeader } from '@ant-design/pro-components';
import { Button, Card, Input, Space, Tooltip, Typography } from 'antd';
import { Button, Card, Input, Space, Tooltip, Typography } from '@/shared/antd-imports';
import React, { useEffect, useState } from 'react';
import OrganizationAdminsTable from '@/components/admin-center/overview/organization-admins-table/organization-admins-table';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -21,8 +21,8 @@ import {
TableProps,
Tooltip,
Typography,
} from 'antd';
import { DeleteOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons';
} from '@/shared/antd-imports';
import { DeleteOutlined, SearchOutlined, SyncOutlined } from '@/shared/antd-imports';
import { PageHeader } from '@ant-design/pro-components';
import { projectsApiService } from '@/api/projects/projects.api.service';

View File

@@ -1,5 +1,5 @@
import { RightOutlined } from '@ant-design/icons';
import { ConfigProvider, Flex, Menu, MenuProps } from 'antd';
import { RightOutlined } from '@/shared/antd-imports';
import { ConfigProvider, Flex, Menu, MenuProps } from '@/shared/antd-imports';
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { colors } from '../../../styles/colors';

View File

@@ -1,6 +1,6 @@
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { SearchOutlined, SyncOutlined } from '@/shared/antd-imports';
import { PageHeader } from '@ant-design/pro-components';
import { Button, Flex, Input, Tooltip } from 'antd';
import { Button, Flex, Input, Tooltip } from '@/shared/antd-imports';
import React, { useEffect, useState } from 'react';

View File

@@ -1,6 +1,6 @@
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { SearchOutlined, SyncOutlined } from '@/shared/antd-imports';
import { PageHeader } from '@ant-design/pro-components';
import { Button, Card, Flex, Input, Table, TableProps, Tooltip, Typography } from 'antd';
import { Button, Card, Flex, Input, Table, TableProps, Tooltip, Typography } from '@/shared/antd-imports';
import React, { useEffect, useState } from 'react';
import { RootState } from '@/app/store';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';
import { Link, useNavigate } from 'react-router-dom';
import { UserOutlined } from '@ant-design/icons';
import { UserOutlined } from '@/shared/antd-imports';
import { Form, Card, Input, Flex, Button, Typography, Result } from 'antd/es';
import PageHeader from '@components/AuthPageHeader';
@@ -118,7 +118,7 @@ const ForgotPasswordPage = () => {
>
<Input
prefix={<UserOutlined />}
placeholder={t('emailPlaceholder')}
placeholder={t('emailPlaceholder', {defaultValue: 'Enter your email'})}
size="large"
style={{ borderRadius: 4 }}
/>
@@ -134,7 +134,7 @@ const ForgotPasswordPage = () => {
loading={isLoading}
style={{ borderRadius: 4 }}
>
{t('resetPasswordButton')}
{t('resetPasswordButton', {defaultValue: 'Reset Password'})}
</Button>
<Typography.Text style={{ textAlign: 'center' }}>{t('orText')}</Typography.Text>
<Link to="/auth/login">
@@ -146,7 +146,7 @@ const ForgotPasswordPage = () => {
borderRadius: 4,
}}
>
{t('returnToLoginButton')}
{t('returnToLoginButton', {defaultValue: 'Return to Login'})}
</Button>
</Link>
</Flex>

View File

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { useAuthService } from '@/hooks/useAuth';
import { useMediaQuery } from 'react-responsive';
import { authApiService } from '@/api/auth/auth.api.service';
import CacheCleanup from '@/utils/cache-cleanup';
const LoggingOutPage = () => {
const navigate = useNavigate();
@@ -14,14 +15,30 @@ const LoggingOutPage = () => {
useEffect(() => {
const logout = async () => {
await auth.signOut();
await authApiService.logout();
setTimeout(() => {
window.location.href = '/';
}, 1500);
try {
// Clear local session
await auth.signOut();
// Call backend logout
await authApiService.logout();
// Clear all caches using the utility
await CacheCleanup.clearAllCaches();
// Force a hard reload to ensure fresh state
setTimeout(() => {
CacheCleanup.forceReload('/auth/login');
}, 1000);
} catch (error) {
console.error('Logout error:', error);
// Fallback: force reload to login page
CacheCleanup.forceReload('/auth/login');
}
};
void logout();
}, [auth, navigate]);
}, [auth]);
const cardStyles = {
width: '100%',

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Card, Input, Flex, Checkbox, Button, Typography, Space, Form, message } from 'antd/es';
import { Rule } from 'antd/es/form';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { LockOutlined, UserOutlined } from '@/shared/antd-imports';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';

View File

@@ -2,9 +2,11 @@ import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';
import { LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons';
import { LockOutlined, MailOutlined, UserOutlined } from '@/shared/antd-imports';
import { Form, Card, Input, Flex, Button, Typography, Space, message } from 'antd/es';
import { Rule } from 'antd/es/form';
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import googleIcon from '@/assets/images/google-icon.png';
import PageHeader from '@components/AuthPageHeader';
@@ -297,6 +299,10 @@ const SignupPage = () => {
min: 8,
message: t('passwordMinCharacterRequired'),
},
{
max: 32,
message: t('passwordMaxCharacterRequired'),
},
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])/,
message: t('passwordPatternRequired'),
@@ -304,6 +310,38 @@ const SignupPage = () => {
],
};
const passwordChecklistItems = [
{
key: 'minLength',
test: (v: string) => v.length >= 8,
label: t('passwordChecklist.minLength', { defaultValue: 'At least 8 characters' }),
},
{
key: 'uppercase',
test: (v: string) => /[A-Z]/.test(v),
label: t('passwordChecklist.uppercase', { defaultValue: 'One uppercase letter' }),
},
{
key: 'lowercase',
test: (v: string) => /[a-z]/.test(v),
label: t('passwordChecklist.lowercase', { defaultValue: 'One lowercase letter' }),
},
{
key: 'number',
test: (v: string) => /\d/.test(v),
label: t('passwordChecklist.number', { defaultValue: 'One number' }),
},
{
key: 'special',
test: (v: string) => /[@$!%*?&#]/.test(v),
label: t('passwordChecklist.special', { defaultValue: 'One special character' }),
},
];
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [passwordValue, setPasswordValue] = useState('');
const [passwordActive, setPasswordActive] = useState(false);
return (
<Card
style={{
@@ -317,7 +355,7 @@ const SignupPage = () => {
}}
variant="outlined"
>
<PageHeader description={t('headerDescription')} />
<PageHeader description={t('headerDescription', {defaultValue: 'Sign up to get started'})} />
<Form
form={form}
name="signup"
@@ -331,35 +369,72 @@ const SignupPage = () => {
name: urlParams.name,
}}
>
<Form.Item name="name" label={t('nameLabel')} rules={formRules.name}>
<Form.Item name="name" label={t('nameLabel', {defaultValue: 'Full Name'})} rules={formRules.name}>
<Input
prefix={<UserOutlined />}
placeholder={t('namePlaceholder')}
placeholder={t('namePlaceholder', {defaultValue: 'Enter your full name'})}
size="large"
style={{ borderRadius: 4 }}
/>
</Form.Item>
<Form.Item name="email" label={t('emailLabel')} rules={formRules.email as Rule[]}>
<Form.Item name="email" label={t('emailLabel', {defaultValue: 'Email'})} rules={formRules.email as Rule[]}>
<Input
prefix={<MailOutlined />}
placeholder={t('emailPlaceholder')}
placeholder={t('emailPlaceholder', {defaultValue: 'Enter your email'})}
size="large"
style={{ borderRadius: 4 }}
/>
</Form.Item>
<Form.Item name="password" label={t('passwordLabel')} rules={formRules.password}>
<Form.Item
name="password"
label={t('passwordLabel', {defaultValue: 'Password'})}
rules={formRules.password}
validateTrigger={['onBlur', 'onSubmit']}
>
<div>
<Input.Password
prefix={<LockOutlined />}
placeholder={t('strongPasswordPlaceholder')}
placeholder={t('strongPasswordPlaceholder', {defaultValue: 'Enter a strong password'})}
size="large"
style={{ borderRadius: 4 }}
value={passwordValue}
onFocus={() => setPasswordActive(true)}
onChange={e => {
setPasswordValue(e.target.value);
setPasswordActive(true);
}}
onBlur={() => {
if (!passwordValue) setPasswordActive(false);
}}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{t('passwordValidationAltText')}
<Typography.Text type="secondary" style={{ fontSize: 12, marginTop: 4, marginBottom: 0, display: 'block' }}>
{t('passwordGuideline', {
defaultValue: 'Password must be at least 8 characters, include uppercase and lowercase letters, a number, and a special character.'
})}
</Typography.Text>
{passwordActive && (
<div style={{ marginTop: 8, marginBottom: 4 }}>
{passwordChecklistItems.map(item => {
const passed = item.test(passwordValue);
// Only green if passed, otherwise neutral (never red)
let color = passed
? (themeMode === 'dark' ? '#52c41a' : '#389e0d')
: (themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf');
return (
<Flex key={item.key} align="center" gap={8} style={{ color, fontSize: 13 }}>
{passed ? (
<CheckCircleTwoTone twoToneColor={themeMode === 'dark' ? '#52c41a' : '#52c41a'} />
) : (
<CloseCircleTwoTone twoToneColor={themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf'} />
)}
<span>{item.label}</span>
</Flex>
);
})}
</div>
)}
</div>
</Form.Item>
@@ -416,7 +491,7 @@ const SignupPage = () => {
<Form.Item>
<Space>
<Typography.Text style={{ fontSize: 14 }}>
{t('alreadyHaveAccountText')}
{t('alreadyHaveAccountText', {defaultValue: 'Already have an account?'})}
</Typography.Text>
<Link

View File

@@ -1,9 +1,11 @@
import { useCallback, useEffect, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Form, Card, Input, Flex, Button, Typography, Result } from 'antd/es';
import { LockOutlined } from '@ant-design/icons';
import { LockOutlined } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import PageHeader from '@components/AuthPageHeader';
@@ -36,6 +38,36 @@ const VerifyResetEmailPage = () => {
const { t } = useTranslation('auth/verify-reset-email');
const isMobile = useMediaQuery({ query: '(max-width: 576px)' });
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [passwordValue, setPasswordValue] = useState('');
const [passwordTouched, setPasswordTouched] = useState(false);
const passwordChecklistItems = [
{
key: 'minLength',
test: (v: string) => v.length >= 8,
label: t('passwordChecklist.minLength', { defaultValue: 'At least 8 characters' }),
},
{
key: 'uppercase',
test: (v: string) => /[A-Z]/.test(v),
label: t('passwordChecklist.uppercase', { defaultValue: 'One uppercase letter' }),
},
{
key: 'lowercase',
test: (v: string) => /[a-z]/.test(v),
label: t('passwordChecklist.lowercase', { defaultValue: 'One lowercase letter' }),
},
{
key: 'number',
test: (v: string) => /\d/.test(v),
label: t('passwordChecklist.number', { defaultValue: 'One number' }),
},
{
key: 'special',
test: (v: string) => /[@$!%*?&#]/.test(v),
label: t('passwordChecklist.special', { defaultValue: 'One special character' }),
},
];
useEffect(() => {
trackMixpanelEvent(evt_verify_reset_email_page_visit);
@@ -104,12 +136,38 @@ const VerifyResetEmailPage = () => {
},
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder={t('placeholder')}
size="large"
style={{ borderRadius: 4 }}
/>
<div>
<Input.Password
prefix={<LockOutlined />}
placeholder={t('placeholder')}
size="large"
style={{ borderRadius: 4 }}
value={passwordValue}
onChange={e => {
setPasswordValue(e.target.value);
if (!passwordTouched) setPasswordTouched(true);
}}
onBlur={() => setPasswordTouched(true)}
/>
<div style={{ marginTop: 8, marginBottom: 4 }}>
{passwordChecklistItems.map(item => {
const passed = item.test(passwordValue);
let color = passed
? (themeMode === 'dark' ? '#52c41a' : '#389e0d')
: (themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf');
return (
<Flex key={item.key} align="center" gap={8} style={{ color, fontSize: 13 }}>
{passed ? (
<CheckCircleTwoTone twoToneColor={themeMode === 'dark' ? '#52c41a' : '#52c41a'} />
) : (
<CloseCircleTwoTone twoToneColor={themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf'} />
)}
<span>{item.label}</span>
</Flex>
);
})}
</div>
</div>
</Form.Item>
<Form.Item
name="confirmPassword"
@@ -136,6 +194,8 @@ const VerifyResetEmailPage = () => {
placeholder={t('confirmPasswordPlaceholder')}
size="large"
style={{ borderRadius: 4 }}
value={form.getFieldValue('confirmPassword') || ''}
onChange={e => form.setFieldsValue({ confirmPassword: e.target.value })}
/>
</Form.Item>

View File

@@ -29,6 +29,7 @@ const SIDEBAR_MAX_WIDTH = 400;
// Lazy load heavy components
const TaskDrawer = React.lazy(() => import('@/components/task-drawer/task-drawer'));
const SurveyPromptModal = React.lazy(() => import('@/components/survey/SurveyPromptModal').then(m => ({ default: m.SurveyPromptModal })));
const HomePage = memo(() => {
const dispatch = useAppDispatch();

View File

@@ -1,5 +1,5 @@
import { StarFilled } from '@ant-design/icons';
import { Button, ConfigProvider, Tooltip } from 'antd';
import { StarFilled } from '@/shared/antd-imports';
import { Button, ConfigProvider, Tooltip } from '@/shared/antd-imports';
import { useMemo } from 'react';
import { colors } from '@/styles/colors';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';

View File

@@ -1,4 +1,4 @@
import { SyncOutlined } from '@ant-design/icons';
import { SyncOutlined } from '@/shared/antd-imports';
import {
Badge,
Button,
@@ -10,7 +10,7 @@ import {
TableProps,
Tooltip,
Typography,
} from 'antd';
} from '@/shared/antd-imports';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,4 +1,4 @@
import { Alert, DatePicker, Flex, Form, Input, InputRef, Select, Typography } from 'antd';
import { Alert, DatePicker, Flex, Form, Input, InputRef, Select, Typography } from '@/shared/antd-imports';
import { useEffect, useRef, useState } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { TFunction } from 'i18next';

View File

@@ -1,6 +1,6 @@
import HomeCalendar from '../../../components/calendars/homeCalendar/HomeCalendar';
import { Tag, Typography } from 'antd';
import { ClockCircleOutlined } from '@ant-design/icons';
import { Tag, Typography } from '@/shared/antd-imports';
import { ClockCircleOutlined } from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import AddTaskInlineForm from './add-task-inline-form';
import { useTranslation } from 'react-i18next';

View File

@@ -1,4 +1,4 @@
import { Tabs } from 'antd';
import { Tabs } from '@/shared/antd-imports';
import AddTaskInlineForm from './add-task-inline-form';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useTranslation } from 'react-i18next';

View File

@@ -6,3 +6,81 @@
.ant-table-row:hover .row-action-button {
opacity: 1;
}
/* Responsive styles for task list */
@media (max-width: 768px) {
.task-list-card .ant-card-head {
flex-direction: column;
gap: 12px;
}
.task-list-card .ant-card-head-title {
flex: 1;
width: 100%;
}
.task-list-card .ant-card-extra {
width: 100%;
justify-content: space-between;
}
.task-list-mobile-header {
flex-direction: column;
gap: 8px;
align-items: stretch !important;
}
.task-list-mobile-controls {
flex-direction: column;
gap: 8px;
align-items: stretch !important;
}
.task-list-mobile-select {
width: 100% !important;
}
.task-list-mobile-segmented {
width: 100% !important;
}
}
@media (max-width: 576px) {
.task-list-card .ant-table {
font-size: 12px;
}
.task-list-card .ant-table-thead > tr > th {
padding: 8px 4px;
font-size: 12px;
}
.task-list-card .ant-table-tbody > tr > td {
padding: 8px 4px;
}
.row-action-button {
opacity: 1; /* Always show on mobile */
}
/* Hide project column on very small screens */
.task-list-card .ant-table-thead > tr > th:nth-child(2),
.task-list-card .ant-table-tbody > tr > td:nth-child(2) {
display: none;
}
}
/* Table responsive container */
.task-list-card .ant-table-container {
overflow-x: auto;
}
@media (max-width: 768px) {
.task-list-card .ant-table-wrapper {
overflow-x: auto;
}
.task-list-card .ant-table {
min-width: 600px;
}
}

View File

@@ -1,4 +1,4 @@
import { ExpandAltOutlined, SyncOutlined } from '@ant-design/icons';
import { ExpandAltOutlined, SyncOutlined } from '@/shared/antd-imports';
import {
Badge,
Button,
@@ -12,9 +12,10 @@ import {
Tooltip,
Typography,
Pagination,
} from 'antd';
} from '@/shared/antd-imports';
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';
import ListView from './list-view';
import CalendarView from './calendar-view';
@@ -61,21 +62,22 @@ const TasksList: React.FC = React.memo(() => {
refetchOnFocus: false,
});
const { t } = useTranslation('home');
const { t, ready } = useTranslation('home');
const { model } = useAppSelector(state => state.homePageReducer);
const isMobile = useMediaQuery({ maxWidth: 768 });
const taskModes = useMemo(
() => [
{
value: 0,
label: t('home:tasks.assignedToMe'),
label: ready ? t('tasks.assignedToMe') : 'Assigned to me',
},
{
value: 1,
label: t('home:tasks.assignedByMe'),
label: ready ? t('tasks.assignedByMe') : 'Assigned by me',
},
],
[t]
[t, ready]
);
const handleSegmentChange = (value: 'List' | 'Calendar') => {
@@ -123,7 +125,7 @@ const TasksList: React.FC = React.memo(() => {
<span>{t('tasks.name')}</span>
</Flex>
),
width: '40%',
width: isMobile ? '50%' : '40%',
render: (_, record) => (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Tooltip title={record.name}>
@@ -155,7 +157,7 @@ const TasksList: React.FC = React.memo(() => {
{
key: 'project',
title: t('tasks.project'),
width: '25%',
width: isMobile ? '30%' : '25%',
render: (_, record) => {
return (
<Tooltip title={record.project_name}>
@@ -185,7 +187,7 @@ const TasksList: React.FC = React.memo(() => {
render: (_, record) => <HomeTasksDatePicker record={record} />,
},
],
[t, data?.body?.total, currentPage, pageSize, handlePageChange]
[t, data?.body?.total, currentPage, pageSize, handlePageChange, isMobile]
);
const handleTaskModeChange = (value: number) => {
@@ -210,23 +212,27 @@ const TasksList: React.FC = React.memo(() => {
);
}, [dispatch]);
return (
<Card
className="task-list-card"
title={
<Flex gap={8} align="center">
<Flex gap={8} align="center" className="task-list-mobile-header">
<Typography.Title level={5} style={{ margin: 0 }}>
{t('tasks.tasks')}
</Typography.Title>
<Select
defaultValue={taskModes[0].label}
value={homeTasksConfig.tasks_group_by || 0}
options={taskModes}
onChange={value => handleTaskModeChange(+value)}
fieldNames={{ label: 'label', value: 'value' }}
className="task-list-mobile-select"
style={{ minWidth: 160 }}
/>
</Flex>
}
extra={
<Flex gap={8} align="center">
<Flex gap={8} align="center" className="task-list-mobile-controls">
<Tooltip title={t('tasks.refresh')} trigger={'hover'}>
<Button
shape="circle"
@@ -241,6 +247,7 @@ const TasksList: React.FC = React.memo(() => {
]}
defaultValue="List"
onChange={handleSegmentChange}
className="task-list-mobile-segmented"
/>
</Flex>
}
@@ -283,6 +290,7 @@ const TasksList: React.FC = React.memo(() => {
rowClassName={() => 'custom-row-height'}
loading={homeTasksFetching && skipAutoRefetch}
pagination={false}
scroll={{ x: 'max-content' }}
/>
<div

View File

@@ -1,4 +1,4 @@
import { CheckCircleOutlined } from '@ant-design/icons';
import { CheckCircleOutlined } from '@/shared/antd-imports';
import ConfigProvider from 'antd/es/config-provider';
import Button from 'antd/es/button';
import Tooltip from 'antd/es/tooltip';

View File

@@ -1,4 +1,4 @@
import { CheckCircleOutlined, SyncOutlined } from '@ant-design/icons';
import { CheckCircleOutlined, SyncOutlined } from '@/shared/antd-imports';
import { useRef, useState } from 'react';
import Form from 'antd/es/form';
import Input, { InputRef } from 'antd/es/input';

View File

@@ -1,4 +1,4 @@
import { Button, Result } from 'antd';
import { Button, Result } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useAuthService } from '@/hooks/useAuth';

View File

@@ -0,0 +1,63 @@
import React, { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { AdvancedGanttChart } from '../../components/advanced-gantt';
import { useAppSelector } from '../../hooks/useAppSelector';
import { GanttTask } from '../../types/advanced-gantt.types';
const ProjectGanttView: React.FC = () => {
const { projectId } = useParams<{ projectId: string }>();
// Get tasks from your Redux store (adjust based on your actual state structure)
const tasks = useAppSelector(state => state.tasksReducer?.tasks || []);
// Transform your tasks to GanttTask format
const ganttTasks = useMemo((): GanttTask[] => {
return tasks.map(task => ({
id: task.id,
name: task.name,
startDate: task.start_date ? new Date(task.start_date) : new Date(),
endDate: task.end_date ? new Date(task.end_date) : new Date(),
progress: task.progress || 0,
type: 'task',
status: task.status || 'not-started',
priority: task.priority || 'medium',
assignee: task.assignee ? {
id: task.assignee.id,
name: task.assignee.name,
avatar: task.assignee.avatar,
} : undefined,
parent: task.parent_task_id,
level: task.level || 0,
// Map other fields as needed
}));
}, [tasks]);
const handleTaskUpdate = (taskId: string, updates: Partial<GanttTask>) => {
// Implement your task update logic here
console.log('Update task:', taskId, updates);
// Dispatch Redux action to update task
};
const handleTaskMove = (taskId: string, newDates: { start: Date; end: Date }) => {
// Implement your task move logic here
console.log('Move task:', taskId, newDates);
// Dispatch Redux action to update task dates
};
return (
<div className="project-gantt-view h-full">
<AdvancedGanttChart
tasks={ganttTasks}
onTaskUpdate={handleTaskUpdate}
onTaskMove={handleTaskMove}
enableDragDrop={true}
enableResize={true}
enableProgressEdit={true}
enableInlineEdit={true}
className="h-full"
/>
</div>
);
};
export default ProjectGanttView;

View File

@@ -16,14 +16,14 @@ import {
Table,
TablePaginationConfig,
Tooltip,
} from 'antd';
} from '@/shared/antd-imports';
import { PageHeader } from '@ant-design/pro-components';
import {
SearchOutlined,
SyncOutlined,
UnorderedListOutlined,
AppstoreOutlined,
} from '@ant-design/icons';
} from '@/shared/antd-imports';
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
import ProjectDrawer from '@/components/projects/project-drawer/project-drawer';

View File

@@ -10,8 +10,8 @@ import {
import { useAppSelector } from '@/hooks/useAppSelector';
import TaskListFilters from '../taskList/taskListFilters/TaskListFilters';
import { Button, Skeleton } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Skeleton } from '@/shared/antd-imports';
import { PlusOutlined } from '@/shared/antd-imports';
import { useDispatch } from 'react-redux';
import { toggleDrawer } from '@/features/projects/status/StatusSlice';
import KanbanGroup from '@/components/board/kanban-group/kanban-group';

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { ViewMode } from 'gantt-task-react';
import 'gantt-task-react/dist/index.css';
import './project-view-roadmap.css';
import { Flex } from 'antd';
import { Flex } from '@/shared/antd-imports';
import { useAppSelector } from '../../../../hooks/useAppSelector';
import { TimeFilter } from './time-filter';
import RoadmapTable from './roadmap-table/roadmap-table';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { DatePicker, Typography } from 'antd';
import { DatePicker, Typography } from '@/shared/antd-imports';
import dayjs, { Dayjs } from 'dayjs';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';

View File

@@ -1,10 +1,10 @@
import { Flex, Typography, Button, Tooltip } from 'antd';
import { Flex, Typography, Button, Tooltip } from '@/shared/antd-imports';
import {
DoubleRightOutlined,
DownOutlined,
RightOutlined,
ExpandAltOutlined,
} from '@ant-design/icons';
} from '@/shared/antd-imports';
import { NewTaskType, toggleTaskExpansion } from '@features/roadmap/roadmap-slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { toggleTaskDrawer } from '@features/tasks/taskSlice';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import 'gantt-task-react/dist/index.css';
import { ViewMode } from 'gantt-task-react';
import { Flex, Select } from 'antd';
import { Flex, Select } from '@/shared/antd-imports';
type TimeFilterProps = {
onViewModeChange: (viewMode: ViewMode) => void;
};

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { Checkbox, Flex, Tag, Tooltip } from 'antd';
import { HolderOutlined } from '@ant-design/icons';
import { Checkbox, Flex, Tag, Tooltip } from '@/shared/antd-imports';
import { HolderOutlined } from '@/shared/antd-imports';
import {
DndContext,
DragEndEvent,

View File

@@ -1,7 +1,7 @@
import { Avatar, Checkbox, DatePicker, Flex, Select, Tag } from 'antd';
import { Avatar, Checkbox, DatePicker, Flex, Select, Tag } from '@/shared/antd-imports';
import { createColumnHelper, ColumnDef } from '@tanstack/react-table';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { HolderOutlined, PlusOutlined } from '@ant-design/icons';
import { HolderOutlined, PlusOutlined } from '@/shared/antd-imports';
import StatusDropdown from '@/components/task-list-common/status-dropdown/status-dropdown';
import Avatars from '@/components/avatars/avatars';
import LabelsSelector from '@/components/task-list-common/labelsSelector/labels-selector';

View File

@@ -1,5 +1,5 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { Checkbox, theme } from 'antd';
import { Checkbox, theme } from '@/shared/antd-imports';
import {
useReactTable,
getCoreRowModel,

View File

@@ -1,12 +1,12 @@
import React, { useState } from 'react';
import { Button, Dropdown, Input, Menu, Badge, Tooltip } from 'antd';
import { Button, Dropdown, Input, Menu, Badge, Tooltip } from '@/shared/antd-imports';
import {
RightOutlined,
LoadingOutlined,
EllipsisOutlined,
EditOutlined,
RetweetOutlined,
} from '@ant-design/icons';
} from '@/shared/antd-imports';
import { ITaskListGroup } from '@/types/tasks/taskList.types';
import { ITaskStatusCategory } from '@/types/status.types';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -1,4 +1,4 @@
import { Input, InputRef, theme } from 'antd';
import { Input, InputRef, theme } from '@/shared/antd-imports';
import React, { useState, useMemo, useRef } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { colors } from '@/styles/colors';

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Avatar, Checkbox, DatePicker, Flex, Tag, Tooltip, Typography } from 'antd';
import { Avatar, Checkbox, DatePicker, Flex, Tag, Tooltip, Typography } from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import { columnList } from '@/pages/projects/project-view-1/taskList/taskListTable/columns/columnList';

View File

@@ -1,7 +1,7 @@
import { Badge, Button, Collapse, ConfigProvider, Dropdown, Flex, Input, Typography } from 'antd';
import { Badge, Button, Collapse, ConfigProvider, Dropdown, Flex, Input, Typography } from '@/shared/antd-imports';
import { useState } from 'react';
import { TaskType } from '@/types/task.types';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@ant-design/icons';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import './task-list-table-wrapper.css';
import TaskListTable from '../table-v2';

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { Flex, Skeleton } from 'antd';
import { Flex, Skeleton } from '@/shared/antd-imports';
import TaskListFilters from '@/pages/projects/project-view-1/taskList/taskListFilters/TaskListFilters';
import { useAppSelector } from '@/hooks/useAppSelector';
import { ITaskListConfigV2, ITaskListGroup } from '@/types/tasks/taskList.types';

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { Flex } from 'antd';
import { Flex } from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';

View File

@@ -1,6 +1,6 @@
import { TaskType } from '@/types/task.types';
import { useAppSelector } from '@/hooks/useAppSelector';
import { Flex } from 'antd';
import { Flex } from '@/shared/antd-imports';
import TaskListTableWrapper from '@/pages/projects/project-view-1/taskList/taskListTable/TaskListTableWrapper';
import { createPortal } from 'react-dom';
import BulkTasksActionContainer from '@/features/projects/bulkActions/BulkTasksActionContainer';

View File

@@ -1,5 +1,5 @@
import { CaretDownFilled } from '@ant-design/icons';
import { ConfigProvider, Flex, Select } from 'antd';
import { CaretDownFilled } from '@/shared/antd-imports';
import { ConfigProvider, Flex, Select } from '@/shared/antd-imports';
import React, { useState } from 'react';
import { colors } from '@/styles/colors';
import ConfigPhaseButton from '@features/projects/singleProject/phase/ConfigPhaseButton';

View File

@@ -1,4 +1,4 @@
import { CaretDownFilled } from '@ant-design/icons';
import { CaretDownFilled } from '@/shared/antd-imports';
import {
Badge,
Button,
@@ -11,7 +11,7 @@ import {
InputRef,
List,
Space,
} from 'antd';
} from '@/shared/antd-imports';
import { useEffect, useRef, useState } from 'react';
import { colors } from '@/styles/colors';
import { useTranslation } from 'react-i18next';

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { CaretDownFilled } from '@ant-design/icons';
import { CaretDownFilled } from '@/shared/antd-imports';
import {
Badge,
Button,
@@ -13,7 +13,7 @@ import {
List,
Space,
Typography,
} from 'antd';
} from '@/shared/antd-imports';
import { useMemo, useRef, useState } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { colors } from '@/styles/colors';

View File

@@ -1,5 +1,5 @@
import { CaretDownFilled } from '@ant-design/icons';
import { Badge, Button, Card, Checkbox, Dropdown, List, Space } from 'antd';
import { CaretDownFilled } from '@/shared/antd-imports';
import { Badge, Button, Card, Checkbox, Dropdown, List, Space } from '@/shared/antd-imports';
import { useState } from 'react';
import { colors } from '@/styles/colors';

View File

@@ -1,5 +1,5 @@
import { SearchOutlined } from '@ant-design/icons';
import { Button, Card, Dropdown, Flex, Input, InputRef, Space } from 'antd';
import { SearchOutlined } from '@/shared/antd-imports';
import { Button, Card, Dropdown, Flex, Input, InputRef, Space } from '@/shared/antd-imports';
import { useRef } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,5 +1,5 @@
import { MoreOutlined } from '@ant-design/icons';
import { Button, Card, Checkbox, Dropdown, List, Space } from 'antd';
import { MoreOutlined } from '@/shared/antd-imports';
import { Button, Card, Checkbox, Dropdown, List, Space } from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import {

View File

@@ -1,5 +1,5 @@
import { CaretDownFilled, SortAscendingOutlined, SortDescendingOutlined } from '@ant-design/icons';
import { Badge, Button, Card, Checkbox, Dropdown, List, Space } from 'antd';
import { CaretDownFilled, SortAscendingOutlined, SortDescendingOutlined } from '@/shared/antd-imports';
import { Badge, Button, Card, Checkbox, Dropdown, List, Space } from '@/shared/antd-imports';
import React, { useState } from 'react';
import { colors } from '../../../../../styles/colors';
import { useTranslation } from 'react-i18next';

View File

@@ -1,4 +1,4 @@
import { Checkbox, Flex, Typography } from 'antd';
import { Checkbox, Flex, Typography } from '@/shared/antd-imports';
import SearchDropdown from './SearchDropdown';
import SortFilterDropdown from './SortFilterDropdown';
import LabelsFilterDropdown from './LabelsFilterDropdown';

View File

@@ -1,7 +1,7 @@
import { useAppSelector } from '@/hooks/useAppSelector';
import { columnList } from './columns/columnList';
import AddTaskListRow from './taskListTableRows/AddTaskListRow';
import { Checkbox, Flex, Tag, Tooltip } from 'antd';
import { Checkbox, Flex, Tag, Tooltip } from '@/shared/antd-imports';
import React, { useEffect, useState } from 'react';
import { useSelectedProject } from '@/hooks/useSelectedProject';
import TaskCell from './taskListTableCells/TaskCell';
@@ -12,7 +12,7 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
import { deselectAll } from '@features/projects/bulkActions/bulkActionSlice';
import { useTranslation } from 'react-i18next';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { HolderOutlined } from '@ant-design/icons';
import { HolderOutlined } from '@/shared/antd-imports';
const TaskListTable = ({
taskList,

View File

@@ -1,7 +1,6 @@
import { Badge, Button, Collapse, ConfigProvider, Dropdown, Flex, Input, Typography } from 'antd';
import { Badge, Button, Collapse, ConfigProvider, Dropdown, Flex, Input, Typography } from '@/shared/antd-imports';
import { useState } from 'react';
import { TaskType } from '../../../../../types/task.types';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@ant-design/icons';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@/shared/antd-imports';
import { colors } from '../../../../../styles/colors';
import './taskListTableWrapper.css';
import TaskListTable from './TaskListTable';

View File

@@ -4,8 +4,8 @@ import {
InboxOutlined,
RetweetOutlined,
UserAddOutlined,
} from '@ant-design/icons';
import { Badge, Dropdown, Flex, Typography } from 'antd';
} from '@/shared/antd-imports';
import { Badge, Dropdown, Flex, Typography } from '@/shared/antd-imports';
import { MenuProps } from 'antd/lib';
import React from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -1,12 +1,12 @@
// TaskNameCell.tsx
import React from 'react';
import { Flex, Typography, Button } from 'antd';
import { Flex, Typography, Button } from '@/shared/antd-imports';
import {
DoubleRightOutlined,
DownOutlined,
RightOutlined,
ExpandAltOutlined,
} from '@ant-design/icons';
} from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';

View File

@@ -1,4 +1,4 @@
import { Progress, Tooltip } from 'antd';
import { Progress, Tooltip } from '@/shared/antd-imports';
import React from 'react';
import './TaskProgress.css';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Divider, Empty, Flex, Popover, Typography } from 'antd';
import { PlayCircleFilled } from '@ant-design/icons';
import { Divider, Empty, Flex, Popover, Typography } from '@/shared/antd-imports';
import { PlayCircleFilled } from '@/shared/antd-imports';
import { colors } from '../../../../../../styles/colors';
import CustomAvatar from '../../../../../../components/CustomAvatar';
import { mockTimeLogs } from './mockTimeLogs';

View File

@@ -1,4 +1,4 @@
import { Input } from 'antd';
import { Input } from '@/shared/antd-imports';
import React, { useState } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { colors } from '@/styles/colors';

View File

@@ -1,4 +1,4 @@
import { Input } from 'antd';
import { Input } from '@/shared/antd-imports';
import React, { useState } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { colors } from '@/styles/colors';

View File

@@ -11,7 +11,7 @@ import {
Dropdown,
Menu,
Popconfirm,
} from 'antd';
} from '@/shared/antd-imports';
import { useEffect, useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import DOMPurify from 'dompurify';
@@ -29,7 +29,7 @@ import { calculateTimeDifference } from '@/utils/calculate-time-difference';
import { getUserSession } from '@/utils/session-helper';
import './project-view-updates.css';
import { useAppSelector } from '@/hooks/useAppSelector';
import { DeleteOutlined } from '@ant-design/icons';
import { DeleteOutlined } from '@/shared/antd-imports';
const MAX_COMMENT_LENGTH = 2000;

View File

@@ -1,5 +1,5 @@
import { Button, Flex } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Flex } from '@/shared/antd-imports';
import { PlusOutlined } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
import { nanoid } from '@reduxjs/toolkit';

View File

@@ -9,7 +9,7 @@ import {
Popconfirm,
Tooltip,
Typography,
} from 'antd';
} from '@/shared/antd-imports';
import {
DeleteOutlined,
EditOutlined,
@@ -18,8 +18,8 @@ import {
MoreOutlined,
PlusOutlined,
RetweetOutlined,
} from '@ant-design/icons';
import { MenuProps } from 'antd';
} from '@/shared/antd-imports';
import { MenuProps } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
import ChangeCategoryDropdown from '@/components/board/changeCategoryDropdown/ChangeCategoryDropdown';

View File

@@ -1,4 +1,4 @@
import { Button, Flex } from 'antd';
import { Button, Flex } from '@/shared/antd-imports';
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDroppable } from '@dnd-kit/core';
@@ -9,7 +9,7 @@ import { CSS } from '@dnd-kit/utilities';
import { useAppSelector } from '@/hooks/useAppSelector';
import { themeWiseColor } from '@/utils/themeWiseColor';
import BoardSectionCardHeader from './board-section-card-header';
import { PlusOutlined } from '@ant-design/icons';
import { PlusOutlined } from '@/shared/antd-imports';
import BoardViewTaskCard from '../board-task-card/board-view-task-card';
import BoardViewCreateTaskCard from '../board-task-card/board-view-create-task-card';
import { ITaskListGroup } from '@/types/tasks/taskList.types';

View File

@@ -1,4 +1,4 @@
import { Flex } from 'antd';
import { Flex } from '@/shared/antd-imports';
import { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';
import BoardSectionCard from './board-section-card/board-section-card';
import BoardCreateSectionCard from './board-section-card/board-create-section-card';

View File

@@ -1,4 +1,4 @@
import { Flex, Input, InputRef } from 'antd';
import { Flex, Input, InputRef } from '@/shared/antd-imports';
import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,12 +1,12 @@
import { useCallback, useState } from 'react';
import dayjs, { Dayjs } from 'dayjs';
import { Col, Flex, Typography, List, Dropdown, MenuProps, Popconfirm } from 'antd';
import { Col, Flex, Typography, List, Dropdown, MenuProps, Popconfirm } from '@/shared/antd-imports';
import {
UserAddOutlined,
DeleteOutlined,
ExclamationCircleFilled,
InboxOutlined,
} from '@ant-design/icons';
} from '@/shared/antd-imports';
import CustomAvatarGroup from '@/components/board/custom-avatar-group';
import CustomDueDatePicker from '@/components/board/custom-due-date-picker';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';

View File

@@ -1,4 +1,4 @@
import { Button, Flex, Input, InputRef } from 'antd';
import { Button, Flex, Input, InputRef } from '@/shared/antd-imports';
import React, { useRef, useState, useEffect } from 'react';
import { Dayjs } from 'dayjs';
import { nanoid } from '@reduxjs/toolkit';

View File

@@ -12,7 +12,7 @@ import {
Divider,
Popconfirm,
Skeleton,
} from 'antd';
} from '@/shared/antd-imports';
import {
DoubleRightOutlined,
PauseOutlined,
@@ -25,7 +25,7 @@ import {
CaretDownFilled,
ExclamationCircleFilled,
PlusOutlined,
} from '@ant-design/icons';
} from '@/shared/antd-imports';
import dayjs, { Dayjs } from 'dayjs';
import { useTranslation } from 'react-i18next';
import { useSortable } from '@dnd-kit/sortable';

View File

@@ -0,0 +1,583 @@
import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import TaskListFilters from '../taskList/task-list-filters/task-list-filters';
import { Flex, Skeleton } from 'antd';
import BoardSectionCardContainer from './board-section/board-section-container';
import {
fetchBoardTaskGroups,
reorderTaskGroups,
moveTaskBetweenGroups,
IGroupBy,
updateTaskProgress,
} from '@features/board/board-slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import {
DndContext,
DragEndEvent,
DragOverEvent,
DragStartEvent,
closestCenter,
DragOverlay,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
getFirstCollision,
pointerWithin,
rectIntersection,
UniqueIdentifier,
} from '@dnd-kit/core';
import BoardViewTaskCard from './board-section/board-task-card/board-view-task-card';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
import useTabSearchParam from '@/hooks/useTabSearchParam';
import { useSocket } from '@/socket/socketContext';
import { useAuthService } from '@/hooks/useAuth';
import { SocketEvents } from '@/shared/socket-events';
import alertService from '@/services/alerts/alertService';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
import { evt_project_board_visit, evt_project_task_list_drag_and_move } from '@/shared/worklenz-analytics-events';
import { ITaskStatusCreateRequest } from '@/types/tasks/task-status-create-request';
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
import logger from '@/utils/errorLogger';
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
import { debounce } from 'lodash';
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
import { updateTaskPriority as updateBoardTaskPriority } from '@/features/board/board-slice';
interface DroppableContainer {
id: UniqueIdentifier;
data: {
current?: {
type?: string;
};
};
}
const ProjectViewBoard = () => {
const dispatch = useAppDispatch();
const { projectView } = useTabSearchParam();
const { socket } = useSocket();
const authService = useAuthService();
const currentSession = authService.getCurrentSession();
const { trackMixpanelEvent } = useMixpanelTracking();
const [currentTaskIndex, setCurrentTaskIndex] = useState(-1);
// Add local loading state to immediately show skeleton
const [isLoading, setIsLoading] = useState(true);
const { projectId } = useAppSelector(state => state.projectReducer);
const { taskGroups, groupBy, loadingGroups, search, archived } = useAppSelector(state => state.boardReducer);
const { statusCategories, loading: loadingStatusCategories } = useAppSelector(
state => state.taskStatusReducer
);
const [activeItem, setActiveItem] = useState<any>(null);
// Store the original source group ID when drag starts
const originalSourceGroupIdRef = useRef<string | null>(null);
const lastOverId = useRef<UniqueIdentifier | null>(null);
const recentlyMovedToNewContainer = useRef(false);
const [clonedItems, setClonedItems] = useState<any>(null);
const isDraggingRef = useRef(false);
// Update loading state based on all loading conditions
useEffect(() => {
setIsLoading(loadingGroups || loadingStatusCategories);
}, [loadingGroups, loadingStatusCategories]);
// Load data efficiently with async/await and Promise.all
useEffect(() => {
const loadData = async () => {
if (projectId && groupBy && projectView === 'kanban') {
const promises = [];
if (!loadingGroups) {
promises.push(dispatch(fetchBoardTaskGroups(projectId)));
}
if (!statusCategories.length) {
promises.push(dispatch(fetchStatusesCategories()));
}
// Wait for all data to load
await Promise.all(promises);
}
};
loadData();
}, [dispatch, projectId, groupBy, projectView, search, archived]);
// Create sensors with memoization to prevent unnecessary re-renders
const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint: {
distance: 10,
delay: 100,
tolerance: 5,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 5,
},
})
);
const collisionDetectionStrategy = useCallback(
(args: {
active: { id: UniqueIdentifier; data: { current?: { type?: string } } };
droppableContainers: DroppableContainer[];
}) => {
if (activeItem?.type === 'section') {
return closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container: DroppableContainer) => container.data.current?.type === 'section'
),
});
}
// Start by finding any intersecting droppable
const pointerIntersections = pointerWithin(args);
const intersections =
pointerIntersections.length > 0
? pointerIntersections
: rectIntersection(args);
let overId = getFirstCollision(intersections, 'id');
if (overId !== null) {
const overContainer = args.droppableContainers.find(
(container: DroppableContainer) => container.id === overId
);
if (overContainer?.data.current?.type === 'section') {
const containerItems = taskGroups.find(
(group) => group.id === overId
)?.tasks || [];
if (containerItems.length > 0) {
overId = closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container: DroppableContainer) =>
container.id !== overId &&
container.data.current?.type === 'task'
),
})[0]?.id;
}
}
lastOverId.current = overId;
return [{ id: overId }];
}
if (recentlyMovedToNewContainer.current) {
lastOverId.current = activeItem?.id;
}
return lastOverId.current ? [{ id: lastOverId.current }] : [];
},
[activeItem, taskGroups]
);
const handleTaskProgress = (data: {
id: string;
status: string;
complete_ratio: number;
completed_count: number;
total_tasks_count: number;
parent_task: string;
}) => {
dispatch(updateTaskProgress(data));
};
// Debounced move task function to prevent rapid updates
const debouncedMoveTask = useCallback(
debounce((taskId: string, sourceGroupId: string, targetGroupId: string, targetIndex: number) => {
dispatch(
moveTaskBetweenGroups({
taskId,
sourceGroupId,
targetGroupId,
targetIndex,
})
);
}, 100),
[dispatch]
);
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
isDraggingRef.current = true;
setActiveItem(active.data.current);
setCurrentTaskIndex(active.data.current?.sortable.index);
if (active.data.current?.type === 'task') {
originalSourceGroupIdRef.current = active.data.current.sectionId;
}
setClonedItems(taskGroups);
};
const findGroupForId = (id: string) => {
// If id is a sectionId
if (taskGroups.some(group => group.id === id)) return id;
// If id is a taskId, find the group containing it
const group = taskGroups.find(g => g.tasks.some(t => t.id === id));
return group?.id;
};
const handleDragOver = (event: DragOverEvent) => {
try {
if (!isDraggingRef.current) return;
const { active, over } = event;
if (!over) return;
// Get the ids
const activeId = active.id;
const overId = over.id;
// Find the group (section) for each
const activeGroupId = findGroupForId(activeId as string);
const overGroupId = findGroupForId(overId as string);
// Only move if both groups exist and are different, and the active is a task
if (
activeGroupId &&
overGroupId &&
active.data.current?.type === 'task'
) {
// Find the target index in the over group
const targetGroup = taskGroups.find(g => g.id === overGroupId);
let targetIndex = 0;
if (targetGroup) {
// If over is a task, insert before it; if over is a section, append to end
if (over.data.current?.type === 'task') {
targetIndex = targetGroup.tasks.findIndex(t => t.id === overId);
if (targetIndex === -1) targetIndex = targetGroup.tasks.length;
} else {
targetIndex = targetGroup.tasks.length;
}
}
// Use debounced move task to prevent rapid updates
debouncedMoveTask(
activeId as string,
activeGroupId,
overGroupId,
targetIndex
);
}
} catch (error) {
console.error('handleDragOver error:', error);
}
};
const handlePriorityChange = (taskId: string, priorityId: string) => {
if (!taskId || !priorityId || !socket) return;
const payload = {
task_id: taskId,
priority_id: priorityId,
team_id: currentSession?.team_id,
};
socket.emit(SocketEvents.TASK_PRIORITY_CHANGE.toString(), JSON.stringify(payload));
socket.once(SocketEvents.TASK_PRIORITY_CHANGE.toString(), (data: ITaskListPriorityChangeResponse) => {
dispatch(updateBoardTaskPriority(data));
});
};
const handleDragEnd = async (event: DragEndEvent) => {
isDraggingRef.current = false;
const { active, over } = event;
if (!over || !projectId) {
setActiveItem(null);
originalSourceGroupIdRef.current = null;
setClonedItems(null);
return;
}
const isActiveTask = active.data.current?.type === 'task';
const isActiveSection = active.data.current?.type === 'section';
// Handle task dragging between columns
if (isActiveTask) {
const task = active.data.current?.task;
// Use the original source group ID from ref instead of the potentially modified one
const sourceGroupId = originalSourceGroupIdRef.current || active.data.current?.sectionId;
// Fix: Ensure we correctly identify the target group ID
let targetGroupId;
if (over.data.current?.type === 'task') {
// If dropping on a task, get its section ID
targetGroupId = over.data.current?.sectionId;
} else if (over.data.current?.type === 'section') {
// If dropping directly on a section
targetGroupId = over.id;
} else {
// Fallback to the over ID if type is not specified
targetGroupId = over.id;
}
// Find source and target groups
const sourceGroup = taskGroups.find(group => group.id === sourceGroupId);
const targetGroup = taskGroups.find(group => group.id === targetGroupId);
if (!sourceGroup || !targetGroup || !task) {
logger.error('Could not find source or target group, or task is undefined');
setActiveItem(null);
originalSourceGroupIdRef.current = null; // Reset the ref
return;
}
if (targetGroupId !== sourceGroupId) {
const canContinue = await checkTaskDependencyStatus(task.id, targetGroupId);
if (!canContinue) {
alertService.error(
'Task is not completed',
'Please complete the task dependencies before proceeding'
);
dispatch(
moveTaskBetweenGroups({
taskId: task.id,
sourceGroupId: targetGroupId, // Current group (where it was moved optimistically)
targetGroupId: sourceGroupId, // Move it back to the original source group
targetIndex: currentTaskIndex !== -1 ? currentTaskIndex : 0, // Original position or append to end
})
);
setActiveItem(null);
originalSourceGroupIdRef.current = null;
return;
}
}
// Find indices
let fromIndex = sourceGroup.tasks.findIndex(t => t.id === task.id);
// Handle case where task is not found in source group (might have been moved already in UI)
if (fromIndex === -1) {
logger.info('Task not found in source group. Using task sort_order from task object.');
// Use the sort_order from the task object itself
const fromSortOrder = task.sort_order;
// Calculate target index and position
let toIndex = -1;
if (over.data.current?.type === 'task') {
const overTaskId = over.data.current?.task.id;
toIndex = targetGroup.tasks.findIndex(t => t.id === overTaskId);
} else {
// If dropping on a section, append to the end
toIndex = targetGroup.tasks.length;
}
// Calculate toPos similar to Angular implementation
const toPos = targetGroup.tasks[toIndex]?.sort_order ||
targetGroup.tasks[targetGroup.tasks.length - 1]?.sort_order ||
-1;
// Prepare socket event payload
const body = {
project_id: projectId,
from_index: fromSortOrder,
to_index: toPos,
to_last_index: !toPos,
from_group: sourceGroupId,
to_group: targetGroupId,
group_by: groupBy || 'status',
task,
team_id: currentSession?.team_id
};
// Emit socket event
if (socket) {
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body);
// Set up listener for task progress update
socket.once(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), () => {
if (task.is_sub_task) {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
} else {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
}
});
// Handle priority change if groupBy is priority
if (groupBy === IGroupBy.PRIORITY) {
handlePriorityChange(task.id, targetGroupId);
}
}
// Track analytics event
trackMixpanelEvent(evt_project_task_list_drag_and_move);
setActiveItem(null);
originalSourceGroupIdRef.current = null; // Reset the ref
return;
}
// Calculate target index and position
let toIndex = -1;
if (over.data.current?.type === 'task') {
const overTaskId = over.data.current?.task.id;
toIndex = targetGroup.tasks.findIndex(t => t.id === overTaskId);
} else {
// If dropping on a section, append to the end
toIndex = targetGroup.tasks.length;
}
// Calculate toPos similar to Angular implementation
const toPos = targetGroup.tasks[toIndex]?.sort_order ||
targetGroup.tasks[targetGroup.tasks.length - 1]?.sort_order ||
-1;
// Prepare socket event payload
const body = {
project_id: projectId,
from_index: sourceGroup.tasks[fromIndex].sort_order,
to_index: toPos,
to_last_index: !toPos,
from_group: sourceGroupId, // Use the direct IDs instead of group objects
to_group: targetGroupId, // Use the direct IDs instead of group objects
group_by: groupBy || 'status', // Use the current groupBy value
task,
team_id: currentSession?.team_id
};
// Emit socket event
if (socket) {
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body);
// Set up listener for task progress update
socket.once(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), () => {
if (task.is_sub_task) {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
} else {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
}
});
}
// Track analytics event
trackMixpanelEvent(evt_project_task_list_drag_and_move);
}
// Handle column reordering
else if (isActiveSection) {
// Don't allow reordering if groupBy is phases
if (groupBy === IGroupBy.PHASE) {
setActiveItem(null);
originalSourceGroupIdRef.current = null;
return;
}
const sectionId = active.id;
const fromIndex = taskGroups.findIndex(group => group.id === sectionId);
const toIndex = taskGroups.findIndex(group => group.id === over.id);
if (fromIndex !== -1 && toIndex !== -1) {
// Create a new array with the reordered groups
const reorderedGroups = [...taskGroups];
const [movedGroup] = reorderedGroups.splice(fromIndex, 1);
reorderedGroups.splice(toIndex, 0, movedGroup);
// Dispatch action to reorder columns with the new array
dispatch(reorderTaskGroups(reorderedGroups));
// Prepare column order for API
const columnOrder = reorderedGroups.map(group => group.id);
// Call API to update status order
try {
// Use the correct API endpoint based on the Angular code
const requestBody: ITaskStatusCreateRequest = {
status_order: columnOrder
};
const response = await statusApiService.updateStatusOrder(requestBody, projectId);
if (!response.done) {
const revertedGroups = [...reorderedGroups];
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
revertedGroups.splice(fromIndex, 0, movedBackGroup);
dispatch(reorderTaskGroups(revertedGroups));
alertService.error('Failed to update column order', 'Please try again');
}
} catch (error) {
// Revert the change if API call fails
const revertedGroups = [...reorderedGroups];
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
revertedGroups.splice(fromIndex, 0, movedBackGroup);
dispatch(reorderTaskGroups(revertedGroups));
alertService.error('Failed to update column order', 'Please try again');
}
}
}
setActiveItem(null);
originalSourceGroupIdRef.current = null; // Reset the ref
};
const handleDragCancel = () => {
isDraggingRef.current = false;
if (clonedItems) {
dispatch(reorderTaskGroups(clonedItems));
}
setActiveItem(null);
setClonedItems(null);
originalSourceGroupIdRef.current = null;
};
// Reset the recently moved flag after animation frame
useEffect(() => {
requestAnimationFrame(() => {
recentlyMovedToNewContainer.current = false;
});
}, [taskGroups]);
useEffect(() => {
if (socket) {
socket.on(SocketEvents.GET_TASK_PROGRESS.toString(), handleTaskProgress);
}
return () => {
socket?.off(SocketEvents.GET_TASK_PROGRESS.toString(), handleTaskProgress);
};
}, [socket]);
// Track analytics event on component mount
useEffect(() => {
trackMixpanelEvent(evt_project_board_visit);
}, []);
// Cleanup debounced function on unmount
useEffect(() => {
return () => {
debouncedMoveTask.cancel();
};
}, [debouncedMoveTask]);
return (
<Flex vertical gap={16}>
<TaskListFilters position={'board'} />
<Skeleton active loading={isLoading} className='mt-4 p-4'>
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<BoardSectionCardContainer
datasource={taskGroups}
group={groupBy as 'status' | 'priority' | 'phases'}
/>
<DragOverlay>
{activeItem?.type === 'task' && (
<BoardViewTaskCard task={activeItem.task} sectionId={activeItem.sectionId} />
)}
</DragOverlay>
</DndContext>
</Skeleton>
</Flex>
);
};
export default ProjectViewBoard;

View File

@@ -8,7 +8,7 @@ import {
TableProps,
Tooltip,
Typography,
} from 'antd';
} from '@/shared/antd-imports';
import { useEffect, useState } from 'react';
import { colors } from '@/styles/colors';
import {
@@ -18,7 +18,7 @@ import {
DeleteOutlined,
ExclamationCircleFilled,
ExclamationCircleOutlined,
} from '@ant-design/icons';
} from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
import { durationDateFormat } from '@utils/durationDateFormat';
import { DEFAULT_PAGE_SIZE, IconsMap } from '@/shared/constants';

View File

@@ -1,4 +1,4 @@
import { Card, Flex, Typography } from 'antd';
import { Card, Flex, Typography } from '@/shared/antd-imports';
import TaskByMembersTable from './tables/tasks-by-members';
import MemberStats from '../member-stats/member-stats';

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Flex, Tooltip, Typography } from 'antd';
import { Flex, Tooltip, Typography } from '@/shared/antd-imports';
import { projectInsightsApiService } from '@/api/projects/insights/project-insights.api.service';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import { Flex, Progress } from 'antd';
import { Flex, Progress } from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import { useAppSelector } from '@/hooks/useAppSelector';
import { themeWiseColor } from '@/utils/themeWiseColor';
import { DownOutlined, ExclamationCircleOutlined, RightOutlined } from '@ant-design/icons';
import { DownOutlined, ExclamationCircleOutlined, RightOutlined } from '@/shared/antd-imports';
import logger from '@/utils/errorLogger';
import { projectsApiService } from '@/api/projects/projects.api.service';
import { useTranslation } from 'react-i18next';

View File

@@ -1,7 +1,7 @@
import { Bar } from 'react-chartjs-2';
import { Chart, ArcElement, Tooltip, CategoryScale, LinearScale, BarElement } from 'chart.js';
import { ChartOptions } from 'chart.js';
import { Flex } from 'antd';
import { Flex } from '@/shared/antd-imports';
import { ITaskPriorityCounts } from '@/types/project/project-insights.types';
import { useEffect, useState } from 'react';
import { projectInsightsApiService } from '@/api/projects/insights/project-insights.api.service';

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Doughnut } from 'react-chartjs-2';
import { Chart, ArcElement } from 'chart.js';
import { Badge, Flex, Tooltip, Typography, Spin } from 'antd';
import { Badge, Flex, Tooltip, Typography, Spin } from '@/shared/antd-imports';
import { ChartOptions } from 'chart.js';
import { projectInsightsApiService } from '@/api/projects/insights/project-insights.api.service';
import { ITaskStatusCounts } from '@/types/project/project-insights.types';

View File

@@ -1,4 +1,4 @@
import { Button, Card, Flex, Typography } from 'antd';
import { Button, Card, Flex, Typography } from '@/shared/antd-imports';
import StatusOverview from './graphs/status-overview';
import PriorityOverview from './graphs/priority-overview';

View File

@@ -1,4 +1,4 @@
import { Flex, Table, Tooltip, Typography } from 'antd';
import { Flex, Table, Tooltip, Typography } from '@/shared/antd-imports';
import { useEffect, useState } from 'react';
import { colors } from '@/styles/colors';
import { TableProps } from 'antd/lib';

View File

@@ -1,4 +1,4 @@
import { Card, Flex, Skeleton, Table, Typography } from 'antd';
import { Card, Flex, Skeleton, Table, Typography } from '@/shared/antd-imports';
import { useEffect, useState } from 'react';
import { colors } from '@/styles/colors';
import { TableProps } from 'antd/lib';

View File

@@ -1,5 +1,5 @@
import { Button, Card, Flex, Tooltip, Typography } from 'antd';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { Button, Card, Flex, Tooltip, Typography } from '@/shared/antd-imports';
import { ExclamationCircleOutlined } from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import OverdueTasksTable from './tables/overdue-tasks-table';
import OverLoggedTasksTable from './tables/over-logged-tasks-table';

View File

@@ -1,8 +1,8 @@
import { Avatar, Button, Flex, Table, Typography } from 'antd';
import { Avatar, Button, Flex, Table, Typography } from '@/shared/antd-imports';
import { useState, useEffect } from 'react';
import { colors } from '@/styles/colors';
import { TableProps } from 'antd/lib';
import { PlusOutlined } from '@ant-design/icons';
import { PlusOutlined } from '@/shared/antd-imports';
import { IInsightTasks } from '@/types/project/projectInsights.types';
import logger from '@/utils/errorLogger';
import { projectInsightsApiService } from '@/api/projects/insights/project-insights.api.service';

View File

@@ -1,4 +1,4 @@
import { Flex, Table, Typography } from 'antd';
import { Flex, Table, Typography } from '@/shared/antd-imports';
import { useEffect, useState } from 'react';
import { colors } from '@/styles/colors';
import { TableProps } from 'antd/lib';

View File

@@ -1,4 +1,4 @@
import { Flex, Table, Typography } from 'antd';
import { Flex, Table, Typography } from '@/shared/antd-imports';
import { TableProps } from 'antd/lib';
import { useEffect, useState } from 'react';

View File

@@ -1,4 +1,4 @@
import { Flex, Table, Typography } from 'antd';
import { Flex, Table, Typography } from '@/shared/antd-imports';
import { useEffect, useState } from 'react';
import { colors } from '@/styles/colors';
import { TableProps } from 'antd/lib';

View File

@@ -1,5 +1,5 @@
import ProjectStatsCard from '@/components/projects/project-stats-card';
import { Flex } from 'antd';
import { Flex } from '@/shared/antd-imports';
import groupIcon from '@/assets/icons/insightsIcons/group.png';
import warningIcon from '@/assets/icons/insightsIcons/warning.png';
import unassignedIcon from '@/assets/icons/insightsIcons/block-user.png';

View File

@@ -1,5 +1,5 @@
import ProjectStatsCard from '@/components/projects/project-stats-card';
import { Flex, Tooltip } from 'antd';
import { Flex, Tooltip } from '@/shared/antd-imports';
import checkIcon from '@assets/icons/insightsIcons/insights-check.png';
import clipboardIcon from '@assets/icons/insightsIcons/clipboard.png';
import clockIcon from '@assets/icons/insightsIcons/clock-green.png';

View File

@@ -1,5 +1,5 @@
import { DownloadOutlined } from '@ant-design/icons';
import { Badge, Button, Checkbox, Flex, Segmented } from 'antd';
import { DownloadOutlined } from '@/shared/antd-imports';
import { Badge, Button, Checkbox, Flex, Segmented } from '@/shared/antd-imports';
import { useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';

View File

@@ -11,10 +11,11 @@ import {
TableProps,
Tooltip,
Typography,
} from 'antd';
Input
} from '@/shared/antd-imports';
// Icons
import { DeleteOutlined, ExclamationCircleFilled, SyncOutlined } from '@ant-design/icons';
import { DeleteOutlined, ExclamationCircleFilled, SyncOutlined } from '@/shared/antd-imports';
// React & Router
import { useEffect, useState } from 'react';
@@ -70,26 +71,29 @@ const ProjectViewMembers = () => {
field: 'name',
order: 'ascend',
total: 0,
pageSizeOptions: ['5', '10', '15', '20', '50', '100'],
pageSizeOptions: ['10', '20', '50', '100'],
size: 'small',
});
const [searchQuery, setSearchQuery] = useState(''); // <-- Add search state
// API Functions
const getProjectMembers = async () => {
const getProjectMembers = async (search: string = searchQuery) => {
if (!projectId) return;
setIsLoading(true);
try {
const offset = (pagination.current - 1) * pagination.pageSize;
const res = await projectsApiService.getMembers(
projectId,
pagination.current,
pagination.pageSize,
pagination.current, // index
pagination.pageSize, // size // offset
pagination.field,
pagination.order,
null
search
);
if (res.done) {
setMembers(res.body);
setPagination(p => ({ ...p, total: res.body.total ?? 0 })); // update total from backend, default to 0
}
} catch (error) {
logger.error('Error fetching members:', error);
@@ -123,16 +127,14 @@ const ProjectViewMembers = () => {
return Math.floor((completed / total) * 100);
};
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
setPagination({
current: pagination.current,
pageSize: pagination.pageSize,
field: sorter.field || pagination.field,
order: sorter.order || pagination.order,
total: pagination.total,
pageSizeOptions: pagination.pageSizeOptions,
size: pagination.size,
});
const handleTableChange = (tablePagination: any, filters: any, sorter: any) => {
setPagination(prev => ({
...prev,
current: tablePagination.current,
pageSize: tablePagination.pageSize,
field: sorter.field || prev.field,
order: sorter.order || prev.order,
}));
};
// Effects
@@ -145,6 +147,7 @@ const ProjectViewMembers = () => {
pagination.pageSize,
pagination.field,
pagination.order,
// searchQuery, // <-- Do NOT include here, search is triggered manually
]);
useEffect(() => {
@@ -269,18 +272,33 @@ const ProjectViewMembers = () => {
<Card
style={{ width: '100%' }}
title={
<Flex justify="space-between">
<Flex justify="space-between" align="center">
<Typography.Text style={{ fontSize: 16, fontWeight: 500 }}>
{members?.total} {members?.total !== 1 ? t('membersCountPlural') : t('memberCount')}
</Typography.Text>
<Tooltip title={t('refreshButtonTooltip')}>
<Button
shape="circle"
icon={<SyncOutlined />}
onClick={() => void getProjectMembers()}
<Flex gap={8} align="center">
<Input.Search
allowClear
placeholder={t('searchPlaceholder')}
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onSearch={value => {
setPagination(p => ({ ...p, current: 1 })); // Reset to first page
void getProjectMembers(value);
}}
style={{ width: 220 }}
enterButton
size="middle"
/>
</Tooltip>
<Tooltip title={t('refreshButtonTooltip')}>
<Button
shape="circle"
icon={<SyncOutlined />}
onClick={() => void getProjectMembers()}
/>
</Tooltip>
</Flex>
</Flex>
}
>
@@ -299,8 +317,12 @@ const ProjectViewMembers = () => {
columns={columns}
rowKey={record => record.id}
pagination={{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
defaultPageSize: 20,
pageSizeOptions: pagination.pageSizeOptions,
size: pagination.size,
}}
onChange={handleTableChange}
onRow={record => ({

View File

@@ -65,7 +65,7 @@ import { addTaskCardToTheTop, fetchBoardTaskGroups } from '@/features/board/boar
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
import { ShareAltOutlined } from '@ant-design/icons';
import { ShareAltOutlined } from '@/shared/antd-imports';
import { fetchStatuses } from '@/features/taskAttributes/taskStatusSlice';
const ProjectViewHeader = memo(() => {

View File

@@ -10,7 +10,6 @@ import {
Tabs,
PushpinFilled,
PushpinOutlined,
type TabsProps,
} from '@/shared/antd-imports';
import { useAppDispatch } from '@/hooks/useAppDispatch';
@@ -39,7 +38,7 @@ import { resetState as resetEnhancedKanbanState } from '@/features/enhanced-kanb
import { setProjectId as setInsightsProjectId } from '@/features/projects/insights/project-insights.slice';
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
import { useTranslation } from 'react-i18next';
import { useTimerInitialization } from '@/hooks/useTimerInitialization';
// Import critical components synchronously to avoid suspense interruptions
import TaskDrawer from '@components/task-drawer/task-drawer';
@@ -89,6 +88,9 @@ const ProjectView = React.memo(() => {
const [taskid, setTaskId] = useState<string>(urlParams.taskId);
const [isInitialized, setIsInitialized] = useState(false);
// Initialize timer state from backend when project view loads
useTimerInitialization();
// Update local state when URL params change
useEffect(() => {
setActiveTab(urlParams.tab);

View File

@@ -8,7 +8,7 @@ import Dropdown from 'antd/es/dropdown';
import Input from 'antd/es/input';
import Typography from 'antd/es/typography';
import { MenuProps } from 'antd/es/menu';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@ant-design/icons';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -17,7 +17,7 @@ import {
DragEndEvent,
DragStartEvent,
} from '@dnd-kit/core';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@ant-design/icons';
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@/shared/antd-imports';
import { colors } from '@/styles/colors';
import { useAppSelector } from '@/hooks/useAppSelector';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { TaskPriorityType, TaskType } from '../../../../../../types/task.types';
import { Flex } from 'antd';
import { Flex } from '@/shared/antd-imports';
import TaskListTableWrapper from '../../task-list-table/task-list-table-wrapper/task-list-table-wrapper';
import { useAppSelector } from '../../../../../../hooks/useAppSelector';
import { getPriorityColor } from '../../../../../../utils/getPriorityColors';

View File

@@ -5,8 +5,8 @@ import {
LoadingOutlined,
RetweetOutlined,
UserAddOutlined,
} from '@ant-design/icons';
import { Badge, Dropdown, Flex, Typography, Modal } from 'antd';
} from '@/shared/antd-imports';
import { Badge, Dropdown, Flex, Typography, Modal } from '@/shared/antd-imports';
import { MenuProps } from 'antd/lib';
import { useState } from 'react';
import { TFunction } from 'i18next';

View File

@@ -1,6 +1,6 @@
import { Badge, Card, Dropdown, Empty, Flex, Menu, MenuProps, Typography } from 'antd';
import { Badge, Card, Dropdown, Empty, Flex, Menu, MenuProps, Typography } from '@/shared/antd-imports';
import React, { useState, useEffect } from 'react';
import { DownOutlined } from '@ant-design/icons';
import { DownOutlined } from '@/shared/antd-imports';
// custom css file
import './custom-column-label-cell.css';
import { useTranslation } from 'react-i18next';

View File

@@ -1,6 +1,6 @@
import { Badge, Card, Dropdown, Empty, Flex, Menu, MenuProps, Typography } from 'antd';
import { Badge, Card, Dropdown, Empty, Flex, Menu, MenuProps, Typography } from '@/shared/antd-imports';
import React, { useState, useEffect } from 'react';
import { DownOutlined } from '@ant-design/icons';
import { DownOutlined } from '@/shared/antd-imports';
// custom css file
import './custom-column-selection-cell.css';
import { useTranslation } from 'react-i18next';

View File

@@ -1,5 +1,5 @@
import { SettingOutlined } from '@ant-design/icons';
import { Button, Flex, Tooltip, Typography } from 'antd';
import { SettingOutlined } from '@/shared/antd-imports';
import { Button, Flex, Tooltip, Typography } from '@/shared/antd-imports';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import CustomColumnModal from '../custom-column-modal/custom-column-modal';

View File

@@ -1,5 +1,5 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import { PlusOutlined } from '@/shared/antd-imports';
import { Button, Tooltip } from '@/shared/antd-imports';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import {
setCustomColumnModalAttributes,

View File

@@ -51,7 +51,7 @@ import {
} from '@/features/task-management/task-management.slice';
import { useParams } from 'react-router-dom';
import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { ExclamationCircleFilled } from '@/shared/antd-imports';
const CustomColumnModal = () => {
const [mainForm] = Form.useForm();

Some files were not shown because too many files have changed in this diff Show More