Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## Unreleased

- Surface the new task date fields in the kanban UI.
Snoozed tasks (`scheduled` in the future) drop off the
board until the day arrives, then return with a small
alarm-clock badge top-left — clicking it acknowledges,
which clears the snooze. A toolbar pill shows the
snoozed count and expands to a list with one-click
"Wake" per task. Deadlines within three days (or
overdue) get a separate bell badge that can be
acknowledged per-device via localStorage — the deadline
date stays on the task, the urgency cue just gets
muted. Add-task and edit-task forms gain two date
inputs.

- Add `scheduled` and `deadline` date fields to the task
model across all four backends (org, markdown, sql,
json) and the cloud wire format. Both are date-only ISO
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ export function createTask(
});
}

/** Update one or more fields on an existing task. */
/** Update one or more fields on an existing task.
* ``scheduled`` / ``deadline`` accept date-only ISO
* strings (``YYYY-MM-DD``). Pass ``""`` to clear, omit
* to leave unchanged. */
export function updateTask(
taskId: string,
updates: {
Expand All @@ -152,6 +155,8 @@ export function updateTask(
status?: string;
body?: string;
github_url?: string;
scheduled?: string;
deadline?: string;
}
): Promise<Task> {
return patch<Task>(`/kanban/tasks/${taskId}`, updates);
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/components/kanban/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { useCollapsedColumns } from "../../hooks/useCollapsedColumns";
import { DOCS } from "../../docs/panelDocs";
import { TaskCard } from "./TaskCard";
import { KanbanColumn } from "./KanbanColumn";
import { SnoozedPill } from "./SnoozedPill";
import { registerPanelAction } from "../../utils/panelActions";
import { usePendingSearch } from "../../context/ViewContext";
import { stripCustomerPrefix } from "../../utils/customerPrefix";
Expand Down Expand Up @@ -352,7 +353,19 @@ export function KanbanBoard() {
const {
isCollapsed, toggle: toggleCollapsed,
} = useCollapsedColumns();
const tasks = rawTasks.filter((t) => matchesSearch(t, search));
// Hide tasks whose ``scheduled`` date is still in the
// future — they reappear on the day. The count is shown
// in a toolbar pill so the user can still see them.
const todayStr = new Date().toISOString().slice(0, 10);
const snoozed = rawTasks.filter(
(t) => t.scheduled && t.scheduled > todayStr,
);
const visibleTasks = rawTasks.filter(
(t) => !t.scheduled || t.scheduled <= todayStr,
);
const tasks = visibleTasks.filter(
(t) => matchesSearch(t, search),
);
const { pendingSearch, clearPendingSearch } = usePendingSearch();

useEffect(
Expand Down Expand Up @@ -629,6 +642,7 @@ export function KanbanBoard() {
</div>
</>}
right={<>
<SnoozedPill snoozed={snoozed} />
<Button
variant="tonal"
size="sm"
Expand Down
142 changes: 142 additions & 0 deletions frontend/src/components/kanban/SnoozedPill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Toolbar pill listing tasks that are snoozed to a future
* date. Clicking the pill opens a small popover with one
* row per snoozed task; the per-row "Wake" button clears
* the ``scheduled`` field so the card returns to the
* board immediately.
*
* The pill is hidden when no tasks are snoozed so the
* toolbar stays clean in the common case.
*/
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { AlarmClock, X } from "lucide-react";

import { stripCustomerPrefix } from "../../utils/customerPrefix";
import { useUpdateTask } from "../../hooks/useTasks";
import type { Task } from "../../types";

interface SnoozedPillProps {
snoozed: Task[];
}

export function SnoozedPill({ snoozed }: SnoozedPillProps) {
const { t } = useTranslation("kanban");
const updateTask = useUpdateTask();
const [open, setOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);

useEffect(() => {
function onDocClick(e: MouseEvent) {
if (!wrapperRef.current) return;
if (
!wrapperRef.current.contains(e.target as Node)
) {
setOpen(false);
}
}
if (open) {
window.addEventListener("click", onDocClick);
return () => {
window.removeEventListener(
"click", onDocClick,
);
};
}
}, [open]);

if (snoozed.length === 0) return null;

const ordered = [...snoozed].sort((a, b) => {
const sa = a.scheduled ?? "";
const sb = b.scheduled ?? "";
return sa.localeCompare(sb);
});

function wake(task: Task) {
updateTask.mutate({
taskId: task.id,
updates: { scheduled: "" },
});
}

return (
<div className="relative" ref={wrapperRef}>
<button
onClick={() => setOpen((v) => !v)}
className={[
"flex items-center gap-1 px-2 py-1 rounded-md",
"text-2xs font-medium",
"text-fg-muted hover:text-fg-strong",
"bg-surface-raised hover:bg-surface-card",
"border border-border-subtle",
"transition-colors",
].join(" ")}
title={t("snoozedTooltip")}
>
<AlarmClock size={11} strokeWidth={2.2} />
<span>
{t(
"snoozedCount", { count: snoozed.length },
)}
</span>
</button>
{open && (
<div
className={[
"absolute right-0 mt-1 z-30",
"w-72 max-h-80 overflow-y-auto",
"bg-surface-overlay border border-border",
"rounded-lg shadow-card-hover p-1",
].join(" ")}
>
{ordered.map((task) => (
<div
key={task.id}
className={[
"flex items-center gap-2 px-2 py-1.5",
"rounded hover:bg-surface-raised",
"group",
].join(" ")}
>
<div className="flex-1 min-w-0">
<div className="text-xs text-fg-strong truncate">
{stripCustomerPrefix(task.title)}
</div>
<div className="text-2xs text-fg-muted">
{task.customer
? `${task.customer} · `
: ""}
{task.scheduled}
</div>
</div>
<button
onClick={() => wake(task)}
className={[
"px-2 py-0.5 rounded text-2xs",
"text-cta hover:bg-cta-muted",
"transition-colors",
].join(" ")}
title={t("wake")}
>
{t("wake")}
</button>
</div>
))}
<button
onClick={() => setOpen(false)}
className={[
"w-full mt-1 px-2 py-1 rounded",
"text-2xs text-fg-muted",
"hover:text-fg hover:bg-surface-raised",
"flex items-center justify-center gap-1",
].join(" ")}
>
<X size={10} />
{t("close")}
</button>
</div>
)}
</div>
);
}
17 changes: 17 additions & 0 deletions frontend/src/components/kanban/TaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { TaskEditForm } from "./TaskEditForm";
import { TaskCardActions } from "./TaskCardActions";
import { TaskCardContent } from "./TaskCardContent";
import { StateHistoryPopup } from "./StateHistoryPopup";
import { TaskDateBadges } from "./TaskDateBadges";

interface TaskCardProps {
task: Task;
Expand Down Expand Up @@ -87,6 +88,8 @@ export function TaskCard({
const [editGithubUrl, setEditGithubUrl] = useState(
"",
);
const [editScheduled, setEditScheduled] = useState("");
const [editDeadline, setEditDeadline] = useState("");
const { overlayUrl, openOverlay, closeOverlay } =
useLinkOverlay();
const updateTask = useUpdateTask();
Expand All @@ -106,6 +109,8 @@ export function TaskCard({
setEditTags([...task.tags]);
setEditBody(task.body ?? "");
setEditGithubUrl(task.github_url ?? "");
setEditScheduled(task.scheduled ?? "");
setEditDeadline(task.deadline ?? "");
setEditing(true);
}

Expand All @@ -121,6 +126,11 @@ export function TaskCard({
customer: editCustomer.trim(),
body: editBody,
github_url: editGithubUrl,
// Always send so the user can also clear by
// emptying the input. ``""`` clears server-side;
// an unchanged value just rewrites the same date.
scheduled: editScheduled,
deadline: editDeadline,
},
},
{
Expand Down Expand Up @@ -169,6 +179,9 @@ export function TaskCard({
style={{ backgroundColor: statusColor }}
/>

{/* Snooze + deadline badges (top-left) */}
{!editing && <TaskDateBadges task={task} />}

<div className="flex items-stretch">
{/* Drag handle + state picker */}
<div className="flex flex-col items-center shrink-0 relative">
Expand Down Expand Up @@ -228,11 +241,15 @@ export function TaskCard({
updateTask.isPending ||
setTaskTags.isPending
}
editScheduled={editScheduled}
editDeadline={editDeadline}
onCustomerChange={setEditCustomer}
onTitleChange={setEditTitle}
onBodyChange={setEditBody}
onGithubUrlChange={setEditGithubUrl}
onTagsChange={setEditTags}
onScheduledChange={setEditScheduled}
onDeadlineChange={setEditDeadline}
onSave={handleSave}
onCancel={() => setEditing(false)}
/>
Expand Down
Loading
Loading