Repository navigation
[scheduler] Dependencies: arrow editor (type + lag) and immediate cascade on create/edit #23395
Description
Part of #22853 (event-scheduling umbrella). Depends on #23393 (PR #23623) for the lag and the SS/FF/SF engine.
An editor to change an existing dependency's type and lag from the UI. It is the explicit counterpart of the edge terminals (#23394), where the type comes from the drag gesture, and the only UI to set a lag.
Design
- Entry point: mirrors the event gestures. Click selects the arrow (already shipped), double click opens the editor, right click opens a context menu with Edit dependency / Delete (like [scheduler] Add EventContextMenu with Edit and Delete actions #23379's
EventContextMenu). - Hover style: hovering an arrow gives it a distinct style. It shows which arrow the double click will open and makes a dense graph easier to read. Today only the selected arrow is styled.
- Surface: a dialog built from the same pieces as the event dialog: anchored at the double-click position, draggable and non-modal, open state in the store. The arrow and its two events stay visible while editing.
- Fields:
- The source and target event titles, labelled From and To, displayed as plain text (not inputs), so the user knows which arrow they are editing. Re-linking is delete + recreate.
- Type: FS / SS / FF / SF.
- Lag: a whole number ≥ 0 and a unit (
minute | hour | day | week, defaultday), calendar time in the successor's timezone. The editor can never produce a lag thatgetDependencyLagIssuerejects. - A lag from props that the engine ignores (negative, not a whole number, unknown unit) shows as unset, since it is not applied.
- On an all-day successor the lag is carried in whole days, rounded down (
getEffectiveDependencyLag). The editor shows the wait that actually applies, so 36 hours does not silently become 1 day.
- Rejections: an edit keeps the source → target pair, so it cannot create a cycle. It can hit a duplicate
(source, target, type)when changing the type, and the read-only veto below. Both go through the same result contract asaddDependency.
Read-only dependencies
Today a dependency is read-only when one of its events is (isDependencyReadOnly, which folds in the global readOnly, resource.areEventsReadOnly and the event's readOnly). That blocks too much: a dependency never moves its predecessor, and a successor only has to move when the constraint breaks, which the cascade veto already rejects. A read-only event locks its own dates and data, not the arrows attached to it.
This issue separates the two:
- A dependency is read-only only when the whole scheduler is
readOnly. A dependency-levelreadOnlyfield can come later if needed. A read-only dependency behaves like a read-only event: the context menu shows "Show details" instead of Edit and has no Delete, and the dialog shows the type and lag without letting the user edit them. - A read-only event only counts through the veto: any create or edit whose cascade would have to move it is rejected. A lag the slack absorbs, or a type change that breaks nothing, goes through.
The rule applies to creation and deletion too, which are already shipped: read-only events get terminals and are valid drop targets, and addDependency / deleteDependency stop rejecting on a read-only endpoint.
Cascade on create and edit
If a create or an edit leaves the constraint broken, the target is pushed immediately and the cascade continues behind it, like dragging a predecessor does. Data arriving via props is still rendered as-is, never rewritten.
Today the engine is only seeded by events whose dates change, and a seed with unchanged dates pushes nothing, so a dependency change needs its own way in:
- The engine takes an optional
enforcedEdges(the created or edited dependency). IncollectBounds, an edge constrains when its source's bounding edge advanced in the batch or it is enforced. Discovery also starts from the enforced edge's target. Everything else (Kahn, push-only, all-day whole days, read-only veto) is unchanged. Only that edge is enforced, so pre-existing violations on other arrows stay as-is. dependenciesis fully controlled: the new or edited edge is not in the state until the parent re-renders. So it is passed to the engine explicitly, not read from the state.- One engine run: the cascade is computed first, then the dependency change and the event moves are emitted together. If the cascade would move a read-only event, nothing is emitted (neither
onDependenciesChangenor the event updates) and the rejection is surfaced like the drag veto. The event updates must not trigger a second run: the pushed target would be treated as a repositioned seed and clamped by all its predecessors, repairing violations on other arrows.
Out of scope
- Lead (negative lag).
- Bulk editing.
- Re-linking from the editor.
- Reaching an arrow with the keyboard: parked with the rest of the dependency a11y story ([scheduler] Dependencies: create, edit and delete via terminals #22856). The editor's open state lives in the store, so a future keyboard trigger (focusable arrows, or the dependencies section in the event dialog) opens it without rework.
- Localized strings: hard-coded English until the public flip ([scheduler] Dependencies: public API flip and docs #23420), rejections through [scheduler] Make every user-facing store error translatable through
localeText#23665.
Tests
- Hovering an arrow applies the hover style to that arrow only.
- Double click an arrow → change type → the arrow re-renders and the constraint is enforced.
- A lag that breaks the constraint pushes the target; slack absorbs a lag that does not.
- Creating an arrow whose constraint is born broken pushes the target.
- Editing one arrow leaves pre-existing violations on other arrows (including other arrows into the same target) untouched.
- A cascade into a read-only event rejects the whole change.
- A type change that duplicates an existing dependency is rejected.
- With the scheduler
readOnly, an arrow offers "Show details" and no Delete, and its dialog cannot be edited. - An arrow into or out of a read-only event can be created, edited and deleted, as long as the read-only event does not have to move.
- An invalid lag from props shows as unset; an all-day successor shows the rounded-down wait.
Activity
[-][scheduler] Dependencies: arrow editor (type + lag)[/-][+][scheduler] Dependencies: arrow editor (type + lag) and immediate cascade on create/edit[/+]github-actions commentedon Oct 20, 2026
This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue.
Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.