Skip to main content

Planner Component

planner is a grouped, ordered planning grid. Each resource is a row and its work items occupy ordinal slots (Move 1, Move 2, and so on). Unlike timeline-grid, its columns represent position rather than time.

Data pipeline​

The component extracts items from queries, groups them with rows.groupBy, sorts each row with columns.orderBy, and places each sorted item by index. rows.fromQuery can supply empty resources; unmatched items appear in an optional Unassigned row.

component: planner
name: driverPlanner
props:
height: 700
queries:
- name: moves
query:
command: 'query Moves($date: DateTime!) { orders { items { orderMoves { orderMoveId } } } }'
variables: { date: '{{ plannerDate }}' }
path: orders.items
itemsPath: orderMoves
parentAs: order
- name: drivers
query:
command: 'query Drivers { contacts { items { contactId firstName lastName } } }'
path: contacts.items
rows:
groupBy: driver.contactId
fromQuery: drivers
key: contactId
label: '{{ item.firstName }} {{ item.lastName }}'
headerWidth: 260
columns:
appendEmpty: true
orderBy:
- field: startDate
direction: asc
label: 'Move {{ index }}'
width: 420
itemTemplate:
component: card

itemsPath automatically flattens arrays along a dot path; parentAs attaches the owner of the final collection and mapping can add or replace item fields. Column and row sorting is stable and supports multiple fields. Explicit rows.headerWidth and columns.width keep virtualized rows aligned.

Templates include rowHeaderTemplate, itemTemplate, emptyCellTemplate, summaryComponent, and emptyStateTemplate. Events include onItemClick, onCellClick, onRowClick, and onItemsLoaded. Set options.enableVirtualization and virtualizeThreshold for large boards, refreshHandler for action-driven reloads, and debug: true for extraction/grouping diagnostics.

Phone layout​

On web viewports below the Material UI md breakpoint, the planner changes from a horizontally scrolling grid to a single-column list of collapsible lane cards. Column headers and the summary footer are hidden, each lane starts collapsed, and tapping its row header expands or collapses its items. An expanded lane renders its occupied slots plus the first empty slot.

Drag sources, drop targets, and row virtualization are disabled in this phone layout. Keep click-driven actions available for assigning or moving items; onRowClick still runs when the user toggles a lane. The configured desktop height, rows.headerWidth, and columns.width do not constrain the phone layout.

Card actions​

dotsMenu adds the same top-right menu used by dataGrid rows. Menu-item templates and actions receive the complete item context, including item. Opening the menu or selecting an action does not trigger onItemClick.

dotsMenu:
items:
- label: { en-US: Open order }
permission: Orders.View
disabled: '{{ item.isLocked }}'
onClick:
- navigate: '/orders/{{ item.orderId }}'

Set columns.appendEmpty: true to keep one trailing empty slot on every lane. The board then renders one more ordinal column than the largest group, which is useful for drop-to-append workflows even when the fullest lane would otherwise have no empty cell.

Drop targets​

Planner cells accept cards from a DataGrid collection view when props.drop and events.onItemDrop are both configured. accept matches the source type; onlyEmptyCells defaults to true; canDrop can veto a row; and autoScroll defaults to true.

drop:
accept: [orderMove]
onlyEmptyCells: true
canDrop: '{{ eval !row.isUnassigned }}'
events:
onItemDrop:
- workflow:
workflowId: assign-move
inputs: { moveId: '{{ dragItem.orderMoveId }}', driverId: '{{ row.record.contactId }}' }

The event receives dragItem, dragType, row, column, rowIndex, and columnIndex. Eligible cells may expose isDragArmed and isDragOver to emptyCellTemplate. Native drag is desktop-only, so retain an onCellClick equivalent for touch and keyboard users.

Dragging planner cards between lanes​

props.dragSource makes the planner's own cards draggable using the same shared contract as DataGrid collection cards. type is required; data and canDrag are resolved per card with the normal item, row, and column variables.

dragSource:
type: assignedMove
data:
orderMoveId: '{{ item.orderMoveId }}'
sourceDriverId: '{{ row.record.contactId }}'
canDrag: '{{ eval !row.isUnassigned }}'
drop:
accept: [assignedMove]
canDrop: '{{ eval String(row.record.contactId) !== String(dragItem.sourceDriverId) }}'

The drop action still decides how reassignment is persisted. Keep payloads JSON-safe and retain a click-driven path for touch and keyboard users.

Consecutive item groups​

props.itemGroups visually joins adjacent cards in a lane when its key template resolves to the same non-empty value. Grouping is order-sensitive: a a b a a creates two groups, and a run of one remains an ordinary card.

itemGroups:
key: '{{ item.customValues.groupKey }}'
accent: warning
headerTemplate:
component: text
props: { value: '{{ group.size }} linked moves' }
dotsMenu:
items:
- label: { en-US: Open group }
onClick:
- navigate: '/groups/{{ group.key }}'

The header, accent, and group menu receive group = { key, items, size, startIndex, endIndex }, plus row and rowIndex. Grouped card templates receive group, groupPosition, and groupOffset. When a grouped card is dragged, its payload also includes dragGroup with the group key, size, dragged offset, and the resolved drag data for every member; drop actions can still use dragItem for the individual card.