Allowed tasks to be moved freely up and down the order, made private windows display in vertical columns rather then horizontal and made both screens display tasks in the SAME order
This commit is contained in:
@@ -169,12 +169,16 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveTask = async (taskId, toUserId, position = null) => {
|
const moveTask = async (taskId, toUserId, position = null, status = null) => {
|
||||||
try {
|
try {
|
||||||
const updates = { assignee_id: toUserId };
|
const updates = { assignee_id: toUserId };
|
||||||
if (position !== null) updates.position = position;
|
if (position !== null) updates.position = position;
|
||||||
|
if (status !== null) updates.status = status;
|
||||||
await api.updateTask(taskId, updates);
|
await api.updateTask(taskId, updates);
|
||||||
await api.addAudit({ actor: meId, action: 'task_moved', summary: 'Moved task to ' + (merge(toUserId)||{}).name, target: taskId });
|
|
||||||
|
const u = (merge(toUserId)||{}).name;
|
||||||
|
const summary = status ? `Moved task to ${u} and set to ${status}` : `Moved task to ${u}`;
|
||||||
|
await api.addAudit({ actor: meId, action: 'task_moved', summary, target: taskId });
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -291,6 +295,7 @@ function App() {
|
|||||||
user={merge(tab)} tasks={filteredTasks} density={t.density}
|
user={merge(tab)} tasks={filteredTasks} density={t.density}
|
||||||
onOpen={(task) => setOpenTaskId(task.id)}
|
onOpen={(task) => setOpenTaskId(task.id)}
|
||||||
onAddFor={(uid) => setAdding(uid)}
|
onAddFor={(uid) => setAdding(uid)}
|
||||||
|
onMoveTask={moveTask}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+123
-36
@@ -272,36 +272,56 @@ function OverviewScreen({ tasks, onOpen, onAddFor, density, onMoveTask, dbUsers
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Column({ user, tasks, onOpen, onAdd, density, onDropTask, dragOver, onDragOver, onDragLeave, onDragStartCard, onDragEndCard, draggingId, dragOverTaskId, dropSide, onDragOverTask }) {
|
function Column({ user, title, icon, tasks, onOpen, onAdd, density, onDropTask, dragOver, onDragOver, onDragLeave, onDragStartCard, onDragEndCard, draggingId, dragOverTaskId, dropSide, onDragOverTask, colId, faded }) {
|
||||||
|
const columnId = colId || (user ? user.id : title);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={"column" + (dragOver && tasks.length === 0 ? " column--over" : "")}
|
className={"column" + (dragOver && tasks.length === 0 ? " column--over" : "")}
|
||||||
data-comment-anchor={"col-" + user.id}
|
onDragOver={(e) => { e.preventDefault(); onDragOver && onDragOver(columnId); }}
|
||||||
onDragOver={(e) => { e.preventDefault(); onDragOver && onDragOver(user.id); }}
|
|
||||||
onDragLeave={(e) => {
|
onDragLeave={(e) => {
|
||||||
if (e.relatedTarget && !e.currentTarget.contains(e.relatedTarget)) {
|
if (e.relatedTarget && !e.currentTarget.contains(e.relatedTarget)) {
|
||||||
onDragLeave && onDragLeave(user.id);
|
onDragLeave && onDragLeave(columnId);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDrop={(e) => { e.preventDefault(); onDropTask && onDropTask(user.id); }}
|
onDrop={(e) => { e.preventDefault(); onDropTask && onDropTask(columnId); }}
|
||||||
>
|
>
|
||||||
<header className="column__head">
|
<header className="column__head">
|
||||||
<div className="column__head-l">
|
<div className="column__head-l">
|
||||||
<Avatar user={user} size={28} />
|
{user ? (
|
||||||
<div className="column__head-meta">
|
<>
|
||||||
<h2 className="column__name">{user.name}</h2>
|
<Avatar user={user} size={28} />
|
||||||
<span className="column__role">{user.role}</span>
|
<div className="column__head-meta">
|
||||||
</div>
|
<h2 className="column__name">{user.name}</h2>
|
||||||
|
<span className="column__role">{user.role}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{icon && <span className="column__icon">{icon}</span>}
|
||||||
|
<div className="column__head-meta">
|
||||||
|
<h2 className="column__name">{title}</h2>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="column__head-r">
|
<div className="column__head-r">
|
||||||
<span className="column__count mono">{tasks.length}</span>
|
<span className="column__count mono">{tasks.length}</span>
|
||||||
<button className="add-btn" onClick={onAdd} aria-label={"Add task for " + user.name}>
|
{onAdd && (
|
||||||
<Icon.Plus />
|
<button className="add-btn" onClick={onAdd} aria-label={"Add task"}>
|
||||||
</button>
|
<Icon.Plus />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="column__list" style={{ flex: 1, position: 'relative' }}>
|
<div className="column__list" style={{ flex: 1, position: 'relative', opacity: faded ? 0.45 : 1 }}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onDragOverTask(null, 'bottom');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
{tasks.length === 0 && !dragOver && (
|
{tasks.length === 0 && !dragOver && (
|
||||||
<div className="column__empty">
|
<div className="column__empty">
|
||||||
<span className="mono">— inbox zero —</span>
|
<span className="mono">— inbox zero —</span>
|
||||||
@@ -341,6 +361,7 @@ function Column({ user, tasks, onOpen, onAdd, density, onDropTask, dragOver, onD
|
|||||||
onDragStart={onDragStartCard}
|
onDragStart={onDragStartCard}
|
||||||
onDragEnd={onDragEndCard}
|
onDragEnd={onDragEndCard}
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
const mid = rect.top + rect.height / 2;
|
const mid = rect.top + rect.height / 2;
|
||||||
onDragOverTask(t.id, e.clientY < mid ? 'top' : 'bottom');
|
onDragOverTask(t.id, e.clientY < mid ? 'top' : 'bottom');
|
||||||
@@ -354,11 +375,48 @@ function Column({ user, tasks, onOpen, onAdd, density, onDropTask, dragOver, onD
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserScreen({ user, tasks, onOpen, onAddFor, density }) {
|
function UserScreen({ user, tasks, onOpen, onAddFor, density, onMoveTask }) {
|
||||||
const mine = tasks.filter(t => t.assignee === user.id);
|
const mine = tasks.filter(t => t.assignee === user.id);
|
||||||
const open = mine.filter(t => t.status === 'open');
|
const open = mine.filter(t => t.status === 'open');
|
||||||
const flagged = mine.filter(t => t.status === 'unsuccessful' || t.status === 'billing');
|
const flagged = mine.filter(t => t.status === 'unsuccessful' || t.status === 'billing');
|
||||||
const closed = mine.filter(t => t.status === 'closed');
|
const closed = mine.filter(t => t.status === 'closed');
|
||||||
|
|
||||||
|
const [draggingTask, setDraggingTask] = React.useState(null);
|
||||||
|
const [dragOverCol, setDragOverCol] = React.useState(null);
|
||||||
|
const [dragOverTaskId, setDragOverTaskId] = React.useState(null);
|
||||||
|
const [dropSide, setDropSide] = React.useState('bottom');
|
||||||
|
|
||||||
|
const onDrop = (status) => {
|
||||||
|
if (!draggingTask) return;
|
||||||
|
|
||||||
|
const targetTasks = status === 'flagged' ? flagged : (status === 'open' ? open : closed);
|
||||||
|
const cleanTarget = targetTasks.filter(t => t.id !== draggingTask.id);
|
||||||
|
|
||||||
|
let newPos = 0;
|
||||||
|
if (dragOverTaskId) {
|
||||||
|
const idx = cleanTarget.findIndex(t => t.id === dragOverTaskId);
|
||||||
|
if (dropSide === 'top') {
|
||||||
|
const prev = cleanTarget[idx - 1];
|
||||||
|
newPos = prev ? (cleanTarget[idx].position + prev.position) / 2 : cleanTarget[idx].position / 2;
|
||||||
|
} else {
|
||||||
|
const next = cleanTarget[idx + 1];
|
||||||
|
newPos = next ? (cleanTarget[idx].position + next.position) / 2 : cleanTarget[idx].position + 1000;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const last = cleanTarget[cleanTarget.length - 1];
|
||||||
|
newPos = last ? last.position + 1000 : 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine target status
|
||||||
|
let targetStatus = status;
|
||||||
|
if (status === 'flagged') targetStatus = 'unsuccessful'; // Default flagged status
|
||||||
|
if (status === 'closed') targetStatus = 'closed';
|
||||||
|
if (status === 'open') targetStatus = 'open';
|
||||||
|
|
||||||
|
onMoveTask && onMoveTask(draggingTask.id, user.id, newPos, targetStatus);
|
||||||
|
setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-view">
|
<div className="user-view">
|
||||||
<div className="user-view__hero">
|
<div className="user-view__hero">
|
||||||
@@ -374,27 +432,56 @@ function UserScreen({ user, tasks, onOpen, onAddFor, density }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{flagged.length > 0 && (
|
<div className="board board--user">
|
||||||
<Section title="Needs review" sub="Auto-flagged by the system">
|
<Column
|
||||||
<div className="user-view__grid">
|
title="Needs review" icon="⚠" colId="flagged"
|
||||||
{flagged.map(t => <TaskCard key={t.id} task={t} onOpen={onOpen} density={density} />)}
|
tasks={flagged.filter(t => !draggingTask || t.id !== draggingTask.id)}
|
||||||
</div>
|
onOpen={onOpen} density={density}
|
||||||
</Section>
|
dragOver={dragOverCol === 'flagged'}
|
||||||
)}
|
onDragOver={setDragOverCol}
|
||||||
|
onDragLeave={() => { setDragOverCol(null); setDragOverTaskId(null); }}
|
||||||
<Section title="Open" sub={open.length + ' tasks'}>
|
onDragStartCard={(t) => {
|
||||||
<div className="user-view__grid">
|
setTimeout(() => setDraggingTask(t), 0);
|
||||||
{open.map(t => <TaskCard key={t.id} task={t} onOpen={onOpen} density={density} />)}
|
}}
|
||||||
</div>
|
onDragEndCard={() => { setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null); }}
|
||||||
</Section>
|
draggingId={draggingTask?.id}
|
||||||
|
dragOverTaskId={dragOverTaskId} dropSide={dropSide}
|
||||||
{closed.length > 0 && (
|
onDragOverTask={(tid, side) => { setDragOverTaskId(tid); setDropSide(side); }}
|
||||||
<Section title="Completed" sub={closed.length + ' tasks'}>
|
onDropTask={onDrop}
|
||||||
<div className="user-view__grid" style={{ opacity: 0.6 }}>
|
/>
|
||||||
{closed.map(t => <TaskCard key={t.id} task={t} onOpen={onOpen} density={density} />)}
|
<Column
|
||||||
</div>
|
title="Open" icon="○" colId="open"
|
||||||
</Section>
|
tasks={open.filter(t => !draggingTask || t.id !== draggingTask.id)}
|
||||||
)}
|
onOpen={onOpen} density={density}
|
||||||
|
dragOver={dragOverCol === 'open'}
|
||||||
|
onDragOver={setDragOverCol}
|
||||||
|
onDragLeave={() => { setDragOverCol(null); setDragOverTaskId(null); }}
|
||||||
|
onDragStartCard={(t) => {
|
||||||
|
setTimeout(() => setDraggingTask(t), 0);
|
||||||
|
}}
|
||||||
|
onDragEndCard={() => { setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null); }}
|
||||||
|
draggingId={draggingTask?.id}
|
||||||
|
dragOverTaskId={dragOverTaskId} dropSide={dropSide}
|
||||||
|
onDragOverTask={(tid, side) => { setDragOverTaskId(tid); setDropSide(side); }}
|
||||||
|
onDropTask={onDrop}
|
||||||
|
/>
|
||||||
|
<Column
|
||||||
|
title="Completed" icon="✓" colId="closed" faded={true}
|
||||||
|
tasks={closed.filter(t => !draggingTask || t.id !== draggingTask.id)}
|
||||||
|
onOpen={onOpen} density={density}
|
||||||
|
dragOver={dragOverCol === 'closed'}
|
||||||
|
onDragOver={setDragOverCol}
|
||||||
|
onDragLeave={() => { setDragOverCol(null); setDragOverTaskId(null); }}
|
||||||
|
onDragStartCard={(t) => {
|
||||||
|
setTimeout(() => setDraggingTask(t), 0);
|
||||||
|
}}
|
||||||
|
onDragEndCard={() => { setDraggingTask(null); setDragOverCol(null); setDragOverTaskId(null); }}
|
||||||
|
draggingId={draggingTask?.id}
|
||||||
|
dragOverTaskId={dragOverTaskId} dropSide={dropSide}
|
||||||
|
onDragOverTask={(tid, side) => { setDragOverTaskId(tid); setDropSide(side); }}
|
||||||
|
onDropTask={onDrop}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user