Merge branch 'feature/project-list-grouping' into upstream/feature/project-groupby
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Flex, Typography } from 'antd';
|
||||
import logo from '../assets/images/logo.png';
|
||||
import logoDark from '@/assets/images/logo-dark-mode.png';
|
||||
import logo from '@/assets/images/worklenz-light-mode.png';
|
||||
import logoDark from '@/assets/images/worklenz-dark-mode.png';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
type AuthPageHeaderProp = {
|
||||
|
||||
@@ -8,7 +8,7 @@ type EmptyListPlaceholderProps = {
|
||||
};
|
||||
|
||||
const EmptyListPlaceholder = ({
|
||||
imageSrc = '/assets/images/empty-box.webp',
|
||||
imageSrc = 'https://s3.us-west-2.amazonaws.com/worklenz.com/assets/empty-box.webp',
|
||||
imageHeight = 60,
|
||||
text,
|
||||
}: EmptyListPlaceholderProps) => {
|
||||
|
||||
24
worklenz-frontend/src/components/HubSpot.tsx
Normal file
24
worklenz-frontend/src/components/HubSpot.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const HubSpot = () => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.id = 'hs-script-loader';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.src = '//js.hs-scripts.com/22348300.js';
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
const existingScript = document.getElementById('hs-script-loader');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default HubSpot;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FloatButton, Space, Tooltip } from 'antd';
|
||||
import { FormatPainterOutlined } from '@ant-design/icons';
|
||||
import LanguageSelector from '../features/i18n/language-selector';
|
||||
import ThemeSelector from '../features/theme/ThemeSelector';
|
||||
// import LanguageSelector from '../features/i18n/language-selector';
|
||||
// import ThemeSelector from '../features/theme/ThemeSelector';
|
||||
|
||||
const PreferenceSelector = () => {
|
||||
return (
|
||||
@@ -17,7 +17,7 @@ const PreferenceSelector = () => {
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<ThemeSelector />
|
||||
{/* <ThemeSelector /> */}
|
||||
</Space>
|
||||
</FloatButton.Group>
|
||||
</div>
|
||||
|
||||
@@ -246,7 +246,7 @@ const CurrentPlanDetails = () => {
|
||||
|
||||
const renderFreePlan = () => (
|
||||
<Flex vertical>
|
||||
<Typography.Text strong>Free Plan</Typography.Text>
|
||||
<Typography.Text strong>{t('freePlan')}</Typography.Text>
|
||||
<Typography.Text>
|
||||
<br />-{' '}
|
||||
{freePlanSettings?.team_member_limit === 0
|
||||
@@ -309,16 +309,16 @@ const CurrentPlanDetails = () => {
|
||||
|
||||
const renderCreditSubscriptionInfo = () => {
|
||||
return <Flex vertical>
|
||||
<Typography.Text strong>Credit Plan</Typography.Text>
|
||||
<Typography.Text strong>{t('creditPlan','Credit Plan')}</Typography.Text>
|
||||
</Flex>
|
||||
};
|
||||
};
|
||||
|
||||
const renderCustomSubscriptionInfo = () => {
|
||||
return <Flex vertical>
|
||||
<Typography.Text strong>Custom Plan</Typography.Text>
|
||||
<Typography.Text>Your plan is valid till {billingInfo?.valid_till_date}</Typography.Text>
|
||||
<Typography.Text strong>{t('customPlan','Custom Plan')}</Typography.Text>
|
||||
<Typography.Text>{t('planValidTill','Your plan is valid till {{date}}',{date: billingInfo?.valid_till_date})}</Typography.Text>
|
||||
</Flex>
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -381,15 +381,15 @@ const CurrentPlanDetails = () => {
|
||||
>
|
||||
<Flex vertical gap="middle" style={{ marginTop: '8px' }}>
|
||||
<Typography.Paragraph style={{ fontSize: '16px', margin: '0 0 16px 0', fontWeight: 500 }}>
|
||||
To continue, you'll need to purchase additional seats.
|
||||
{t('purchaseSeatsText','To continue, you\'ll need to purchase additional seats.')}
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Typography.Paragraph style={{ margin: '0 0 16px 0' }}>
|
||||
You currently have {billingInfo?.total_seats} seats available.
|
||||
{t('currentSeatsText','You currently have {{seats}} seats available.',{seats: billingInfo?.total_seats})}
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Typography.Paragraph style={{ margin: '0 0 24px 0' }}>
|
||||
Please select the number of additional seats to purchase.
|
||||
{t('selectSeatsText','Please select the number of additional seats to purchase.')}
|
||||
</Typography.Paragraph>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
@@ -416,14 +416,14 @@ const CurrentPlanDetails = () => {
|
||||
borderRadius: '2px'
|
||||
}}
|
||||
>
|
||||
Purchase
|
||||
{t('purchase','Purchase')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
>
|
||||
Contact sales
|
||||
{t('contactSales','Contact sales')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -10,22 +10,17 @@ import {
|
||||
Table,
|
||||
TableProps,
|
||||
Typography,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { toggleSettingDrawer, updateTeam } from '@/features/teams/teamSlice';
|
||||
import { TeamsType } from '@/types/admin-center/team.types';
|
||||
import './settings-drawer.css';
|
||||
import CustomAvatar from '@/components/CustomAvatar';
|
||||
import { teamsApiService } from '@/api/teams/teams.api.service';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
||||
import {
|
||||
IOrganizationTeam,
|
||||
IOrganizationTeamMember,
|
||||
} from '@/types/admin-center/admin-center.types';
|
||||
import Avatars from '@/components/avatars/avatars';
|
||||
import { AvatarNamesMap } from '@/shared/constants';
|
||||
import SingleAvatar from '@/components/common/single-avatar/single-avatar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -68,26 +63,30 @@ const SettingTeamDrawer: React.FC<SettingTeamDrawerProps> = ({
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (values: any) => {
|
||||
console.log(values);
|
||||
// const newTeam: TeamsType = {
|
||||
// teamId: teamId,
|
||||
// teamName: values.name,
|
||||
// membersCount: team?.membersCount || 1,
|
||||
// members: team?.members || ['Raveesha Dilanka'],
|
||||
// owner: values.name,
|
||||
// created: team?.created || new Date(),
|
||||
// isActive: false,
|
||||
// };
|
||||
// dispatch(updateTeam(newTeam));
|
||||
// dispatch(toggleSettingDrawer());
|
||||
// form.resetFields();
|
||||
// message.success('Team updated!');
|
||||
try {
|
||||
setUpdatingTeam(true);
|
||||
|
||||
const body = {
|
||||
name: values.name,
|
||||
teamMembers: teamData?.team_members || []
|
||||
};
|
||||
|
||||
const response = await adminCenterApiService.updateTeam(teamId, body);
|
||||
|
||||
if (response.done) {
|
||||
setIsSettingDrawerOpen(false);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error updating team', error);
|
||||
} finally {
|
||||
setUpdatingTeam(false);
|
||||
}
|
||||
};
|
||||
|
||||
const roleOptions = [
|
||||
{ value: 'Admin', label: t('admin') },
|
||||
{ value: 'Member', label: t('member') },
|
||||
{ value: 'Owner', label: t('owner') },
|
||||
{ key: 'Admin', value: 'Admin', label: t('admin') },
|
||||
{ key: 'Member', value: 'Member', label: t('member') },
|
||||
{ key: 'Owner', value: 'Owner', label: t('owner'), disabled: true },
|
||||
];
|
||||
|
||||
const columns: TableProps['columns'] = [
|
||||
@@ -104,16 +103,57 @@ const SettingTeamDrawer: React.FC<SettingTeamDrawerProps> = ({
|
||||
{
|
||||
title: t('role'),
|
||||
key: 'role',
|
||||
render: (_, record: IOrganizationTeamMember) => (
|
||||
<div>
|
||||
render: (_, record: IOrganizationTeamMember) => {
|
||||
const handleRoleChange = (value: string) => {
|
||||
if (value === 'Owner') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the team member's role in teamData
|
||||
if (teamData && teamData.team_members) {
|
||||
const updatedMembers = teamData.team_members.map(member => {
|
||||
if (member.id === record.id) {
|
||||
return { ...member, role_name: value };
|
||||
}
|
||||
return member;
|
||||
});
|
||||
|
||||
setTeamData({
|
||||
...teamData,
|
||||
team_members: updatedMembers
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = record.role_name === 'Owner' || record.pending_invitation;
|
||||
const tooltipTitle = record.role_name === 'Owner'
|
||||
? t('cannotChangeOwnerRole')
|
||||
: record.pending_invitation
|
||||
? t('pendingInvitation')
|
||||
: '';
|
||||
|
||||
const selectComponent = (
|
||||
<Select
|
||||
style={{ width: '150px', height: '32px' }}
|
||||
options={roleOptions.map(option => ({ ...option, key: option.value }))}
|
||||
options={roleOptions}
|
||||
defaultValue={record.role_name || ''}
|
||||
disabled={record.role_name === 'Owner'}
|
||||
disabled={isDisabled}
|
||||
onChange={handleRoleChange}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isDisabled ? (
|
||||
<Tooltip title={tooltipTitle}>
|
||||
{selectComponent}
|
||||
</Tooltip>
|
||||
) : (
|
||||
selectComponent
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ const HomeTasksStatusDropdown = ({ task, teamId }: HomeTasksStatusDropdownProps)
|
||||
const { socket, connected } = useSocket();
|
||||
const { homeTasksConfig } = useAppSelector(state => state.homePageReducer);
|
||||
const {
|
||||
refetch
|
||||
} = useGetMyTasksQuery(homeTasksConfig, {
|
||||
skip: true // Skip automatic queries entirely
|
||||
});
|
||||
refetch
|
||||
} = useGetMyTasksQuery(homeTasksConfig, {
|
||||
skip: false, // Ensure this query runs
|
||||
});
|
||||
|
||||
const [selectedStatus, setSelectedStatus] = useState<ITaskStatus | undefined>(undefined);
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ const HomeTasksDatePicker = ({ record }: HomeTasksDatePickerProps) => {
|
||||
const { t } = useTranslation('home');
|
||||
const { homeTasksConfig } = useAppSelector(state => state.homePageReducer);
|
||||
const { refetch } = useGetMyTasksQuery(homeTasksConfig, {
|
||||
skip: true // Skip automatic queries entirely
|
||||
skip: false
|
||||
});
|
||||
|
||||
|
||||
// Use useMemo to avoid re-renders when record.end_date is the same
|
||||
const initialDate = useMemo(() =>
|
||||
const initialDate = useMemo(() =>
|
||||
record.end_date ? dayjs(record.end_date) : null
|
||||
, [record.end_date]);
|
||||
|
||||
, [record.end_date]);
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<Dayjs | null>(initialDate);
|
||||
|
||||
// Update selected date when record changes
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Popconfirm,
|
||||
Skeleton,
|
||||
Space,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
@@ -46,7 +47,11 @@ import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse
|
||||
import { calculateTimeDifference } from '@/utils/calculate-time-difference';
|
||||
import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { setProjectData, toggleProjectDrawer, setProjectId as setDrawerProjectId } from '@/features/project/project-drawer.slice';
|
||||
import {
|
||||
setProjectData,
|
||||
toggleProjectDrawer,
|
||||
setProjectId as setDrawerProjectId,
|
||||
} from '@/features/project/project-drawer.slice';
|
||||
import useIsProjectManager from '@/hooks/useIsProjectManager';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { evt_projects_create } from '@/shared/worklenz-analytics-events';
|
||||
@@ -60,7 +65,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
|
||||
|
||||
// State
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const [selectedProjectManager, setSelectedProjectManager] = useState<ITeamMemberViewModel | null>(
|
||||
@@ -96,6 +101,9 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
working_days: project?.working_days || 0,
|
||||
man_days: project?.man_days || 0,
|
||||
hours_per_day: project?.hours_per_day || 8,
|
||||
use_manual_progress: project?.use_manual_progress || false,
|
||||
use_weighted_progress: project?.use_weighted_progress || false,
|
||||
use_time_progress: project?.use_time_progress || false,
|
||||
}),
|
||||
[project, projectStatuses, projectHealths]
|
||||
);
|
||||
@@ -155,6 +163,9 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
man_days: parseInt(values.man_days),
|
||||
hours_per_day: parseInt(values.hours_per_day),
|
||||
project_manager: selectedProjectManager,
|
||||
use_manual_progress: values.use_manual_progress || false,
|
||||
use_weighted_progress: values.use_weighted_progress || false,
|
||||
use_time_progress: values.use_time_progress || false,
|
||||
};
|
||||
|
||||
const action =
|
||||
@@ -169,7 +180,9 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
dispatch(toggleProjectDrawer());
|
||||
if (!editMode) {
|
||||
trackMixpanelEvent(evt_projects_create);
|
||||
navigate(`/worklenz/projects/${response.data.body.id}?tab=tasks-list&pinned_tab=tasks-list`);
|
||||
navigate(
|
||||
`/worklenz/projects/${response.data.body.id}?tab=tasks-list&pinned_tab=tasks-list`
|
||||
);
|
||||
}
|
||||
refetchProjects();
|
||||
window.location.reload(); // Refresh the page
|
||||
@@ -184,8 +197,17 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
logger.error('Error saving project', error);
|
||||
}
|
||||
};
|
||||
const calculateWorkingDays = (startDate: dayjs.Dayjs | null, endDate: dayjs.Dayjs | null): number => {
|
||||
if (!startDate || !endDate || !startDate.isValid() || !endDate.isValid() || startDate.isAfter(endDate)) {
|
||||
const calculateWorkingDays = (
|
||||
startDate: dayjs.Dayjs | null,
|
||||
endDate: dayjs.Dayjs | null
|
||||
): number => {
|
||||
if (
|
||||
!startDate ||
|
||||
!endDate ||
|
||||
!startDate.isValid() ||
|
||||
!endDate.isValid() ||
|
||||
startDate.isAfter(endDate)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -213,7 +235,16 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
...project,
|
||||
start_date: project.start_date ? dayjs(project.start_date) : null,
|
||||
end_date: project.end_date ? dayjs(project.end_date) : null,
|
||||
working_days: form.getFieldValue('start_date') && form.getFieldValue('end_date') ? calculateWorkingDays(form.getFieldValue('start_date'), form.getFieldValue('end_date')) : project.working_days || 0,
|
||||
working_days:
|
||||
form.getFieldValue('start_date') && form.getFieldValue('end_date')
|
||||
? calculateWorkingDays(
|
||||
form.getFieldValue('start_date'),
|
||||
form.getFieldValue('end_date')
|
||||
)
|
||||
: project.working_days || 0,
|
||||
use_manual_progress: project.use_manual_progress || false,
|
||||
use_weighted_progress: project.use_weighted_progress || false,
|
||||
use_time_progress: project.use_time_progress || false,
|
||||
});
|
||||
setSelectedProjectManager(project.project_manager || null);
|
||||
setLoading(false);
|
||||
@@ -235,7 +266,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
setLoading(true);
|
||||
resetForm();
|
||||
dispatch(setProjectData({} as IProjectViewModel));
|
||||
dispatch(setProjectId(null));
|
||||
// dispatch(setProjectId(null));
|
||||
dispatch(setDrawerProjectId(null));
|
||||
dispatch(toggleProjectDrawer());
|
||||
onClose();
|
||||
@@ -284,6 +315,49 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
setIsFormValid(isValid);
|
||||
};
|
||||
|
||||
// Progress calculation method handlers
|
||||
const handleManualProgressChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
form.setFieldsValue({
|
||||
use_manual_progress: true,
|
||||
use_weighted_progress: false,
|
||||
use_time_progress: false,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
use_manual_progress: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleWeightedProgressChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
form.setFieldsValue({
|
||||
use_manual_progress: false,
|
||||
use_weighted_progress: true,
|
||||
use_time_progress: false,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
use_weighted_progress: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleTimeProgressChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
form.setFieldsValue({
|
||||
use_manual_progress: false,
|
||||
use_weighted_progress: false,
|
||||
use_time_progress: true,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
use_time_progress: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
// loading={loading}
|
||||
@@ -329,12 +403,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
}
|
||||
>
|
||||
{!isEditable && (
|
||||
<Alert
|
||||
message={t('noPermission')}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Alert message={t('noPermission')} type="warning" showIcon style={{ marginBottom: 16 }} />
|
||||
)}
|
||||
<Skeleton active paragraph={{ rows: 12 }} loading={projectLoading}>
|
||||
<Form
|
||||
@@ -395,14 +464,11 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
|
||||
<Form.Item name="date" layout="horizontal">
|
||||
<Flex gap={8}>
|
||||
<Form.Item
|
||||
name="start_date"
|
||||
label={t('startDate')}
|
||||
>
|
||||
<Form.Item name="start_date" label={t('startDate')}>
|
||||
<DatePicker
|
||||
disabledDate={disabledStartDate}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
onChange={(date) => {
|
||||
onChange={date => {
|
||||
const endDate = form.getFieldValue('end_date');
|
||||
if (date && endDate) {
|
||||
const days = calculateWorkingDays(date, endDate);
|
||||
@@ -411,14 +477,11 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="end_date"
|
||||
label={t('endDate')}
|
||||
>
|
||||
<Form.Item name="end_date" label={t('endDate')}>
|
||||
<DatePicker
|
||||
disabledDate={disabledEndDate}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
onChange={(date) => {
|
||||
onChange={date => {
|
||||
const startDate = form.getFieldValue('start_date');
|
||||
if (startDate && date) {
|
||||
const days = calculateWorkingDays(startDate, date);
|
||||
@@ -429,22 +492,51 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
{/* <Form.Item
|
||||
|
||||
<Form.Item
|
||||
name="working_days"
|
||||
label={t('estimateWorkingDays')}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value === undefined || value >= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t('workingDaysValidationMessage', { min: 0 })));
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" min={0} disabled={!isProjectManager && !isOwnerorAdmin} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="man_days"
|
||||
label={t('estimateManDays')}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value === undefined || value >= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t('manDaysValidationMessage', { min: 0 })));
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
disabled // Make it read-only since it's calculated
|
||||
min={0}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
onBlur={e => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (value < 0) {
|
||||
form.setFieldsValue({ man_days: 0 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item> */}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="working_days" label={t('estimateWorkingDays')}>
|
||||
<Input type="number" disabled={!isProjectManager && !isOwnerorAdmin} />
|
||||
</Form.Item>
|
||||
<Form.Item name="man_days" label={t('estimateManDays')}>
|
||||
<Input type="number" disabled={!isProjectManager && !isOwnerorAdmin} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="hours_per_day"
|
||||
label={t('hoursPerDay')}
|
||||
@@ -454,12 +546,80 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
|
||||
if (value === undefined || (value >= 0 && value <= 24)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t('hoursPerDayValidationMessage', { min: 0, max: 24 })));
|
||||
return Promise.reject(
|
||||
new Error(t('hoursPerDayValidationMessage', { min: 0, max: 24 }))
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" disabled={!isProjectManager && !isOwnerorAdmin} />
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
onBlur={e => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (value < 0) {
|
||||
form.setFieldsValue({ hours_per_day: 8 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">{t('progressSettings')}</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="use_manual_progress"
|
||||
label={
|
||||
<Space>
|
||||
<Typography.Text>{t('manualProgress')}</Typography.Text>
|
||||
<Tooltip title={t('manualProgressTooltip')}>
|
||||
<Button type="text" size="small" icon={<Typography.Text>ⓘ</Typography.Text>} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
onChange={handleManualProgressChange}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="use_weighted_progress"
|
||||
label={
|
||||
<Space>
|
||||
<Typography.Text>{t('weightedProgress')}</Typography.Text>
|
||||
<Tooltip title={t('weightedProgressTooltip')}>
|
||||
<Button type="text" size="small" icon={<Typography.Text>ⓘ</Typography.Text>} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
onChange={handleWeightedProgressChange}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="use_time_progress"
|
||||
label={
|
||||
<Space>
|
||||
<Typography.Text>{t('timeProgress')}</Typography.Text>
|
||||
<Tooltip title={t('timeProgressTooltip')}>
|
||||
<Button type="text" size="small" icon={<Typography.Text>ⓘ</Typography.Text>} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
onChange={handleTimeProgressChange}
|
||||
disabled={!isProjectManager && !isOwnerorAdmin}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
|
||||
@@ -111,6 +111,32 @@ const TaskDrawerActivityLog = () => {
|
||||
</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
case IActivityLogAttributeTypes.PROGRESS:
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color="blue">
|
||||
{activity.previous || '0'}%
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<Tag color="blue">
|
||||
{activity.current || '0'}%
|
||||
</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
case IActivityLogAttributeTypes.WEIGHT:
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color="purple">
|
||||
Weight: {activity.previous || '100'}
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<Tag color="purple">
|
||||
Weight: {activity.current || '100'}
|
||||
</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
import { Form, InputNumber, Tooltip, Modal } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||
import Flex from 'antd/lib/flex';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.types';
|
||||
import { setTaskStatus } from '@/features/task-drawer/task-drawer.slice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { updateBoardTaskStatus } from '@/features/board/board-slice';
|
||||
import { updateTaskProgress, updateTaskStatus } from '@/features/tasks/tasks.slice';
|
||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||
|
||||
interface TaskDrawerProgressProps {
|
||||
task: ITaskViewModel;
|
||||
form: any;
|
||||
}
|
||||
|
||||
const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
||||
const { t } = useTranslation('task-drawer/task-drawer');
|
||||
const dispatch = useAppDispatch();
|
||||
const { tab } = useTabSearchParam();
|
||||
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
const { socket, connected } = useSocket();
|
||||
const [isCompletionModalVisible, setIsCompletionModalVisible] = useState(false);
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
|
||||
const isSubTask = !!task?.parent_task_id;
|
||||
// Safe handling of sub_tasks_count which might be undefined in some cases
|
||||
const hasSubTasks = (task?.sub_tasks_count || 0) > 0;
|
||||
|
||||
// HIGHEST PRIORITY CHECK: Never show progress inputs for parent tasks with subtasks
|
||||
if (hasSubTasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Never show manual progress input for parent tasks (tasks with subtasks)
|
||||
// Only show progress input for tasks without subtasks
|
||||
const showManualProgressInput = !hasSubTasks;
|
||||
|
||||
// Only show weight input for subtasks in weighted progress mode
|
||||
const showTaskWeightInput = project?.use_weighted_progress && isSubTask && !hasSubTasks;
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for progress updates from the server
|
||||
const handleProgressUpdate = (data: any) => {
|
||||
if (data.task_id === task.id) {
|
||||
if (data.progress_value !== undefined) {
|
||||
form.setFieldsValue({ progress_value: data.progress_value });
|
||||
}
|
||||
if (data.weight !== undefined) {
|
||||
form.setFieldsValue({ weight: data.weight });
|
||||
}
|
||||
|
||||
// Check if we should prompt the user to mark the task as done
|
||||
if (data.should_prompt_for_done) {
|
||||
setIsCompletionModalVisible(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
socket?.on(SocketEvents.TASK_PROGRESS_UPDATED.toString(), handleProgressUpdate);
|
||||
|
||||
// When the component mounts, explicitly request the latest progress for this task
|
||||
if (connected && task.id) {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||
}
|
||||
|
||||
return () => {
|
||||
socket?.off(SocketEvents.TASK_PROGRESS_UPDATED.toString(), handleProgressUpdate);
|
||||
};
|
||||
}, [socket, connected, task.id, form]);
|
||||
|
||||
// One last check before rendering
|
||||
if (hasSubTasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleProgressChange = (value: number | null) => {
|
||||
if (connected && task.id && value !== null && !hasSubTasks) {
|
||||
// Check if progress is set to 100% to show completion confirmation
|
||||
if (value === 100) {
|
||||
setIsCompletionModalVisible(true);
|
||||
}
|
||||
|
||||
// Ensure parent_task_id is not undefined
|
||||
const parent_task_id = task.parent_task_id || null;
|
||||
|
||||
socket?.emit(
|
||||
SocketEvents.UPDATE_TASK_PROGRESS.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
progress_value: value,
|
||||
parent_task_id: parent_task_id,
|
||||
})
|
||||
);
|
||||
|
||||
socket?.once(SocketEvents.GET_TASK_PROGRESS.toString(), (data: any) => {
|
||||
dispatch(
|
||||
updateTaskProgress({
|
||||
taskId: task.id,
|
||||
progress: data.complete_ratio,
|
||||
totalTasksCount: data.total_tasks_count,
|
||||
completedCount: data.completed_count,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (task.id) {
|
||||
setTimeout(() => {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// If this is a subtask, request the parent's progress to be updated in UI
|
||||
if (parent_task_id) {
|
||||
setTimeout(() => {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), parent_task_id);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleWeightChange = (value: number | null) => {
|
||||
if (connected && task.id && value !== null && !hasSubTasks) {
|
||||
// Ensure parent_task_id is not undefined
|
||||
const parent_task_id = task.parent_task_id || null;
|
||||
|
||||
socket?.emit(
|
||||
SocketEvents.UPDATE_TASK_WEIGHT.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
weight: value,
|
||||
parent_task_id: parent_task_id,
|
||||
})
|
||||
);
|
||||
|
||||
// If this is a subtask, request the parent's progress to be updated in UI
|
||||
if (parent_task_id) {
|
||||
setTimeout(() => {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), parent_task_id);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkTaskAsComplete = () => {
|
||||
// Close the modal
|
||||
setIsCompletionModalVisible(false);
|
||||
|
||||
// Find a "Done" status for this project
|
||||
if (connected && task.id) {
|
||||
// Emit socket event to get "done" category statuses
|
||||
socket?.emit(
|
||||
SocketEvents.GET_DONE_STATUSES.toString(),
|
||||
task.project_id,
|
||||
(doneStatuses: any[]) => {
|
||||
if (doneStatuses && doneStatuses.length > 0) {
|
||||
// Use the first "done" status
|
||||
const doneStatusId = doneStatuses[0].id;
|
||||
|
||||
// Emit socket event to update the task status
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
status_id: doneStatusId,
|
||||
project_id: task.project_id,
|
||||
team_id: currentSession?.team_id,
|
||||
parent_task: task.parent_task_id || null,
|
||||
})
|
||||
);
|
||||
socket?.once(
|
||||
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
||||
(data: ITaskListStatusChangeResponse) => {
|
||||
dispatch(setTaskStatus(data));
|
||||
|
||||
if (tab === 'tasks-list') {
|
||||
dispatch(updateTaskStatus(data));
|
||||
}
|
||||
if (tab === 'board') {
|
||||
dispatch(updateBoardTaskStatus(data));
|
||||
}
|
||||
if (data.parent_task)
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), data.parent_task);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
logger.error(`No "done" statuses found for project ${task.project_id}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const percentFormatter = (value: number | undefined) => (value ? `${value}%` : '0%');
|
||||
const percentParser = (value: string | undefined) => {
|
||||
const parsed = parseInt(value?.replace('%', '') || '0', 10);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
};
|
||||
|
||||
if (!showManualProgressInput && !showTaskWeightInput) {
|
||||
return null; // Don't show any progress inputs if not applicable
|
||||
}
|
||||
|
||||
// Final safety check
|
||||
if (hasSubTasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTaskWeightInput && (
|
||||
<Form.Item
|
||||
name="weight"
|
||||
label={
|
||||
<Flex align="center" gap={4}>
|
||||
{t('taskInfoTab.details.taskWeight')}
|
||||
<Tooltip title={t('taskInfoTab.details.taskWeightTooltip')}>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100,
|
||||
message: t('taskInfoTab.details.taskWeightRange'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
formatter={percentFormatter}
|
||||
parser={percentParser}
|
||||
onBlur={e => {
|
||||
let value = percentParser(e.target.value);
|
||||
// Ensure value doesn't exceed 100
|
||||
if (value > 100) {
|
||||
value = 100;
|
||||
form.setFieldsValue({ weight: 100 });
|
||||
}
|
||||
handleWeightChange(value);
|
||||
}}
|
||||
onChange={value => {
|
||||
if (value !== null && value > 100) {
|
||||
form.setFieldsValue({ weight: 100 });
|
||||
handleWeightChange(100);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{showManualProgressInput && (
|
||||
<Form.Item
|
||||
name="progress_value"
|
||||
label={
|
||||
<Flex align="center" gap={4}>
|
||||
{t('taskInfoTab.details.progressValue')}
|
||||
<Tooltip title={t('taskInfoTab.details.progressValueTooltip')}>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100,
|
||||
message: t('taskInfoTab.details.progressValueRange'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
formatter={percentFormatter}
|
||||
parser={percentParser}
|
||||
onBlur={e => {
|
||||
let value = percentParser(e.target.value);
|
||||
// Ensure value doesn't exceed 100
|
||||
if (value > 100) {
|
||||
value = 100;
|
||||
form.setFieldsValue({ progress_value: 100 });
|
||||
}
|
||||
handleProgressChange(value);
|
||||
}}
|
||||
onChange={value => {
|
||||
if (value !== null && value > 100) {
|
||||
form.setFieldsValue({ progress_value: 100 });
|
||||
handleProgressChange(100);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={t('taskProgress.markAsDoneTitle', 'Mark Task as Done?')}
|
||||
open={isCompletionModalVisible}
|
||||
onOk={handleMarkTaskAsComplete}
|
||||
onCancel={() => setIsCompletionModalVisible(false)}
|
||||
okText={t('taskProgress.confirmMarkAsDone', 'Yes, mark as done')}
|
||||
cancelText={t('taskProgress.cancelMarkAsDone', 'No, keep current status')}
|
||||
>
|
||||
<p>{t('taskProgress.markAsDoneDescription', 'You\'ve set the progress to 100%. Would you like to update the task status to "Done"?')}</p>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDrawerProgress;
|
||||
@@ -0,0 +1,382 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
Form,
|
||||
Switch,
|
||||
Button,
|
||||
Popover,
|
||||
Select,
|
||||
Checkbox,
|
||||
Radio,
|
||||
InputNumber,
|
||||
Skeleton,
|
||||
Row,
|
||||
Col,
|
||||
} from 'antd';
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { IRepeatOption, ITaskRecurring, ITaskRecurringSchedule, ITaskRecurringScheduleData } from '@/types/tasks/task-recurring-schedule';
|
||||
import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { updateRecurringChange } from '@/features/tasks/tasks.slice';
|
||||
import { taskRecurringApiService } from '@/api/tasks/task-recurring.api.service';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { setTaskRecurringSchedule } from '@/features/task-drawer/task-drawer.slice';
|
||||
|
||||
const monthlyDateOptions = Array.from({ length: 28 }, (_, i) => i + 1);
|
||||
|
||||
const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
||||
const { socket, connected } = useSocket();
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation('task-drawer/task-drawer-recurring-config');
|
||||
|
||||
const repeatOptions: IRepeatOption[] = [
|
||||
{ label: t('daily'), value: ITaskRecurring.Daily },
|
||||
{ label: t('weekly'), value: ITaskRecurring.Weekly },
|
||||
{ label: t('everyXDays'), value: ITaskRecurring.EveryXDays },
|
||||
{ label: t('everyXWeeks'), value: ITaskRecurring.EveryXWeeks },
|
||||
{ label: t('everyXMonths'), value: ITaskRecurring.EveryXMonths },
|
||||
{ label: t('monthly'), value: ITaskRecurring.Monthly },
|
||||
];
|
||||
|
||||
const daysOfWeek = [
|
||||
{ label: t('sun'), value: 0, checked: false },
|
||||
{ label: t('mon'), value: 1, checked: false },
|
||||
{ label: t('tue'), value: 2, checked: false },
|
||||
{ label: t('wed'), value: 3, checked: false },
|
||||
{ label: t('thu'), value: 4, checked: false },
|
||||
{ label: t('fri'), value: 5, checked: false },
|
||||
{ label: t('sat'), value: 6, checked: false }
|
||||
];
|
||||
|
||||
const weekOptions = [
|
||||
{ label: t('first'), value: 1 },
|
||||
{ label: t('second'), value: 2 },
|
||||
{ label: t('third'), value: 3 },
|
||||
{ label: t('fourth'), value: 4 },
|
||||
{ label: t('last'), value: 5 }
|
||||
];
|
||||
|
||||
const dayOptions = daysOfWeek.map(d => ({ label: d.label, value: d.value }));
|
||||
|
||||
const [recurring, setRecurring] = useState(false);
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [repeatOption, setRepeatOption] = useState<IRepeatOption>({});
|
||||
const [selectedDays, setSelectedDays] = useState<number[]>([]);
|
||||
const [monthlyOption, setMonthlyOption] = useState('date');
|
||||
const [selectedMonthlyDate, setSelectedMonthlyDate] = useState(1);
|
||||
const [selectedMonthlyWeek, setSelectedMonthlyWeek] = useState(weekOptions[0].value);
|
||||
const [selectedMonthlyDay, setSelectedMonthlyDay] = useState(dayOptions[0].value);
|
||||
const [intervalDays, setIntervalDays] = useState(1);
|
||||
const [intervalWeeks, setIntervalWeeks] = useState(1);
|
||||
const [intervalMonths, setIntervalMonths] = useState(1);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
const [updatingData, setUpdatingData] = useState(false);
|
||||
const [scheduleData, setScheduleData] = useState<ITaskRecurringSchedule>({});
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
if (!task.id) return;
|
||||
|
||||
socket?.emit(SocketEvents.TASK_RECURRING_CHANGE.toString(), {
|
||||
task_id: task.id,
|
||||
schedule_id: task.schedule_id,
|
||||
});
|
||||
|
||||
socket?.once(
|
||||
SocketEvents.TASK_RECURRING_CHANGE.toString(),
|
||||
(schedule: ITaskRecurringScheduleData) => {
|
||||
if (schedule.id && schedule.schedule_type) {
|
||||
const selected = repeatOptions.find(e => e.value == schedule.schedule_type);
|
||||
if (selected) setRepeatOption(selected);
|
||||
}
|
||||
dispatch(updateRecurringChange(schedule));
|
||||
dispatch(setTaskRecurringSchedule({ schedule_id: schedule.id as string, task_id: task.id }));
|
||||
|
||||
setRecurring(checked);
|
||||
if (!checked) setShowConfig(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const configVisibleChange = (visible: boolean) => {
|
||||
setShowConfig(visible);
|
||||
};
|
||||
|
||||
const isMonthlySelected = useMemo(
|
||||
() => repeatOption.value === ITaskRecurring.Monthly,
|
||||
[repeatOption]
|
||||
);
|
||||
|
||||
const handleDayCheckboxChange = (checkedValues: number[]) => {
|
||||
setSelectedDays(checkedValues);
|
||||
};
|
||||
|
||||
const getSelectedDays = () => {
|
||||
return daysOfWeek
|
||||
.filter(day => day.checked) // Get only the checked days
|
||||
.map(day => day.value); // Extract their numeric values
|
||||
}
|
||||
|
||||
const getUpdateBody = () => {
|
||||
if (!task.id || !task.schedule_id || !repeatOption.value) return;
|
||||
|
||||
const body: ITaskRecurringSchedule = {
|
||||
id: task.id,
|
||||
schedule_type: repeatOption.value
|
||||
};
|
||||
|
||||
switch (repeatOption.value) {
|
||||
case ITaskRecurring.Weekly:
|
||||
body.days_of_week = getSelectedDays();
|
||||
break;
|
||||
|
||||
case ITaskRecurring.Monthly:
|
||||
if (monthlyOption === 'date') {
|
||||
body.date_of_month = selectedMonthlyDate;
|
||||
setSelectedMonthlyDate(0);
|
||||
setSelectedMonthlyDay(0);
|
||||
} else {
|
||||
body.week_of_month = selectedMonthlyWeek;
|
||||
body.day_of_month = selectedMonthlyDay;
|
||||
setSelectedMonthlyDate(0);
|
||||
}
|
||||
break;
|
||||
|
||||
case ITaskRecurring.EveryXDays:
|
||||
body.interval_days = intervalDays;
|
||||
break;
|
||||
|
||||
case ITaskRecurring.EveryXWeeks:
|
||||
body.interval_weeks = intervalWeeks;
|
||||
break;
|
||||
|
||||
case ITaskRecurring.EveryXMonths:
|
||||
body.interval_months = intervalMonths;
|
||||
break;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!task.id || !task.schedule_id) return;
|
||||
|
||||
try {
|
||||
setUpdatingData(true);
|
||||
const body = getUpdateBody();
|
||||
|
||||
const res = await taskRecurringApiService.updateTaskRecurringData(task.schedule_id, body);
|
||||
if (res.done) {
|
||||
setRecurring(true);
|
||||
setShowConfig(false);
|
||||
configVisibleChange(false);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("handleSave", e);
|
||||
} finally {
|
||||
setUpdatingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateDaysOfWeek = () => {
|
||||
for (let i = 0; i < daysOfWeek.length; i++) {
|
||||
daysOfWeek[i].checked = scheduleData.days_of_week?.includes(daysOfWeek[i].value) ?? false;
|
||||
}
|
||||
};
|
||||
|
||||
const getScheduleData = async () => {
|
||||
if (!task.schedule_id) return;
|
||||
setLoadingData(true);
|
||||
try {
|
||||
const res = await taskRecurringApiService.getTaskRecurringData(task.schedule_id);
|
||||
if (res.done) {
|
||||
setScheduleData(res.body);
|
||||
if (!res.body) {
|
||||
setRepeatOption(repeatOptions[0]);
|
||||
} else {
|
||||
const selected = repeatOptions.find(e => e.value == res.body.schedule_type);
|
||||
if (selected) {
|
||||
setRepeatOption(selected);
|
||||
setSelectedMonthlyDate(scheduleData.date_of_month || 1);
|
||||
setSelectedMonthlyDay(scheduleData.day_of_month || 0);
|
||||
setSelectedMonthlyWeek(scheduleData.week_of_month || 0);
|
||||
setIntervalDays(scheduleData.interval_days || 1);
|
||||
setIntervalWeeks(scheduleData.interval_weeks || 1);
|
||||
setIntervalMonths(scheduleData.interval_months || 1);
|
||||
setMonthlyOption(selectedMonthlyDate ? 'date' : 'day');
|
||||
updateDaysOfWeek();
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error("getScheduleData", e);
|
||||
}
|
||||
finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleResponse = (response: ITaskRecurringScheduleData) => {
|
||||
if (!task || !response.task_id) return;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!task) return;
|
||||
|
||||
if (task) setRecurring(!!task.schedule_id);
|
||||
if (task.schedule_id) void getScheduleData();
|
||||
socket?.on(SocketEvents.TASK_RECURRING_CHANGE.toString(), handleResponse);
|
||||
}, [task?.schedule_id]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item className="w-100 mb-2 align-form-item" style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Switch checked={recurring} onChange={handleChange} />
|
||||
|
||||
{recurring && (
|
||||
<Popover
|
||||
title={t('recurringTaskConfiguration')}
|
||||
content={
|
||||
<Skeleton loading={loadingData} active>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label={t('repeats')}>
|
||||
<Select
|
||||
value={repeatOption.value}
|
||||
onChange={val => {
|
||||
const option = repeatOptions.find(opt => opt.value === val);
|
||||
if (option) {
|
||||
setRepeatOption(option);
|
||||
}
|
||||
}}
|
||||
options={repeatOptions}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{repeatOption.value === ITaskRecurring.Weekly && (
|
||||
<Form.Item label={t('selectDaysOfWeek')}>
|
||||
<Checkbox.Group
|
||||
options={daysOfWeek.map(day => ({
|
||||
label: day.label,
|
||||
value: day.value
|
||||
}))}
|
||||
value={selectedDays}
|
||||
onChange={handleDayCheckboxChange}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Row>
|
||||
{daysOfWeek.map(day => (
|
||||
<Col span={8} key={day.value}>
|
||||
<Checkbox value={day.value}>{day.label}</Checkbox>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{isMonthlySelected && (
|
||||
<>
|
||||
<Form.Item label={t('monthlyRepeatType')}>
|
||||
<Radio.Group
|
||||
value={monthlyOption}
|
||||
onChange={e => setMonthlyOption(e.target.value)}
|
||||
>
|
||||
<Radio.Button value="date">{t('onSpecificDate')}</Radio.Button>
|
||||
<Radio.Button value="day">{t('onSpecificDay')}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{monthlyOption === 'date' && (
|
||||
<Form.Item label={t('dateOfMonth')}>
|
||||
<Select
|
||||
value={selectedMonthlyDate}
|
||||
onChange={setSelectedMonthlyDate}
|
||||
options={monthlyDateOptions.map(date => ({
|
||||
label: date.toString(),
|
||||
value: date,
|
||||
}))}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{monthlyOption === 'day' && (
|
||||
<>
|
||||
<Form.Item label={t('weekOfMonth')}>
|
||||
<Select
|
||||
value={selectedMonthlyWeek}
|
||||
onChange={setSelectedMonthlyWeek}
|
||||
options={weekOptions}
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('dayOfWeek')}>
|
||||
<Select
|
||||
value={selectedMonthlyDay}
|
||||
onChange={setSelectedMonthlyDay}
|
||||
options={dayOptions}
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{repeatOption.value === ITaskRecurring.EveryXDays && (
|
||||
<Form.Item label={t('intervalDays')}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
value={intervalDays}
|
||||
onChange={value => value && setIntervalDays(value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{repeatOption.value === ITaskRecurring.EveryXWeeks && (
|
||||
<Form.Item label={t('intervalWeeks')}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
value={intervalWeeks}
|
||||
onChange={value => value && setIntervalWeeks(value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{repeatOption.value === ITaskRecurring.EveryXMonths && (
|
||||
<Form.Item label={t('intervalMonths')}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
value={intervalMonths}
|
||||
onChange={value => value && setIntervalMonths(value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={updatingData}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('saveChanges')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Skeleton>
|
||||
}
|
||||
overlayStyle={{ width: 510 }}
|
||||
open={showConfig}
|
||||
onOpenChange={configVisibleChange}
|
||||
trigger="click"
|
||||
>
|
||||
<Button type="link" loading={loadingData} style={{ padding: 0 }}>
|
||||
{repeatOption.label} <SettingOutlined />
|
||||
</Button>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDrawerRecurringConfig;
|
||||
@@ -210,10 +210,11 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
icon={<ExclamationCircleFilled style={{ color: colors.vibrantOrange }} />}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onConfirm={() => handleDeleteSubTask(record.id)}
|
||||
onPopupClick={(e) => e.stopPropagation()}
|
||||
onConfirm={(e) => {handleDeleteSubTask(record.id)}}
|
||||
>
|
||||
<Tooltip title="Delete">
|
||||
<Button shape="default" icon={<DeleteOutlined />} size="small" />
|
||||
<Button shape="default" icon={<DeleteOutlined />} size="small" onClick={(e)=> e.stopPropagation()} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
|
||||
@@ -26,14 +26,59 @@ import TaskDrawerDueDate from './details/task-drawer-due-date/task-drawer-due-da
|
||||
import TaskDrawerEstimation from './details/task-drawer-estimation/task-drawer-estimation';
|
||||
import TaskDrawerPrioritySelector from './details/task-drawer-priority-selector/task-drawer-priority-selector';
|
||||
import TaskDrawerBillable from './details/task-drawer-billable/task-drawer-billable';
|
||||
import TaskDrawerProgress from './details/task-drawer-progress/task-drawer-progress';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import TaskDrawerRecurringConfig from './details/task-drawer-recurring-config/task-drawer-recurring-config';
|
||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||
|
||||
interface TaskDetailsFormProps {
|
||||
taskFormViewModel?: ITaskFormViewModel | null;
|
||||
}
|
||||
|
||||
// Custom wrapper that enforces stricter rules for displaying progress input
|
||||
interface ConditionalProgressInputProps {
|
||||
task: ITaskViewModel;
|
||||
form: any; // Using any for the form as the exact type may be complex
|
||||
}
|
||||
|
||||
const ConditionalProgressInput = ({ task, form }: ConditionalProgressInputProps) => {
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
const hasSubTasks = task?.sub_tasks_count > 0;
|
||||
const isSubTask = !!task?.parent_task_id;
|
||||
|
||||
// STRICT RULE: Never show progress input for parent tasks with subtasks
|
||||
// This is the most important check and must be done first
|
||||
if (hasSubTasks) {
|
||||
logger.debug(`Task ${task.id} has ${task.sub_tasks_count} subtasks. Hiding progress input.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only for tasks without subtasks, determine which input to show based on project mode
|
||||
if (project?.use_time_progress) {
|
||||
// In time-based mode, show progress input ONLY for tasks without subtasks
|
||||
return (
|
||||
<TaskDrawerProgress task={{ ...task, sub_tasks_count: hasSubTasks ? 1 : 0 }} form={form} />
|
||||
);
|
||||
} else if (project?.use_manual_progress) {
|
||||
// In manual mode, show progress input ONLY for tasks without subtasks
|
||||
return (
|
||||
<TaskDrawerProgress task={{ ...task, sub_tasks_count: hasSubTasks ? 1 : 0 }} form={form} />
|
||||
);
|
||||
} else if (project?.use_weighted_progress && isSubTask) {
|
||||
// In weighted mode, show weight input for subtasks
|
||||
return (
|
||||
<TaskDrawerProgress task={{ ...task, sub_tasks_count: hasSubTasks ? 1 : 0 }} form={form} />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) => {
|
||||
const { t } = useTranslation('task-drawer/task-drawer');
|
||||
const [form] = Form.useForm();
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
|
||||
useEffect(() => {
|
||||
if (!taskFormViewModel) {
|
||||
@@ -53,6 +98,8 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
labels: task?.labels || [],
|
||||
billable: task?.billable || false,
|
||||
notify: [],
|
||||
progress_value: task?.progress_value || null,
|
||||
weight: task?.weight || null,
|
||||
});
|
||||
}, [taskFormViewModel, form]);
|
||||
|
||||
@@ -89,6 +136,8 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
billable: false,
|
||||
progress_value: null,
|
||||
weight: null,
|
||||
}}
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
@@ -103,7 +152,13 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
|
||||
<Form.Item name="assignees" label={t('taskInfoTab.details.assignees')}>
|
||||
<Flex gap={4} align="center">
|
||||
<Avatars members={taskFormViewModel?.task?.names || []} />
|
||||
<Avatars
|
||||
members={
|
||||
taskFormViewModel?.task?.assignee_names ||
|
||||
(taskFormViewModel?.task?.names as unknown as InlineMember[]) ||
|
||||
[]
|
||||
}
|
||||
/>
|
||||
<TaskDrawerAssigneeSelector
|
||||
task={(taskFormViewModel?.task as ITaskViewModel) || null}
|
||||
/>
|
||||
@@ -114,6 +169,10 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
|
||||
<TaskDrawerEstimation t={t} task={taskFormViewModel?.task as ITaskViewModel} form={form} />
|
||||
|
||||
{taskFormViewModel?.task && (
|
||||
<ConditionalProgressInput task={taskFormViewModel?.task as ITaskViewModel} form={form} />
|
||||
)}
|
||||
|
||||
<Form.Item name="priority" label={t('taskInfoTab.details.priority')}>
|
||||
<TaskDrawerPrioritySelector task={taskFormViewModel?.task as ITaskViewModel} />
|
||||
</Form.Item>
|
||||
@@ -124,6 +183,10 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
||||
<TaskDrawerBillable task={taskFormViewModel?.task as ITaskViewModel} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="recurring" label={t('taskInfoTab.details.recurring')}>
|
||||
<TaskDrawerRecurringConfig task={taskFormViewModel?.task as ITaskViewModel} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="notify" label={t('taskInfoTab.details.notify')}>
|
||||
<NotifyMemberSelector task={taskFormViewModel?.task as ITaskViewModel} t={t} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -125,7 +125,7 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<ReloadOutlined spin={loadingSubTasks} />}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation(); // Prevent click from bubbling up
|
||||
fetchSubTasks();
|
||||
}}
|
||||
@@ -182,19 +182,15 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
|
||||
label: <Typography.Text strong>{t('taskInfoTab.comments.title')}</Typography.Text>,
|
||||
style: panelStyle,
|
||||
className: 'custom-task-drawer-info-collapse',
|
||||
children: (
|
||||
<TaskComments
|
||||
taskId={selectedTaskId || ''}
|
||||
t={t}
|
||||
/>
|
||||
),
|
||||
children: <TaskComments taskId={selectedTaskId || ''} t={t} />,
|
||||
},
|
||||
];
|
||||
|
||||
// Filter out the 'subTasks' item if this task is a subtask
|
||||
const infoItems = taskFormViewModel?.task?.parent_task_id
|
||||
? allInfoItems.filter(item => item.key !== 'subTasks')
|
||||
: allInfoItems;
|
||||
// Filter out the 'subTasks' item if this task is more than level 2
|
||||
const infoItems =
|
||||
(taskFormViewModel?.task?.task_level ?? 0) >= 2
|
||||
? allInfoItems.filter(item => item.key !== 'subTasks')
|
||||
: allInfoItems;
|
||||
|
||||
const fetchSubTasks = async () => {
|
||||
if (!selectedTaskId || loadingSubTasks) return;
|
||||
@@ -281,7 +277,7 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
|
||||
defaultActiveKey={[
|
||||
'details',
|
||||
'description',
|
||||
...(taskFormViewModel?.task?.parent_task_id ? [] : ['subTasks']),
|
||||
'subTasks',
|
||||
'dependencies',
|
||||
'attachments',
|
||||
'comments',
|
||||
|
||||
@@ -27,6 +27,7 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
||||
const { socket, connected } = useSocket();
|
||||
const { clearTaskFromUrl } = useTaskDrawerUrlSync();
|
||||
const isDeleting = useRef(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
|
||||
const [taskName, setTaskName] = useState<string>(taskFormViewModel?.task?.name ?? '');
|
||||
@@ -88,6 +89,7 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
setIsEditing(false);
|
||||
if (
|
||||
!selectedTaskId ||
|
||||
!connected ||
|
||||
@@ -113,21 +115,39 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
||||
return (
|
||||
<Flex gap={12} align="center" style={{ marginBlockEnd: 6 }}>
|
||||
<Flex style={{ position: 'relative', width: '100%' }}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
size="large"
|
||||
value={taskName}
|
||||
onChange={e => onTaskNameChange(e)}
|
||||
onBlur={handleInputBlur}
|
||||
placeholder={t('taskHeader.taskNamePlaceholder')}
|
||||
className="task-name-input"
|
||||
style={{
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
showCount={false}
|
||||
maxLength={250}
|
||||
/>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
size="large"
|
||||
value={taskName}
|
||||
onChange={e => onTaskNameChange(e)}
|
||||
onBlur={handleInputBlur}
|
||||
placeholder={t('taskHeader.taskNamePlaceholder')}
|
||||
className="task-name-input"
|
||||
style={{
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
showCount={true}
|
||||
maxLength={250}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
onClick={() => setIsEditing(true)}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '4px 11px',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{taskName || t('taskHeader.taskNamePlaceholder')}
|
||||
</p>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<TaskDrawerStatusDropdown
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||
import { ITaskStatus } from '@/types/tasks/taskStatus.types';
|
||||
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
||||
import { Select } from 'antd';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface TaskDrawerStatusDropdownProps {
|
||||
statuses: ITaskStatus[];
|
||||
@@ -21,7 +21,7 @@ interface TaskDrawerStatusDropdownProps {
|
||||
}
|
||||
|
||||
const TaskDrawerStatusDropdown = ({ statuses, task, teamId }: TaskDrawerStatusDropdownProps) => {
|
||||
const { socket, connected } = useSocket();
|
||||
const { socket } = useSocket();
|
||||
const dispatch = useAppDispatch();
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const { tab } = useTabSearchParam();
|
||||
@@ -46,6 +46,7 @@ const TaskDrawerStatusDropdown = ({ statuses, task, teamId }: TaskDrawerStatusDr
|
||||
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
||||
(data: ITaskListStatusChangeResponse) => {
|
||||
dispatch(setTaskStatus(data));
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||
|
||||
if (tab === 'tasks-list') {
|
||||
dispatch(updateTaskStatus(data));
|
||||
@@ -65,7 +66,6 @@ const TaskDrawerStatusDropdown = ({ statuses, task, teamId }: TaskDrawerStatusDr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const options = useMemo(
|
||||
|
||||
@@ -32,7 +32,7 @@ const TaskDrawer = () => {
|
||||
const [refreshTimeLogTrigger, setRefreshTimeLogTrigger] = useState(0);
|
||||
|
||||
const { showTaskDrawer, timeLogEditing } = useAppSelector(state => state.taskDrawerReducer);
|
||||
|
||||
const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
|
||||
const taskNameInputRef = useRef<InputRef>(null);
|
||||
const isClosingManually = useRef(false);
|
||||
|
||||
@@ -47,20 +47,32 @@ const TaskDrawer = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleOnClose = () => {
|
||||
// Set flag to indicate we're manually closing the drawer
|
||||
isClosingManually.current = true;
|
||||
setActiveTab('info');
|
||||
|
||||
// Explicitly clear the task parameter from URL
|
||||
clearTaskFromUrl();
|
||||
|
||||
// Update the Redux state
|
||||
const resetTaskState = () => {
|
||||
dispatch(setShowTaskDrawer(false));
|
||||
dispatch(setSelectedTaskId(null));
|
||||
dispatch(setTaskFormViewModel({}));
|
||||
dispatch(setTaskSubscribers([]));
|
||||
};
|
||||
|
||||
const handleOnClose = (
|
||||
e?: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>
|
||||
) => {
|
||||
// Set flag to indicate we're manually closing the drawer
|
||||
isClosingManually.current = true;
|
||||
setActiveTab('info');
|
||||
clearTaskFromUrl();
|
||||
|
||||
const isClickOutsideDrawer =
|
||||
e?.target && (e.target as HTMLElement).classList.contains('ant-drawer-mask');
|
||||
|
||||
if (isClickOutsideDrawer || !taskFormViewModel?.task?.is_sub_task) {
|
||||
resetTaskState();
|
||||
} else {
|
||||
dispatch(setSelectedTaskId(null));
|
||||
dispatch(setTaskFormViewModel({}));
|
||||
dispatch(setTaskSubscribers([]));
|
||||
dispatch(setSelectedTaskId(taskFormViewModel?.task?.parent_task_id || null));
|
||||
}
|
||||
// Reset the flag after a short delay
|
||||
setTimeout(() => {
|
||||
isClosingManually.current = false;
|
||||
@@ -176,8 +188,8 @@ const TaskDrawer = () => {
|
||||
// Get conditional body style
|
||||
const getBodyStyle = () => {
|
||||
const baseStyle = {
|
||||
padding: '24px',
|
||||
overflow: 'auto'
|
||||
padding: '24px',
|
||||
overflow: 'auto',
|
||||
};
|
||||
|
||||
if (activeTab === 'timeLog' && timeLogEditing.isEditing) {
|
||||
|
||||
@@ -58,7 +58,7 @@ import alertService from '@/services/alerts/alertService';
|
||||
|
||||
interface ITaskAssignee {
|
||||
id: string;
|
||||
name?: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
avatar_url?: string;
|
||||
team_member_id: string;
|
||||
@@ -437,7 +437,7 @@ const TaskListBulkActionsBar = () => {
|
||||
placement="top"
|
||||
arrow
|
||||
trigger={['click']}
|
||||
destroyPopupOnHide
|
||||
destroyOnHidden
|
||||
onOpenChange={value => {
|
||||
if (!value) {
|
||||
setSelectedLabels([]);
|
||||
|
||||
Reference in New Issue
Block a user