Merge branch 'release-v2.1.4' into feature/task-activities-by-user
This commit is contained in:
@@ -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 = () => {
|
||||
|
||||
8
worklenz-frontend/src/pages/GanttDemoPage.tsx
Normal file
8
worklenz-frontend/src/pages/GanttDemoPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { AdvancedGanttDemo } from '../components/advanced-gantt';
|
||||
|
||||
const GanttDemoPage: React.FC = () => {
|
||||
return <AdvancedGanttDemo />;
|
||||
};
|
||||
|
||||
export default GanttDemoPage;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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%',
|
||||
@@ -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';
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
63
worklenz-frontend/src/pages/projects/ProjectGanttView.tsx
Normal file
63
worklenz-frontend/src/pages/projects/ProjectGanttView.tsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Progress, Tooltip } from 'antd';
|
||||
import { Progress, Tooltip } from '@/shared/antd-imports';
|
||||
import React from 'react';
|
||||
import './TaskProgress.css';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user