Skip to content

Latest commit

 

History

History
573 lines (483 loc) · 18.5 KB

File metadata and controls

573 lines (483 loc) · 18.5 KB
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.

Installation

npm install @object-ui/plugin-gantt

Overview

The @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.

Features

  • 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 through dataSource.update() automatically (optimistic local update + revert on failure). See the package README for details on the lower-level onTaskUpdate hook when embedding <GanttView> directly.

<PluginLoader plugins={['gantt']}>

Interactive Examples

Basic Gantt Chart

Software Development Sprint

Construction Project

Usage

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.

Basic Usage with ObjectQL

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'
    }
  }
}

With Static Data

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'
    }
  }
}

Schema API

{
  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.

GanttConfig

{
  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.

Configuration

Field Mapping

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
    }
  }
};

Progress Field

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
}

Dependencies Field

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)
  }
]

Data Providers

Object Provider (Database)

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'
    }
  }
};

Value Provider (Static)

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'
    }
  }
};

API Provider

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.

Event Handling

Task Click

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.

Task click navigation

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.

Examples

Software Project

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'
    }
  }
}

Construction Project

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'
    }
  }
}

Marketing Campaign

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'
    }
  }
}

Comparison with Timeline Plugin

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

Typical Use Cases

  1. Project Management: Track project tasks, milestones, and dependencies
  2. Sprint Planning: Visualize agile sprint tasks and their timeline
  3. Construction Planning: Display construction phases and their relationships
  4. Event Planning: Show event preparation tasks and schedules
  5. Product Roadmap: Display product features and release timelines

TypeScript Support

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.

Related Documentation