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 {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: calc(100vh - 200px);
|
||||
min-height: calc(100vh - 350px);
|
||||
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 alertService from '@/services/alerts/alertService';
|
||||
import { IGroupBy } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import EnhancedKanbanCreateSection from './EnhancedKanbanCreateSection';
|
||||
|
||||
// Import the TaskListFilters component
|
||||
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 (
|
||||
<div className={`enhanced-kanban-board ${className}`}>
|
||||
{/* Performance Monitor - only show for large datasets */}
|
||||
{/* {performanceMetrics.totalTasks > 100 && <PerformanceMonitor />} */}
|
||||
<>
|
||||
<Card
|
||||
size="small"
|
||||
className="mb-4"
|
||||
@@ -392,57 +391,62 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
||||
<TaskListFilters position="board" />
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
<div className={`enhanced-kanban-board ${className}`}>
|
||||
{/* Performance Monitor - only show for large datasets */}
|
||||
{/* {performanceMetrics.totalTasks > 100 && <PerformanceMonitor />} */}
|
||||
|
||||
{loadingGroups ? (
|
||||
<Card>
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</Card>
|
||||
) : taskGroups.length === 0 ? (
|
||||
<Card>
|
||||
<Empty description="No tasks found" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Card>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={collisionDetectionStrategy}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={allGroupIds} strategy={horizontalListSortingStrategy}>
|
||||
<div className="kanban-groups-container">
|
||||
{taskGroups.map(group => (
|
||||
<EnhancedKanbanGroup
|
||||
key={group.id}
|
||||
group={group}
|
||||
activeTaskId={dragState.activeTaskId}
|
||||
overId={overId as string | null}
|
||||
/>
|
||||
))}
|
||||
{loadingGroups ? (
|
||||
<Card>
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay>
|
||||
{activeTask && (
|
||||
<EnhancedKanbanTaskCard
|
||||
task={activeTask}
|
||||
isDragOverlay={true}
|
||||
/>
|
||||
)}
|
||||
{activeGroup && (
|
||||
<div className="group-drag-overlay">
|
||||
<div className="group-header-content">
|
||||
<h3>{activeGroup.name}</h3>
|
||||
<span className="task-count">({activeGroup.tasks.length})</span>
|
||||
</div>
|
||||
</Card>
|
||||
) : taskGroups.length === 0 ? (
|
||||
<Card>
|
||||
<Empty description="No tasks found" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Card>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={collisionDetectionStrategy}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={allGroupIds} strategy={horizontalListSortingStrategy}>
|
||||
<div className="kanban-groups-container">
|
||||
{taskGroups.map(group => (
|
||||
<EnhancedKanbanGroup
|
||||
key={group.id}
|
||||
group={group}
|
||||
activeTaskId={dragState.activeTaskId}
|
||||
overId={overId as string | null}
|
||||
/>
|
||||
))}
|
||||
<EnhancedKanbanCreateSection />
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
)}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay>
|
||||
{activeTask && (
|
||||
<EnhancedKanbanTaskCard
|
||||
task={activeTask}
|
||||
isDragOverlay={true}
|
||||
/>
|
||||
)}
|
||||
{activeGroup && (
|
||||
<div className="group-drag-overlay">
|
||||
<div className="group-header-content">
|
||||
<h3>{activeGroup.name}</h3>
|
||||
<span className="task-count">({activeGroup.tasks.length})</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
)}
|
||||
</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 { Button } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons/lib/icons';
|
||||
import { deleteSection, IGroupBy, setBoardGroupName } from '@/features/board/board-slice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||
import { deleteStatusToggleDrawer, seletedStatusCategory } from '@/features/projects/status/DeleteStatusSlice';
|
||||
import { fetchEnhancedKanbanGroups, IGroupBy } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import EnhancedKanbanCreateTaskCard from './EnhancedKanbanCreateTaskCard';
|
||||
|
||||
interface EnhancedKanbanGroupProps {
|
||||
group: ITaskListGroup;
|
||||
activeTaskId?: string | null;
|
||||
@@ -57,10 +59,11 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const dispatch = useAppDispatch();
|
||||
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 { trackMixpanelEvent } = useMixpanelTracking();
|
||||
const [showNewCard, setShowNewCard] = useState(false);
|
||||
const [showNewCardTop, setShowNewCardTop] = useState(false);
|
||||
const [showNewCardBottom, setShowNewCardBottom] = useState(false);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
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);
|
||||
if (res.done) {
|
||||
dispatch(
|
||||
setBoardGroupName({
|
||||
groupId: group.id,
|
||||
name: sectionName ?? '',
|
||||
colorCode: res.body.color_code ?? '',
|
||||
colorCodeDark: res.body.color_code_dark ?? '',
|
||||
categoryId: category,
|
||||
})
|
||||
);
|
||||
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||
dispatch(fetchStatuses(projectId));
|
||||
setName(sectionName);
|
||||
} else {
|
||||
@@ -198,8 +193,8 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
};
|
||||
|
||||
const handleBlur = async () => {
|
||||
if (group.name === 'Untitled section') {
|
||||
dispatch(deleteSection({ sectionId: group.id }));
|
||||
if (name === 'Untitled section') {
|
||||
dispatch(fetchEnhancedKanbanGroups(projectId ?? ''));
|
||||
}
|
||||
setIsEditable(false);
|
||||
|
||||
@@ -218,14 +213,14 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
const res = await phasesApiService.updateNameOfPhase(group.id, body as ITaskPhase, projectId);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_board_column_setting_click, { Rename: 'Phase' });
|
||||
// dispatch(fetchPhasesByProjectId(projectId));
|
||||
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePressEnter = () => {
|
||||
setShowNewCard(true);
|
||||
setIsEditable(false);
|
||||
setShowNewCardTop(true);
|
||||
setShowNewCardBottom(false);
|
||||
handleBlur();
|
||||
};
|
||||
const handleDeleteSection = async () => {
|
||||
@@ -237,7 +232,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
const res = await statusApiService.deleteStatus(group.id, projectId, replacingStatusId);
|
||||
if (res.message === 'At least one status should exists under each category.') return
|
||||
if (res.done) {
|
||||
dispatch(deleteSection({ sectionId: group.id }));
|
||||
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||
} else {
|
||||
dispatch(seletedStatusCategory({ id: group.id, name: name, category_id: group.category_id ?? '', message: res.message ?? '' }));
|
||||
dispatch(deleteStatusToggleDrawer());
|
||||
@@ -245,7 +240,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
} else if (groupBy === IGroupBy.PHASE) {
|
||||
const res = await phasesApiService.deletePhaseOption(group.id, projectId);
|
||||
if (res.done) {
|
||||
dispatch(deleteSection({ sectionId: group.id }));
|
||||
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -327,6 +322,9 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
borderRadius: 6,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onMouseEnter={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
@@ -335,9 +333,13 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
gap={6}
|
||||
align="center"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if ((isProjectManager || isOwnerOrAdmin) && group.name !== 'Unmapped') setIsEditable(true);
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
@@ -356,7 +358,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
{isEditable ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={group.name}
|
||||
value={name}
|
||||
variant="borderless"
|
||||
style={{
|
||||
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
|
||||
@@ -364,6 +366,15 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onPressEnter={handlePressEnter}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip title={isEllipsisActive ? name : null}>
|
||||
@@ -378,6 +389,17 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
color: themeMode === 'dark' ? '#383838' : '',
|
||||
display: 'inline-block',
|
||||
overflow: 'hidden',
|
||||
userSelect: 'text',
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
@@ -392,7 +414,10 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
size="small"
|
||||
shape="circle"
|
||||
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
|
||||
onClick={() => setShowNewCard(true)}
|
||||
onClick={() => {
|
||||
setShowNewCardTop(true);
|
||||
setShowNewCardBottom(false);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
@@ -427,6 +452,10 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
</div>
|
||||
|
||||
<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 && (
|
||||
<div className="drop-preview-empty">
|
||||
<div className="drop-indicator">Drop here</div>
|
||||
@@ -475,6 +504,30 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -440,6 +440,14 @@ const enhancedKanbanSlice = createSlice({
|
||||
}, {} as Record<string, ITaskListGroup>);
|
||||
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) => {
|
||||
builder
|
||||
@@ -528,6 +536,7 @@ export const {
|
||||
resetState,
|
||||
reorderTasks,
|
||||
reorderGroups,
|
||||
addTaskToGroup,
|
||||
} = enhancedKanbanSlice.actions;
|
||||
|
||||
export default enhancedKanbanSlice.reducer;
|
||||
Reference in New Issue
Block a user