Skip to content

Commit e6c51da

Browse files
authored
Merge pull request #24 from ZDOSS/codex/calendar-dated-work-items
Add calendar dated work creation
2 parents a7037d7 + 94c29af commit e6c51da

8 files changed

Lines changed: 157 additions & 8 deletions

File tree

AI.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ The core domain in `packages/core/src/domain/` covers the entities the plan call
238238
- `DocEditor` now keeps that selection-sync effect above its null guard so hook ordering stays valid even if future refactors ever allow the component to see a transient `bundle === null`
239239
- roadmap lanes now show milestone target dates, completed/total/percent progress, dependency indicators from `relationshipsForItem()`, explicit date inputs, milestone reassignment controls, and inline invalid-range feedback while preserving date-only semantics through `item.update`; clearing Start or Due clears only that side of the range
240240
- calendar now keeps the month grid and adds a derived agenda for upcoming item start/due dates plus active reminders; reminder agenda rows use the reminder's IANA timezone to decide the visible date, boundary filtering keeps reminders visible when their local display date and UTC date straddle the agenda start, agenda links use existing work-item routes, and the feature does not introduce a calendar-specific storage model
241+
- calendar scheduled-work creation intentionally creates ordinary work items through the shared `CreateItemDialog` and `item.create` command, not a separate calendar-event entity; day-cell add buttons prefill the explicit cell `dueDate`, the toolbar opens the same dialog without carrying the hidden month `anchor` date, and the dialog exposes editable Start/Due date fields plus the existing type/status/priority/assignee controls
241242
- bug triage now exposes a visible `New bug` action in the intake column, maps software-project `inbox` bugs into Intake, opens the shared create dialog with the correct intake status preselected, and limits Intake's planned-status fallback so custom workflows can still populate Ready; `buildBugTriageColumns()` is shared with overview so accepted Ready bugs do not reappear as intake pressure
242243
- bug triage cards are now `<article>` surfaces with an internal item link and real form controls, not whole-card links, so triage buttons/selects are valid and testable; accept/decline/assign dispatch `item.update`, decline resolves to an existing canceled status or completed fallback before dispatching, snooze dispatches `reminder.create`, and duplicate linking opens a picker modal before dispatching `relationship.create` with `relatesTo`
243244
- bug triage toolbar now includes severity and priority filters, cards can edit severity/priority plus plugin-owned source/context data stored under `moduleData.bug`, and the optional Workflow setting blocks Accept/Decline out of intake until a bug has either severity or priority
@@ -302,7 +303,7 @@ The core domain in `packages/core/src/domain/` covers the entities the plan call
302303
- table bulk status/priority/assignee editing across selected rows, including selections hidden by a local filter
303304
- overview route summaries, default root-to-overview routing for open projects, project create/open/demo/import landing on overview, future-only reminders, and triage-lane bug intake matching the bug board
304305
- roadmap milestone progress, dependency indicators, date inputs, independent date clearing, and milestone reassignment controls
305-
- calendar agenda rendering from item dates and timezone-local reminder dates
306+
- calendar agenda rendering from item dates and timezone-local reminder dates, plus dated work-item creation from calendar day cells
306307
- trash restore and confirmed permanent deletion for work items, documents, and attachments
307308
- document soft-delete cleanup for document-scoped reminders and attachments, including reminder restoration from the document trash payload
308309
- document permanent-delete cleanup for document-scoped reminders and attachments
@@ -477,6 +478,7 @@ The core domain in `packages/core/src/domain/` covers the entities the plan call
477478
- adding table visible-row selection and bulk status/priority/assignee updates through existing `item.update` commands
478479
- expanding roadmap lanes with target dates, progress, relationship-based blocked indicators, date controls, invalid-range feedback, and milestone reassignment
479480
- adding a calendar agenda derived from item start/due dates and reminders
481+
- adding calendar day-cell creation for dated work items through the shared create dialog
480482
- turning bug triage into an actionable workflow with filters plus accept, decline, snooze, owner assignment, and duplicate-link actions
481483
- adding backlog shared filters and table column visibility plus command-backed inline status, priority, assignee, milestone, and due-date edits
482484
- adding focused core and UI regression coverage for saved-view filters, viewbar rendering, default board routing, backlog saved views, board saved views, and table inline/editable saved views

Readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The MVP implementation is in place. The current build supports:
1414
- **Table** with accessible sortable header buttons, corrected priority/updated sorting, saved working views, shared filters, column visibility and saved column order, row selection that survives filter changes, bulk status/priority/assignee edits, inline status/priority/assignee/milestone/due-date edits, custom-field columns, and shared metadata badges
1515
- **Docs** with Markdown editing, sanitized rendering, internal embeds, command-backed sections, reusable document templates, in-surface doc search, linked-work and backlink context panels, router-safe in-app navigation for preview links, stale document-route recovery, correct pane updates when switching documents, draft-preserving editor resets, shared confirmation for document deletion, and safe navigation to the next active document after deleting the open one
1616
- **Roadmap / timeline** with date drag/resize, milestone lanes, milestone progress and target dates, dependency indicators, explicit date controls that clear each side of a range independently, milestone reassignment, invalid-range feedback, and shared zoom/anchor controls
17-
- **Calendar** with accessible month navigation controls, month grid, date-based item visibility, and an agenda for upcoming start/due dates and timezone-aware reminders that stay visible around UTC/local day boundaries
17+
- **Calendar** with accessible month navigation controls, month grid, date-based item visibility, explicit day-level scheduled-work creation through the shared work-item dialog, and an agenda for upcoming start/due dates and timezone-aware reminders that stay visible around UTC/local day boundaries
1818
- **Bug triage** with severity, priority, source/context metadata, reproduction steps, expected/actual behavior, environment, affected version, shared work-card metadata, practical triage filters, accept/decline/snooze/assign actions, a searchable duplicate-link picker, configurable severity-or-priority intake gates, workflow-safe decline handling, and a visible new-bug entry point that defaults to the Intake lane without starving Ready in custom planned workflows
1919
- **My work** filtered to the locally selected member with a real member select control, shared work rows, and assigned-to-me creation
2020
- **Search** across items, docs, comments, and labels with shared search controls and grouped results

docs/plans/July 2026 plan.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ This combines the original saved-views slice and backlog/table parity slice into
418418

419419
**Intent:** Make planning across dates and milestones useful without becoming heavyweight scheduling software.
420420

421-
**Delivery note:** This scope was bundled into the July Planning Workflow milestone PR alongside table bulk actions, bug triage actions, calendar agenda, and the project overview route.
421+
**Delivery note:** This scope was bundled into the July Planning Workflow milestone PR alongside table bulk actions, bug triage actions, calendar agenda, and the project overview route. A follow-up added calendar day-cell creation for dated work items through the shared create dialog instead of adding a separate calendar-event entity.
422422

423423
**Files:**
424424

@@ -434,6 +434,7 @@ This combines the original saved-views slice and backlog/table parity slice into
434434
- [x] Show explicit invalid-range feedback.
435435
- [x] Support moving items between milestone lanes.
436436
- [x] Add an agenda-style calendar list for upcoming start/due dates and reminders.
437+
- [x] Add day-level scheduled-work creation from the calendar using normal work items with editable type/status/priority/assignee/start/due fields.
437438
- [x] Keep date-only semantics unchanged.
438439

439440
### PR 6: Bug Intake And Triage Workflow

packages/ui/src/theme/global.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@
127127
font-weight: 500;
128128
color: var(--color-text-primary);
129129
}
130+
.form-grid {
131+
display: grid;
132+
gap: var(--space-2);
133+
}
134+
.form-grid-2 {
135+
grid-template-columns: repeat(2, minmax(0, 1fr));
136+
}
137+
.form-error {
138+
color: var(--color-danger);
139+
font-size: var(--font-size-xs);
140+
}
130141

131142
/* ---- App shell ---- */
132143
.app-shell {
@@ -900,11 +911,29 @@
900911
.calendar-day[data-today="true"] {
901912
background: var(--color-accent-soft);
902913
}
914+
.calendar-day-head {
915+
display: flex;
916+
align-items: center;
917+
justify-content: space-between;
918+
gap: var(--space-1);
919+
min-height: 22px;
920+
}
903921
.calendar-day-num {
904922
font-weight: 600;
905923
color: var(--color-text-secondary);
906924
font-size: var(--font-size-2xs);
907925
}
926+
.calendar-day-add {
927+
width: 22px;
928+
height: 22px;
929+
min-height: 22px;
930+
padding: 0;
931+
opacity: 0.72;
932+
}
933+
.calendar-day-add:hover,
934+
.calendar-day-add:focus-visible {
935+
opacity: 1;
936+
}
908937
.calendar-day[data-other-month="true"] {
909938
background: var(--color-bg-muted);
910939
color: var(--color-text-muted);
@@ -2327,6 +2356,9 @@
23272356
.settings-inline-form .select {
23282357
min-width: 0;
23292358
}
2359+
.form-grid-2 {
2360+
grid-template-columns: 1fr;
2361+
}
23302362
.settings-capability-row {
23312363
grid-template-columns: 1fr;
23322364
}

packages/ui/src/views/calendar/CalendarView.test.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { cleanup, render, screen, within } from "@testing-library/react";
2+
import userEvent from "@testing-library/user-event";
23
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
34
import { MemoryRouter } from "react-router-dom";
45
import { buildProjectFromTemplate } from "@gph/core";
6+
import { closeCreateItem } from "../../commands/palette-bus";
57
import { useProjectStore } from "../../store/project-store";
8+
import { CreateItemDialog } from "../../work-item";
69
import { CalendarView } from "./CalendarView";
710

811
function datePlus(days: number): string {
@@ -20,10 +23,12 @@ function timestampPlus(days: number): string {
2023
describe("CalendarView", () => {
2124
beforeEach(() => {
2225
cleanup();
26+
closeCreateItem();
2327
useProjectStore.setState({ bundle: null });
2428
});
2529

2630
afterEach(() => {
31+
closeCreateItem();
2732
vi.useRealTimers();
2833
});
2934

@@ -125,4 +130,49 @@ describe("CalendarView", () => {
125130
expect(row).toBeTruthy();
126131
expect(within(row as HTMLElement).getByText("2026-07-04")).toBeInTheDocument();
127132
});
133+
134+
it("opens a dated work item dialog from a calendar day", async () => {
135+
const today = datePlus(0);
136+
const bundle = buildProjectFromTemplate("software-project", "Calendar");
137+
useProjectStore.setState({ bundle });
138+
139+
render(
140+
<MemoryRouter>
141+
<CalendarView />
142+
<CreateItemDialog />
143+
</MemoryRouter>
144+
);
145+
146+
await userEvent.click(screen.getByRole("button", { name: `Add work on ${today}` }));
147+
148+
expect(screen.getByRole("dialog", { name: "Create work item" })).toBeInTheDocument();
149+
expect(screen.getByLabelText("Due date")).toHaveValue(today);
150+
151+
await userEvent.selectOptions(screen.getByLabelText("Type"), "bug");
152+
await userEvent.type(screen.getByLabelText("Title"), "Investigate launch blocker");
153+
await userEvent.click(screen.getByRole("button", { name: "Create" }));
154+
155+
const created = useProjectStore.getState().bundle?.core.items.find((item) => item.title === "Investigate launch blocker");
156+
expect(created).toMatchObject({
157+
typeId: "bug",
158+
dueDate: today
159+
});
160+
});
161+
162+
it("opens the toolbar create dialog without carrying a hidden calendar date", async () => {
163+
const bundle = buildProjectFromTemplate("software-project", "Calendar");
164+
useProjectStore.setState({ bundle });
165+
166+
render(
167+
<MemoryRouter>
168+
<CalendarView />
169+
<CreateItemDialog />
170+
</MemoryRouter>
171+
);
172+
173+
await userEvent.click(screen.getByRole("button", { name: "New scheduled item" }));
174+
175+
expect(screen.getByRole("dialog", { name: "Create work item" })).toBeInTheDocument();
176+
expect(screen.getByLabelText("Due date")).toHaveValue("");
177+
});
128178
});

packages/ui/src/views/calendar/CalendarView.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useMemo, useState } from "react";
2-
import { ChevronLeft, ChevronRight } from "lucide-react";
2+
import { ChevronLeft, ChevronRight, Plus } from "lucide-react";
33
import { Link } from "react-router-dom";
44
import { dateOnlyFromTimestamp, todayDateOnly, type Reminder, type WorkItem } from "@gph/core";
55
import { Button, IconButton, MetadataBadge, ViewToolbar } from "../../components";
6+
import { openCreateItem } from "../../commands/palette-bus";
67
import { useProjectStore } from "../../store/project-store";
78

89
/**
@@ -86,6 +87,14 @@ export function CalendarView() {
8687
<Button size="sm" onClick={() => setAnchor(todayDateOnly())}>
8788
Today
8889
</Button>
90+
<Button
91+
size="sm"
92+
variant="primary"
93+
icon={<Plus />}
94+
onClick={() => openCreateItem()}
95+
>
96+
New scheduled item
97+
</Button>
8998
</ViewToolbar>
9099
<div className="calendar-layout">
91100
<div className="calendar-grid" role="grid" aria-label="Calendar">
@@ -101,7 +110,16 @@ export function CalendarView() {
101110
data-other-month={day.other}
102111
data-today={day.today}
103112
>
104-
<div className="calendar-day-num">{Number(day.date.slice(-2))}</div>
113+
<div className="calendar-day-head">
114+
<div className="calendar-day-num">{Number(day.date.slice(-2))}</div>
115+
<IconButton
116+
aria-label={`Add work on ${day.date}`}
117+
className="calendar-day-add"
118+
onClick={() => openCreateItem({ dueDate: day.date })}
119+
>
120+
<Plus aria-hidden="true" />
121+
</IconButton>
122+
</div>
105123
{(itemsByDate.get(day.date) ?? []).map((item) => (
106124
<Link key={item.id} to={`/item/${item.id}`} className="calendar-pill">
107125
{item.title}

packages/ui/src/work-item/CreateItemDialog.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,28 @@ describe("CreateItemDialog", () => {
4949
expect(screen.getByLabelText("Title")).toHaveValue("Keep my draft");
5050
expect(screen.getByLabelText("Description")).toHaveValue("Do not clear this");
5151
});
52+
53+
it("creates a work item with prefilled planning dates", async () => {
54+
render(
55+
<MemoryRouter>
56+
<CreateItemDialog />
57+
</MemoryRouter>
58+
);
59+
60+
act(() => openCreateItem({ typeId: "bug", startDate: "2026-07-09", dueDate: "2026-07-12" }));
61+
62+
expect(screen.getByLabelText("Type")).toHaveValue("bug");
63+
expect(screen.getByLabelText("Start date")).toHaveValue("2026-07-09");
64+
expect(screen.getByLabelText("Due date")).toHaveValue("2026-07-12");
65+
66+
await userEvent.type(screen.getByLabelText("Title"), "Fix scheduled regression");
67+
await userEvent.click(screen.getByRole("button", { name: "Create" }));
68+
69+
const created = useProjectStore.getState().bundle?.core.items.find((item) => item.title === "Fix scheduled regression");
70+
expect(created).toMatchObject({
71+
typeId: "bug",
72+
startDate: "2026-07-09",
73+
dueDate: "2026-07-12"
74+
});
75+
});
5276
});

packages/ui/src/work-item/CreateItemDialog.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export function CreateItemDialog() {
1717
const [description, setDescription] = useState("");
1818
const [priorityId, setPriorityId] = useState<string>("");
1919
const [assigneeId, setAssigneeId] = useState<string>("");
20+
const [startDate, setStartDate] = useState<string>("");
21+
const [dueDate, setDueDate] = useState<string>("");
2022
const navigate = useNavigate();
2123
const itemTypes = bundle?.core.itemTypes;
2224
const projectDefaultInitialStatusId = bundle?.project.defaultInitialStatusId ?? "";
@@ -53,10 +55,14 @@ export function CreateItemDialog() {
5355
: defaultPriorityIdForType(nextTypeId)
5456
);
5557
setAssigneeId(prefill?.assigneeId ?? "");
58+
setStartDate(prefill?.startDate ?? "");
59+
setDueDate(prefill?.dueDate ?? "");
5660
}, [
5761
open,
5862
prefill?.assigneeId,
63+
prefill?.dueDate,
5964
prefill?.priorityId,
65+
prefill?.startDate,
6066
prefill?.statusId,
6167
prefill?.typeId,
6268
projectDefaultTypeId,
@@ -71,6 +77,7 @@ export function CreateItemDialog() {
7177
const statuses = bundle.core.statuses;
7278
const priorities = bundle.core.priorities;
7379
const members = bundle.core.members.filter((member) => !member.archived);
80+
const invalidDateRange = Boolean(startDate && dueDate && startDate > dueDate);
7481

7582
const changeType = (nextTypeId: string) => {
7683
setTypeId(nextTypeId);
@@ -79,7 +86,7 @@ export function CreateItemDialog() {
7986
};
8087

8188
const submit = () => {
82-
if (!title.trim()) return;
89+
if (!title.trim() || invalidDateRange) return;
8390
const r = applyCommand({
8491
type: "item.create",
8592
projectId: bundle.project.id,
@@ -88,7 +95,9 @@ export function CreateItemDialog() {
8895
description: description.trim(),
8996
statusId: statusId || undefined,
9097
priorityId: priorityId || null,
91-
assigneeId: assigneeId || null
98+
assigneeId: assigneeId || null,
99+
startDate: startDate || null,
100+
dueDate: dueDate || null
92101
});
93102
closeCreateItem();
94103
const newId = r.bundle.core.items[r.bundle.core.items.length - 1].id;
@@ -145,6 +154,19 @@ export function CreateItemDialog() {
145154
))}
146155
</select>
147156
</label>
157+
<div className="form-grid form-grid-2">
158+
<label className="label">
159+
Start date
160+
<input className="input" type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
161+
</label>
162+
<label className="label">
163+
Due date
164+
<input className="input" type="date" value={dueDate} onChange={(e) => setDueDate(e.target.value)} />
165+
</label>
166+
</div>
167+
{invalidDateRange ? (
168+
<div className="form-error">Start date must not be later than due date.</div>
169+
) : null}
148170
<label className="label label-row">
149171
Assignee
150172
<select className="select" value={assigneeId} onChange={(e) => setAssigneeId(e.target.value)}>
@@ -168,7 +190,7 @@ export function CreateItemDialog() {
168190
</div>
169191
<div className="modal-footer">
170192
<button className="btn" onClick={() => closeCreateItem()}>Cancel</button>
171-
<button className="btn btn-primary" onClick={submit} disabled={!title.trim()}>Create</button>
193+
<button className="btn btn-primary" onClick={submit} disabled={!title.trim() || invalidDateRange}>Create</button>
172194
</div>
173195
</div>
174196
</div>

0 commit comments

Comments
 (0)