feat(project-view-constants): add enhanced board view to project tabs
- Introduced ProjectViewEnhancedBoard to the project view constants. - Added a new tab item for the enhanced board view, improving project management options. - Updated tab items structure to include the new board variant for better user navigation.
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import KanbanGroup from './kanbanGroup';
|
||||||
|
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||||
|
import { IGroupBy } from '@/features/tasks/tasks.slice';
|
||||||
|
|
||||||
|
interface SortableKanbanGroupProps {
|
||||||
|
group: ITaskListGroup;
|
||||||
|
projectId: string;
|
||||||
|
currentGrouping: IGroupBy;
|
||||||
|
selectedTaskIds: string[];
|
||||||
|
onAddTask?: (groupId: string) => void;
|
||||||
|
onToggleCollapse?: (groupId: string) => void;
|
||||||
|
onSelectTask?: (taskId: string, selected: boolean) => void;
|
||||||
|
onToggleSubtasks?: (taskId: string) => void;
|
||||||
|
activeTaskId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableKanbanGroup: React.FC<SortableKanbanGroupProps> = (props) => {
|
||||||
|
const { group, activeTaskId } = props;
|
||||||
|
const {
|
||||||
|
setNodeRef,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id: group.id,
|
||||||
|
data: { type: 'group', groupId: group.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
zIndex: isDragging ? 10 : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} style={style}>
|
||||||
|
<KanbanGroup
|
||||||
|
{...props}
|
||||||
|
dragHandleProps={{ ...attributes, ...listeners }}
|
||||||
|
activeTaskId={activeTaskId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortableKanbanGroup;
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
|
import { Button, Typography } from 'antd';
|
||||||
|
import { PlusOutlined, MenuOutlined } from '@ant-design/icons';
|
||||||
|
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||||
|
import { IGroupBy } from '@/features/tasks/tasks.slice';
|
||||||
|
import KanbanTaskCard from './kanbanTaskCard';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
interface TaskGroupProps {
|
||||||
|
group: ITaskListGroup;
|
||||||
|
projectId: string;
|
||||||
|
currentGrouping: IGroupBy;
|
||||||
|
selectedTaskIds: string[];
|
||||||
|
onAddTask?: (groupId: string) => void;
|
||||||
|
onToggleCollapse?: (groupId: string) => void;
|
||||||
|
onSelectTask?: (taskId: string, selected: boolean) => void;
|
||||||
|
onToggleSubtasks?: (taskId: string) => void;
|
||||||
|
dragHandleProps?: any;
|
||||||
|
activeTaskId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KanbanGroup: React.FC<TaskGroupProps> = ({
|
||||||
|
group,
|
||||||
|
projectId,
|
||||||
|
currentGrouping,
|
||||||
|
selectedTaskIds,
|
||||||
|
onAddTask,
|
||||||
|
onToggleCollapse,
|
||||||
|
onSelectTask,
|
||||||
|
onToggleSubtasks,
|
||||||
|
dragHandleProps,
|
||||||
|
activeTaskId,
|
||||||
|
}) => {
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
const { setNodeRef, isOver } = useDroppable({
|
||||||
|
id: group.id,
|
||||||
|
data: {
|
||||||
|
type: 'group',
|
||||||
|
groupId: group.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get task IDs for sortable context
|
||||||
|
const taskIds = group.tasks.map(task => task.id!);
|
||||||
|
|
||||||
|
// Get group color based on grouping type
|
||||||
|
const getGroupColor = () => {
|
||||||
|
if (group.color_code) return group.color_code;
|
||||||
|
switch (currentGrouping) {
|
||||||
|
case 'status':
|
||||||
|
return group.id === 'todo' ? '#faad14' : group.id === 'doing' ? '#1890ff' : '#52c41a';
|
||||||
|
case 'priority':
|
||||||
|
return group.id === 'critical'
|
||||||
|
? '#ff4d4f'
|
||||||
|
: group.id === 'high'
|
||||||
|
? '#fa8c16'
|
||||||
|
: group.id === 'medium'
|
||||||
|
? '#faad14'
|
||||||
|
: '#52c41a';
|
||||||
|
case 'phase':
|
||||||
|
return '#722ed1';
|
||||||
|
default:
|
||||||
|
return '#d9d9d9';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddTask = () => {
|
||||||
|
onAddTask?.(group.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
className={`kanban-group-column${isOver ? ' drag-over' : ''}`}
|
||||||
|
>
|
||||||
|
{/* Group Header */}
|
||||||
|
<div className="kanban-group-header" style={{ backgroundColor: getGroupColor() }}>
|
||||||
|
{/* Drag handle for column */}
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<MenuOutlined />}
|
||||||
|
className="kanban-group-drag-handle"
|
||||||
|
style={{ marginRight: 8, cursor: 'grab', opacity: 0.7 }}
|
||||||
|
{...(dragHandleProps || {})}
|
||||||
|
/>
|
||||||
|
<Text strong className="kanban-group-header-text">
|
||||||
|
{group.name} <span className="kanban-group-count">({group.tasks.length})</span>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tasks as Cards */}
|
||||||
|
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
||||||
|
<div className="kanban-group-tasks">
|
||||||
|
{group.tasks.length === 0 ? (
|
||||||
|
<div className="kanban-group-empty">
|
||||||
|
<Text type="secondary">No tasks in this group</Text>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
group.tasks.map((task, index) => (
|
||||||
|
task.id === activeTaskId ? (
|
||||||
|
<div key={task.id} className="kanban-task-card kanban-task-card-placeholder" />
|
||||||
|
) : (
|
||||||
|
<KanbanTaskCard
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
groupId={group.id}
|
||||||
|
currentGrouping={currentGrouping}
|
||||||
|
isSelected={selectedTaskIds.includes(task.id!)}
|
||||||
|
index={index}
|
||||||
|
onSelect={onSelectTask}
|
||||||
|
onToggleSubtasks={onToggleSubtasks}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
|
||||||
|
{/* Add Task Button */}
|
||||||
|
<div className="kanban-group-add-task">
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
block
|
||||||
|
onClick={handleAddTask}
|
||||||
|
>
|
||||||
|
Add Task
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.kanban-group-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--task-bg-primary, #fff);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px var(--task-shadow, rgba(0,0,0,0.08));
|
||||||
|
border: 1px solid var(--task-border-primary, #e8e8e8);
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 350px;
|
||||||
|
margin: 0 12px;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.kanban-group-header {
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
padding: 16px 16px 12px 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
.kanban-group-header-text {
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.kanban-group-count {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.kanban-group-tasks {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 12px 0 12px;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
.kanban-group-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #bfbfbf;
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
.kanban-group-add-task {
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid var(--task-border-secondary, #f0f0f0);
|
||||||
|
background: var(--task-bg-primary, #fff);
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
border-bottom-right-radius: 8px;
|
||||||
|
}
|
||||||
|
.drag-over {
|
||||||
|
box-shadow: 0 0 0 3px #bae7ff;
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
.kanban-group-drag-handle {
|
||||||
|
color: #fff !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 4px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
.kanban-group-drag-handle:hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
.kanban-task-card-placeholder {
|
||||||
|
min-height: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border: 2px dashed var(--task-drag-over-border, #40a9ff);
|
||||||
|
background: var(--task-bg-primary, #fff);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.dark .kanban-task-card-placeholder,
|
||||||
|
[data-theme="dark"] .kanban-task-card-placeholder {
|
||||||
|
background: var(--task-bg-primary, #1f1f1f);
|
||||||
|
border: 2px dashed var(--task-drag-over-border, #40a9ff);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KanbanGroup;
|
||||||
@@ -0,0 +1,401 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { Avatar, Tag, Progress, Typography, Button, Tooltip, Space } from 'antd';
|
||||||
|
import {
|
||||||
|
HolderOutlined,
|
||||||
|
MessageOutlined,
|
||||||
|
PaperClipOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
import { IGroupBy } from '@/features/tasks/tasks.slice';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
interface TaskRowProps {
|
||||||
|
task: IProjectTask;
|
||||||
|
projectId: string;
|
||||||
|
groupId: string;
|
||||||
|
currentGrouping: IGroupBy;
|
||||||
|
isSelected: boolean;
|
||||||
|
isDragOverlay?: boolean;
|
||||||
|
index?: number;
|
||||||
|
onSelect?: (taskId: string, selected: boolean) => void;
|
||||||
|
onToggleSubtasks?: (taskId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KanbanTaskCard: React.FC<TaskRowProps> = ({
|
||||||
|
task,
|
||||||
|
projectId,
|
||||||
|
groupId,
|
||||||
|
currentGrouping,
|
||||||
|
isSelected,
|
||||||
|
isDragOverlay = false,
|
||||||
|
index,
|
||||||
|
onSelect,
|
||||||
|
onToggleSubtasks,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id: task.id!,
|
||||||
|
data: {
|
||||||
|
type: 'task',
|
||||||
|
taskId: task.id,
|
||||||
|
groupId,
|
||||||
|
},
|
||||||
|
disabled: isDragOverlay,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format due date
|
||||||
|
const formatDueDate = (dateString?: string) => {
|
||||||
|
if (!dateString) return null;
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const now = new Date();
|
||||||
|
const diffTime = date.getTime() - now.getTime();
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
if (diffDays < 0) {
|
||||||
|
return { text: `${Math.abs(diffDays)}d overdue`, color: 'error' };
|
||||||
|
} else if (diffDays === 0) {
|
||||||
|
return { text: 'Due today', color: 'warning' };
|
||||||
|
} else if (diffDays <= 3) {
|
||||||
|
return { text: `Due in ${diffDays}d`, color: 'warning' };
|
||||||
|
} else {
|
||||||
|
return { text: `Due ${date.toLocaleDateString()}`, color: 'default' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const dueDate = formatDueDate(task.end_date);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={`kanban-task-card${isSelected ? ' kanban-task-card-selected' : ''}${isDragOverlay ? ' kanban-task-card-drag-overlay' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="kanban-task-card-header">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<HolderOutlined />}
|
||||||
|
className="kanban-drag-handle"
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
/>
|
||||||
|
<Text strong className={`kanban-task-title${task.complete_ratio === 100 ? ' kanban-task-completed' : ''}`}>
|
||||||
|
{task.name}
|
||||||
|
</Text>
|
||||||
|
{task.sub_tasks_count && task.sub_tasks_count > 0 && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={() => onToggleSubtasks?.(task.id!)}
|
||||||
|
className="kanban-subtask-toggle"
|
||||||
|
>
|
||||||
|
{task.show_sub_tasks ? '−' : '+'} {task.sub_tasks_count}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="kanban-task-card-body">
|
||||||
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||||
|
{/* Task Key and Status */}
|
||||||
|
<div className="kanban-task-row">
|
||||||
|
{task.task_key && (
|
||||||
|
<Text code className="kanban-task-key">{task.task_key}</Text>
|
||||||
|
)}
|
||||||
|
{task.status_name && (
|
||||||
|
<Tag className="kanban-task-status" style={{ backgroundColor: task.status_color, color: 'white', marginLeft: 8 }}>
|
||||||
|
{task.status_name}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{task.priority_name && (
|
||||||
|
<Tag className="kanban-task-priority" style={{ backgroundColor: task.priority_color, color: 'white', marginLeft: 8 }}>
|
||||||
|
{task.priority_name}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Progress and Due Date */}
|
||||||
|
<div className="kanban-task-row">
|
||||||
|
{typeof task.complete_ratio === 'number' && (
|
||||||
|
<Progress
|
||||||
|
type="circle"
|
||||||
|
percent={task.complete_ratio}
|
||||||
|
size={28}
|
||||||
|
strokeColor={task.complete_ratio === 100 ? '#52c41a' : '#1890ff'}
|
||||||
|
strokeWidth={4}
|
||||||
|
showInfo={false}
|
||||||
|
className="kanban-task-progress"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{dueDate && (
|
||||||
|
<Text type={dueDate.color as any} className="kanban-task-due-date" style={{ marginLeft: 12 }}>
|
||||||
|
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
||||||
|
{dueDate.text}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Assignees and Labels */}
|
||||||
|
<div className="kanban-task-row">
|
||||||
|
{task.assignees && task.assignees.length > 0 && (
|
||||||
|
<Avatar.Group size="small" maxCount={3}>
|
||||||
|
{task.assignees.map((assignee) => (
|
||||||
|
<Tooltip key={assignee.id} title={assignee.name}>
|
||||||
|
<Avatar size="small">{assignee.name?.charAt(0)?.toUpperCase()}</Avatar>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Avatar.Group>
|
||||||
|
)}
|
||||||
|
{task.labels && task.labels.length > 0 && (
|
||||||
|
<div className="kanban-task-labels">
|
||||||
|
{task.labels.slice(0, 2).map((label) => (
|
||||||
|
<Tag
|
||||||
|
key={label.id}
|
||||||
|
className="kanban-task-label"
|
||||||
|
style={{ backgroundColor: label.color_code, border: 'none', color: 'white', marginLeft: 4 }}
|
||||||
|
>
|
||||||
|
{label.name}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
{task.labels.length > 2 && (
|
||||||
|
<Text type="secondary" className="kanban-task-labels-more">
|
||||||
|
+{task.labels.length - 2}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Indicators */}
|
||||||
|
<div className="kanban-task-row kanban-task-indicators">
|
||||||
|
{task.time_spent_string && (
|
||||||
|
<span className="kanban-task-time">
|
||||||
|
<ClockCircleOutlined /> {task.time_spent_string}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.comments_count && task.comments_count > 0 && (
|
||||||
|
<span className="kanban-task-indicator">
|
||||||
|
<MessageOutlined /> {task.comments_count}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.attachments_count && task.attachments_count > 0 && (
|
||||||
|
<span className="kanban-task-indicator">
|
||||||
|
<PaperClipOutlined /> {task.attachments_count}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
{/* Subtasks */}
|
||||||
|
{task.show_sub_tasks && task.sub_tasks && task.sub_tasks.length > 0 && (
|
||||||
|
<div className="kanban-task-subtasks">
|
||||||
|
{task.sub_tasks.map((subtask) => (
|
||||||
|
<KanbanTaskCard
|
||||||
|
key={subtask.id}
|
||||||
|
task={subtask}
|
||||||
|
projectId={projectId}
|
||||||
|
groupId={groupId}
|
||||||
|
currentGrouping={currentGrouping}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<style>{`
|
||||||
|
.kanban-task-card {
|
||||||
|
background: var(--task-bg-primary, #fff);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 4px var(--task-shadow, rgba(0,0,0,0.08));
|
||||||
|
border: 1px solid var(--task-border-primary, #f0f0f0);
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 14px 16px 10px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
transition: box-shadow 0.2s, border-color 0.2s, background 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.kanban-task-card-selected {
|
||||||
|
border: 2px solid var(--task-selected-border, #1890ff);
|
||||||
|
box-shadow: 0 2px 8px var(--task-selected-bg, #e6f7ff);
|
||||||
|
}
|
||||||
|
.kanban-task-card-drag-overlay {
|
||||||
|
background: var(--task-bg-primary, #fff);
|
||||||
|
border: 2px dashed var(--task-drag-over-border, #40a9ff);
|
||||||
|
box-shadow: 0 4px 12px var(--task-shadow, rgba(24,144,255,0.15));
|
||||||
|
}
|
||||||
|
.kanban-task-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.kanban-drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
.kanban-drag-handle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.kanban-task-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--task-text-primary, #262626);
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.kanban-task-completed {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
.kanban-subtask-toggle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
padding: 0 4px;
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.kanban-task-card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.kanban-task-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
.kanban-task-key {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
background: var(--task-bg-secondary, #f0f0f0);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.kanban-task-status {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.kanban-task-priority {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.kanban-task-progress {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.kanban-task-due-date {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.kanban-task-labels {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.kanban-task-label {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 0 4px;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.kanban-task-labels-more {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
.kanban-task-indicators {
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
.kanban-task-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.kanban-task-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.kanban-task-subtasks {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: 24px;
|
||||||
|
border-left: 2px solid var(--task-border-secondary, #f0f0f0);
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
/* Dark mode overrides */
|
||||||
|
.dark .kanban-task-card,
|
||||||
|
[data-theme="dark"] .kanban-task-card {
|
||||||
|
background: var(--task-bg-primary, #1f1f1f);
|
||||||
|
border: 1px solid var(--task-border-primary, #303030);
|
||||||
|
box-shadow: 0 1px 4px var(--task-shadow, rgba(0,0,0,0.3));
|
||||||
|
}
|
||||||
|
.dark .kanban-task-card-selected,
|
||||||
|
[data-theme="dark"] .kanban-task-card-selected {
|
||||||
|
border: 2px solid var(--task-selected-border, #1890ff);
|
||||||
|
box-shadow: 0 2px 8px var(--task-selected-bg, #1a2332);
|
||||||
|
}
|
||||||
|
.dark .kanban-task-card-drag-overlay,
|
||||||
|
[data-theme="dark"] .kanban-task-card-drag-overlay {
|
||||||
|
background: var(--task-bg-primary, #1f1f1f);
|
||||||
|
border: 2px dashed var(--task-drag-over-border, #40a9ff);
|
||||||
|
box-shadow: 0 4px 12px var(--task-shadow, rgba(24,144,255,0.15));
|
||||||
|
}
|
||||||
|
.dark .kanban-task-title,
|
||||||
|
[data-theme="dark"] .kanban-task-title {
|
||||||
|
color: var(--task-text-primary, #fff);
|
||||||
|
}
|
||||||
|
.dark .kanban-task-completed,
|
||||||
|
[data-theme="dark"] .kanban-task-completed {
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
.dark .kanban-task-key,
|
||||||
|
[data-theme="dark"] .kanban-task-key {
|
||||||
|
background: var(--task-bg-secondary, #141414);
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
.dark .kanban-task-labels-more,
|
||||||
|
[data-theme="dark"] .kanban-task-labels-more {
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
.dark .kanban-task-indicators,
|
||||||
|
[data-theme="dark"] .kanban-task-indicators {
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
.dark .kanban-drag-handle,
|
||||||
|
[data-theme="dark"] .kanban-drag-handle {
|
||||||
|
color: var(--task-text-tertiary, #8c8c8c);
|
||||||
|
}
|
||||||
|
.dark .kanban-task-subtasks,
|
||||||
|
[data-theme="dark"] .kanban-task-subtasks {
|
||||||
|
border-left: 2px solid var(--task-border-secondary, #404040);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KanbanTaskCard;
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragOverlay,
|
||||||
|
DragStartEvent,
|
||||||
|
DragEndEvent,
|
||||||
|
DragOverEvent,
|
||||||
|
closestCorners,
|
||||||
|
KeyboardSensor,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
horizontalListSortingStrategy,
|
||||||
|
SortableContext,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
import { Card, Spin, Empty, Flex } from 'antd';
|
||||||
|
import { RootState } from '@/app/store';
|
||||||
|
import {
|
||||||
|
IGroupBy,
|
||||||
|
setGroup,
|
||||||
|
fetchTaskGroups,
|
||||||
|
reorderTasks,
|
||||||
|
} from '@/features/tasks/tasks.slice';
|
||||||
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
import { AppDispatch } from '@/app/store';
|
||||||
|
import BoardSectionCard from '@/pages/projects/projectView/board/board-section/board-section-card/board-section-card';
|
||||||
|
import BoardCreateSectionCard from '@/pages/projects/projectView/board/board-section/board-section-card/board-create-section-card';
|
||||||
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
import useIsProjectManager from '@/hooks/useIsProjectManager';
|
||||||
|
import BoardViewTaskCard from '@/pages/projects/projectView/board/board-section/board-task-card/board-view-task-card';
|
||||||
|
import TaskGroup from '../task-management/TaskGroup';
|
||||||
|
import TaskRow from '../task-management/TaskRow';
|
||||||
|
import KanbanGroup from './kanbanGroup';
|
||||||
|
import KanbanTaskCard from './kanbanTaskCard';
|
||||||
|
import SortableKanbanGroup from './SortableKanbanGroup';
|
||||||
|
|
||||||
|
|
||||||
|
// Import the TaskListFilters component
|
||||||
|
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
|
||||||
|
|
||||||
|
interface TaskListBoardProps {
|
||||||
|
projectId: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DragState {
|
||||||
|
activeTask: IProjectTask | null;
|
||||||
|
activeGroupId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KanbanTaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const [dragState, setDragState] = useState<DragState>({
|
||||||
|
activeTask: null,
|
||||||
|
activeGroupId: null,
|
||||||
|
});
|
||||||
|
// New state for active/over ids
|
||||||
|
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
|
||||||
|
const [overId, setOverId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Redux selectors
|
||||||
|
|
||||||
|
const { taskGroups, groupBy, loadingGroups, error, search, archived } = useSelector((state: RootState) => state.boardReducer);
|
||||||
|
|
||||||
|
// Selection state
|
||||||
|
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Drag and Drop sensors
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 8,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
|
||||||
|
const isProjectManager = useIsProjectManager();
|
||||||
|
|
||||||
|
// Fetch task groups when component mounts or dependencies change
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectId) {
|
||||||
|
dispatch(fetchTaskGroups(projectId));
|
||||||
|
}
|
||||||
|
}, [dispatch, projectId, groupBy, search, archived]);
|
||||||
|
|
||||||
|
// Memoized calculations
|
||||||
|
const allTaskIds = useMemo(() => {
|
||||||
|
return taskGroups.flatMap(group => group.tasks.map(task => task.id!));
|
||||||
|
}, [taskGroups]);
|
||||||
|
|
||||||
|
const totalTasksCount = useMemo(() => {
|
||||||
|
return taskGroups.reduce((sum, group) => sum + group.tasks.length, 0);
|
||||||
|
}, [taskGroups]);
|
||||||
|
|
||||||
|
const hasSelection = selectedTaskIds.length > 0;
|
||||||
|
|
||||||
|
// // Handlers
|
||||||
|
// const handleGroupingChange = (newGroupBy: IGroupBy) => {
|
||||||
|
// dispatch(setGroup(newGroupBy));
|
||||||
|
// };
|
||||||
|
|
||||||
|
const handleDragStart = (event: DragStartEvent) => {
|
||||||
|
const { active } = event;
|
||||||
|
const taskId = active.id as string;
|
||||||
|
setActiveTaskId(taskId);
|
||||||
|
setOverId(null);
|
||||||
|
// Find the task and its group
|
||||||
|
let activeTask: IProjectTask | null = null;
|
||||||
|
let activeGroupId: string | null = null;
|
||||||
|
for (const group of taskGroups) {
|
||||||
|
const task = group.tasks.find(t => t.id === taskId);
|
||||||
|
if (task) {
|
||||||
|
activeTask = task;
|
||||||
|
activeGroupId = group.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDragState({
|
||||||
|
activeTask,
|
||||||
|
activeGroupId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragOverEvent) => {
|
||||||
|
setOverId(event.over?.id as string || null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
setActiveTaskId(null);
|
||||||
|
setOverId(null);
|
||||||
|
setDragState({
|
||||||
|
activeTask: null,
|
||||||
|
activeGroupId: null,
|
||||||
|
});
|
||||||
|
if (!over || !dragState.activeTask || !dragState.activeGroupId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeTaskId = active.id as string;
|
||||||
|
const overIdVal = over.id as string;
|
||||||
|
// Find the group and index for drop
|
||||||
|
let targetGroupId = overIdVal;
|
||||||
|
let targetIndex = -1;
|
||||||
|
let isOverTask = false;
|
||||||
|
// Check if over is a group or a task
|
||||||
|
const overGroup = taskGroups.find(g => g.id === overIdVal);
|
||||||
|
if (!overGroup) {
|
||||||
|
// Dropping on a task, find which group it belongs to
|
||||||
|
for (const group of taskGroups) {
|
||||||
|
const taskIndex = group.tasks.findIndex(t => t.id === overIdVal);
|
||||||
|
if (taskIndex !== -1) {
|
||||||
|
targetGroupId = group.id;
|
||||||
|
targetIndex = taskIndex;
|
||||||
|
isOverTask = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sourceGroup = taskGroups.find(g => g.id === dragState.activeGroupId);
|
||||||
|
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||||
|
if (!sourceGroup || !targetGroup) return;
|
||||||
|
const sourceIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
|
||||||
|
if (sourceIndex === -1) return;
|
||||||
|
// Calculate new positions
|
||||||
|
let finalTargetIndex = targetIndex;
|
||||||
|
if (!isOverTask || finalTargetIndex === -1) {
|
||||||
|
finalTargetIndex = targetGroup.tasks.length;
|
||||||
|
}
|
||||||
|
// If moving within the same group and after itself, adjust index
|
||||||
|
if (sourceGroup.id === targetGroup.id && sourceIndex < finalTargetIndex) {
|
||||||
|
finalTargetIndex--;
|
||||||
|
}
|
||||||
|
// Create updated task arrays
|
||||||
|
const updatedSourceTasks = [...sourceGroup.tasks];
|
||||||
|
const [movedTask] = updatedSourceTasks.splice(sourceIndex, 1);
|
||||||
|
let updatedTargetTasks: IProjectTask[];
|
||||||
|
if (sourceGroup.id === targetGroup.id) {
|
||||||
|
updatedTargetTasks = updatedSourceTasks;
|
||||||
|
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
|
||||||
|
} else {
|
||||||
|
updatedTargetTasks = [...targetGroup.tasks];
|
||||||
|
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
|
||||||
|
}
|
||||||
|
// Dispatch the reorder action
|
||||||
|
dispatch(reorderTasks({
|
||||||
|
activeGroupId: sourceGroup.id,
|
||||||
|
overGroupId: targetGroup.id,
|
||||||
|
fromIndex: sourceIndex,
|
||||||
|
toIndex: finalTargetIndex,
|
||||||
|
task: movedTask,
|
||||||
|
updatedSourceTasks,
|
||||||
|
updatedTargetTasks,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleSelectTask = (taskId: string, selected: boolean) => {
|
||||||
|
setSelectedTaskIds(prev => {
|
||||||
|
if (selected) {
|
||||||
|
return [...prev, taskId];
|
||||||
|
} else {
|
||||||
|
return prev.filter(id => id !== taskId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleSubtasks = (taskId: string) => {
|
||||||
|
// Implementation for toggling subtasks
|
||||||
|
console.log('Toggle subtasks for task:', taskId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<Empty
|
||||||
|
description={`Error loading tasks: ${error}`}
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`task-list-board ${className}`}>
|
||||||
|
{/* Task Filters */}
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
className="mb-4"
|
||||||
|
styles={{ body: { padding: '12px 16px' } }}
|
||||||
|
>
|
||||||
|
<React.Suspense fallback={<div>Loading filters...</div>}>
|
||||||
|
<TaskListFilters position="board" />
|
||||||
|
</React.Suspense>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Task Groups Container */}
|
||||||
|
<div className="task-groups-outer-container">
|
||||||
|
{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={closestCorners}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={taskGroups.map(g => g.id)}
|
||||||
|
strategy={horizontalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<div className="task-groups-container">
|
||||||
|
{taskGroups.map((group) => (
|
||||||
|
<SortableKanbanGroup
|
||||||
|
key={group.id}
|
||||||
|
group={group}
|
||||||
|
projectId={projectId}
|
||||||
|
currentGrouping={groupBy}
|
||||||
|
selectedTaskIds={selectedTaskIds}
|
||||||
|
onSelectTask={handleSelectTask}
|
||||||
|
onToggleSubtasks={handleToggleSubtasks}
|
||||||
|
activeTaskId={activeTaskId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
<DragOverlay>
|
||||||
|
{dragState.activeTask ? (
|
||||||
|
<KanbanTaskCard
|
||||||
|
task={dragState.activeTask}
|
||||||
|
projectId={projectId}
|
||||||
|
groupId={dragState.activeGroupId!}
|
||||||
|
currentGrouping={groupBy}
|
||||||
|
isSelected={false}
|
||||||
|
isDragOverlay
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.task-groups-outer-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
.task-groups-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
min-width: 100%;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 8px 8px 8px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.task-groups-container {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.task-groups-container {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.task-groups-container {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Kanban column responsiveness */
|
||||||
|
.kanban-group-column {
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 98vw;
|
||||||
|
width: 100%;
|
||||||
|
height: 70vh;
|
||||||
|
max-height: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.kanban-group-column {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.kanban-group-column {
|
||||||
|
min-width: 180px;
|
||||||
|
padding-left: 2px;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Kanban card responsiveness */
|
||||||
|
.kanban-task-card {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
/* Make only the task list inside each group scrollable */
|
||||||
|
.kanban-group-tasks {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(70vh - 110px);
|
||||||
|
}
|
||||||
|
/* Dark mode support */
|
||||||
|
:root {
|
||||||
|
--task-bg-primary: #ffffff;
|
||||||
|
--task-bg-secondary: #f5f5f5;
|
||||||
|
--task-bg-tertiary: #f8f9fa;
|
||||||
|
--task-border-primary: #e8e8e8;
|
||||||
|
--task-border-secondary: #f0f0f0;
|
||||||
|
--task-border-tertiary: #d9d9d9;
|
||||||
|
--task-text-primary: #262626;
|
||||||
|
--task-text-secondary: #595959;
|
||||||
|
--task-text-tertiary: #8c8c8c;
|
||||||
|
--task-shadow: rgba(0, 0, 0, 0.1);
|
||||||
|
--task-hover-bg: #fafafa;
|
||||||
|
--task-selected-bg: #e6f7ff;
|
||||||
|
--task-selected-border: #1890ff;
|
||||||
|
--task-drag-over-bg: #f0f8ff;
|
||||||
|
--task-drag-over-border: #40a9ff;
|
||||||
|
}
|
||||||
|
.dark .task-groups-outer-container,
|
||||||
|
[data-theme="dark"] .task-groups-outer-container {
|
||||||
|
--task-bg-primary: #1f1f1f;
|
||||||
|
--task-bg-secondary: #141414;
|
||||||
|
--task-bg-tertiary: #262626;
|
||||||
|
--task-border-primary: #303030;
|
||||||
|
--task-border-secondary: #404040;
|
||||||
|
--task-border-tertiary: #505050;
|
||||||
|
--task-text-primary: #ffffff;
|
||||||
|
--task-text-secondary: #d9d9d9;
|
||||||
|
--task-text-tertiary: #8c8c8c;
|
||||||
|
--task-shadow: rgba(0, 0, 0, 0.3);
|
||||||
|
--task-hover-bg: #2a2a2a;
|
||||||
|
--task-selected-bg: #1a2332;
|
||||||
|
--task-selected-border: #1890ff;
|
||||||
|
--task-drag-over-bg: #1a2332;
|
||||||
|
--task-drag-over-border: #40a9ff;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KanbanTaskListBoard;
|
||||||
@@ -6,6 +6,7 @@ import ProjectViewUpdates from '@/pages/projects/project-view-1/updates/project-
|
|||||||
import ProjectViewTaskList from '@/pages/projects/projectView/taskList/project-view-task-list';
|
import ProjectViewTaskList from '@/pages/projects/projectView/taskList/project-view-task-list';
|
||||||
import ProjectViewBoard from '@/pages/projects/projectView/board/project-view-board';
|
import ProjectViewBoard from '@/pages/projects/projectView/board/project-view-board';
|
||||||
import ProjectViewEnhancedTasks from '@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks';
|
import ProjectViewEnhancedTasks from '@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks';
|
||||||
|
import ProjectViewEnhancedBoard from '@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board';
|
||||||
|
|
||||||
// type of a tab items
|
// type of a tab items
|
||||||
type TabItems = {
|
type TabItems = {
|
||||||
@@ -37,6 +38,13 @@ export const tabItems: TabItems[] = [
|
|||||||
key: 'board',
|
key: 'board',
|
||||||
label: 'Board',
|
label: 'Board',
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
|
element: React.createElement(ProjectViewEnhancedBoard),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 3,
|
||||||
|
key: 'board-v1',
|
||||||
|
label: 'Board v1',
|
||||||
|
isPinned: true,
|
||||||
element: React.createElement(ProjectViewBoard),
|
element: React.createElement(ProjectViewBoard),
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import TaskListBoard from '@/components/task-management/TaskListBoard';
|
||||||
|
import KanbanTaskListBoard from '@/components/kanban-board-management-v2/kanbanTaskListBoard';
|
||||||
|
|
||||||
|
const ProjectViewEnhancedBoard: React.FC = () => {
|
||||||
|
const { project } = useAppSelector(state => state.projectReducer);
|
||||||
|
|
||||||
|
if (!project?.id) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center text-gray-500">
|
||||||
|
Project not found
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="project-view-enhanced-tasks">
|
||||||
|
<KanbanTaskListBoard projectId={project.id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectViewEnhancedBoard;
|
||||||
Reference in New Issue
Block a user