Skip to main content
Version: 1.0.4

Scheduler

A use-case-agnostic resource/timeline planning board: nested resources down the side, day/week/month scale views across the top, author-defined categories and configurable bars. Bind resources and events to named queries for a live schedule.

The Scheduler component as it renders with its default settings.The Scheduler component as it renders with its default settings.

Reach for it when you need:

  • Production scheduling across lines
  • Planned maintenance windows
  • Shift and crew rosters

Component name: Scheduler · Category: Scheduler

In the palette: A use-case-agnostic resource/timeline planning board — nested resources, Day/Week/Month scale views, author-defined categories and configurable bars. Bind resources/events to Named Queries for live schedules.

Properties

Any property can be bound to a tag, another component's property, or a query.

PropertyTypeDefaultDescription
resourcesarray[{"id":"team-eng","label":"Engineering","parentId":"","group… (truncated)Sidebar rows (nest via parentId). Edit inline, or bind to a Named Query returning { id, label, parentId }.
eventsarray[{"id":"e1","resourceId":"alice","label":"Sprint planning","… (truncated)Timeline blocks. Edit inline, or bind to a Named Query returning { id, resourceId, label, start, end, category }.
categoriesarray[{"name":"design","color":"#7c3aed","label":"Design"},{"name… (truncated)Category colours + legend labels. Any category name works; blank colour = auto-palette.
bandsarray[{"resourceId":"*","start":"2026-01-05T00:00:00","end":"2026… (truncated)Background stripes (shift windows, sprints, availability). resourceId '*' spans every row.
dependenciesarray[{"from":"e2","to":"e3","type":"FS"},{"from":"e6","to":"e8",… (truncated)Links between events, drawn as arrows. Each = { from: eventId, to: eventId, type: FS|SS|FF|SF }.
dependencyStyleobject{"endMarker":"arrow","line":"orthogonal","color":"","width":… (truncated)Dependency link look — end marker (arrow/circle/diamond/none), line (orthogonal/curved/straight), colour, width.
rangeStartstring2026-01-05T00:00:00Visible window start — ISO string or epoch-ms.
rangeEndstring2026-01-10T00:00:00Visible window end — ISO string or epoch-ms.
snapMinutesnumber15Drag/resize snap grid in minutes (0 = free).
appearanceobject{"barStyle":"gradient","barRadius":6,"barShowTime":false,"ro… (truncated)Bar style + row/sidebar sizing.
axisobject{"timeFormat":"24h","dateFormat":"MMM d","showGrid":true,"sh… (truncated)Time axis — clock format, grid, now-line.
toolbarobject{"show":true,"scales":"day,week,month,quarter,year","showLeg… (truncated)Toolbar — visibility, scale presets, legend.
popupobject{"enabled":true,"view":"","params":{}}Event click popover — built-in details, plus an optional embedded View.
slotPopupobject{"enabled":false,"view":"","params":{},"durationMinutes":60,… (truncated)Empty-slot click popover — opens on blank timeline and embeds a View (your form) with the clicked slot as a param.
selectedEventobject{}Output: the event last clicked in LiveView ({ id, resourceId, label, start, end, category }).
selectedSlotobject{}Output: the empty slot last clicked in LiveView ({ resourceId, resourceLabel, resourcePath, start, end, startISO, endISO, time, timeISO, durationMinutes }).
stylestyle{}Custom CSS properties

Events

Attach event actions to these in the Event Manager.

EventLabelGroupPayload
clickClickMouseThe DOM event.
dblclickDouble clickMouseThe DOM event.
contextmenuRight clickMouseThe DOM event.
mouseenterMouse enterMouseThe DOM event.
mouseleaveMouse leaveMouseThe DOM event.
eventClickEvent clickSchedulerThe clicked block, under event.
eventChangeEvent change (drag/resize)SchedulerThe block plus its new { start, end } after a drag/resize commit.
eventDoubleClickEvent double clickSchedulerThe clicked block, under event.
slotSelectEmpty slot selectScheduler{ resourceId, time } for a click on empty timeline.
Right-click behaves differently in the Designer

contextmenu fires your configured actions at runtime only — in LiveView and in preview. On the Designer canvas the right-click is captured to open the Event Manager, so testing it there will not run your actions. Test right-click in preview.

Notes

Its events carry enough context to write back: eventChange fires after a drag or resize commit with the block and its new { start, end }, and slotSelect gives you { resourceId, time } for a click on empty timeline — the hook for scheduling something new.

Driving the board from a database

Everything above is editable inline, which is how you prototype. A real schedule comes out of tables. Two are enough:

CREATE TABLE schedule_resource (
id TEXT PRIMARY KEY,
label TEXT NOT NULL,
parent_id TEXT NULL REFERENCES schedule_resource(id)
);

CREATE TABLE schedule_event (
id TEXT PRIMARY KEY,
resource_id TEXT NOT NULL REFERENCES schedule_resource(id),
label TEXT NOT NULL,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
category TEXT NOT NULL
);

CREATE INDEX schedule_event_window ON schedule_event (starts_at, ends_at);

Named query SchedulerResources feeds the sidebar. The column names have to match the property contract — id, label, parentId — so alias them, and quote the aliases on PostgreSQL or they arrive folded to lower case:

SELECT id, label, parent_id AS "parentId"
FROM schedule_resource
ORDER BY parent_id NULLS FIRST, label

Named query SchedulerEvents feeds the bars. Take the visible window as parameters so the board never pulls the whole history:

SELECT id,
resource_id AS "resourceId",
label,
starts_at AS "start",
ends_at AS "end",
category
FROM schedule_event
WHERE starts_at < :rangeEnd
AND ends_at > :rangeStart
ORDER BY starts_at

That WHERE is an overlap test, not containment. A maintenance window that began yesterday and ends tomorrow must still be drawn on today's board; starts_at BETWEEN :rangeStart AND :rangeEnd would silently drop it.

Then bind:

PropertyBinding
resourcesQuery → named query SchedulerResources
eventsQuery → named query SchedulerEvents, with :rangeStart / :rangeEnd supplied from the board's own range or a date picker
categoriesLeft inline — these are your colour scheme, not data

Writing a drag back

eventChange hands you the moved block. Send it to a gateway script:

def on_event_change(event):
system.db.runNamedQuery("SchedulerMoveEvent", {
"id": event["id"],
"resourceId": event["resourceId"],
"start": event["start"],
"end": event["end"],
})
-- SchedulerMoveEvent
UPDATE schedule_event
SET resource_id = :resourceId,
starts_at = :start,
ends_at = :end
WHERE id = :id

Re-run the events query afterwards so the board reflects what was actually stored — if the write is rejected, the bar should snap back rather than lie.

Things that bite

  • category is a join key, not a colour. A row whose category matches no entry in categories still draws, but with an auto-assigned colour that changes as the data changes. Constrain the column, or seed categories from the same table.
  • Timestamps may be ISO strings or epoch milliseconds. Mixing the two in one result set is the usual cause of bars landing in 1970.
  • parentId must reference a resource in the same result set. A child whose parent is filtered out by the query does not render.