Changed it so that tasks can be dragged and reordered, as well as having tasks breathe to leave a space where the task will go when you let go

This commit is contained in:
NPS Agent
2026-05-12 12:31:44 +09:30
parent 84592b8b3b
commit b0fd767c80
9 changed files with 127 additions and 30 deletions
+89 -23
View File
@@ -212,29 +212,59 @@ function HeadsUp({ items, onDismiss, onOpenTask }) {
function OverviewScreen({ tasks, onOpen, onAddFor, density, onMoveTask, dbUsers = [] }) {
const byUser = Object.fromEntries(dbUsers.map(u => [u.id, []]));
tasks.forEach(t => { if (byUser[t.assignee] && t.status !== 'closed') byUser[t.assignee].push(t); });
const [draggingTask, setDraggingTask] = React.useState(null);
const [dragOverCol, setDragOverCol] = React.useState(null);
const [dragOverTaskId, setDragOverTaskId] = React.useState(null);
const [dropSide, setDropSide] = React.useState('bottom'); // 'top' or 'bottom'
return (
<div className="board">
{dbUsers.map(u => (
<Column
key={u.id}
user={u}
tasks={byUser[u.id]}
// Filter out the dragging task from its current column to make it "disappear" from origin
tasks={byUser[u.id].filter(t => !draggingTask || t.id !== draggingTask.id)}
onOpen={onOpen}
onAdd={() => onAddFor(u.id)}
density={density}
dragOver={dragOverCol === u.id && draggingTask && draggingTask.assignee !== u.id}
onDragOver={(uid) => setDragOverCol(uid)}
onDragLeave={() => setDragOverCol(prev => prev === u.id ? null : prev)}
onDragStartCard={(t) => setDraggingTask(t)}
onDragEndCard={() => { setDraggingTask(null); setDragOverCol(null); }}
onDragLeave={() => { setDragOverCol(null); setDragOverTaskId(null); }}
onDragStartCard={(t) => {
// Use a tiny timeout to ensure the drag image is captured before we hide the element
setTimeout(() => setDraggingTask(t), 0);
}}
onDragEndCard={() => { setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null); }}
draggingId={draggingTask && draggingTask.id}
dragOverTaskId={dragOverTaskId}
dropSide={dropSide}
onDragOverTask={(tid, side) => { setDragOverTaskId(tid); setDropSide(side); }}
onDropTask={(toId) => {
if (draggingTask && draggingTask.assignee !== toId) {
onMoveTask && onMoveTask(draggingTask.id, toId);
if (!draggingTask) return;
// Calculate position using the list that DOES NOT include the dragging task
const colTasks = byUser[toId].filter(t => t.id !== draggingTask.id);
let newPos = 0;
if (dragOverTaskId) {
const idx = colTasks.findIndex(t => t.id === dragOverTaskId);
if (dropSide === 'top') {
const prev = colTasks[idx - 1];
newPos = prev ? (colTasks[idx].position + prev.position) / 2 : colTasks[idx].position / 2;
} else {
const next = colTasks[idx + 1];
newPos = next ? (colTasks[idx].position + next.position) / 2 : colTasks[idx].position + 1000;
}
} else {
// Dropped on empty area or no specific task
const last = colTasks[colTasks.length - 1];
newPos = last ? last.position + 1000 : 1000;
}
setDraggingTask(null); setDragOverCol(null);
onMoveTask && onMoveTask(draggingTask.id, toId, newPos);
setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null);
}}
/>
))}
@@ -242,13 +272,17 @@ function OverviewScreen({ tasks, onOpen, onAddFor, density, onMoveTask, dbUsers
);
}
function Column({ user, tasks, onOpen, onAdd, density, onDropTask, dragOver, onDragOver, onDragLeave, onDragStartCard, onDragEndCard, draggingId }) {
function Column({ user, tasks, onOpen, onAdd, density, onDropTask, dragOver, onDragOver, onDragLeave, onDragStartCard, onDragEndCard, draggingId, dragOverTaskId, dropSide, onDragOverTask }) {
return (
<section
className={"column" + (dragOver ? " column--over" : "")}
className={"column" + (dragOver && tasks.length === 0 ? " column--over" : "")}
data-comment-anchor={"col-" + user.id}
onDragOver={(e) => { e.preventDefault(); onDragOver && onDragOver(user.id); }}
onDragLeave={(e) => { onDragLeave && onDragLeave(user.id); }}
onDragLeave={(e) => {
if (e.relatedTarget && !e.currentTarget.contains(e.relatedTarget)) {
onDragLeave && onDragLeave(user.id);
}
}}
onDrop={(e) => { e.preventDefault(); onDropTask && onDropTask(user.id); }}
>
<header className="column__head">
@@ -266,23 +300,55 @@ function Column({ user, tasks, onOpen, onAdd, density, onDropTask, dragOver, onD
</button>
</div>
</header>
<div className="column__list">
{tasks.length === 0 && (
<div className="column__list" style={{ flex: 1, position: 'relative' }}>
{tasks.length === 0 && !dragOver && (
<div className="column__empty">
<span className="mono"> inbox zero </span>
</div>
)}
{tasks.map(t => (
<TaskCard
key={t.id}
task={t}
onOpen={onOpen}
density={density}
dragging={draggingId === t.id}
onDragStart={onDragStartCard}
onDragEnd={onDragEndCard}
/>
))}
{tasks.length === 0 && dragOver && (
<div className="drop-placeholder" />
)}
{tasks.map(t => {
const isOver = dragOverTaskId === t.id;
const isTop = isOver && dropSide === 'top';
const isBottom = isOver && dropSide === 'bottom';
return (
<div key={t.id} style={{
position: 'relative',
transition: 'padding 0.18s cubic-bezier(0.2, 0.8, 0.2, 1)',
paddingTop: isTop ? '60px' : '0px',
paddingBottom: isBottom ? '60px' : '0px'
}}>
{isOver && (
<div className="drop-placeholder" style={{
position: 'absolute',
left: 0, right: 0,
top: isTop ? '4px' : 'auto',
bottom: isBottom ? '4px' : 'auto',
margin: 0
}} />
)}
<TaskCard
task={t}
onOpen={onOpen}
density={density}
dragging={draggingId === t.id}
onDragStart={onDragStartCard}
onDragEnd={onDragEndCard}
onDragOver={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const mid = rect.top + rect.height / 2;
onDragOverTask(t.id, e.clientY < mid ? 'top' : 'bottom');
}}
/>
</div>
);
})}
</div>
</section>
);