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.


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. Each of the five array props (resources, events, categories, bands, dependencies) binds to its own Named Query whose COLUMN NAMES are that prop's field names; hover a prop for a worked SQL example. rangeStart/rangeEnd accept relative times (now, now-2h, today, today+5d) as well as ISO or epoch-ms. In an event script the payload is a BARE identifier: event for eventChange/eventClick/eventDoubleClick, slot for slotSelect. slotSelect also carries resourceId/time, reachable as slot.resourceId / slot.time. A bound events prop is not rewritten by a drag: handle eventChange and write through to the source, then refreshBinding.
Properties
Any property can be bound to a tag, another component's property, or a query.
| Property | Type | Default | Description |
|---|---|---|---|
resources | array | [{"id":"team-eng","label":"Engineering","parentId":"","group… (truncated) | Sidebar rows. A row nests under another via parentId; set group=true to make it a collapsible header band. Bind to a Named Query whose COLUMN NAMES are these fields: id, label, parentId, group, color Postgres example — areas as group headers, equipment nested under them. Quote camelCase or the column comes back lower-cased, and "group" is a reserved word: SELECT 'area-' || lower(area) AS id, area AS label, '' AS "parentId", true AS "group" FROM (SELECT DISTINCT area FROM equipment_master) a UNION ALL SELECT equipment_tag, equipment_name, 'area-' || lower(area), false FROM equipment_master |
↳ resources.id | string | "" | Stable id — events reference this via resourceId; children reference it via parentId. |
↳ resources.label | string | "" | Row label shown in the sidebar. |
↳ resources.parentId | string | "" | Parent resource id for nesting (blank = top-level). |
↳ resources.group | boolean | false | Render this row as a GROUP header — a merged, darker, full-width band that collapses its children on click. |
↳ resources.color | color | "" | Optional row accent colour. |
events | array | [{"id":"e1","resourceId":"alice","label":"Sprint planning","… (truncated) | The timeline blocks. resourceId must match a resource id. Bind to a Named Query whose COLUMN NAMES are these fields: id, resourceId, label, start, end, category, color Postgres example. Emit times as LOCAL ISO strings — epoch-ms is read as UTC and slides the whole board by your timezone offset. Casting to text gives '2026-09-21 06', so swapping the space for a T makes it ISO. "end" is a reserved word, so quote it: SELECT schedule_id AS id, equipment_tag AS "resourceId", title AS label, replace(cast(starts_at AS text), ' ', 'T') AS start, replace(cast(ends_at AS text), ' ', 'T') AS "end", category FROM plant_schedule While this prop is bound, dragging a block does NOT rewrite it — that would replace the binding with a frozen snapshot. Handle the eventChange event to write the move back to the database. |
↳ events.id | string | "" | — |
↳ events.resourceId | string | "" | Which resource row this block sits on (matches a resource id). |
↳ events.label | string | "" | Text shown on the block. |
↳ events.start | string | "" | Start time. A local ISO string (2026-09-21T06:00:00), epoch-ms, or a relative form (today+9h, now-2h). ISO WITHOUT a timezone is read as local time — that is what a plant schedule wants; epoch-ms is read as UTC. |
↳ events.end | string | "" | End time — same format as start. A row whose end is before its start is dropped from the board. |
↳ events.category | string | "" | Any category name — matched against the categories list for its colour; unknown names get a stable auto-colour. |
↳ events.color | color | "" | Explicit block colour (overrides the category colour). |
categories | array | [{"name":"design","color":"#7c3aed","label":"Design"},{"name… (truncated) | Category colours + legend labels. Any category name works; a blank colour gets a stable auto-palette colour, so this is optional. Bind to a Named Query whose COLUMN NAMES are these fields: name, color, label SELECT category AS name, color, label FROM plant_schedule_category ORDER BY sort_order |
↳ categories.name | string | "" | Category key referenced by events/bands via category. |
↳ categories.color | color | "" | Colour for this category (blank = auto-palette). |
↳ categories.label | string | "" | Legend label (blank = the name). |
bands | array | [{"resourceId":"*","start":"today","end":"today+9h","categor… (truncated) | Background stripes — shift windows, weekends, planned availability. A resourceId of '' spans every row. Bind to a Named Query whose COLUMN NAMES are these fields: resourceId, start, end, category, color SELECT '' AS "resourceId", replace(cast(starts_at AS text), ' ', 'T') AS start, replace(cast(ends_at AS text), ' ', 'T') AS "end", 'night' AS category, '#1e293b' AS color FROM plant_shift |
↳ bands.resourceId | string | * | Resource row to stripe, or "*" for every row. |
↳ bands.start | string | "" | Start time — ISO string or epoch-ms. |
↳ bands.end | string | "" | End time — ISO string or epoch-ms. |
↳ bands.category | string | "" | Category name for the stripe colour. |
↳ bands.color | color | "" | Explicit stripe colour. |
dependencies | array | [{"from":"e2","to":"e4","type":"FS"},{"from":"e8","to":"e6",… (truncated) | Links between events, drawn as arrows. type is FS (finish→start), SS, FF or SF. Bind to a Named Query whose COLUMN NAMES are these fields: from, to, type "from" and "to" are reserved words, so quote them: SELECT from_id AS "from", to_id AS "to", link_type AS type FROM plant_schedule_link Note: an FS link whose target STARTS BEFORE its source ends is drawn doubling back on itself. That is a real conflict in the schedule, not a drawing fault — fix the dates, not the arrow. |
↳ dependencies.from | string | "" | Source event id. |
↳ dependencies.to | string | "" | Target event id. |
↳ dependencies.type | enum | FS | Link type. Options: FS · SS · FF · SF. |
dependencyStyle | object | {"endMarker":"arrow","line":"orthogonal","color":"","width":… (truncated) | Dependency link look — end marker (arrow/circle/diamond/none), line (orthogonal/curved/straight), colour, width. |
↳ dependencyStyle.endMarker | enum | arrow | Arrowhead shape at the target end. Options: arrow · circle · diamond · none. |
↳ dependencyStyle.line | enum | orthogonal | Connector line shape. Options: orthogonal · curved · straight. |
↳ dependencyStyle.color | color | "" | Link colour (blank = muted). |
↳ dependencyStyle.width | number | 1.5 | Line thickness (px). |
rangeStart | datetime | today | Visible window start. Either an absolute time or one relative to the clock: now today (local midnight) now-2h today-1d now+30m today+5d units: m, h, d, w 2026-09-21T00:00:00 an ISO string, read as LOCAL time 1758412800000 epoch-ms, read as UTC Relative is usually what you want on a live board — "today" to "today+5d" keeps the week in view without anyone re-dating the widget every morning, and the red now-line stays on screen. It is resolved once when the board loads, so panning and zooming are never fought; the toolbar clock button re-centres on now. Leave both range fields blank and the board opens on today. |
rangeEnd | datetime | today+5d | Visible window end — same vocabulary as rangeStart ("today+5d", "now+12h", an ISO string or epoch-ms). Must resolve after the start, or the board falls back to today. |
readOnly | boolean | false | Present the board, never edit it. Off go the gestures that would reschedule something — drag to move, drag an edge to resize, and the arrow-key nudge — along with the empty-slot popover, which exists to open a form for an entry that does not exist yet. The widget then never rewrites the events prop and never fires eventChange. Reading it is untouched: click to select, the detail popover, eventClick / eventDoubleClick, the selectedEvent output, pan, zoom, the scale presets and collapsing a group all still work, so this is not "disabled" — it is a board that answers questions instead of taking instructions. Turn ON for a timeline that REPORTS rather than plans: an OEE / machine-state chart, a shift history, a downtime review, any board fed by a Named Query whose source of truth is the database. Pair it with appearance.barShowLabel = false and barRadius = 0 for the classic state-band look. This is the author's choice about the board itself, and is independent of what the operator's LiveView role allows — a read-only board is read-only for everyone. 1.0.8 |
snapMinutes | number | 15 | Drag/resize snap grid in minutes (0 = free). |
preventOverlap | boolean | false | Stop a drag or a resize from putting a block on top of its neighbour on the same row. The block stops at the neighbour's edge instead; a move keeps its duration. This is a GESTURE constraint only. Data is never rewritten: rows that arrive from a query already overlapping still render overlapping, stacked into lanes, because an overlap in the source is a real scheduling conflict and hiding it would misreport the plan. For the same reason a neighbour that is ALREADY overlapping is not treated as a wall — it would otherwise freeze the gesture. Turn ON for a board where a resource can only do one thing at a time — a machine, a room, a person. Leave OFF (the default) where parallel work on one row is legitimate, or where an operator needs to be able to double-book deliberately. 1.0.7 |
loading | object | {"show":false,"text":"Loading…","blur":3} | Busy overlay — blurs the board, shows a spinner and blocks drags and clicks while show is on. Use it around a write so the operator can see the change is in flight — e.g. in an eventChange or a form submit script: set loading.show to true, run the insert/update, refresh the events binding, then set loading.show back to false. Or bind show to an expression / tag that reflects the job state. 1.0.8 |
↳ loading.show | boolean | false | Show the busy overlay (blur + spinner) and block interaction. 1.0.8 |
↳ loading.text | string | Loading… | Caption next to the spinner. 1.0.8 |
↳ loading.blur | number | 3 | Background blur strength (px). 0 = dim only. 1.0.8 |
appearance | object | {"barStyle":"gradient","barRadius":6,"barShowLabel":true,"ba… (truncated) | Bar style + row/sidebar sizing. |
↳ appearance.barStyle | enum | gradient | Event bar appearance. Options: gradient · solid · soft · outline. |
↳ appearance.barRadius | number | 6 | Event bar corner radius (px). |
↳ appearance.barShowLabel | boolean | true | Show each event's label inside its bar. Turn OFF for a state / OEE timeline, where the category colour is the information (the label still shows on hover). 1.0.8 |
↳ appearance.barShowTime | boolean | false | Show the start time inside wide bars. |
↳ appearance.rowHeight | number | 30 | Height of one lane; a row grows to lanes × this. |
↳ appearance.sidebarAutoWidth | boolean | true | Size the resource column to its longest name (120–560px, and never more than half the widget). Still drag-resizable; double-click the handle to fit again. Off = use sidebarWidth. 1.0.8 |
↳ appearance.sidebarWidth | number | 180 | Resource sidebar width (px) when sidebarAutoWidth is off. |
↳ appearance.resizableSidebar | boolean | true | Let the operator drag-resize the resource column. |
axis | object | {"timeFormat":"24h","dateFormat":"MMM d","showGrid":true,"sh… (truncated) | Time axis — clock format, grid, now-line. |
↳ axis.timeFormat | enum | 24h | Clock format for hour labels + tooltips. Options: 24h · 12h. |
↳ axis.dateFormat | string | MMM d | Date format for the popover + drag tooltip — any date-fns pattern, e.g. 'MMM d', 'dd/MM/yy', 'yyyy-MM-dd', 'EEE d MMM'. Invalid patterns fall back to 'MMM d'. |
↳ axis.showGrid | boolean | true | Draw vertical time grid lines. |
↳ axis.showNow | boolean | true | Draw a red now-line when the current time is in range. |
toolbar | object | {"show":true,"scales":"day,week,month,quarter,year","showLeg… (truncated) | Toolbar — visibility, scale presets, legend. |
↳ toolbar.show | boolean | true | Show the toolbar. |
↳ toolbar.scales | string | day,week,month,quarter,year | Comma-separated scale presets to show, in order. Any of: day, week, month, quarter, year. Blank = none. |
↳ toolbar.showLegend | boolean | true | Show the category legend button. |
popup | object | {"enabled":true,"view":"","params":{}} | Event click popover — built-in details, plus an optional embedded View. |
↳ popup.enabled | boolean | true | Open a detail popover when an event is clicked. |
↳ popup.view | view | "" | Optional View embedded in the popover — its params include 'event' ({ id, resourceId, label, start, end, category }). |
↳ popup.params | object | {} | Extra params passed into the embedded View. |
slotPopup | object | {"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. |
↳ slotPopup.enabled | boolean | false | Open a popover when an EMPTY part of a row is clicked — the seam for a data-entry form. |
↳ slotPopup.view | view | "" | View embedded in the slot popover — your form template. Its params include 'slot' ({ resourceId, resourceLabel, resourcePath, start, end, startISO, endISO, time, timeISO, durationMinutes }). |
↳ slotPopup.params | object | {} | Extra params passed into the embedded View alongside slot. |
↳ slotPopup.durationMinutes | number | 60 | How long the clicked slot is taken to be — the end handed to the form is its snapped start plus this. |
↳ slotPopup.title | string | New entry | Popover heading. |
selectedEvent | object | {} | Output: the event last clicked in LiveView ({ id, resourceId, label, start, end, category }). |
selectedSlot | object | {} | Output: the empty slot last clicked in LiveView ({ resourceId, resourceLabel, resourcePath, start, end, startISO, endISO, time, timeISO, durationMinutes }). |
style | style | {} | Custom CSS properties |
Events
Attach event actions to these in the Event Manager.
| Event | Label | Group | Payload |
|---|---|---|---|
click | Click | Mouse | The DOM event. |
dblclick | Double click | Mouse | The DOM event. |
contextmenu | Right click | Mouse | The DOM event. |
mouseenter | Mouse enter | Mouse | The DOM event. |
mouseleave | Mouse leave | Mouse | The DOM event. |
eventClick | Event click | Scheduler | event — { id, resourceId, label, start, end, category }. Times are epoch-ms. |
eventChange | Event change (drag/resize) | Scheduler | event — the moved block with its NEW { start, end }, epoch-ms. Fires for a drag and for either resize edge. |
eventDoubleClick | Event double click | Scheduler | event — { id, resourceId, label, start, end, category }. |
slotSelect | Empty slot select | Scheduler | slot — { resourceId, resourceLabel, resourcePath, start, end, startISO, endISO, time, timeISO, durationMinutes }. Also on context.resourceId / context.time. |
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 the resource and time of a click on empty timeline — the hook for scheduling something new.
An event script reads the block as context.event (for eventChange, eventClick and eventDoubleClick) and the empty slot as context.slot (for slotSelect), in every version. From 1.0.8 Changed in 1.0.8 the bare names event and slot work as well, the way a Table's row always has; in 1.0.7 and earlier a bare event fails with a reference error. resourceId and time stay on slot and on context only — never bare, so they cannot clash with a variable of your own. The examples below use context.event, which works in both.
A board that reports New in 1.0.8
Used as an OEE or machine-state timeline, the board is a report: the database is the truth, and nobody should be able to reschedule a downtime block by brushing against it. Turn on readOnly and the gestures that would write are withheld — drag to move, drag an edge to resize, the arrow-key nudge, and the empty-slot popover. The board then never rewrites events and never fires eventChange, and a bar shows a pointer rather than a grab hand.
Everything that only reads still works: click to select, the detail popover, eventClick, eventDoubleClick, slotSelect, the selectedEvent output, pan, zoom, the scale presets and collapsing a group. That is the difference from disabling the widget. snapMinutes and preventOverlap hide themselves while it is on, since they only shape a gesture. A bound value of the text "true" counts as on.
For the classic state-band look, pair it with appearance.barShowLabel off — the bars become plain colour blocks, where the category colour is the information, and the label moves to the hover title — and barRadius 0.
Showing a write in flight New in 1.0.8
loading raises a busy overlay: with loading.show on, the axis and rows blur by loading.blur px, a spinner and loading.text appear, and drags, clicks and arrow keys are swallowed, so nobody moves a block while the write it depends on is still running. Set loading.show true at the start of an eventChange script and false once the refresh is done, or bind it to a tag or expression that reflects the job. A plain true/false bound to the whole loading property is read as show.
The resource column Changed in 1.0.8
appearance.sidebarAutoWidth, on by default, sizes the resource column to its longest name — measured over every resource, collapsed groups included, so collapsing a group never makes it jump — between 120 and 560 px and never more than half the widget. It is still drag-resizable, and double-clicking the handle fits it again. Off uses sidebarWidth, as before. A board saved before 1.0.8 has no stored value, so it switches to the fitted width.
A toolbar that fits narrow spaces Changed in 1.0.8
When the widget is narrow — in a small grid cell, a sidebar or on a phone — its toolbar shrinks to fit rather than spilling over. It goes by the widget's own width, not the screen's, so a small widget on a large monitor shrinks too. The order is now date picker, centre on now, zoom out, zoom in, fit — so the date label, which changes width, never shifts the buttons under the pointer. Below 560 px the date stays inline, the legend becomes an icon, and the scale presets, centre, zoom and fit move into one menu. Nothing is taken away: every control is still there, and a shortened control keeps its full name for screen readers.
A window relative to now New in 1.0.7
rangeStart and rangeEnd take a time relative to the clock as well as an absolute one:
| You write | It means |
|---|---|
now, now-2h, now+30m | The current moment, offset |
today, today-1d, today+5d | Local midnight today, offset |
2026-09-21T00:00:00 | An ISO time, read as local |
1758412800000 | Epoch milliseconds, read as UTC |
Units are m, h, d and w. A new Scheduler opens on today to today+5d, and leaving both fields blank also opens it on today. That is usually what a live board wants: nobody has to re-date the widget every morning, and the red now-line is on screen when it opens. An unrecognised form — now+5 with no unit, now+5y with a unit that does not exist — is rejected rather than guessed at, and a range whose end does not come after its start falls back to today.
The window is worked out once, when the board loads, from a single instant, so a board opened at midnight cannot take its start from one day and its end from the next, and the view does not creep under an operator who is panning or zooming. Center on now on the toolbar brings the now-line back into view. Hover the now-line's handle to see the time it marks, in the board's own date format and 12/24-hour setting.
Dragging
Dependency arrows follow the block while you drag it. Changed in 1.0.7 They used to stay behind and catch up only on drop, which read as the link having broken.
preventOverlap New in 1.0.7 stops a drag or a resize from putting a block on top of its neighbour on the same row: a resize stops at the neighbour's edge, and a move butts up against it with its duration kept. If there is no room for the block between two neighbours, it goes back to where it started. Turn it on where a row can only do one thing at a time — a machine, a room, a person. It is off by default, because turning it on changes how an existing board behaves, and because double-booking is sometimes legitimate.
It governs the gesture, never the data. Rows that arrive from a query already overlapping still draw overlapping, stacked into lanes: an overlap in the source is a real conflict in the plan, and hiding it would misreport the plan. For the same reason a neighbour that already overlaps the block is not treated as a wall — it would freeze the drag.
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, and hand the times back as local ISO strings:
SELECT id,
resource_id AS "resourceId",
label,
replace(cast(starts_at AS text), ' ', 'T') AS "start",
replace(cast(ends_at AS text), ' ', 'T') AS "end",
category
FROM schedule_event
WHERE starts_at < :rangeEnd
AND ends_at > :rangeStart
ORDER BY starts_at
Casting a PostgreSQL TIMESTAMP to text gives 2026-09-21 06:00:00; swapping the space for a
T makes it an ISO time with no zone, which the board reads as local — what a plant schedule
means. Epoch milliseconds are read as UTC and slide the whole board by your timezone offset.
"end" is a reserved word, so quote it.
That cast is PostgreSQL's. The same local ISO string on the other engines:
| Engine | Expression |
|---|---|
| PostgreSQL | replace(cast(starts_at AS text), ' ', 'T') |
| MySQL | DATE_FORMAT(starts_at, '%Y-%m-%dT%H:%i:%s') |
| SQL Server | CONVERT(varchar(19), starts_at, 126) |
On MySQL, quote the aliases with backticks instead of double quotes. On SQL Server, use square brackets.
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:
| Property | Binding |
|---|---|
resources | Query → named query SchedulerResources |
events | Query → named query SchedulerEvents, with :rangeStart / :rangeEnd supplied from a date picker |
categories | Left inline — these are your colour scheme, not data |
Feed the parameters real timestamps. A relative window such as today+5d is something the
board understands; the database does not, so passing the board's own rangeStart straight
into the query hands SQL a string it cannot compare. The resources, events, categories,
bands and dependencies properties each describe their column names, with a worked query,
in the properties panel.
Writing a drag back
A bound events property is never rewritten by a drag. Changed in 1.0.7
Before 1.0.7 a drag wrote the query's result into the widget, replacing the binding with a frozen
snapshot — the board silently stopped following the database, and no refresh ever landed again.
Now the board announces the change on eventChange and leaves the data to you. (An events
array authored inline is still updated in place, which is what makes prototyping work.)
Handle eventChange with a script action. The block arrives as context.event —
{ id, resourceId, label, start, end, category }, with start and end in epoch
milliseconds — so convert them to local time before they reach the database:
const job = context.event;
// epoch-ms → local ISO. The raw number would be stored as UTC.
const p = (n) => String(n).padStart(2, "0");
const toLocalIso = (ms) => {
const d = new Date(ms);
return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) +
"T" + p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
};
try {
await system.db.runNamedQuery("SchedulerMoveEvent", {
id: job.id, start: toLocalIso(job.start), end: toLocalIso(job.end),
});
context.refreshBinding("self"); // show what was stored, not what the drag guessed
} catch (err) {
context.ui.notify({ message: "Could not save " + job.label, severity: "error" });
}
-- SchedulerMoveEvent
UPDATE schedule_event
SET starts_at = :start,
ends_at = :end
WHERE id = :id
The same handler covers a resize: eventChange fires for a move and for a drag of either
edge, and start/end are always the new span. Refresh only on success, so a rejected write
leaves the board showing what the database actually holds. Anything conditional — refusing an
overlap, checking a permission, logging who moved what — belongs in that script, before the update.
Adding a new block from an empty slot
Clicking empty timeline fires slotSelect, and the script receives the slot as context.slot:
| Field | Is |
|---|---|
resourceId, resourceLabel, resourcePath | The row that was clicked |
start, end | The slot, in epoch milliseconds: the click floored to snapMinutes, and slotPopup.durationMinutes long |
startISO, endISO | The same span as local ISO strings, ready for a TIMESTAMP column |
time, timeISO | The exact instant under the pointer, unsnapped |
durationMinutes | The duration the span was built from |
Handle slotSelect on the Scheduler with a script action. It runs on the board itself, so
refreshBinding("self") re-runs the board's own events query and the new bar appears where
you clicked:
const slot = context.slot;
try {
await system.db.runNamedQuery("SchedulerInsertEvent", {
id: crypto.randomUUID(),
resourceId: slot.resourceId,
label: "New job",
start: slot.startISO, // already local ISO, no conversion needed
end: slot.endISO,
category: "planned",
});
context.refreshBinding("self");
} catch (err) {
context.ui.notify({ message: "Could not add a job to " + slot.resourceLabel, severity: "error" });
}
-- SchedulerInsertEvent
INSERT INTO schedule_event (id, resource_id, label, starts_at, ends_at, category)
VALUES (:id, :resourceId, :label, :start, :end, :category)
The id is made in the script with crypto.randomUUID() rather than by the database. That keeps
the SQL the same on every engine, since each one spells "generate a UUID" differently. If your
table generates its own key, drop id from both the column list and the parameters.
To ask for a label or a category first, turn on slotPopup and point slotPopup.view at a
small form View. That View receives the same object as the slot param, so its submit script
reads context.params.slot and runs the same INSERT. One difference from the script above: the
form's script runs inside the popover's own View, so refreshBinding("self") there refreshes the
form, not the board. Make sure something re-runs the board's events query after the popover
closes.
Things that bite
categoryis a join key, not a colour. A row whosecategorymatches no entry incategoriesstill draws, but with an auto-assigned colour. Constrain the column, or seedcategoriesfrom 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 hours — or decades — away from where they belong. A row whose end is before its start is dropped.
idshould be unique. A duplicate or missingidno longer makes two bars share one drag handle, but every write-back above keys on it.groupmarks a resource as a collapsible header band.true, PostgreSQL'stand a MySQL or SQL Server1all count.- Pass every parameter the SQL names. Changed in 1.0.7 Every declared
parameter of a saved query is required, because the statement contains
:startliterally and cannot be bound without it. A script that leaves a key out gets an error back instead of a row, and the Req tick box that suggested otherwise is gone. → SQL databases - A colon inside a string or a comment is not a parameter. Changed in 1.0.7
-- SchedulerInsertEvent, a'06:00'literal or a PostgreSQL::textcast are left alone, so comment your queries freely. → SQL databases parentIdmust reference a resource in the same result set. A child whose parent is filtered out by the query does not render.