init
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { InputRef } from 'antd/es/input';
|
||||
import Card from 'antd/es/card';
|
||||
import Checkbox from 'antd/es/checkbox';
|
||||
import Divider from 'antd/es/divider';
|
||||
import Dropdown from 'antd/es/dropdown';
|
||||
import Empty from 'antd/es/empty';
|
||||
import Flex from 'antd/es/flex';
|
||||
import Input from 'antd/es/input';
|
||||
import List from 'antd/es/list';
|
||||
import Typography from 'antd/es/typography';
|
||||
import Button from 'antd/es/button';
|
||||
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { toggleProjectMemberDrawer } from '../../../features/projects/singleProject/members/projectMembersSlice';
|
||||
import { colors } from '../../../styles/colors';
|
||||
import { PlusOutlined, UsergroupAddOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SingleAvatar from '@/components/common/single-avatar/single-avatar';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
|
||||
import { sortByBooleanField, sortBySelection, sortTeamMembers } from '@/utils/sort-team-members';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
||||
|
||||
interface BoardAssigneeSelectorProps {
|
||||
task: IProjectTask;
|
||||
groupId: string | null;
|
||||
}
|
||||
|
||||
const BoardAssigneeSelector = ({ task, groupId = null }: BoardAssigneeSelectorProps) => {
|
||||
const membersInputRef = useRef<InputRef>(null);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [teamMembers, setTeamMembers] = useState<ITeamMembersViewModel>({ data: [], total: 0 });
|
||||
const { projectId } = useAppSelector(state => state.projectReducer);
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const { socket } = useSocket();
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const members = useAppSelector(state => state.teamMembersReducer.teamMembers);
|
||||
|
||||
const filteredMembersData = useMemo(() => {
|
||||
return teamMembers?.data?.filter(member =>
|
||||
member.name?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [teamMembers, searchQuery]);
|
||||
|
||||
const handleInviteProjectMemberDrawer = () => {
|
||||
dispatch(toggleProjectMemberDrawer());
|
||||
};
|
||||
|
||||
const handleMembersDropdownOpen = (open: boolean) => {
|
||||
if (open) {
|
||||
const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
|
||||
const membersData = (members?.data || []).map(member => ({
|
||||
...member,
|
||||
selected: assignees?.includes(member.id),
|
||||
}));
|
||||
let sortedMembers = sortTeamMembers(membersData);
|
||||
|
||||
setTeamMembers({ data: sortedMembers });
|
||||
|
||||
setTimeout(() => {
|
||||
membersInputRef.current?.focus();
|
||||
}, 0);
|
||||
} else {
|
||||
setTeamMembers(members || { data: [] });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMemberChange = (e: CheckboxChangeEvent | null, memberId: string) => {
|
||||
if (!memberId || !projectId || !task?.id || !currentSession?.id) return;
|
||||
const checked =
|
||||
e?.target.checked ||
|
||||
!task?.assignees?.some(assignee => assignee.team_member_id === memberId) ||
|
||||
false;
|
||||
|
||||
const body = {
|
||||
team_member_id: memberId,
|
||||
project_id: projectId,
|
||||
task_id: task.id,
|
||||
reporter_id: currentSession?.id,
|
||||
mode: checked ? 0 : 1,
|
||||
parent_task: task.parent_task_id,
|
||||
};
|
||||
|
||||
socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body));
|
||||
};
|
||||
|
||||
const checkMemberSelected = (memberId: string) => {
|
||||
if (!memberId) return false;
|
||||
const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
|
||||
return assignees?.includes(memberId);
|
||||
};
|
||||
|
||||
|
||||
const membersDropdownContent = (
|
||||
<Card className="custom-card" styles={{ body: { padding: 8 } }}>
|
||||
<Flex vertical>
|
||||
<Input
|
||||
ref={membersInputRef}
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.currentTarget.value)}
|
||||
placeholder={t('searchInputPlaceholder')}
|
||||
/>
|
||||
|
||||
<List style={{ padding: 0, height: 250, overflow: 'auto' }}>
|
||||
{filteredMembersData?.length ? (
|
||||
filteredMembersData.map(member => (
|
||||
<List.Item
|
||||
className={`${themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'} ${member.pending_invitation ? 'disabled cursor-not-allowed' : ''}`}
|
||||
key={member.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
justifyContent: 'flex-start',
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={e => handleMemberChange(null, member.id || '')}
|
||||
>
|
||||
<Checkbox
|
||||
id={member.id}
|
||||
checked={checkMemberSelected(member.id || '')}
|
||||
onChange={e => handleMemberChange(e, member.id || '')}
|
||||
disabled={member.pending_invitation}
|
||||
/>
|
||||
<div>
|
||||
<SingleAvatar
|
||||
avatarUrl={member.avatar_url}
|
||||
name={member.name}
|
||||
email={member.email}
|
||||
/>
|
||||
</div>
|
||||
<Flex vertical>
|
||||
<Typography.Text>{member.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{member.email}
|
||||
{member.pending_invitation && (
|
||||
<Typography.Text type="danger" style={{ fontSize: 10 }}>
|
||||
({t('pendingInvitation')})
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</List.Item>
|
||||
))
|
||||
) : (
|
||||
<Empty />
|
||||
)}
|
||||
</List>
|
||||
|
||||
<Divider style={{ marginBlock: 0 }} />
|
||||
|
||||
<Button
|
||||
icon={<UsergroupAddOutlined />}
|
||||
type="text"
|
||||
style={{
|
||||
color: colors.skyBlue,
|
||||
border: 'none',
|
||||
backgroundColor: colors.transparent,
|
||||
width: '100%',
|
||||
}}
|
||||
onClick={handleInviteProjectMemberDrawer}
|
||||
>
|
||||
{t('assigneeSelectorInviteButton')}
|
||||
</Button>
|
||||
|
||||
{/* <Divider style={{ marginBlock: 8 }} /> */}
|
||||
|
||||
{/* <Button
|
||||
type="primary"
|
||||
style={{ alignSelf: 'flex-end' }}
|
||||
size="small"
|
||||
onClick={handleAssignMembers}
|
||||
>
|
||||
{t('okButton')}
|
||||
</Button> */}
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="custom-dropdown"
|
||||
trigger={['click']}
|
||||
dropdownRender={() => membersDropdownContent}
|
||||
onOpenChange={handleMembersDropdownOpen}
|
||||
>
|
||||
<Button
|
||||
type="dashed"
|
||||
shape="circle"
|
||||
size="small"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
icon={
|
||||
<PlusOutlined
|
||||
style={{
|
||||
fontSize: 12,
|
||||
width: 22,
|
||||
height: 22,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardAssigneeSelector;
|
||||
@@ -0,0 +1,13 @@
|
||||
.status-drawer-dropdown .ant-dropdown-menu {
|
||||
padding: 0 !important;
|
||||
margin-top: 8px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-drawer-dropdown .ant-dropdown-menu-item {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.status-drawer-dropdown .ant-card-body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Badge, Card, Dropdown, Flex, Menu, MenuProps } from 'antd';
|
||||
import React from 'react';
|
||||
import { TaskStatusType } from '../../../types/task.types';
|
||||
import { colors } from '../../../styles/colors';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { RetweetOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import './ChangeCategoryDropdown.css';
|
||||
import { updateStatusCategory } from '../../../features/projects/status/StatusSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ChangeCategoryDropdownProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ChangeCategoryDropdown: React.FC<ChangeCategoryDropdownProps> = ({ id }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
// const [currentStatus, setCurrentStatus] = useState(category);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const getStatuColor = (status: TaskStatusType) => {
|
||||
if (status === 'todo') return colors.deepLightGray;
|
||||
else if (status === 'doing') return colors.midBlue;
|
||||
else return colors.lightGreen;
|
||||
};
|
||||
|
||||
// menu type
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
// status menu item
|
||||
const statusMenuItems: MenuItem[] = [
|
||||
{
|
||||
key: 'todo',
|
||||
label: (
|
||||
<Flex gap={4}>
|
||||
<Badge color={getStatuColor('todo')} /> Todo
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'doing',
|
||||
label: (
|
||||
<Flex gap={4}>
|
||||
<Badge color={getStatuColor('doing')} /> Doing
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'done',
|
||||
label: (
|
||||
<Flex gap={4}>
|
||||
<Badge color={getStatuColor('done')} /> Done
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onClick: MenuProps['onClick'] = e => {
|
||||
if (e.key === 'todo') {
|
||||
dispatch(updateStatusCategory({ id: id, category: 'todo' }));
|
||||
} else if (e.key === 'doing') {
|
||||
dispatch(updateStatusCategory({ id: id, category: 'doing' }));
|
||||
} else if (e.key === 'done') {
|
||||
dispatch(updateStatusCategory({ id: id, category: 'done' }));
|
||||
}
|
||||
};
|
||||
|
||||
const statusDropdownItems: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<Card className="status-dropdown-card" bordered={false}>
|
||||
<Menu
|
||||
className="status-menu"
|
||||
items={statusMenuItems}
|
||||
defaultValue={'todo'}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{ items: statusDropdownItems }}
|
||||
overlayStyle={{
|
||||
paddingLeft: '185px',
|
||||
paddingBottom: '100px',
|
||||
top: '350px',
|
||||
}}
|
||||
overlayClassName="status-drawer-dropdown"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<RetweetOutlined /> <span>{t('changeCategory')}</span>{' '}
|
||||
<RightOutlined style={{ color: '#00000073', fontSize: '10px' }} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeCategoryDropdown;
|
||||
@@ -0,0 +1,297 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LoadingOutlined,
|
||||
MoreOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { setTaskCardDisabled, initializeStatus } from '../../../features/board/create-card.slice';
|
||||
import { TaskType } from '../../../types/task.types';
|
||||
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
|
||||
import TaskCard from '../taskCard/TaskCard';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
import '../commonStatusSection/CommonStatusSection';
|
||||
|
||||
import { deleteStatus } from '../../../features/projects/status/StatusSlice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CommonMembersSectionProps {
|
||||
status: string;
|
||||
dataSource: TaskType[];
|
||||
category: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const CommonMembersSection: React.FC<CommonMembersSectionProps> = ({
|
||||
status,
|
||||
dataSource,
|
||||
category,
|
||||
id,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const createTaskInputRef = useRef<InputRef>(null);
|
||||
|
||||
const colorPalette = ['#d1d0d3', '#b9cef1', '#c2e4d0', '#f9e3b1', '#f6bfc0'];
|
||||
|
||||
const getRandomColorFromPalette = () =>
|
||||
colorPalette[Math.floor(Math.random() * colorPalette.length)];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(initializeStatus(status));
|
||||
}, [dispatch, status]);
|
||||
|
||||
const isTopCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[status]?.top
|
||||
);
|
||||
const isBottomCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[status]?.bottom
|
||||
);
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const [addTaskCount, setAddTaskCount] = useState(0);
|
||||
const [name, setName] = useState(status);
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const taskCardRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const handleAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position: 'bottom', disabled: false }));
|
||||
setAddTaskCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
const handleTopAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position: 'top', disabled: false }));
|
||||
setAddTaskCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditable && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditable]);
|
||||
|
||||
useEffect(() => {
|
||||
createTaskInputRef.current?.focus();
|
||||
taskCardRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}, [dataSource, addTaskCount]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsEditable(false);
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => setIsEditable(true)}
|
||||
>
|
||||
<EditOutlined /> <span>{t('rename')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: <ChangeCategoryDropdown id={id} />,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => dispatch(deleteStatus(id))}
|
||||
>
|
||||
<DeleteOutlined /> <span>{t('delete')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: '6px' }}>
|
||||
<div
|
||||
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
flexBasis: 0,
|
||||
maxWidth: '375px',
|
||||
width: '375px',
|
||||
marginRight: '8px',
|
||||
padding: '8px',
|
||||
borderRadius: '25px',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
touchAction: 'none',
|
||||
userSelect: 'none',
|
||||
cursor: 'grab',
|
||||
fontSize: '14px',
|
||||
paddingTop: '0',
|
||||
margin: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
marginBottom: '12px',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: getRandomColorFromPalette(),
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
|
||||
onClick={() => setIsEditable(true)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
}}
|
||||
>
|
||||
{dataSource.length}
|
||||
</Button>
|
||||
)}
|
||||
{isEditable ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={name}
|
||||
variant="borderless"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onPressEnter={handleBlur}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{
|
||||
textTransform: 'capitalize',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
onClick={handleTopAddTaskClick}
|
||||
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
<Dropdown
|
||||
overlayClassName="todo-threedot-dropdown"
|
||||
trigger={['click']}
|
||||
menu={{ items }}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<Button type="text" size="small" shape="circle">
|
||||
<MoreOutlined
|
||||
style={{
|
||||
rotate: '90deg',
|
||||
fontSize: '25px',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
padding: '2px 6px 2px 2px',
|
||||
}}
|
||||
>
|
||||
{!isTopCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={status} position={'top'} />
|
||||
)}
|
||||
|
||||
{dataSource.map(task => (
|
||||
<TaskCard key={task.taskId} task={task} />
|
||||
))}
|
||||
|
||||
{!isBottomCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={status} position={'bottom'} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
margin: '7px 8px 8px 8px',
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
padding: '0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddTaskClick}
|
||||
>
|
||||
{t('addTask')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonMembersSection;
|
||||
@@ -0,0 +1,294 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LoadingOutlined,
|
||||
MoreOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { setTaskCardDisabled, initializeStatus } from '../../../features/board/create-card.slice';
|
||||
import { TaskType } from '../../../types/task.types';
|
||||
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
|
||||
import TaskCard from '../taskCard/TaskCard';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
import '../commonStatusSection/CommonStatusSection';
|
||||
|
||||
import { deleteStatus } from '../../../features/projects/status/StatusSlice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CommonPhaseSectionProps {
|
||||
status: string;
|
||||
dataSource: TaskType[];
|
||||
category: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const CommonPhaseSection: React.FC<CommonPhaseSectionProps> = ({
|
||||
status,
|
||||
dataSource,
|
||||
category,
|
||||
id,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const createTaskInputRef = useRef<InputRef>(null);
|
||||
|
||||
// Initialize status in the Redux store if not already set
|
||||
useEffect(() => {
|
||||
dispatch(initializeStatus(status));
|
||||
}, [dispatch, status]);
|
||||
|
||||
// Get status-specific disable controls from Redux state
|
||||
const isTopCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[status]?.top
|
||||
);
|
||||
const isBottomCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[status]?.bottom
|
||||
);
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const [addTaskCount, setAddTaskCount] = useState(0);
|
||||
const [name, setName] = useState(status);
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const taskCardRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const handleAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position: 'bottom', disabled: false }));
|
||||
setAddTaskCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
const handleTopAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position: 'top', disabled: false }));
|
||||
setAddTaskCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditable && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditable]);
|
||||
|
||||
useEffect(() => {
|
||||
createTaskInputRef.current?.focus();
|
||||
taskCardRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}, [dataSource, addTaskCount]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsEditable(false);
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => setIsEditable(true)}
|
||||
>
|
||||
<EditOutlined /> <span>{t('rename')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: <ChangeCategoryDropdown id={id} />,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => dispatch(deleteStatus(id))}
|
||||
>
|
||||
<DeleteOutlined /> <span>{t('delete')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: '6px' }}>
|
||||
<div
|
||||
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
flexBasis: 0,
|
||||
maxWidth: '375px',
|
||||
width: '375px',
|
||||
marginRight: '8px',
|
||||
padding: '8px',
|
||||
borderRadius: '25px',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
touchAction: 'none',
|
||||
userSelect: 'none',
|
||||
cursor: 'grab',
|
||||
fontSize: '14px',
|
||||
paddingTop: '0',
|
||||
margin: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
marginBottom: '12px',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: category === 'unmapped' ? 'rgba(251, 200, 76, 0.41)' : '#d1d0d3',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
|
||||
onClick={() => setIsEditable(true)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
}}
|
||||
>
|
||||
{dataSource.length}
|
||||
</Button>
|
||||
)}
|
||||
{isEditable ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={name}
|
||||
variant="borderless"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onPressEnter={handleBlur}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{
|
||||
textTransform: 'capitalize',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
onClick={handleTopAddTaskClick}
|
||||
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
<Dropdown
|
||||
overlayClassName="todo-threedot-dropdown"
|
||||
trigger={['click']}
|
||||
menu={{ items }}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<Button type="text" size="small" shape="circle">
|
||||
<MoreOutlined
|
||||
style={{
|
||||
rotate: '90deg',
|
||||
fontSize: '25px',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
padding: '2px 6px 2px 2px',
|
||||
}}
|
||||
>
|
||||
{!isTopCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={status} position={'top'} />
|
||||
)}
|
||||
|
||||
{dataSource.map(task => (
|
||||
<TaskCard key={task.taskId} task={task} />
|
||||
))}
|
||||
|
||||
{!isBottomCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={status} position={'bottom'} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
margin: '7px 8px 8px 8px',
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
padding: '0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddTaskClick}
|
||||
>
|
||||
{t('addTask')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonPhaseSection;
|
||||
@@ -0,0 +1,295 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LoadingOutlined,
|
||||
MoreOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { setTaskCardDisabled, initializeStatus } from '../../../features/board/create-card.slice';
|
||||
import { TaskType } from '../../../types/task.types';
|
||||
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
|
||||
import TaskCard from '../taskCard/TaskCard';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
import '../commonStatusSection/CommonStatusSection';
|
||||
|
||||
import { deleteStatus } from '../../../features/projects/status/StatusSlice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CommonPrioritySectionProps {
|
||||
status: string;
|
||||
dataSource: TaskType[];
|
||||
category: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const CommonPrioritySection: React.FC<CommonPrioritySectionProps> = ({
|
||||
status,
|
||||
dataSource,
|
||||
category,
|
||||
id,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const createTaskInputRef = useRef<InputRef>(null);
|
||||
|
||||
// Initialize status in the Redux store if not already set
|
||||
useEffect(() => {
|
||||
dispatch(initializeStatus(status));
|
||||
}, [dispatch, status]);
|
||||
|
||||
// Get status-specific disable controls from Redux state
|
||||
const isTopCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[status]?.top
|
||||
);
|
||||
const isBottomCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[status]?.bottom
|
||||
);
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const [addTaskCount, setAddTaskCount] = useState(0);
|
||||
const [name, setName] = useState(status);
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const taskCardRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const handleAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position: 'bottom', disabled: false }));
|
||||
setAddTaskCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
const handleTopAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position: 'top', disabled: false }));
|
||||
setAddTaskCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditable && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditable]);
|
||||
|
||||
useEffect(() => {
|
||||
createTaskInputRef.current?.focus();
|
||||
taskCardRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}, [dataSource, addTaskCount]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsEditable(false);
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => setIsEditable(true)}
|
||||
>
|
||||
<EditOutlined /> <span>{t('rename')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: <ChangeCategoryDropdown id={id} />,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => dispatch(deleteStatus(id))}
|
||||
>
|
||||
<DeleteOutlined /> <span>{t('delete')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: '6px' }}>
|
||||
<div
|
||||
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
flexBasis: 0,
|
||||
maxWidth: '375px',
|
||||
width: '375px',
|
||||
marginRight: '8px',
|
||||
padding: '8px',
|
||||
borderRadius: '25px',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
touchAction: 'none',
|
||||
userSelect: 'none',
|
||||
cursor: 'grab',
|
||||
fontSize: '14px',
|
||||
paddingTop: '0',
|
||||
margin: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
marginBottom: '12px',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor:
|
||||
category === 'low' ? '#c2e4d0' : category === 'medium' ? '#f9e3b1' : '#f6bfc0',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
|
||||
onClick={() => setIsEditable(true)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
}}
|
||||
>
|
||||
{dataSource.length}
|
||||
</Button>
|
||||
)}
|
||||
{isEditable ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={name}
|
||||
variant="borderless"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onPressEnter={handleBlur}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{
|
||||
textTransform: 'capitalize',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
onClick={handleTopAddTaskClick}
|
||||
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
<Dropdown
|
||||
overlayClassName="todo-threedot-dropdown"
|
||||
trigger={['click']}
|
||||
menu={{ items }}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<Button type="text" size="small" shape="circle">
|
||||
<MoreOutlined
|
||||
style={{
|
||||
rotate: '90deg',
|
||||
fontSize: '25px',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
padding: '2px 6px 2px 2px',
|
||||
}}
|
||||
>
|
||||
{!isTopCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={status} position={'top'} />
|
||||
)}
|
||||
|
||||
{dataSource.map(task => (
|
||||
<TaskCard key={task.taskId} task={task} />
|
||||
))}
|
||||
|
||||
{!isBottomCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={status} position={'bottom'} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
margin: '7px 8px 8px 8px',
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
padding: '0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddTaskClick}
|
||||
>
|
||||
{t('addTask')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonPrioritySection;
|
||||
@@ -0,0 +1,16 @@
|
||||
.todo-wraper:hover {
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.todo-wraper.dark-mode:hover {
|
||||
border: 1px solid #3a3a3a;
|
||||
}
|
||||
|
||||
.todo-threedot-dropdown .ant-dropdown-menu-item {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.todo-threedot-dropdown-button .ant-btn {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LoadingOutlined,
|
||||
MoreOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { setTaskCardDisabled, initializeStatus } from '../../../features/board/create-card.slice';
|
||||
import { TaskType } from '../../../types/task.types';
|
||||
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
|
||||
import TaskCard from '../taskCard/TaskCard';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
import './CommonStatusSection.css';
|
||||
|
||||
import { deleteStatus } from '../../../features/projects/status/StatusSlice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CommonStatusSectionProps {
|
||||
status: string;
|
||||
dataSource: TaskType[];
|
||||
category: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const CommonStatusSection: React.FC<CommonStatusSectionProps> = ({
|
||||
status,
|
||||
dataSource,
|
||||
category,
|
||||
id,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const createTaskInputRef = useRef<InputRef>(null);
|
||||
|
||||
// Initialize status in the Redux store if not already set
|
||||
useEffect(() => {
|
||||
dispatch(initializeStatus(status));
|
||||
}, [dispatch, status]);
|
||||
|
||||
// Get status-specific disable controls from Redux state
|
||||
const isTopCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[status]?.top
|
||||
);
|
||||
const isBottomCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[status]?.bottom
|
||||
);
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const [addTaskCount, setAddTaskCount] = useState(0);
|
||||
const [name, setName] = useState(status);
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const taskCardRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const handleAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position: 'bottom', disabled: false }));
|
||||
setAddTaskCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
const handleTopAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position: 'top', disabled: false }));
|
||||
setAddTaskCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditable && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditable]);
|
||||
|
||||
useEffect(() => {
|
||||
createTaskInputRef.current?.focus();
|
||||
taskCardRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}, [dataSource, addTaskCount]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsEditable(false);
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => setIsEditable(true)}
|
||||
>
|
||||
<EditOutlined /> <span>{t('rename')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: <ChangeCategoryDropdown id={id} />,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => dispatch(deleteStatus(id))}
|
||||
>
|
||||
<DeleteOutlined /> <span>{t('delete')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: '6px' }}>
|
||||
<div
|
||||
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
flexBasis: 0,
|
||||
maxWidth: '375px',
|
||||
width: '375px',
|
||||
marginRight: '8px',
|
||||
padding: '8px',
|
||||
borderRadius: '25px',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
touchAction: 'none',
|
||||
userSelect: 'none',
|
||||
cursor: 'grab',
|
||||
fontSize: '14px',
|
||||
paddingTop: '0',
|
||||
margin: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
marginBottom: '12px',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor:
|
||||
category === 'todo' ? '#d1d0d3' : category === 'doing' ? '#b9cef1' : '#c2e4d0',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
|
||||
onClick={() => setIsEditable(true)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
}}
|
||||
>
|
||||
{dataSource.length}
|
||||
</Button>
|
||||
)}
|
||||
{isEditable ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={name}
|
||||
variant="borderless"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onPressEnter={handleBlur}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{
|
||||
textTransform: 'capitalize',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
onClick={handleTopAddTaskClick}
|
||||
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
<Dropdown
|
||||
overlayClassName="todo-threedot-dropdown"
|
||||
trigger={['click']}
|
||||
menu={{ items }}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<Button type="text" size="small" shape="circle">
|
||||
<MoreOutlined
|
||||
style={{
|
||||
rotate: '90deg',
|
||||
fontSize: '25px',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
padding: '2px 6px 2px 2px',
|
||||
}}
|
||||
>
|
||||
{!isTopCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={status} position={'top'} />
|
||||
)}
|
||||
|
||||
{dataSource.map(task => (
|
||||
<TaskCard key={task.taskId} task={task} />
|
||||
))}
|
||||
|
||||
{!isBottomCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={status} position={'bottom'} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
margin: '7px 8px 8px 8px',
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
padding: '0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddTaskClick}
|
||||
>
|
||||
{t('addTask')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonStatusSection;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Button, Flex } from 'antd';
|
||||
import AddMembersDropdown from '@/components/add-members-dropdown/add-members-dropdown';
|
||||
import Avatars from '../avatars/avatars';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import BoardAssigneeSelector from './board-assignee-selector/board-assignee-selector';
|
||||
|
||||
type CustomAvatarGroupProps = {
|
||||
task: IProjectTask;
|
||||
sectionId: string;
|
||||
};
|
||||
|
||||
const CustomAvatarGroup = ({ task, sectionId }: CustomAvatarGroupProps) => {
|
||||
return (
|
||||
<Flex
|
||||
gap={4}
|
||||
align="center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Avatars members={task?.names || []} />
|
||||
|
||||
<BoardAssigneeSelector task={task} groupId={sectionId} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomAvatarGroup;
|
||||
@@ -0,0 +1,101 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { DatePicker, Button, Flex } from 'antd';
|
||||
import { CalendarOutlined } from '@ant-design/icons';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { getUserSession } from '@/utils/session-helper';
|
||||
|
||||
const CustomDueDatePicker = ({
|
||||
task,
|
||||
onDateChange,
|
||||
}: {
|
||||
task: IProjectTask;
|
||||
onDateChange: (date: Dayjs | null) => void;
|
||||
}) => {
|
||||
const { socket } = useSocket();
|
||||
const [isDatePickerOpen, setIsDatePickerOpen] = useState<boolean>(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null); // Add ref to container
|
||||
|
||||
const dueDayjs = task?.end_date ? dayjs(task.end_date) : null;
|
||||
|
||||
const handleDateChange = (date: Dayjs | null) => {
|
||||
onDateChange(date);
|
||||
setIsDatePickerOpen(false);
|
||||
try {
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_END_DATE_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
end_date: date?.format(),
|
||||
parent_task: task.parent_task_id,
|
||||
time_zone: getUserSession()?.timezone_name
|
||||
? getUserSession()?.timezone_name
|
||||
: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Failed to update due date:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Stop propagation at the container level
|
||||
const handleContainerClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} onClick={handleContainerClick}>
|
||||
{task && task.end_date ? (
|
||||
<DatePicker
|
||||
value={dueDayjs}
|
||||
format={'MMM DD, YYYY'}
|
||||
onChange={handleDateChange}
|
||||
variant="borderless"
|
||||
suffixIcon={null}
|
||||
style={{ textAlign: 'right', padding: 0, maxWidth: 100 }}
|
||||
// Remove individual onClick handler since container handles it
|
||||
/>
|
||||
) : (
|
||||
<Flex gap={4} align="center" style={{ position: 'relative', width: 26, height: 26 }}>
|
||||
<DatePicker
|
||||
open={isDatePickerOpen}
|
||||
value={dueDayjs}
|
||||
format={'MMM DD, YYYY'}
|
||||
onChange={handleDateChange}
|
||||
style={{ opacity: 0, width: 0, height: 0, padding: 0 }}
|
||||
popupStyle={{ paddingBlock: 12 }}
|
||||
onBlur={() => setIsDatePickerOpen(false)}
|
||||
onOpenChange={open => setIsDatePickerOpen(open)}
|
||||
variant="borderless"
|
||||
// Remove individual onClick handler
|
||||
/>
|
||||
<Button
|
||||
shape="circle"
|
||||
type="dashed"
|
||||
size="small"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
boxShadow: 'none',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 26,
|
||||
height: 26,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Keep this as a backup
|
||||
setIsDatePickerOpen(true);
|
||||
}}
|
||||
icon={<CalendarOutlined />}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomDueDatePicker;
|
||||
@@ -0,0 +1,16 @@
|
||||
.todo-wraper:hover {
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.todo-wraper.dark-mode:hover {
|
||||
border: 1px solid #3a3a3a;
|
||||
}
|
||||
|
||||
.todo-threedot-dropdown .ant-dropdown-menu-item {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.todo-threedot-dropdown-button .ant-btn {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Dropdown, Input, InputRef, MenuProps, Typography } from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LoadingOutlined,
|
||||
MoreOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
|
||||
import { setTaskCardDisabled, initializeGroup } from '@/features/board/create-card.slice';
|
||||
import TaskCreateCard from '../taskCreateCard/TaskCreateCard';
|
||||
import TaskCard from '../taskCard/TaskCard';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { deleteStatus } from '@features/projects/status/StatusSlice';
|
||||
import ChangeCategoryDropdown from '../changeCategoryDropdown/ChangeCategoryDropdown';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
|
||||
import './kanban-group.css';
|
||||
|
||||
interface KanbanGroupProps {
|
||||
title: string;
|
||||
tasks: IProjectTask[];
|
||||
id: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface GroupState {
|
||||
name: string;
|
||||
isEditable: boolean;
|
||||
isLoading: boolean;
|
||||
addTaskCount: number;
|
||||
}
|
||||
|
||||
const KanbanGroup: React.FC<KanbanGroupProps> = ({ title, tasks, id, color }) => {
|
||||
// Refs
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const createTaskInputRef = useRef<InputRef>(null);
|
||||
const taskCardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// State
|
||||
const [groupState, setGroupState] = useState<GroupState>({
|
||||
name: title,
|
||||
isEditable: false,
|
||||
isLoading: false,
|
||||
addTaskCount: 0,
|
||||
});
|
||||
|
||||
// Hooks
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
// Selectors
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const isTopCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[id]?.top
|
||||
);
|
||||
const isBottomCardDisabled = useAppSelector(
|
||||
state => state.createCardReducer.taskCardDisabledStatus[id]?.bottom
|
||||
);
|
||||
|
||||
// Add droppable functionality
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: id,
|
||||
});
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
dispatch(initializeGroup(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupState.isEditable && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [groupState.isEditable]);
|
||||
|
||||
useEffect(() => {
|
||||
createTaskInputRef.current?.focus();
|
||||
taskCardRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}, [tasks, groupState.addTaskCount]);
|
||||
|
||||
// Handlers
|
||||
const handleAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ group: id, position: 'bottom', disabled: false }));
|
||||
setGroupState(prev => ({ ...prev, addTaskCount: prev.addTaskCount + 1 }));
|
||||
};
|
||||
|
||||
const handleTopAddTaskClick = () => {
|
||||
dispatch(setTaskCardDisabled({ group: id, position: 'top', disabled: false }));
|
||||
setGroupState(prev => ({ ...prev, addTaskCount: prev.addTaskCount + 1 }));
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setGroupState(prev => ({ ...prev, name: e.target.value }));
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setGroupState(prev => ({ ...prev, isEditable: false, isLoading: true }));
|
||||
setTimeout(() => {
|
||||
setGroupState(prev => ({ ...prev, isLoading: false }));
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const handleEditClick = () => {
|
||||
setGroupState(prev => ({ ...prev, isEditable: true }));
|
||||
};
|
||||
|
||||
// Menu items
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={handleEditClick}
|
||||
>
|
||||
<EditOutlined /> <span>{t('rename')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: <ChangeCategoryDropdown id={id} />,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
padding: '5px 12px',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => dispatch(deleteStatus(id))}
|
||||
>
|
||||
<DeleteOutlined /> <span>{t('delete')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Styles
|
||||
const containerStyle = {
|
||||
paddingTop: '6px',
|
||||
};
|
||||
|
||||
const wrapperStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
flexGrow: 1,
|
||||
flexBasis: 0,
|
||||
maxWidth: '375px',
|
||||
width: '375px',
|
||||
marginRight: '8px',
|
||||
padding: '8px',
|
||||
borderRadius: '25px',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
backgroundColor: themeMode === 'dark' ? '#282828' : '#F8FAFC',
|
||||
};
|
||||
|
||||
const headerStyle = {
|
||||
touchAction: 'none' as const,
|
||||
userSelect: 'none' as const,
|
||||
cursor: 'grab',
|
||||
fontSize: '14px',
|
||||
paddingTop: '0',
|
||||
margin: '0.25rem',
|
||||
};
|
||||
|
||||
const titleBarStyle = {
|
||||
fontWeight: 600,
|
||||
marginBottom: '12px',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: color,
|
||||
borderRadius: '10px',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<div
|
||||
className={`todo-wraper ${themeMode === 'dark' ? 'dark-mode' : ''}`}
|
||||
style={wrapperStyle}
|
||||
>
|
||||
<div style={headerStyle}>
|
||||
<div style={titleBarStyle}>
|
||||
<div
|
||||
style={{ display: 'flex', gap: '5px', alignItems: 'center' }}
|
||||
onClick={handleEditClick}
|
||||
>
|
||||
{groupState.isLoading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
}}
|
||||
>
|
||||
{tasks.length}
|
||||
</Button>
|
||||
)}
|
||||
{groupState.isEditable ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={groupState.name}
|
||||
variant="borderless"
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? 'black' : 'white',
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onPressEnter={handleBlur}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{
|
||||
textTransform: 'capitalize',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
>
|
||||
{groupState.name}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
onClick={handleTopAddTaskClick}
|
||||
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
<Dropdown
|
||||
overlayClassName="todo-threedot-dropdown"
|
||||
trigger={['click']}
|
||||
menu={{ items }}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<Button type="text" size="small" shape="circle">
|
||||
<MoreOutlined
|
||||
style={{
|
||||
rotate: '90deg',
|
||||
fontSize: '25px',
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
maxHeight: 'calc(100vh - 250px)',
|
||||
padding: '2px 6px 2px 2px',
|
||||
}}
|
||||
>
|
||||
{!isTopCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={title} position={'top'} />
|
||||
)}
|
||||
|
||||
<SortableContext
|
||||
items={tasks.map(task => task.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="App" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{tasks.map(task => (
|
||||
<TaskCard key={task.id} task={task} />
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
{!isBottomCardDisabled && (
|
||||
<TaskCreateCard ref={createTaskInputRef} status={title} position={'bottom'} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
margin: '7px 8px 8px 8px',
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
padding: '0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddTaskClick}
|
||||
>
|
||||
{t('addTask')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KanbanGroup;
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Avatar, Col, DatePicker, Divider, Flex, Row, Tooltip, Typography } from 'antd';
|
||||
import StatusDropdown from '../../taskListCommon/statusDropdown/StatusDropdown';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import Avatars from '@/components/avatars/avatars';
|
||||
|
||||
interface SubTaskProps {
|
||||
subtask: IProjectTask;
|
||||
}
|
||||
|
||||
const SubTaskCard: React.FC<SubTaskProps> = ({ subtask }) => {
|
||||
const [isSubToday, setIsSubToday] = useState(false);
|
||||
const [isSubTomorrow, setIsSubTomorrow] = useState(false);
|
||||
const [isItSubPrevDate, setIsItSubPrevDate] = useState(false);
|
||||
const [subTaskDueDate, setSubTaskDueDate] = useState<Dayjs | null>(null);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const handleSubTaskDateChange = (date: Dayjs | null) => {
|
||||
setSubTaskDueDate(date);
|
||||
};
|
||||
|
||||
const formatDate = (date: Dayjs | null) => {
|
||||
if (!date) return '';
|
||||
|
||||
const today = dayjs();
|
||||
const tomorrow = today.add(1, 'day');
|
||||
|
||||
if (date.isSame(today, 'day')) {
|
||||
return 'Today';
|
||||
} else if (date.isSame(tomorrow, 'day')) {
|
||||
return 'Tomorrow';
|
||||
} else {
|
||||
return date.isSame(today, 'year') ? date.format('MMM DD') : date.format('MMM DD, YYYY');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (subTaskDueDate) {
|
||||
setIsSubToday(subTaskDueDate.isSame(dayjs(), 'day'));
|
||||
setIsSubTomorrow(subTaskDueDate.isSame(dayjs().add(1, 'day'), 'day'));
|
||||
setIsItSubPrevDate(subTaskDueDate.isBefore(dayjs()));
|
||||
} else {
|
||||
setIsSubToday(false);
|
||||
setIsSubTomorrow(false);
|
||||
setIsItSubPrevDate(false);
|
||||
}
|
||||
}, [subTaskDueDate]);
|
||||
|
||||
return (
|
||||
<Row
|
||||
key={subtask.id}
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Col span={10}>
|
||||
<Typography.Text
|
||||
style={{ fontWeight: 500, fontSize: '12px' }}
|
||||
delete={subtask.status === 'done'}
|
||||
>
|
||||
{subtask.name}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Avatar.Group
|
||||
size="small"
|
||||
max={{
|
||||
count: 1,
|
||||
style: {
|
||||
color: '#f56a00',
|
||||
backgroundColor: '#fde3cf',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Avatars members={subtask.names || []} />
|
||||
</Avatar.Group>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Flex>
|
||||
<DatePicker
|
||||
className={`custom-placeholder ${!subTaskDueDate ? 'empty-date' : isSubToday || isSubTomorrow ? 'selected-date' : isItSubPrevDate ? 'red-colored' : ''}`}
|
||||
placeholder={t('dueDate')}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
opacity: subTaskDueDate ? 1 : 0,
|
||||
}}
|
||||
onChange={handleSubTaskDateChange}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
suffixIcon={false}
|
||||
format={value => formatDate(value)}
|
||||
/>
|
||||
<div>
|
||||
<StatusDropdown currentStatus={subtask.status} />
|
||||
</div>
|
||||
</Flex>
|
||||
</Col>
|
||||
|
||||
{/* <div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography.Text
|
||||
style={{ fontWeight: 500 }}
|
||||
delete={subtask.status === 'done'}
|
||||
>
|
||||
{subtask.task}
|
||||
</Typography.Text>
|
||||
<StatusDropdown currentStatus={subtask.status} />
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Avatar.Group
|
||||
size="small"
|
||||
max={{
|
||||
count: 1,
|
||||
style: {
|
||||
color: '#f56a00',
|
||||
backgroundColor: '#fde3cf',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{subtask.members?.map((member) => (
|
||||
<Avatar
|
||||
style={{
|
||||
backgroundColor: AvatarNamesMap[member.memberName.charAt(0)],
|
||||
fontSize: '12px',
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{member.memberName.charAt(0)}
|
||||
</Avatar>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<DatePicker
|
||||
className={`custom-placeholder ${!subTaskDueDate ? 'empty-date' : isSubToday || isSubTomorrow ? 'selected-date' : isItSubPrevDate ? 'red-colored' : ''}`}
|
||||
placeholder="Due date"
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
opacity: subTaskDueDate ? 1 : 0,
|
||||
}}
|
||||
onChange={handleSubTaskDateChange}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
suffixIcon={false}
|
||||
format={(value) => formatDate(value)}
|
||||
/>
|
||||
</div> */}
|
||||
<Divider style={{ margin: '5px' }} />
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubTaskCard;
|
||||
58
worklenz-frontend/src/components/board/taskCard/TaskCard.css
Normal file
58
worklenz-frontend/src/components/board/taskCard/TaskCard.css
Normal file
@@ -0,0 +1,58 @@
|
||||
.task-card {
|
||||
transition: box-shadow 0.3s ease;
|
||||
box-shadow:
|
||||
#edeae9 0 0 0 1px,
|
||||
#6d6e6f14 0 1px 4px;
|
||||
}
|
||||
|
||||
.task-card:hover {
|
||||
box-shadow:
|
||||
#a8a8a8 0 0 0 1px,
|
||||
#6d6e6f14 0 1px 4px;
|
||||
}
|
||||
|
||||
.task-card.dark-mode {
|
||||
box-shadow:
|
||||
#2e2e2e 0 0 0 1px,
|
||||
#0000001a 0 1px 4px;
|
||||
}
|
||||
|
||||
.custom-placeholder .ant-picker-input input::placeholder {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.task-card:hover .empty-date {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.task-card:hover .hide-add-member-avatar {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.custom-placeholder .ant-picker-input input {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.selected-date .ant-picker-input {
|
||||
color: #87d068;
|
||||
}
|
||||
|
||||
.red-colored .ant-picker-input {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.sub-selected-date .ant-picker-input {
|
||||
color: #87d068;
|
||||
}
|
||||
|
||||
.sub-red-colored .ant-picker-input {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.add-member-avatar {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hide-add-member-avatar {
|
||||
opacity: 0;
|
||||
}
|
||||
318
worklenz-frontend/src/components/board/taskCard/TaskCard.tsx
Normal file
318
worklenz-frontend/src/components/board/taskCard/TaskCard.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
DatePicker,
|
||||
Tooltip,
|
||||
Tag,
|
||||
Avatar,
|
||||
Progress,
|
||||
Typography,
|
||||
Dropdown,
|
||||
MenuProps,
|
||||
Button,
|
||||
} from 'antd';
|
||||
import {
|
||||
DoubleRightOutlined,
|
||||
PauseOutlined,
|
||||
UserAddOutlined,
|
||||
InboxOutlined,
|
||||
DeleteOutlined,
|
||||
MinusOutlined,
|
||||
ForkOutlined,
|
||||
CaretRightFilled,
|
||||
CaretDownFilled,
|
||||
} from '@ant-design/icons';
|
||||
import './TaskCard.css';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import AddMembersDropdown from '../../add-members-dropdown/add-members-dropdown';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { deleteTask } from '../../../features/tasks/tasks.slice';
|
||||
import SubTaskCard from '../subTaskCard/SubTaskCard';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import Avatars from '@/components/avatars/avatars';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { UniqueIdentifier } from '@dnd-kit/core';
|
||||
|
||||
interface taskProps {
|
||||
task: IProjectTask;
|
||||
}
|
||||
|
||||
const TaskCard: React.FC<taskProps> = ({ task }) => {
|
||||
const [isSubTaskShow, setIsSubTaskShow] = useState(false);
|
||||
const [dueDate, setDueDate] = useState<Dayjs | null>(null);
|
||||
const [isToday, setIsToday] = useState(false);
|
||||
const [isTomorrow, setIsTomorrow] = useState(false);
|
||||
const [isItPrevDate, setIsItPrevDate] = useState(false);
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: task.id as UniqueIdentifier,
|
||||
data: {
|
||||
type: 'task',
|
||||
task,
|
||||
},
|
||||
});
|
||||
|
||||
const handleDateChange = (date: Dayjs | null) => {
|
||||
setDueDate(date);
|
||||
};
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const formatDate = (date: Dayjs | null) => {
|
||||
if (!date) return '';
|
||||
|
||||
const today = dayjs();
|
||||
const tomorrow = today.add(1, 'day');
|
||||
|
||||
if (date.isSame(today, 'day')) {
|
||||
return t('today');
|
||||
} else if (date.isSame(tomorrow, 'day')) {
|
||||
return t('tomorrow');
|
||||
} else {
|
||||
return date.isSame(today, 'year') ? date.format('MMM DD') : date.format('MMM DD, YYYY');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dueDate) {
|
||||
setIsToday(dueDate.isSame(dayjs(), 'day'));
|
||||
setIsTomorrow(dueDate.isSame(dayjs().add(1, 'day'), 'day'));
|
||||
setIsItPrevDate(dueDate.isBefore(dayjs()));
|
||||
} else {
|
||||
setIsToday(false);
|
||||
setIsTomorrow(false);
|
||||
setIsItPrevDate(false);
|
||||
}
|
||||
}, [dueDate]);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!task.id) return;
|
||||
dispatch(deleteTask(task.id)); // Call delete function with taskId
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: (
|
||||
<span>
|
||||
<UserAddOutlined /> <Typography.Text>{t('assignToMe')}</Typography.Text>
|
||||
</span>
|
||||
),
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<span>
|
||||
<InboxOutlined /> <Typography.Text>{t('archive')}</Typography.Text>
|
||||
</span>
|
||||
),
|
||||
key: '2',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<span onClick={handleDelete}>
|
||||
<DeleteOutlined /> <Typography.Text>{t('delete')}</Typography.Text>
|
||||
</span>
|
||||
),
|
||||
key: '3',
|
||||
},
|
||||
];
|
||||
|
||||
// const progress = (task.subTasks?.length || 0 + 1 )/ (task.subTasks?.length || 0 + 1)
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
position: 'relative',
|
||||
touchAction: 'none',
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
||||
<Dropdown menu={{ items }} trigger={['contextMenu']}>
|
||||
<div
|
||||
className={`task-card ${themeMode === 'dark' ? 'dark-mode' : ''}`}
|
||||
style={{
|
||||
zIndex: 99,
|
||||
padding: '12px',
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '12px',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
{/* Labels and Progress */}
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div>
|
||||
{task.labels?.length ? (
|
||||
<>
|
||||
{task.labels.slice(0, 2).map((label, index) => (
|
||||
<Tag key={index} style={{ marginRight: '4px' }} color={label.color_code}>
|
||||
<span style={{ color: themeMode === 'dark' ? '#383838' : '' }}>
|
||||
{label.name}
|
||||
</span>
|
||||
</Tag>
|
||||
))}
|
||||
{task.labels?.length > 2 && <Tag>+ {task.labels.length - 2}</Tag>}
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '30px',
|
||||
height: '30px',
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<Tooltip title="1/1">
|
||||
<Progress type="circle" percent={task.progress} size={26} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Icons */}
|
||||
<div style={{ display: 'flex' }}>
|
||||
{task.priority === 'low' ? (
|
||||
<MinusOutlined
|
||||
style={{
|
||||
color: '#52c41a',
|
||||
marginRight: '0.25rem',
|
||||
}}
|
||||
/>
|
||||
) : task.priority === 'medium' ? (
|
||||
<PauseOutlined
|
||||
style={{
|
||||
color: '#faad14',
|
||||
transform: 'rotate(90deg)',
|
||||
marginRight: '0.25rem',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<DoubleRightOutlined
|
||||
style={{
|
||||
color: '#f5222d',
|
||||
transform: 'rotate(-90deg)',
|
||||
marginRight: '0.25rem',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Typography.Text style={{ fontWeight: 500 }}>{task.name}</Typography.Text>
|
||||
</div>
|
||||
|
||||
{/* Subtask Section */}
|
||||
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
opacity: 1,
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<Avatars members={task.names || []} />
|
||||
<Avatar
|
||||
size="small"
|
||||
className={
|
||||
task.assignees?.length ? 'add-member-avatar' : 'hide-add-member-avatar'
|
||||
}
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px dashed #c4c4c4',
|
||||
color: '#000000d9',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
<AddMembersDropdown />
|
||||
</Avatar>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'right',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<DatePicker
|
||||
className={`custom-placeholder ${
|
||||
!dueDate
|
||||
? 'empty-date'
|
||||
: isToday
|
||||
? 'selected-date'
|
||||
: isTomorrow
|
||||
? 'selected-date'
|
||||
: isItPrevDate
|
||||
? 'red-colored'
|
||||
: ''
|
||||
}`}
|
||||
placeholder={t('dueDate')}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
opacity: dueDate ? 1 : 0,
|
||||
width: dueDate ? 'auto' : '100%',
|
||||
maxWidth: '100px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
onChange={handleDateChange}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
suffixIcon={false}
|
||||
format={value => formatDate(value)}
|
||||
/>
|
||||
</div>
|
||||
{task.sub_tasks_count && task.sub_tasks_count > 0 && (
|
||||
<Button
|
||||
onClick={() => setIsSubTaskShow(!isSubTaskShow)}
|
||||
size="small"
|
||||
style={{ padding: 0 }}
|
||||
type="text"
|
||||
>
|
||||
<Tag
|
||||
bordered={false}
|
||||
style={{ display: 'flex', alignItems: 'center', margin: 0 }}
|
||||
>
|
||||
<ForkOutlined rotate={90} />
|
||||
<span>{task.sub_tasks_count}</span>
|
||||
{isSubTaskShow ? <CaretDownFilled /> : <CaretRightFilled />}
|
||||
</Tag>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSubTaskShow &&
|
||||
task.sub_tasks_count &&
|
||||
task.sub_tasks_count > 0 &&
|
||||
task.sub_tasks?.map(subtask => <SubTaskCard subtask={subtask} />)}
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskCard;
|
||||
@@ -0,0 +1,3 @@
|
||||
.task-card .create-task-empty-date {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { Avatar, Button, DatePicker, Input, InputRef } from 'antd';
|
||||
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import AddMembersDropdown from '../../add-members-dropdown/add-members-dropdown';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import './TaskCreateCard.css';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { addTask, addTaskToTop } from '../../../features/tasks/tasks.slice';
|
||||
import { setTaskCardDisabled } from '../../../features/board/create-card.slice';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
interface StatusProps {
|
||||
status: string;
|
||||
position: 'top' | 'bottom';
|
||||
}
|
||||
|
||||
const TaskCreateCard = forwardRef<InputRef, StatusProps>(({ status, position }, ref) => {
|
||||
const [characterLength, setCharacterLength] = useState<number>(0);
|
||||
const [dueDate, setDueDate] = useState<Dayjs | null>(null);
|
||||
const [isToday, setIsToday] = useState(false);
|
||||
const [isTomorrow, setIsTomorrow] = useState(false);
|
||||
const [isItPrevDate, setIsItPrevDate] = useState(false);
|
||||
const [taskName, setTaskName] = useState('');
|
||||
const dispatch = useAppDispatch();
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCharacterLength(e.target.value.length);
|
||||
setTaskName(e.target.value);
|
||||
};
|
||||
|
||||
const handleDateChange = (date: Dayjs | null) => {
|
||||
setDueDate(date);
|
||||
};
|
||||
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (cardRef.current) {
|
||||
cardRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
if (ref && typeof ref === 'object' && ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const formatDate = (date: Dayjs | null) => {
|
||||
if (!date) return '';
|
||||
|
||||
const today = dayjs();
|
||||
const tomorrow = today.add(1, 'day');
|
||||
|
||||
if (date.isSame(today, 'day')) {
|
||||
return 'Today';
|
||||
} else if (date.isSame(tomorrow, 'day')) {
|
||||
return 'Tomorrow';
|
||||
} else {
|
||||
return date.isSame(today, 'year') ? date.format('MMM DD') : date.format('MMM DD, YYYY');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dueDate) {
|
||||
setIsToday(dueDate.isSame(dayjs(), 'day'));
|
||||
setIsTomorrow(dueDate.isSame(dayjs().add(1, 'day'), 'day'));
|
||||
setIsItPrevDate(dueDate.isBefore(dayjs()));
|
||||
} else {
|
||||
setIsToday(false);
|
||||
setIsTomorrow(false);
|
||||
setIsItPrevDate(false);
|
||||
}
|
||||
}, [dueDate]);
|
||||
|
||||
const handleAddTask = () => {
|
||||
if (taskName.trim()) {
|
||||
if (position === 'bottom') {
|
||||
dispatch(
|
||||
addTask({
|
||||
taskId: `SP-${Date.now()}`,
|
||||
task: taskName,
|
||||
description: '-',
|
||||
progress: status === 'done' ? 100 : 0,
|
||||
members: [],
|
||||
labels: [],
|
||||
status: status,
|
||||
priority: 'medium',
|
||||
timeTracking: 0,
|
||||
estimation: '-',
|
||||
startDate: new Date(),
|
||||
dueDate: dueDate ? dueDate.toDate() : null,
|
||||
completedDate: null,
|
||||
createdDate: new Date(),
|
||||
lastUpdated: new Date(),
|
||||
reporter: '-',
|
||||
phase: '',
|
||||
subTasks: [],
|
||||
})
|
||||
);
|
||||
} else if (position === 'top') {
|
||||
dispatch(
|
||||
addTaskToTop({
|
||||
taskId: `SP-${Date.now()}`,
|
||||
task: taskName,
|
||||
description: '-',
|
||||
progress: status === 'done' ? 100 : 0,
|
||||
members: [],
|
||||
labels: [],
|
||||
status: status,
|
||||
priority: 'medium',
|
||||
timeTracking: 0,
|
||||
estimation: '-',
|
||||
startDate: new Date(),
|
||||
dueDate: dueDate ? dueDate.toDate() : null,
|
||||
completedDate: null,
|
||||
createdDate: new Date(),
|
||||
lastUpdated: new Date(),
|
||||
reporter: '-',
|
||||
phase: '-',
|
||||
subTasks: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
setTaskName('');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position, disabled: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={`task-card ${themeMode === 'dark' ? 'dark-mode' : ''}`}
|
||||
style={{
|
||||
zIndex: 99,
|
||||
padding: '12px',
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '12px',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Input field */}
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
maxLength={100}
|
||||
onChange={handleChange}
|
||||
value={taskName}
|
||||
onPressEnter={handleAddTask}
|
||||
placeholder="Enter task name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ opacity: characterLength > 0 ? 1 : 0 }}>
|
||||
{/* Character Length */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
right: '15px',
|
||||
top: '43px',
|
||||
color: themeMode === 'dark' ? '#ffffffd9' : '#00000073',
|
||||
fontSize: '10px',
|
||||
}}
|
||||
>
|
||||
<span>{characterLength}/100</span>
|
||||
</div>
|
||||
|
||||
{/* DatePicker and Avatars */}
|
||||
<div
|
||||
style={{
|
||||
paddingTop: '0.25rem',
|
||||
marginTop: '0.75rem',
|
||||
display: 'flex',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<div style={{ height: '100%', width: '100%' }}>
|
||||
<DatePicker
|
||||
className={`custom-placeholder ${!dueDate ? 'create-task-empty-date' : isToday ? 'selected-date' : isTomorrow ? 'selected-date' : isItPrevDate ? 'red-colored' : ''}`}
|
||||
placeholder={t('dueDate')}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
opacity: dueDate ? 1 : 0,
|
||||
}}
|
||||
onChange={handleDateChange}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
suffixIcon={false}
|
||||
format={value => formatDate(value)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<div
|
||||
style={{
|
||||
opacity: 1,
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<Avatar.Group>
|
||||
{/* <Avatar
|
||||
style={{
|
||||
backgroundColor:
|
||||
AvatarNamesMap[
|
||||
member?.charAt(0)
|
||||
],
|
||||
verticalAlign: 'middle',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{member.charAt(0)}
|
||||
</Avatar> */}
|
||||
</Avatar.Group>
|
||||
<Avatar
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px dashed #c4c4c4',
|
||||
color: '#000000d9',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
<AddMembersDropdown />
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Task Button and Cancel Button*/}
|
||||
<div>
|
||||
<Button size="small" style={{ marginRight: '8px', fontSize: '12px' }} onClick={handleClose}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button size="small" type="primary" style={{ fontSize: '12px' }} onClick={handleAddTask}>
|
||||
{t('addTask')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default TaskCreateCard;
|
||||
@@ -0,0 +1,255 @@
|
||||
import { Avatar, Button, DatePicker, Input, InputRef } from 'antd';
|
||||
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import AddMembersDropdown from '../../add-members-dropdown/add-members-dropdown';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import './TaskCreateCard.css';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { addTask, addTaskToTop } from '../../../features/tasks/tasks.slice';
|
||||
import { setTaskCardDisabled } from '../../../features/board/create-card.slice';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
interface PriorityProps {
|
||||
status: string;
|
||||
position: 'top' | 'bottom';
|
||||
}
|
||||
|
||||
const PriorityTaskCreateCard = forwardRef<InputRef, PriorityProps>(({ status, position }, ref) => {
|
||||
const [characterLength, setCharacterLength] = useState<number>(0);
|
||||
const [dueDate, setDueDate] = useState<Dayjs | null>(null);
|
||||
const [isToday, setIsToday] = useState(false);
|
||||
const [isTomorrow, setIsTomorrow] = useState(false);
|
||||
const [isItPrevDate, setIsItPrevDate] = useState(false);
|
||||
const [taskName, setTaskName] = useState('');
|
||||
const dispatch = useAppDispatch();
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCharacterLength(e.target.value.length);
|
||||
setTaskName(e.target.value);
|
||||
};
|
||||
|
||||
const handleDateChange = (date: Dayjs | null) => {
|
||||
setDueDate(date);
|
||||
};
|
||||
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (cardRef.current) {
|
||||
cardRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
if (ref && typeof ref === 'object' && ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const formatDate = (date: Dayjs | null) => {
|
||||
if (!date) return '';
|
||||
|
||||
const today = dayjs();
|
||||
const tomorrow = today.add(1, 'day');
|
||||
|
||||
if (date.isSame(today, 'day')) {
|
||||
return 'Today';
|
||||
} else if (date.isSame(tomorrow, 'day')) {
|
||||
return 'Tomorrow';
|
||||
} else {
|
||||
return date.isSame(today, 'year') ? date.format('MMM DD') : date.format('MMM DD, YYYY');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dueDate) {
|
||||
setIsToday(dueDate.isSame(dayjs(), 'day'));
|
||||
setIsTomorrow(dueDate.isSame(dayjs().add(1, 'day'), 'day'));
|
||||
setIsItPrevDate(dueDate.isBefore(dayjs()));
|
||||
} else {
|
||||
setIsToday(false);
|
||||
setIsTomorrow(false);
|
||||
setIsItPrevDate(false);
|
||||
}
|
||||
}, [dueDate]);
|
||||
|
||||
const handleAddTask = () => {
|
||||
if (taskName.trim()) {
|
||||
if (position === 'bottom') {
|
||||
dispatch(
|
||||
addTask({
|
||||
taskId: `SP-${Date.now()}`,
|
||||
task: taskName,
|
||||
description: '-',
|
||||
progress: status === 'done' ? 100 : 0,
|
||||
members: [],
|
||||
labels: [],
|
||||
status: status,
|
||||
priority: 'medium',
|
||||
timeTracking: 0,
|
||||
estimation: '-',
|
||||
startDate: new Date(),
|
||||
dueDate: dueDate ? dueDate.toDate() : null,
|
||||
completedDate: null,
|
||||
createdDate: new Date(),
|
||||
lastUpdated: new Date(),
|
||||
reporter: '-',
|
||||
phase: '',
|
||||
subTasks: [],
|
||||
})
|
||||
);
|
||||
} else if (position === 'top') {
|
||||
dispatch(
|
||||
addTaskToTop({
|
||||
taskId: `SP-${Date.now()}`,
|
||||
task: taskName,
|
||||
description: '-',
|
||||
progress: status === 'done' ? 100 : 0,
|
||||
members: [],
|
||||
labels: [],
|
||||
status: status,
|
||||
priority: 'medium',
|
||||
timeTracking: 0,
|
||||
estimation: '-',
|
||||
startDate: new Date(),
|
||||
dueDate: dueDate ? dueDate.toDate() : null,
|
||||
completedDate: null,
|
||||
createdDate: new Date(),
|
||||
lastUpdated: new Date(),
|
||||
reporter: '-',
|
||||
phase: '-',
|
||||
subTasks: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
setTaskName('');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setTaskCardDisabled({ status, position, disabled: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={`task-card ${themeMode === 'dark' ? 'dark-mode' : ''}`}
|
||||
style={{
|
||||
zIndex: 99,
|
||||
padding: '12px',
|
||||
backgroundColor: themeMode === 'dark' ? '#383838' : 'white',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '12px',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Input field */}
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
maxLength={100}
|
||||
onChange={handleChange}
|
||||
value={taskName}
|
||||
onPressEnter={handleAddTask}
|
||||
placeholder="Enter task name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ opacity: characterLength > 0 ? 1 : 0 }}>
|
||||
{/* Character Length */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
right: '15px',
|
||||
top: '43px',
|
||||
color: themeMode === 'dark' ? '#ffffffd9' : '#00000073',
|
||||
fontSize: '10px',
|
||||
}}
|
||||
>
|
||||
<span>{characterLength}/100</span>
|
||||
</div>
|
||||
|
||||
{/* DatePicker and Avatars */}
|
||||
<div
|
||||
style={{
|
||||
paddingTop: '0.25rem',
|
||||
marginTop: '0.75rem',
|
||||
display: 'flex',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<div style={{ height: '100%', width: '100%' }}>
|
||||
<DatePicker
|
||||
className={`custom-placeholder ${!dueDate ? 'create-task-empty-date' : isToday ? 'selected-date' : isTomorrow ? 'selected-date' : isItPrevDate ? 'red-colored' : ''}`}
|
||||
placeholder={t('dueDate')}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
opacity: dueDate ? 1 : 0,
|
||||
}}
|
||||
onChange={handleDateChange}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
suffixIcon={false}
|
||||
format={value => formatDate(value)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<div
|
||||
style={{
|
||||
opacity: 1,
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
gap: '3px',
|
||||
}}
|
||||
>
|
||||
<Avatar.Group>
|
||||
{/* <Avatar
|
||||
style={{
|
||||
backgroundColor:
|
||||
avatarNamesMap[
|
||||
member?.charAt(0)
|
||||
],
|
||||
verticalAlign: 'middle',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{member.charAt(0)}
|
||||
</Avatar> */}
|
||||
</Avatar.Group>
|
||||
<Avatar
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px dashed #c4c4c4',
|
||||
color: '#000000d9',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
<AddMembersDropdown />
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Task Button and Cancel Button*/}
|
||||
<div>
|
||||
<Button size="small" style={{ marginRight: '8px', fontSize: '12px' }} onClick={handleClose}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button size="small" type="primary" style={{ fontSize: '12px' }} onClick={handleAddTask}>
|
||||
{t('addTask')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default PriorityTaskCreateCard;
|
||||
Reference in New Issue
Block a user