feat(enhanced-kanban): enhance EnhancedKanbanBoard with new task creation options and layout adjustments
- Updated the EnhancedKanbanBoard component to include a new section for creating tasks at both the top and bottom of each group. - Adjusted the CSS for the kanban groups container to improve layout responsiveness. - Refactored EnhancedKanbanGroup to manage task creation visibility and interactions more effectively, enhancing user experience during task management.
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
.kanban-groups-container {
|
.kanban-groups-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
min-height: calc(100vh - 200px);
|
min-height: calc(100vh - 350px);
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { statusApiService } from '@/api/taskAttributes/status/status.api.service
|
|||||||
import { ITaskStatusCreateRequest } from '@/types/tasks/task-status-create-request';
|
import { ITaskStatusCreateRequest } from '@/types/tasks/task-status-create-request';
|
||||||
import alertService from '@/services/alerts/alertService';
|
import alertService from '@/services/alerts/alertService';
|
||||||
import { IGroupBy } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
import { IGroupBy } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
import EnhancedKanbanCreateSection from './EnhancedKanbanCreateSection';
|
||||||
|
|
||||||
// Import the TaskListFilters component
|
// Import the TaskListFilters component
|
||||||
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
|
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
|
||||||
@@ -380,9 +381,7 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`enhanced-kanban-board ${className}`}>
|
<>
|
||||||
{/* Performance Monitor - only show for large datasets */}
|
|
||||||
{/* {performanceMetrics.totalTasks > 100 && <PerformanceMonitor />} */}
|
|
||||||
<Card
|
<Card
|
||||||
size="small"
|
size="small"
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
@@ -392,6 +391,9 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
<TaskListFilters position="board" />
|
<TaskListFilters position="board" />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</Card>
|
</Card>
|
||||||
|
<div className={`enhanced-kanban-board ${className}`}>
|
||||||
|
{/* Performance Monitor - only show for large datasets */}
|
||||||
|
{/* {performanceMetrics.totalTasks > 100 && <PerformanceMonitor />} */}
|
||||||
|
|
||||||
{loadingGroups ? (
|
{loadingGroups ? (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -421,6 +423,7 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
overId={overId as string | null}
|
overId={overId as string | null}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<EnhancedKanbanCreateSection />
|
||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|
||||||
@@ -443,6 +446,7 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Flex } from 'antd';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { nanoid } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { IGroupBy, fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
||||||
|
import { createStatus, fetchStatuses } from '@/features/taskAttributes/taskStatusSlice';
|
||||||
|
import { ALPHA_CHANNEL } from '@/shared/constants';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service';
|
||||||
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
import useIsProjectManager from '@/hooks/useIsProjectManager';
|
||||||
|
|
||||||
|
const EnhancedKanbanCreateSection: React.FC = () => {
|
||||||
|
const { t } = useTranslation('kanban-board');
|
||||||
|
|
||||||
|
const themeMode = useAppSelector((state) => state.themeReducer.mode);
|
||||||
|
const { projectId } = useAppSelector((state) => state.projectReducer);
|
||||||
|
const groupBy = useAppSelector((state) => state.enhancedKanbanReducer.groupBy);
|
||||||
|
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
|
||||||
|
const isProjectManager = useIsProjectManager();
|
||||||
|
|
||||||
|
// Don't show for priority grouping or if user doesn't have permissions
|
||||||
|
if (groupBy === IGroupBy.PRIORITY || (!isOwnerorAdmin && !isProjectManager)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUniqueSectionName = (baseName: string): string => {
|
||||||
|
// Check if the base name already exists
|
||||||
|
const existingNames = existingStatuses.map(status => status.name?.toLowerCase());
|
||||||
|
|
||||||
|
if (!existingNames.includes(baseName.toLowerCase())) {
|
||||||
|
return baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the base name exists, add a number suffix
|
||||||
|
let counter = 1;
|
||||||
|
let newName = `${baseName.trim()} (${counter})`;
|
||||||
|
|
||||||
|
while (existingNames.includes(newName.toLowerCase())) {
|
||||||
|
counter++;
|
||||||
|
newName = `${baseName.trim()} (${counter})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddSection = async () => {
|
||||||
|
const sectionId = nanoid();
|
||||||
|
const baseNameSection = 'Untitled section';
|
||||||
|
const sectionName = getUniqueSectionName(baseNameSection);
|
||||||
|
|
||||||
|
if (groupBy === IGroupBy.STATUS && projectId) {
|
||||||
|
// Find the "To do" category
|
||||||
|
const todoCategory = statusCategories.find(category =>
|
||||||
|
category.name?.toLowerCase() === 'to do' ||
|
||||||
|
category.name?.toLowerCase() === 'todo'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (todoCategory && todoCategory.id) {
|
||||||
|
// Create a new status
|
||||||
|
const body = {
|
||||||
|
name: sectionName,
|
||||||
|
project_id: projectId,
|
||||||
|
category_id: todoCategory.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the status
|
||||||
|
const response = await dispatch(createStatus({ body, currentProjectId: projectId })).unwrap();
|
||||||
|
|
||||||
|
if (response.done && response.body) {
|
||||||
|
// Refresh the board to show the new section
|
||||||
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
|
// Refresh statuses
|
||||||
|
dispatch(fetchStatuses(projectId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to create status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupBy === IGroupBy.PHASE && projectId) {
|
||||||
|
const body = {
|
||||||
|
name: sectionName,
|
||||||
|
project_id: projectId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await phasesApiService.addPhaseOption(projectId);
|
||||||
|
if (response.done && response.body) {
|
||||||
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to create phase:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
gap={16}
|
||||||
|
style={{
|
||||||
|
minWidth: 325,
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 12,
|
||||||
|
}}
|
||||||
|
className="h-[400px] max-h-[400px] overflow-y-scroll"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: 8,
|
||||||
|
height: 300,
|
||||||
|
background: themeWiseColor(
|
||||||
|
'linear-gradient( 180deg, #fafafa, rgba(245, 243, 243, 0))',
|
||||||
|
'linear-gradient( 180deg, #2a2b2d, rgba(42, 43, 45, 0))',
|
||||||
|
themeMode
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{
|
||||||
|
height: '38px',
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 6,
|
||||||
|
boxShadow: 'none',
|
||||||
|
}}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleAddSection}
|
||||||
|
>
|
||||||
|
{t('addSectionButton')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(EnhancedKanbanCreateSection);
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
|
import { Button, Flex, Input, InputRef } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { nanoid } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||||
|
import { useSocket } from '@/socket/socketContext';
|
||||||
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
import { ITaskCreateRequest } from '@/types/tasks/task-create-request';
|
||||||
|
import { addTaskToGroup } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
|
||||||
|
interface EnhancedKanbanCreateTaskCardProps {
|
||||||
|
sectionId: string;
|
||||||
|
setShowNewCard: (x: boolean) => void;
|
||||||
|
position?: 'top' | 'bottom';
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnhancedKanbanCreateTaskCard: React.FC<EnhancedKanbanCreateTaskCardProps> = ({
|
||||||
|
sectionId,
|
||||||
|
setShowNewCard,
|
||||||
|
position = 'bottom',
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('kanban-board');
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { socket } = useSocket();
|
||||||
|
const currentSession = useAuthService().getCurrentSession();
|
||||||
|
|
||||||
|
const [newTaskName, setNewTaskName] = useState('');
|
||||||
|
const [creatingTask, setCreatingTask] = useState(false);
|
||||||
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||||
|
const groupBy = useAppSelector(state => state.enhancedKanbanReducer.groupBy);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createRequestBody = (): ITaskCreateRequest | null => {
|
||||||
|
if (!projectId || !currentSession) return null;
|
||||||
|
const body: ITaskCreateRequest = {
|
||||||
|
project_id: projectId,
|
||||||
|
name: newTaskName.trim(),
|
||||||
|
reporter_id: currentSession.id,
|
||||||
|
team_id: currentSession.team_id,
|
||||||
|
};
|
||||||
|
if (groupBy === 'status') body.status_id = sectionId;
|
||||||
|
else if (groupBy === 'priority') body.priority_id = sectionId;
|
||||||
|
else if (groupBy === 'phase') body.phase_id = sectionId;
|
||||||
|
return body;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setNewTaskName('');
|
||||||
|
setCreatingTask(false);
|
||||||
|
setShowNewCard(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddTask = async () => {
|
||||||
|
if (creatingTask || !projectId || !currentSession || newTaskName.trim() === '') return;
|
||||||
|
setCreatingTask(true);
|
||||||
|
const body = createRequestBody();
|
||||||
|
if (!body) return;
|
||||||
|
|
||||||
|
// Real-time socket event handler
|
||||||
|
const eventHandler = (task: IProjectTask) => {
|
||||||
|
setCreatingTask(false);
|
||||||
|
dispatch(addTaskToGroup({ sectionId, task: { ...task, id: task.id || nanoid(), name: task.name || newTaskName.trim() } }));
|
||||||
|
socket?.off(SocketEvents.QUICK_TASK.toString(), eventHandler);
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
socket?.once(SocketEvents.QUICK_TASK.toString(), eventHandler);
|
||||||
|
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setNewTaskName('');
|
||||||
|
setShowNewCard(false);
|
||||||
|
setCreatingTask(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
ref={cardRef}
|
||||||
|
vertical
|
||||||
|
gap={12}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: themeWiseColor('#292929', '#fafafa', themeMode),
|
||||||
|
borderRadius: 6,
|
||||||
|
cursor: 'pointer',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={newTaskName}
|
||||||
|
onChange={e => setNewTaskName(e.target.value)}
|
||||||
|
onPressEnter={handleAddTask}
|
||||||
|
placeholder={t('newTaskNamePlaceholder')}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
disabled={creatingTask}
|
||||||
|
/>
|
||||||
|
{newTaskName.trim() && (
|
||||||
|
<Flex gap={8} justify="flex-end">
|
||||||
|
<Button size="small" onClick={handleCancel}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" size="small" onClick={handleAddTask} loading={creatingTask}>
|
||||||
|
{t('addTask')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnhancedKanbanCreateTaskCard;
|
||||||
@@ -19,7 +19,6 @@ import { Typography } from 'antd';
|
|||||||
import { Dropdown } from 'antd';
|
import { Dropdown } from 'antd';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { PlusOutlined } from '@ant-design/icons/lib/icons';
|
import { PlusOutlined } from '@ant-design/icons/lib/icons';
|
||||||
import { deleteSection, IGroupBy, setBoardGroupName } from '@/features/board/board-slice';
|
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ITaskStatusUpdateModel } from '@/types/tasks/task-status-update-model.types';
|
import { ITaskStatusUpdateModel } from '@/types/tasks/task-status-update-model.types';
|
||||||
@@ -31,6 +30,9 @@ import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service
|
|||||||
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
||||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||||
import { deleteStatusToggleDrawer, seletedStatusCategory } from '@/features/projects/status/DeleteStatusSlice';
|
import { deleteStatusToggleDrawer, seletedStatusCategory } from '@/features/projects/status/DeleteStatusSlice';
|
||||||
|
import { fetchEnhancedKanbanGroups, IGroupBy } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
import EnhancedKanbanCreateTaskCard from './EnhancedKanbanCreateTaskCard';
|
||||||
|
|
||||||
interface EnhancedKanbanGroupProps {
|
interface EnhancedKanbanGroupProps {
|
||||||
group: ITaskListGroup;
|
group: ITaskListGroup;
|
||||||
activeTaskId?: string | null;
|
activeTaskId?: string | null;
|
||||||
@@ -57,10 +59,11 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { projectId } = useAppSelector(state => state.projectReducer);
|
const { projectId } = useAppSelector(state => state.projectReducer);
|
||||||
const { groupBy } = useAppSelector(state => state.boardReducer);
|
const { groupBy } = useAppSelector(state => state.enhancedKanbanReducer);
|
||||||
const { statusCategories, status } = useAppSelector(state => state.taskStatusReducer);
|
const { statusCategories, status } = useAppSelector(state => state.taskStatusReducer);
|
||||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||||
const [showNewCard, setShowNewCard] = useState(false);
|
const [showNewCardTop, setShowNewCardTop] = useState(false);
|
||||||
|
const [showNewCardBottom, setShowNewCardBottom] = useState(false);
|
||||||
const { t } = useTranslation('kanban-board');
|
const { t } = useTranslation('kanban-board');
|
||||||
|
|
||||||
const { setNodeRef: setDroppableRef, isOver } = useDroppable({
|
const { setNodeRef: setDroppableRef, isOver } = useDroppable({
|
||||||
@@ -167,15 +170,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
};
|
};
|
||||||
const res = await statusApiService.updateStatus(group.id, body, projectId);
|
const res = await statusApiService.updateStatus(group.id, body, projectId);
|
||||||
if (res.done) {
|
if (res.done) {
|
||||||
dispatch(
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
setBoardGroupName({
|
|
||||||
groupId: group.id,
|
|
||||||
name: sectionName ?? '',
|
|
||||||
colorCode: res.body.color_code ?? '',
|
|
||||||
colorCodeDark: res.body.color_code_dark ?? '',
|
|
||||||
categoryId: category,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(fetchStatuses(projectId));
|
dispatch(fetchStatuses(projectId));
|
||||||
setName(sectionName);
|
setName(sectionName);
|
||||||
} else {
|
} else {
|
||||||
@@ -198,8 +193,8 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = async () => {
|
const handleBlur = async () => {
|
||||||
if (group.name === 'Untitled section') {
|
if (name === 'Untitled section') {
|
||||||
dispatch(deleteSection({ sectionId: group.id }));
|
dispatch(fetchEnhancedKanbanGroups(projectId ?? ''));
|
||||||
}
|
}
|
||||||
setIsEditable(false);
|
setIsEditable(false);
|
||||||
|
|
||||||
@@ -218,14 +213,14 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
const res = await phasesApiService.updateNameOfPhase(group.id, body as ITaskPhase, projectId);
|
const res = await phasesApiService.updateNameOfPhase(group.id, body as ITaskPhase, projectId);
|
||||||
if (res.done) {
|
if (res.done) {
|
||||||
trackMixpanelEvent(evt_project_board_column_setting_click, { Rename: 'Phase' });
|
trackMixpanelEvent(evt_project_board_column_setting_click, { Rename: 'Phase' });
|
||||||
// dispatch(fetchPhasesByProjectId(projectId));
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePressEnter = () => {
|
const handlePressEnter = () => {
|
||||||
setShowNewCard(true);
|
setShowNewCardTop(true);
|
||||||
setIsEditable(false);
|
setShowNewCardBottom(false);
|
||||||
handleBlur();
|
handleBlur();
|
||||||
};
|
};
|
||||||
const handleDeleteSection = async () => {
|
const handleDeleteSection = async () => {
|
||||||
@@ -237,7 +232,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
const res = await statusApiService.deleteStatus(group.id, projectId, replacingStatusId);
|
const res = await statusApiService.deleteStatus(group.id, projectId, replacingStatusId);
|
||||||
if (res.message === 'At least one status should exists under each category.') return
|
if (res.message === 'At least one status should exists under each category.') return
|
||||||
if (res.done) {
|
if (res.done) {
|
||||||
dispatch(deleteSection({ sectionId: group.id }));
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
} else {
|
} else {
|
||||||
dispatch(seletedStatusCategory({ id: group.id, name: name, category_id: group.category_id ?? '', message: res.message ?? '' }));
|
dispatch(seletedStatusCategory({ id: group.id, name: name, category_id: group.category_id ?? '', message: res.message ?? '' }));
|
||||||
dispatch(deleteStatusToggleDrawer());
|
dispatch(deleteStatusToggleDrawer());
|
||||||
@@ -245,7 +240,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
} else if (groupBy === IGroupBy.PHASE) {
|
} else if (groupBy === IGroupBy.PHASE) {
|
||||||
const res = await phasesApiService.deletePhaseOption(group.id, projectId);
|
const res = await phasesApiService.deletePhaseOption(group.id, projectId);
|
||||||
if (res.done) {
|
if (res.done) {
|
||||||
dispatch(deleteSection({ sectionId: group.id }));
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -327,6 +322,9 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
style={{
|
style={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => setIsHover(true)}
|
onMouseEnter={() => setIsHover(true)}
|
||||||
onMouseLeave={() => setIsHover(false)}
|
onMouseLeave={() => setIsHover(false)}
|
||||||
@@ -335,9 +333,13 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
gap={6}
|
gap={6}
|
||||||
align="center"
|
align="center"
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
if ((isProjectManager || isOwnerOrAdmin) && group.name !== 'Unmapped') setIsEditable(true);
|
if ((isProjectManager || isOwnerOrAdmin) && group.name !== 'Unmapped') setIsEditable(true);
|
||||||
}}
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
@@ -356,7 +358,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
{isEditable ? (
|
{isEditable ? (
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={group.name}
|
value={name}
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
|
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
|
||||||
@@ -364,6 +366,15 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onPressEnter={handlePressEnter}
|
onPressEnter={handlePressEnter}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip title={isEllipsisActive ? name : null}>
|
<Tooltip title={isEllipsisActive ? name : null}>
|
||||||
@@ -378,6 +389,17 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
color: themeMode === 'dark' ? '#383838' : '',
|
color: themeMode === 'dark' ? '#383838' : '',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
userSelect: 'text',
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onMouseUp={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
@@ -392,7 +414,10 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
size="small"
|
size="small"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
|
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
|
||||||
onClick={() => setShowNewCard(true)}
|
onClick={() => {
|
||||||
|
setShowNewCardTop(true);
|
||||||
|
setShowNewCardBottom(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -427,6 +452,10 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="enhanced-kanban-group-tasks" ref={groupRef}>
|
<div className="enhanced-kanban-group-tasks" ref={groupRef}>
|
||||||
|
{/* Create card at top */}
|
||||||
|
{showNewCardTop && (isOwnerOrAdmin || isProjectManager) && (
|
||||||
|
<EnhancedKanbanCreateTaskCard sectionId={group.id} setShowNewCard={setShowNewCardTop} position="top" />
|
||||||
|
)}
|
||||||
{group.tasks.length === 0 && isDraggingOver && (
|
{group.tasks.length === 0 && isDraggingOver && (
|
||||||
<div className="drop-preview-empty">
|
<div className="drop-preview-empty">
|
||||||
<div className="drop-indicator">Drop here</div>
|
<div className="drop-indicator">Drop here</div>
|
||||||
@@ -475,6 +504,30 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
)}
|
)}
|
||||||
|
{/* Create card at bottom */}
|
||||||
|
{showNewCardBottom && (isOwnerOrAdmin || isProjectManager) && (
|
||||||
|
<EnhancedKanbanCreateTaskCard sectionId={group.id} setShowNewCard={setShowNewCardBottom} position="bottom" />
|
||||||
|
)}
|
||||||
|
{/* Footer Add Task Button */}
|
||||||
|
{(isOwnerOrAdmin || isProjectManager) && !showNewCardTop && !showNewCardBottom && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{
|
||||||
|
height: '38px',
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 6,
|
||||||
|
boxShadow: 'none',
|
||||||
|
marginTop: 8,
|
||||||
|
}}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setShowNewCardBottom(true);
|
||||||
|
setShowNewCardTop(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('addTask')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -440,6 +440,14 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
}, {} as Record<string, ITaskListGroup>);
|
}, {} as Record<string, ITaskListGroup>);
|
||||||
state.columnOrder = reorderedGroups.map(group => group.id);
|
state.columnOrder = reorderedGroups.map(group => group.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addTaskToGroup: (state, action) => {
|
||||||
|
const { sectionId, task } = action.payload;
|
||||||
|
const group = state.taskGroups.find(g => g.id === sectionId);
|
||||||
|
if (group) {
|
||||||
|
group.tasks.push(task);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
@@ -528,6 +536,7 @@ export const {
|
|||||||
resetState,
|
resetState,
|
||||||
reorderTasks,
|
reorderTasks,
|
||||||
reorderGroups,
|
reorderGroups,
|
||||||
|
addTaskToGroup,
|
||||||
} = enhancedKanbanSlice.actions;
|
} = enhancedKanbanSlice.actions;
|
||||||
|
|
||||||
export default enhancedKanbanSlice.reducer;
|
export default enhancedKanbanSlice.reducer;
|
||||||
Reference in New Issue
Block a user