| title | Plugin Gantt |
|---|
import { InteractiveDemo } from '@/app/components/InteractiveDemo'; import { PluginLoader } from '@/app/components/PluginLoader';
Gantt chart component for ObjectQL data sources - visualizes project tasks, timelines, and dependencies.
npm install @object-ui/plugin-ganttThe @object-ui/plugin-gantt plugin provides Gantt chart visualization for ObjectQL data sources. It's designed to work with object-based data providers and automatically maps record fields to Gantt chart tasks.
Note: This plugin is designed for use with ObjectQL data sources. For a simpler timeline component, see Timeline Plugin.
- ObjectQL Integration: Works seamlessly with object/api/value data providers
- Automatic Field Mapping: Maps database fields to Gantt tasks
- Task Timeline: Visual bars showing task duration
- Progress Tracking: Display task completion percentage (0-100%)
- Dependencies: Visualize task dependencies and relationships
- Date Ranges: Automatic date range calculation
- Interactive: Click handling for tasks
- Drag-and-drop rescheduling: Drag a bar to move it; drag either edge to
resize start/end. Snaps to whole days, keeps each date's time of day across
a daylight-saving change (with shift bands configured via
timeSegments, a day-view drag goes by bands instead, and a move keeps the bar's elapsed length), and persists the change throughdataSource.update()automatically (optimistic local update + revert on failure). See the package README for details on the lower-levelonTaskUpdatehook when embedding<GanttView>directly.
<PluginLoader plugins={['gantt']}>
An object-gantt node takes its props in its properties bag, whose members are
@objectstack/spec's ComponentPropsMap['object-gantt'] row, and its field
mapping in the bag's gantt block. objectui validate judges the bag against that
row and refuses a prop written flat on the node by name, naming its bag member
(Did you mean objectName → properties.objectName?, or
startDateField → properties.gantt.startDateField for a field-mapping key), as the
spec's own page component does (objectui#10859). SchemaRenderer hoists the bag
onto the node before ObjectGantt runs, so ObjectGanttSchema is the node as the
renderer reads it, and as code composes it.
import '@object-ui/plugin-gantt'
import type { ObjectGanttBlockNode } from '@object-ui/types'
const schema: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
objectName: 'tasks', // Your ObjectQL object
gantt: {
startDateField: 'startDate',
endDateField: 'endDate',
titleField: 'taskName',
progressField: 'completion',
dependenciesField: 'dependencies'
}
}
}import type { ObjectGanttBlockNode } from '@object-ui/types';
const schema: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
staticData: [
{
id: 1,
taskName: 'Design Phase',
startDate: '2024-01-01',
endDate: '2024-01-15',
completion: 100
},
{
id: 2,
taskName: 'Development',
startDate: '2024-01-16',
endDate: '2024-02-28',
completion: 60,
dependencies: [1]
},
{
id: 3,
taskName: 'Testing',
startDate: '2024-03-01',
endDate: '2024-03-15',
completion: 0,
dependencies: [2]
}
],
gantt: {
startDateField: 'startDate',
endDateField: 'endDate',
titleField: 'taskName',
progressField: 'completion',
dependenciesField: 'dependencies'
}
}
}{
type: 'object-gantt',
dataSource?: ElementDataSource, // Per-element binding; its object stands in for objectName
properties: { // The spec's ComponentPropsMap['object-gantt'] row
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is
// required, or the node's dataSource binding
gantt?: GanttConfig, // Field mapping and gantt options, viewMode among them
filter?: Array<any>, // JSON-Rules query filter — applies on EVERY provider
sort?: Array<{ field, order }>, // ordering — applies on EVERY provider
navigation?: NavigationConfig, // what a task click opens (absent: a drawer)
label?: string | I18nLabel, // the exported file's name, after gantt.exportFileName
skipWeekends?: boolean, // working-day scheduling; the day axis folds Sat/Sun
holidays?: string[], // extra non-working 'yyyy-mm-dd' dates
persistLayout?: boolean, // false: no saved layout, no save-layout button
viewName?: string, // the saved layout's scope (default 'default')
markers?: Array<{ date, label?, color? }>, // vertical reference lines
criticalPath?: boolean, // start with the critical-path highlight on
showBaselines?: boolean, // false: hide the baseline bars
readOnly?: boolean, // disable every edit path
mobileReadOnly?: boolean // false: keep a narrow (< 640px) chart editable
}
}
Every key above is a member of the spec's object-gantt row and is published
by the registration, so the page validator accepts it. The package README's
node-level options table says what each one does, as measured through the real
SchemaRenderer (objectui#11168).
filter and sort are not object-only keys. They narrow and order inline rows
(staticData, or data: { provider: 'value' }) exactly as they narrow and
order fetched ones, and the platform row ceiling — 2,000 drawn rows with a
footnote naming both numbers — applies to inline rows too. The ceiling is
applied to the filtered set, so a large inline array that a filter cuts
below the ceiling draws every matching row and shows no footnote.
search is the chart's full-text term, sent as $search on every provider, with
searchableFields sent as $searchFields beside it. They are not authored keys:
a list view's toolbar Search box writes both onto the gantt node it composes,
because the chart runs its own query, and the spec's row does not declare them, so
objectui validate refuses them in the bag. The server decides which fields a term
matches unless searchableFields narrows them, and a searchableFields with no
term sends nothing.
onTaskClick and className are React props on <ObjectGantt>, not schema
keys — the renderer never reads them off the schema, and a function cannot
survive serializable metadata anyway. viewMode is a member of the gantt
block, read through the gantt config.
{
startDateField: string, // Field containing task start date
endDateField: string, // Field containing task end date
titleField: string, // Field to use as task title
progressField?: string, // Field for progress (0-100)
dependenciesField?: string // Field for task dependencies (array of IDs)
}
A date-only end (YYYY-MM-DD) is inclusive: the bar runs through the end of
that day. So a task from 2024-01-01 to 2024-01-15 covers January 1st to
15th, a successor starting 2024-01-16 begins exactly where it ends, and a
task whose start and end name the same day is one day long. A drag or resize
writes a date-only end back as the last day the bar runs through, which is
also the day the task list, the tooltip and the inline editor show. An end
with a time part is an instant, and the bar ends exactly there. The timeline's
gantt variant reads an end the same way.
Map your database fields to Gantt properties:
import type { ObjectGanttBlockNode } from '@object-ui/types';
const fieldMappedGantt: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
objectName: 'project_tasks',
gantt: {
titleField: 'name', // Database field for task name
startDateField: 'starts_at', // Database field for start date
endDateField: 'ends_at', // Database field for end date
progressField: 'percent_done', // Database field for progress
dependenciesField: 'depends_on' // Database field for dependencies
}
}
};The progress field should contain a number between 0-100 representing the completion percentage:
{/* doc-snippet: fragment — a SHAPE excerpt of one of the READER's own task RECORDS, not an expression: a bare object literal at statement position parses as a block with labels (measured: TS1005 x4, TS1128 x1). No ObjectUI type describes it — taskName / completion are the caller's own record fields, named by gantt.titleField / gantt.progressField above */}
{
id: 1,
taskName: 'Development',
startDate: '2024-01-01',
endDate: '2024-02-01',
completion: 75 // 75% complete
}The dependencies field should contain an array of task IDs that this task depends on:
{/* doc-snippet: fragment — a SHAPE excerpt of the READER's own task RECORDS, not an expression: a bare array literal at statement position is an expression statement whose element object literals parse as labelled blocks (measured: TS1005 x4). No ObjectUI type describes it — dependencies is the caller's own record field, named by gantt.dependenciesField above */}
[
{
id: 1,
taskName: 'Design',
startDate: '2024-01-01',
endDate: '2024-01-15',
dependencies: [] // No dependencies
},
{
id: 2,
taskName: 'Development',
startDate: '2024-01-16',
endDate: '2024-02-28',
dependencies: [1] // Depends on task 1 (Design)
},
{
id: 3,
taskName: 'Testing',
startDate: '2024-03-01',
endDate: '2024-03-15',
dependencies: [2] // Depends on task 2 (Development)
}
]import type { ObjectGanttBlockNode } from '@object-ui/types';
const objectProviderGantt: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
objectName: 'project_tasks',
gantt: {
startDateField: 'start_date',
endDateField: 'due_date',
titleField: 'title',
progressField: 'progress'
}
}
};import type { ObjectGanttBlockNode } from '@object-ui/types';
const valueProviderGantt: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
staticData: [
{ id: 1, title: 'Task 1', start: '2024-01-01', end: '2024-01-15' },
{ id: 2, title: 'Task 2', start: '2024-01-16', end: '2024-01-31' }
],
gantt: {
startDateField: 'start',
endDateField: 'end',
titleField: 'title'
}
}
};import type { ObjectGanttBlockNode } from '@object-ui/types';
const apiProviderGantt: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
data: {
provider: 'api',
// the api member is `read` / `write` HTTP requests — there is no
// top-level `endpoint` key, and nothing reads one
read: { url: '/api/project/tasks', method: 'GET' }
},
gantt: {
startDateField: 'startDate',
endDateField: 'endDate',
titleField: 'taskName',
progressField: 'percentComplete'
}
}
};When the view renders inside a SchemaRendererProvider that supplies an
apiFetch (the console host wires an authenticated fetch there), api-provider
requests carry the same credentials — Authorization, tenant, and locale
headers — as native platform requests. Without it, requests fall back to the
bare global fetch and rely on same-origin cookies alone.
The click handler is a React prop, not a schema key — the schema stays
serializable. A component mounted yourself, without SchemaRenderer, receives the
node as it reads it, so its schema prop keeps its props on the node: nothing
hoists a properties bag there.
import { ObjectGantt } from '@object-ui/plugin-gantt';
import type { DataSource } from '@object-ui/types';
export function ProjectGantt({ dataSource }: { dataSource: DataSource }) {
return (
<ObjectGantt
schema={{
type: 'object-gantt',
objectName: 'tasks',
gantt: {
startDateField: 'start',
endDateField: 'end',
titleField: 'name'
}
}}
dataSource={dataSource}
onTaskClick={(task) => {
console.log('Task clicked:', task);
// Open task details / edit / show dependencies
}}
/>
);
}Rendered through the registered object-gantt type there is usually nothing to
wire: clicking a row already opens the standard detail drawer.
navigation in the properties bag decides what a task click opens. With the
key absent it is the drawer. drawer, modal and popover open the task's
record in that overlay, and split opens it beside the chart. page, and a
block written without mode, open the record page in the same tab, and
new_window opens it in a new tab. none opens nothing, preventNavigation: true opens nothing whatever the mode, openNewTab: true opens the record page
in a new tab and outranks every mode except none, and size sets the overlay
width.
{
"type": "object-gantt",
"properties": {
"objectName": "project_task",
"gantt": { "startDateField": "start_date", "endDateField": "end_date", "titleField": "name" },
"navigation": { "mode": "modal", "size": "lg" }
}
}The record-page address is the gantt's own: it derives it from the page it is
on and does not use a record navigator the host publishes, so mounting one
changes nothing. On the object's own list or view route the address is the
object's record page; on any other page, such as a custom page, the gantt
appends /OBJECT/record/ID to the current address. On inline rows that name
no object (no objectName, no object data provider), no mode opens anything.
import type { ObjectGanttBlockNode } from '@object-ui/types';
const softwareProject: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
objectName: 'sprint_tasks',
gantt: {
startDateField: 'startDate',
endDateField: 'endDate',
titleField: 'taskTitle',
progressField: 'completionPercent',
dependenciesField: 'blockedBy'
}
}
}import type { ObjectGanttBlockNode } from '@object-ui/types';
const constructionGantt: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
objectName: 'construction_phases',
gantt: {
startDateField: 'phase_start',
endDateField: 'phase_end',
titleField: 'phase_name',
progressField: 'percent_complete'
}
}
}import type { ObjectGanttBlockNode } from '@object-ui/types';
const campaignGantt: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
staticData: [
{
id: 1,
activity: 'Market Research',
start: '2024-01-01',
end: '2024-01-14',
done: 100
},
{
id: 2,
activity: 'Content Creation',
start: '2024-01-15',
end: '2024-02-15',
done: 80,
requires: [1]
},
{
id: 3,
activity: 'Campaign Launch',
start: '2024-02-16',
end: '2024-02-29',
done: 0,
requires: [2]
},
{
id: 4,
activity: 'Performance Analysis',
start: '2024-03-01',
end: '2024-03-15',
done: 0,
requires: [3]
}
],
gantt: {
titleField: 'activity',
startDateField: 'start',
endDateField: 'end',
progressField: 'done',
dependenciesField: 'requires'
}
}
}| Feature | object-gantt | timeline (gantt variant) |
|---|---|---|
| Data Source | ObjectQL (database) | Static arrays |
| Dependencies | Yes | No |
| Progress | Yes (0-100%) | No |
| Use Case | Project management | Simple timelines |
| Field Mapping | Configurable | Fixed schema |
| Best For | Database-driven projects | Static presentations |
When to use object-gantt:
- You're using ObjectQL for data management
- You need task dependencies and progress tracking
- Tasks come from a database or API
- You're building project management features
When to use timeline (gantt variant):
- You have static timeline data
- You don't need dependencies or progress
- You're creating simple visual timelines
- You're not using ObjectQL
- Project Management: Track project tasks, milestones, and dependencies
- Sprint Planning: Visualize agile sprint tasks and their timeline
- Construction Planning: Display construction phases and their relationships
- Event Planning: Show event preparation tasks and schedules
- Product Roadmap: Display product features and release timelines
import type { ObjectGanttBlockNode, GanttConfig } from '@object-ui/types'
const ganttConfig: GanttConfig = {
startDateField: 'startDate',
endDateField: 'endDate',
titleField: 'taskName',
progressField: 'completion',
dependenciesField: 'dependencies'
}
const ganttSchema: ObjectGanttBlockNode = {
type: 'object-gantt',
properties: {
objectName: 'project_tasks',
gantt: ganttConfig
}
}ObjectGanttSchema types the node as ObjectGantt reads it: after SchemaRenderer
hoists the bag, or as you hand it to <ObjectGantt schema={…}> yourself.
- Timeline Plugin - Simpler timeline visualization
- Plugin System Overview
- Package README