Connecting (chaining) tickets in project timeline - #44
Merged
wdembinski merged 15 commits intoSep 10, 2026
Merged
Conversation
Add SVG marker defs to TimelinePane's overlay (grey for the existing `blocks` arrows, cyan defined for the chain arrows landing next) and apply markerEnd to the dependency paths, matching ChainOverlay's own Head marker pattern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TimelinePane now also loads chain:links and subscribes to chain:changed, alongside its existing ticketLink:list/ticketLink:changed. Each task_link between two scheduled rows is drawn as a cyan curve, reusing ganttDependencyPath, the rowIndexById/scheduledRows lookup, and the cyan arrowhead defined in the prior step. Render only — drag-to-reschedule is untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each scheduled bar grows a small knob past its right edge, hidden until the row is hovered, past the resize strip so the two gestures never share a pixel. Dragging from it tracks the pointer as an offset from the knob's own origin (no DOM measurement, same trick the reschedule drag already uses) and draws a live rubber band with chainArrows' rubberBandPath, the same function ChainOverlay draws its own band with. Gesture and preview only — releasing creates no link yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
On release, a connect drag resolves its drop point back to the bar it landed on (elementFromPoint against the row's data attribute, since a pointer capture keeps events routed to the knob) and draws a `blocks` dependency from the knob's own ticket to it. canLinkTickets runs the same self/duplicate checks the store's handler will, surfacing a refusal through the pane's existing dragError MessageBar without a round trip. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Holding Ctrl/Cmd at release now asks for both a blocks dependency and an execution-chain edge (chain:link, gated after-merge) over the same pair, not just the dependency. The two asks are independent, so a drop can half land -- e.g. the dependency goes through but the chain drag canLink refuses as a cycle -- and dragError now says which half of a partial result failed and why, reusing the same MessageBar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each Gantt arrow now gets a transparent 14px hit-stroke over its thin visible line, `ChainOverlay`'s own trick for turning a 1.5px line into something worth clicking. A click selects it, drawn heavier in the brand colour with a dot at each end so the two tickets it joins are unambiguous — readying it for the delete/gate controls. Clicking a row clears the selection, the chart's own version of "the board below clears it on any click that reaches it". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Selecting a dependency or chain arrow in the timeline now surfaces a popover: chain arrows reuse ChainLinkPopover (remove + gate picker, wired to chain:unlink/chain:setGate), and dependency arrows get a new TicketLinkPopover (remove only, via ticketLink:remove — a TicketLink gates nothing). Delete/Backspace/Escape on the selected arrow mirror the board's own keyboard contract. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@xyflow/react v12 joins react/react-dom/Fluent as a peerDependency of @tm/ui, externalized in tsup's build so apps/web pulls its own copy through dist rather than getting one bundled in. apps/client and apps/web each install it directly so both hosts actually bundle it — apps/client resolves packages/ui straight from src via its @ui/* alias and never touches the tsup build. Its stylesheet is imported from Projects.tsx, where the graph view this unblocks will live as a third tab alongside Backlog and Timeline. No feature code yet — both app builds and packages/ui's test suite pass unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Projects.tsx's view switch grows a third 'graph' alongside backlog and timeline, with its own ToggleButton. GraphPane.tsx mounts an empty React Flow canvas — project-scoped, keyed on the project the same way BacklogTable and TimelinePane are, with its own load-then-render shape via useInitialLoad/PaneLoading. No nodes or edges yet; this stands up the view and its navigation only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GraphPane now loads a project's tickets via board:tasks and stays live off task:changed/project:tasksChanged, the same seed-and-subscribe shape BacklogTable and TimelinePane already use. Each ticket renders as a Fluent-styled React Flow node (key badge, title, type glyph, epic marker) placed in a deterministic grid. No edges yet — persisted layout and links come in later steps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GraphPane now loads ticketLink:list and chain:links (staying live off their change events, the same seed-and-subscribe shape it already uses for its own tickets) and renders them as React Flow edges: grey for a blocks dependency, cyan for an execution-chain link, each with a closed arrowhead. Read-only — dragging to create or delete a link in the graph comes in later steps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires React Flow's onConnect to GraphLinkPicker, a small dialog that asks which kind of relationship a connect drag meant: a blocks dependency (ticketLink:add) or an execution chain (chain:link, with its own gate choice). Unlike the timeline's connect knob, a React Flow drag has no ctrl-drag mid-gesture to also ask for a chain, so the choice is made explicit in the dialog instead of inferred from a modifier key. canLinkTickets/canLink run the same checks the store's own handlers will, so a refusal it already knows about — self, duplicate, a would-be cycle — reads inline without a round trip. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wire React Flow's onEdgesDelete (edges are selectable/deletable by default, so select + Delete/Backspace is the affordance) to ticketLink:remove / chain:unlink. A deleted edge's id is resolved back to its kind by checking the links list first then chainLinks, mirroring TimelinePane's selectedTicketLink/selectedChainLink split. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a ticket_graph_positions table (projectId, taskId, x, y; cascading from tasks/projects) plus Store.getTicketGraphLayout/saveTicketGraphLayout, relayed as ticketGraph:getLayout/ticketGraph:saveLayout. GraphPane loads saved positions alongside its tickets, grid-places any ticket without one, and saves the whole current layout (debounced) on node drag so arrangements survive a reload. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tore Adds scripts/verify-connections.mjs, headless verification (under ELECTRON_RUN_AS_NODE, following the verify-tickets.mjs convention) that addTicketLink/deleteTicketLink, addTaskLink/deleteTaskLink, and getTicketGraphLayout/saveTicketGraphLayout round-trip correctly against a real createStore instance: refusals, cascading deletes, the upsert-restamps- projectId behaviour saveTicketGraphLayout's own doc comment describes, and the cross-type-vs-cross-gate distinction between ticket_links and task_links. Proven able to fail by mutating the layout upsert's ON CONFLICT clause and confirming the two dependent checks went red, then restoring it. Not covered here, and left for a human: the bar connect-drag, the Ctrl modifier that switches a drag to a chain link, arrow select/delete, and React-Flow canvas create/arrange in the Graph view — all pointer/DOM interaction this headless script cannot drive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wdembinski
force-pushed
the
feat/connecting-chaining-tickets-in-project
branch
from
September 10, 2026 16:50
5291d3e to
f55f83e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
That feature just landed but I can't use it. Why?