Skip to content

Task Editing

Task edits route through mutation services and scheduler recomputation. Before applying task changes, Gantt emits cancelable gantt-before-task-change; dependency field edits emit gantt-before-dependency-change; assignee edits emit gantt-before-assignment-change.

Command/event surface is defined in:

  • packages/enterprise/plugins/gantt/grid/gantt-events.ts
  • packages/enterprise/plugins/gantt/grid/gantt-plugin.ts

For audited changes, pair with Gantt history tooling (features/history).

Source code
Astro astro
---
import GanttScheduling from '@revolist/revogrid-examples/components/gantt/GanttScheduling.vue';
---

<GanttScheduling client:only="vue" />
TypeScript ts
// src/components/gantt/GanttScheduling.ts
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import { GanttPlugin, createDefaultTaskTableColumn } from '@revolist/revogrid-enterprise';
import type { TaskEntity, DependencyEntity, CalendarEntity } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';

const { isDark } = currentTheme();

const PROJECT_ID = 'project-web-redesign';
const CALENDAR_ID = 'cal-us';

// Calendar-aware scheduling: durations are in working days (Mon–Fri, excluding holidays)
const ganttConfig = {
  id: PROJECT_ID,
  name: 'Website Redesign',
  version: '1',
  currency: 'USD',
  timeZone: 'America/New_York',
  primaryCalendarId: CALENDAR_ID,
  updatedAt: '2026-04-06T00:00:00Z',
  zoomPreset: 'week' as const,
  scheduling: {
    excludeHolidaysFromDuration: true, // durations skip weekends and holidays
  },
  visuals: {
    shadeNonWorkingTime: true, // shade weekend columns on the timeline
    projectLineDate: '2026-04-06', // vertical status-date line
  },
};

// US calendar with public holidays
const calendars: CalendarEntity[] = [
  {
    id: CALENDAR_ID,
    name: 'US Standard',
    timeZone: 'America/New_York',
    workingDays: [1, 2, 3, 4, 5], // Mon–Fri
    holidays: [
      '2026-05-25', // Memorial Day
      '2026-07-04', // Independence Day
    ],
    hoursPerDay: 8,
  },
];

const tasks: TaskEntity[] = [
  {
    id: 't1', projectId: PROJECT_ID, parentId: null,
    wbsCode: '1', name: 'Design', type: 'summary', status: 'in-progress',
    startDate: '2026-04-06', endDate: '2026-04-24', durationDays: 15,
    progressPercent: 60, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't2', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.1', name: 'Wireframes', type: 'task', status: 'done',
    startDate: '2026-04-06', endDate: '2026-04-10', durationDays: 5,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't3', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.2', name: 'Design Review', type: 'milestone', status: 'done',
    startDate: '2026-04-10', endDate: '2026-04-10', durationDays: 0,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
  },
  {
    id: 't4', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.3', name: 'Visual Design', type: 'task', status: 'in-progress',
    startDate: '2026-04-13', endDate: '2026-04-24', durationDays: 10,
    progressPercent: 40, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't5', projectId: PROJECT_ID, parentId: null,
    wbsCode: '2', name: 'Development', type: 'summary', status: 'not-started',
    startDate: '2026-04-27', endDate: '2026-05-28', durationDays: 24,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: false, tags: [],
  },
  {
    id: 't6', projectId: PROJECT_ID, parentId: 't5',
    wbsCode: '2.1', name: 'Frontend', type: 'task', status: 'not-started',
    // constraint: cannot start before May 4 (waiting on external API)
    startDate: '2026-05-04', endDate: '2026-05-20', durationDays: 13,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    constraintType: 'start-no-earlier-than',
    constraintDate: '2026-05-04',
  },
  {
    id: 't7', projectId: PROJECT_ID, parentId: 't5',
    wbsCode: '2.2', name: 'Backend', type: 'task', status: 'not-started',
    startDate: '2026-04-27', endDate: '2026-05-28', durationDays: 24,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    deadlineDate: '2026-05-22', // deadline marker — project expects early delivery
  },
  {
    id: 't8', projectId: PROJECT_ID, parentId: null,
    wbsCode: '3', name: 'Launch', type: 'milestone', status: 'not-started',
    startDate: '2026-05-28', endDate: '2026-05-28', durationDays: 0,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
  },
];

const dependencies: DependencyEntity[] = [
  { id: 'd1', predecessorTaskId: 't2', successorTaskId: 't3', type: 'finish-to-start', lagDays: 0 },
  { id: 'd2', predecessorTaskId: 't3', successorTaskId: 't4', type: 'finish-to-start', lagDays: 1 },
  { id: 'd3', predecessorTaskId: 't4', successorTaskId: 't6', type: 'finish-to-start', lagDays: 1 },
  { id: 'd4', predecessorTaskId: 't4', successorTaskId: 't7', type: 'finish-to-start', lagDays: 1 },
  { id: 'd5', predecessorTaskId: 't6', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
  { id: 'd6', predecessorTaskId: 't7', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
];

export function load(parentSelector: string) {
  const grid = document.createElement('revo-grid');

  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.hideAttribution = true;
  grid.plugins = [GanttPlugin];
  grid.gantt = ganttConfig;
  grid.ganttCalendars = calendars;
  grid.ganttDependencies = dependencies;
  grid.source = tasks;
  grid.columns = [
    createDefaultTaskTableColumn('wbs'),
    createDefaultTaskTableColumn('name'),
  ];

  document.querySelector(parentSelector)?.appendChild(grid);
}
Vue vue
<template>
  <RevoGrid
    hide-attribution
    style="height: 500px"
    :theme="isDark ? 'darkCompact' : 'compact'"
    :plugins="plugins"
    :source="tasks"
    :columns="columns"
    :gantt.prop="ganttConfig"
    :gantt-dependencies.prop="dependencies"
    :gantt-calendars.prop="calendars"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { GanttPlugin, createDefaultTaskTableColumn } from '@revolist/revogrid-enterprise';
import type { TaskEntity, DependencyEntity, CalendarEntity } from '@revolist/revogrid-enterprise';
import { currentThemeVue } from '../composables/useRandomData';

const { isDark } = currentThemeVue();

const PROJECT_ID = 'project-web-redesign';
const CALENDAR_ID = 'cal-us';

const plugins = ref([GanttPlugin]);

const ganttConfig = ref({
  id: PROJECT_ID,
  name: 'Website Redesign',
  version: '1',
  currency: 'USD',
  timeZone: 'America/New_York',
  primaryCalendarId: CALENDAR_ID,
  updatedAt: '2026-04-06T00:00:00Z',
  zoomPreset: 'week' as const,
  scheduling: {
    excludeHolidaysFromDuration: true,
  },
  visuals: {
    shadeNonWorkingTime: true,
    projectLineDate: '2026-04-06',
  },
});

const calendars = ref<CalendarEntity[]>([
  {
    id: CALENDAR_ID,
    name: 'US Standard',
    timeZone: 'America/New_York',
    workingDays: [1, 2, 3, 4, 5],
    holidays: ['2026-05-25', '2026-07-04'],
    hoursPerDay: 8,
  },
]);

const tasks = ref<TaskEntity[]>([
  {
    id: 't1', projectId: PROJECT_ID, parentId: null,
    wbsCode: '1', name: 'Design', type: 'summary', status: 'in-progress',
    startDate: '2026-04-06', endDate: '2026-04-24', durationDays: 15,
    progressPercent: 60, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't2', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.1', name: 'Wireframes', type: 'task', status: 'done',
    startDate: '2026-04-06', endDate: '2026-04-10', durationDays: 5,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't3', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.2', name: 'Design Review', type: 'milestone', status: 'done',
    startDate: '2026-04-10', endDate: '2026-04-10', durationDays: 0,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
  },
  {
    id: 't4', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.3', name: 'Visual Design', type: 'task', status: 'in-progress',
    startDate: '2026-04-13', endDate: '2026-04-24', durationDays: 10,
    progressPercent: 40, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't5', projectId: PROJECT_ID, parentId: null,
    wbsCode: '2', name: 'Development', type: 'summary', status: 'not-started',
    startDate: '2026-04-27', endDate: '2026-05-28', durationDays: 24,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: false, tags: [],
  },
  {
    id: 't6', projectId: PROJECT_ID, parentId: 't5',
    wbsCode: '2.1', name: 'Frontend', type: 'task', status: 'not-started',
    startDate: '2026-05-04', endDate: '2026-05-20', durationDays: 13,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    constraintType: 'start-no-earlier-than',
    constraintDate: '2026-05-04',
  },
  {
    id: 't7', projectId: PROJECT_ID, parentId: 't5',
    wbsCode: '2.2', name: 'Backend', type: 'task', status: 'not-started',
    startDate: '2026-04-27', endDate: '2026-05-28', durationDays: 24,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    deadlineDate: '2026-05-22',
  },
  {
    id: 't8', projectId: PROJECT_ID, parentId: null,
    wbsCode: '3', name: 'Launch', type: 'milestone', status: 'not-started',
    startDate: '2026-05-28', endDate: '2026-05-28', durationDays: 0,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
  },
]);

const dependencies = ref<DependencyEntity[]>([
  { id: 'd1', predecessorTaskId: 't2', successorTaskId: 't3', type: 'finish-to-start', lagDays: 0 },
  { id: 'd2', predecessorTaskId: 't3', successorTaskId: 't4', type: 'finish-to-start', lagDays: 1 },
  { id: 'd3', predecessorTaskId: 't4', successorTaskId: 't6', type: 'finish-to-start', lagDays: 1 },
  { id: 'd4', predecessorTaskId: 't4', successorTaskId: 't7', type: 'finish-to-start', lagDays: 1 },
  { id: 'd5', predecessorTaskId: 't6', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
  { id: 'd6', predecessorTaskId: 't7', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
]);

const columns = ref([
  createDefaultTaskTableColumn('wbs'),
  createDefaultTaskTableColumn('name'),
]);
</script>
React tsx
import React, { useMemo } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { GanttPlugin, createDefaultTaskTableColumn } from '@revolist/revogrid-enterprise';
import type { TaskEntity, DependencyEntity, CalendarEntity } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';

const { isDark } = currentTheme();

const PROJECT_ID = 'project-web-redesign';
const CALENDAR_ID = 'cal-us';

const tasks: TaskEntity[] = [
  {
    id: 't1', projectId: PROJECT_ID, parentId: null,
    wbsCode: '1', name: 'Design', type: 'summary', status: 'in-progress',
    startDate: '2026-04-06', endDate: '2026-04-24', durationDays: 15,
    progressPercent: 60, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't2', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.1', name: 'Wireframes', type: 'task', status: 'done',
    startDate: '2026-04-06', endDate: '2026-04-10', durationDays: 5,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't3', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.2', name: 'Design Review', type: 'milestone', status: 'done',
    startDate: '2026-04-10', endDate: '2026-04-10', durationDays: 0,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
  },
  {
    id: 't4', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.3', name: 'Visual Design', type: 'task', status: 'in-progress',
    startDate: '2026-04-13', endDate: '2026-04-24', durationDays: 10,
    progressPercent: 40, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't5', projectId: PROJECT_ID, parentId: null,
    wbsCode: '2', name: 'Development', type: 'summary', status: 'not-started',
    startDate: '2026-04-27', endDate: '2026-05-28', durationDays: 24,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: false, tags: [],
  },
  {
    id: 't6', projectId: PROJECT_ID, parentId: 't5',
    wbsCode: '2.1', name: 'Frontend', type: 'task', status: 'not-started',
    startDate: '2026-05-04', endDate: '2026-05-20', durationDays: 13,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    constraintType: 'start-no-earlier-than',
    constraintDate: '2026-05-04',
  },
  {
    id: 't7', projectId: PROJECT_ID, parentId: 't5',
    wbsCode: '2.2', name: 'Backend', type: 'task', status: 'not-started',
    startDate: '2026-04-27', endDate: '2026-05-28', durationDays: 24,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    deadlineDate: '2026-05-22',
  },
  {
    id: 't8', projectId: PROJECT_ID, parentId: null,
    wbsCode: '3', name: 'Launch', type: 'milestone', status: 'not-started',
    startDate: '2026-05-28', endDate: '2026-05-28', durationDays: 0,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
  },
];

const dependencies: DependencyEntity[] = [
  { id: 'd1', predecessorTaskId: 't2', successorTaskId: 't3', type: 'finish-to-start', lagDays: 0 },
  { id: 'd2', predecessorTaskId: 't3', successorTaskId: 't4', type: 'finish-to-start', lagDays: 1 },
  { id: 'd3', predecessorTaskId: 't4', successorTaskId: 't6', type: 'finish-to-start', lagDays: 1 },
  { id: 'd4', predecessorTaskId: 't4', successorTaskId: 't7', type: 'finish-to-start', lagDays: 1 },
  { id: 'd5', predecessorTaskId: 't6', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
  { id: 'd6', predecessorTaskId: 't7', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
];

const calendars: CalendarEntity[] = [
  {
    id: CALENDAR_ID,
    name: 'US Standard',
    timeZone: 'America/New_York',
    workingDays: [1, 2, 3, 4, 5],
    holidays: ['2026-05-25', '2026-07-04'],
    hoursPerDay: 8,
  },
];

function GanttScheduling() {
  const ganttConfig = useMemo(() => ({
    id: PROJECT_ID,
    name: 'Website Redesign',
    version: '1',
    currency: 'USD',
    timeZone: 'America/New_York',
    primaryCalendarId: CALENDAR_ID,
    updatedAt: '2026-04-06T00:00:00Z',
    zoomPreset: 'week' as const,
    scheduling: {
      excludeHolidaysFromDuration: true,
    },
    visuals: {
      shadeNonWorkingTime: true,
      projectLineDate: '2026-04-06',
    },
  }), []);

  const columns = useMemo(() => [
    createDefaultTaskTableColumn('wbs'),
    createDefaultTaskTableColumn('name'),
  ], []);

  return (
    <RevoGrid
      style={{ height: '500px' }}
      theme={isDark() ? 'darkCompact' : 'compact'}
      hideAttribution
      plugins={[GanttPlugin]}
      source={tasks}
      columns={columns}
      gantt={ganttConfig}
      ganttDependencies={dependencies}
      ganttCalendars={calendars}
    />
  );
}

export default GanttScheduling;
Angular ts
import { Component, NO_ERRORS_SCHEMA, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { GanttPlugin, createDefaultTaskTableColumn } from '@revolist/revogrid-enterprise';
import type { TaskEntity, DependencyEntity, CalendarEntity } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';

const PROJECT_ID = 'project-web-redesign';
const CALENDAR_ID = 'cal-us';

@Component({
  selector: 'gantt-scheduling-grid',
  standalone: true,
  imports: [RevoGrid],
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
  template: `
    <revo-grid
      style="min-height: 500px"
      [theme]="theme"
      [hideAttribution]="true"
      [plugins]="plugins"
      [source]="tasks"
      [columns]="columns"
      [gantt]="ganttConfig"
      [ganttDependencies]="dependencies"
      [ganttCalendars]="calendars"
    ></revo-grid>
  `,
  encapsulation: ViewEncapsulation.None,
})
export class GanttSchedulingGridComponent {
  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  plugins = [GanttPlugin];

  ganttConfig = {
    id: PROJECT_ID,
    name: 'Website Redesign',
    version: '1',
    currency: 'USD',
    timeZone: 'America/New_York',
    primaryCalendarId: CALENDAR_ID,
    updatedAt: '2026-04-06T00:00:00Z',
    zoomPreset: 'week' as const,
    scheduling: {
      excludeHolidaysFromDuration: true,
    },
    visuals: {
      shadeNonWorkingTime: true,
      projectLineDate: '2026-04-06',
    },
  };

  calendars: CalendarEntity[] = [
    {
      id: CALENDAR_ID,
      name: 'US Standard',
      timeZone: 'America/New_York',
      workingDays: [1, 2, 3, 4, 5],
      holidays: ['2026-05-25', '2026-07-04'],
      hoursPerDay: 8,
    },
  ];

  tasks: TaskEntity[] = [
    {
      id: 't1', projectId: PROJECT_ID, parentId: null,
      wbsCode: '1', name: 'Design', type: 'summary', status: 'in-progress',
      startDate: '2026-04-06', endDate: '2026-04-24', durationDays: 15,
      progressPercent: 60, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    },
    {
      id: 't2', projectId: PROJECT_ID, parentId: 't1',
      wbsCode: '1.1', name: 'Wireframes', type: 'task', status: 'done',
      startDate: '2026-04-06', endDate: '2026-04-10', durationDays: 5,
      progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    },
    {
      id: 't3', projectId: PROJECT_ID, parentId: 't1',
      wbsCode: '1.2', name: 'Design Review', type: 'milestone', status: 'done',
      startDate: '2026-04-10', endDate: '2026-04-10', durationDays: 0,
      progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
    },
    {
      id: 't4', projectId: PROJECT_ID, parentId: 't1',
      wbsCode: '1.3', name: 'Visual Design', type: 'task', status: 'in-progress',
      startDate: '2026-04-13', endDate: '2026-04-24', durationDays: 10,
      progressPercent: 40, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    },
    {
      id: 't5', projectId: PROJECT_ID, parentId: null,
      wbsCode: '2', name: 'Development', type: 'summary', status: 'not-started',
      startDate: '2026-04-27', endDate: '2026-05-28', durationDays: 24,
      progressPercent: 0, calendarId: CALENDAR_ID, isCritical: false, tags: [],
    },
    {
      id: 't6', projectId: PROJECT_ID, parentId: 't5',
      wbsCode: '2.1', name: 'Frontend', type: 'task', status: 'not-started',
      startDate: '2026-05-04', endDate: '2026-05-20', durationDays: 13,
      progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
      constraintType: 'start-no-earlier-than',
      constraintDate: '2026-05-04',
    },
    {
      id: 't7', projectId: PROJECT_ID, parentId: 't5',
      wbsCode: '2.2', name: 'Backend', type: 'task', status: 'not-started',
      startDate: '2026-04-27', endDate: '2026-05-28', durationDays: 24,
      progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
      deadlineDate: '2026-05-22',
    },
    {
      id: 't8', projectId: PROJECT_ID, parentId: null,
      wbsCode: '3', name: 'Launch', type: 'milestone', status: 'not-started',
      startDate: '2026-05-28', endDate: '2026-05-28', durationDays: 0,
      progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
    },
  ];

  dependencies: DependencyEntity[] = [
    { id: 'd1', predecessorTaskId: 't2', successorTaskId: 't3', type: 'finish-to-start', lagDays: 0 },
    { id: 'd2', predecessorTaskId: 't3', successorTaskId: 't4', type: 'finish-to-start', lagDays: 1 },
    { id: 'd3', predecessorTaskId: 't4', successorTaskId: 't6', type: 'finish-to-start', lagDays: 1 },
    { id: 'd4', predecessorTaskId: 't4', successorTaskId: 't7', type: 'finish-to-start', lagDays: 1 },
    { id: 'd5', predecessorTaskId: 't6', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
    { id: 'd6', predecessorTaskId: 't7', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
  ];

  columns = [
    createDefaultTaskTableColumn('wbs'),
    createDefaultTaskTableColumn('name'),
  ];
}

Inline Gantt edits update the task through the Gantt mutation services, recompute the project, and emit history snapshots for user changes. Initial project hydration is not recorded as a history step.

Call event.preventDefault() in a before-change listener to reject a field edit before the task, dependency, or assignment store is mutated. Rejected predecessor/successor edits leave the old dependency rows in place; rejected assignee edits leave the old assignment rows in place.

GanttPlugin installs the packaged task editor dialog by default, so every Gantt grid gets the row context-menu editor without registering an extra plugin. The same editor contract is also available as presentation-free helpers: render controls from TASK_EDITOR_FIELD_SCHEMA, seed them with createTaskEditorFormValues(), and turn submitted values into a TaskUpdate patch with normalizeTaskEditorSubmit().

Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import {
  defineGanttToolbar,
  GanttPlugin,
} from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';
import './gantt-task-editor-form.css';
import {
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_RESOURCES,
  SHOWCASE_TOOLBAR_COLUMNS,
  STANDARD_CALENDAR,
} from './gantt-project-data';
import {
  createInitialTaskSource,
  TASK_EDITOR_COLUMNS,
  TASK_EDITOR_DIALOG_CONFIG,
  TASK_EDITOR_GANTT_CONFIG,
} from './GanttTaskEditorFormShared';

const { isDark } = currentTheme();

type TaskEditorGridElement = HTMLRevoGridElement & Record<string, any>;

function createElement<K extends keyof HTMLElementTagNameMap>(
  tagName: K,
  className?: string,
): HTMLElementTagNameMap[K] {
  const element = document.createElement(tagName);
  if (className) {
    element.className = className;
  }
  return element;
}

export function load(parentSelector: string | Element) {
  const parent = typeof parentSelector === 'string'
    ? document.querySelector(parentSelector)
    : parentSelector;
  if (!parent) {
    return () => {};
  }

  const taskSource = createInitialTaskSource();

  const root = createElement('section', 'gantt-task-editor-demo');
  const toolbar = createElement('div', 'gantt-task-editor-toolbar');
  const grid = document.createElement('revo-grid') as TaskEditorGridElement;

  grid.className = 'gantt-task-editor-grid';
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.hideAttribution = true;
  grid.plugins = [GanttPlugin as any];
  grid.columns = TASK_EDITOR_COLUMNS;
  grid.gantt = TASK_EDITOR_GANTT_CONFIG;
  grid.ganttCalendars = [STANDARD_CALENDAR];
  grid.ganttDependencies = SHOWCASE_DEPENDENCIES;
  grid.ganttResources = SHOWCASE_RESOURCES;
  grid.ganttAssignments = SHOWCASE_ASSIGNMENTS;
  grid.ganttTaskEditorDialog = TASK_EDITOR_DIALOG_CONFIG;

  root.appendChild(toolbar);
  root.appendChild(grid);
  parent.appendChild(root);
  defineGanttToolbar(toolbar, {
    grid,
    columns: SHOWCASE_TOOLBAR_COLUMNS,
    controls: {
      export: false,
      baseline: false,
    },
  });

  grid.source = taskSource;

  return () => {
    root.remove();
  };
}
React tsx
import React, { useEffect, useMemo, useRef } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { defineGanttToolbar, GanttPlugin } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';
import {
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_RESOURCES,
  SHOWCASE_TOOLBAR_COLUMNS,
  STANDARD_CALENDAR,
} from './gantt-project-data';
import {
  createInitialTaskSource,
  TASK_EDITOR_COLUMNS,
  TASK_EDITOR_DIALOG_CONFIG,
  TASK_EDITOR_GANTT_CONFIG,
} from './GanttTaskEditorFormShared';
import './gantt-task-editor-form.css';

const { isDark } = currentTheme();

export default function GanttTaskEditorForm() {
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const toolbarRef = useRef<HTMLDivElement>(null);

  const source = useMemo(() => createInitialTaskSource(), []);
  const plugins = useMemo(() => [GanttPlugin], []);
  const columns = useMemo(() => TASK_EDITOR_COLUMNS, []);
  const gantt = useMemo(() => TASK_EDITOR_GANTT_CONFIG, []);
  const calendars = useMemo(() => [STANDARD_CALENDAR], []);
  const dependencies = useMemo(() => SHOWCASE_DEPENDENCIES, []);
  const resources = useMemo(() => SHOWCASE_RESOURCES, []);
  const assignments = useMemo(() => SHOWCASE_ASSIGNMENTS, []);
  const taskEditorDialog = useMemo(() => TASK_EDITOR_DIALOG_CONFIG, []);

  useEffect(() => {
    const toolbar = toolbarRef.current;
    const grid = gridRef.current;
    if (!toolbar || !grid) {
      return undefined;
    }

    defineGanttToolbar(toolbar, {
      grid,
      columns: SHOWCASE_TOOLBAR_COLUMNS,
      controls: {
        export: false,
        baseline: false,
      },
    });

    return () => {
      toolbar.textContent = '';
    };
  }, []);

  return (
    <section className="gantt-task-editor-demo">
      <div ref={toolbarRef} className="gantt-task-editor-toolbar" />
      <RevoGrid
        ref={gridRef}
        className="gantt-task-editor-grid"
        theme={isDark() ? 'darkCompact' : 'compact'}
        hideAttribution
        plugins={plugins}
        source={source}
        columns={columns}
        gantt={gantt}
        ganttCalendars={calendars}
        ganttDependencies={dependencies}
        ganttResources={resources}
        ganttAssignments={assignments}
        ganttTaskEditorDialog={taskEditorDialog}
      />
    </section>
  );
}
Vue vue
<template>
  <section class="gantt-task-editor-demo">
    <div ref="toolbarRef" class="gantt-task-editor-toolbar"></div>
    <RevoGrid
      ref="gridRef"
      class="gantt-task-editor-grid"
      hide-attribution
      :theme="isDark ? 'darkCompact' : 'compact'"
      :plugins="plugins"
      :source="tasks"
      :columns="columns"
      :gantt.prop="ganttConfig"
      :gantt-calendars.prop="calendars"
      :gantt-dependencies.prop="dependencies"
      :gantt-resources.prop="resources"
      :gantt-assignments.prop="assignments"
      :gantt-task-editor-dialog.prop="taskEditorDialog"
    />
  </section>
</template>

<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { defineGanttToolbar, GanttPlugin } from '@revolist/revogrid-enterprise';
import { currentThemeVue } from '../composables/useRandomData';
import {
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_RESOURCES,
  SHOWCASE_TOOLBAR_COLUMNS,
  STANDARD_CALENDAR,
} from './gantt-project-data';
import {
  createInitialTaskSource,
  TASK_EDITOR_COLUMNS,
  TASK_EDITOR_DIALOG_CONFIG,
  TASK_EDITOR_GANTT_CONFIG,
} from './GanttTaskEditorFormShared';
import './gantt-task-editor-form.css';

const { isDark } = currentThemeVue();

const plugins = ref([GanttPlugin]);
const columns = ref(TASK_EDITOR_COLUMNS);
const tasks = ref(createInitialTaskSource());
const ganttConfig = ref(TASK_EDITOR_GANTT_CONFIG);
const calendars = ref([STANDARD_CALENDAR]);
const dependencies = ref(SHOWCASE_DEPENDENCIES);
const resources = ref(SHOWCASE_RESOURCES);
const assignments = ref(SHOWCASE_ASSIGNMENTS);
const taskEditorDialog = ref(TASK_EDITOR_DIALOG_CONFIG);

const gridRef = ref<InstanceType<typeof RevoGrid> | HTMLRevoGridElement | null>(null);
const toolbarRef = ref<HTMLElement | null>(null);
let toolbarMounted = false;
let toolbarFrame = 0;

function getGridEl(): HTMLRevoGridElement | null {
  const refValue = gridRef.value as (InstanceType<typeof RevoGrid> & { $el?: HTMLRevoGridElement }) | HTMLRevoGridElement | null;
  const candidate = (refValue && '$el' in refValue ? refValue.$el : refValue) ?? null;
  return candidate instanceof HTMLElement && candidate.tagName.toLowerCase() === 'revo-grid'
    ? candidate as HTMLRevoGridElement
    : null;
}

function mountToolbar() {
  if (toolbarMounted) {
    return;
  }

  const grid = getGridEl();
  if (!toolbarRef.value || !grid) {
    toolbarFrame = requestAnimationFrame(mountToolbar);
    return;
  }

  toolbarMounted = true;
  defineGanttToolbar(toolbarRef.value, {
    grid,
    columns: SHOWCASE_TOOLBAR_COLUMNS,
    controls: {
      export: false,
      baseline: false,
    },
  });
}

onMounted(async () => {
  await nextTick();
  mountToolbar();
});

onBeforeUnmount(() => {
  if (toolbarFrame) {
    cancelAnimationFrame(toolbarFrame);
  }
  if (toolbarRef.value) {
    toolbarRef.value.textContent = '';
  }
});
</script>
Angular ts
import { AfterViewInit, Component, ElementRef, NO_ERRORS_SCHEMA, ViewChild, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { defineGanttToolbar, GanttPlugin } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';
import {
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_RESOURCES,
  SHOWCASE_TOOLBAR_COLUMNS,
  STANDARD_CALENDAR,
} from './gantt-project-data';
import {
  createInitialTaskSource,
  TASK_EDITOR_COLUMNS,
  TASK_EDITOR_DIALOG_CONFIG,
  TASK_EDITOR_GANTT_CONFIG,
} from './GanttTaskEditorFormShared';

@Component({
  selector: 'gantt-task-editor-form-grid',
  standalone: true,
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
  imports: [RevoGrid],
  template: `
    <section class="gantt-task-editor-demo">
      <div #toolbar class="gantt-task-editor-toolbar"></div>
      <revo-grid
        #grid
        class="gantt-task-editor-grid"
        [theme]="theme"
        [hideAttribution]="true"
        [plugins]="plugins"
        [source]="tasks"
        [columns]="columns"
        [gantt]="ganttConfig"
        [ganttCalendars]="calendars"
        [ganttDependencies]="dependencies"
        [ganttResources]="resources"
        [ganttAssignments]="assignments"
        [ganttTaskEditorDialog]="taskEditorDialog"
      ></revo-grid>
    </section>
  `,
  styleUrls: ['./gantt-task-editor-form.css'],
  encapsulation: ViewEncapsulation.None,
})
export class GanttTaskEditorFormGridComponent implements AfterViewInit {
  @ViewChild('grid', { read: ElementRef }) gridRef!: ElementRef<HTMLRevoGridElement>;
  @ViewChild('toolbar', { read: ElementRef }) toolbarRef!: ElementRef<HTMLElement>;

  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  plugins = [GanttPlugin];
  columns = TASK_EDITOR_COLUMNS;
  tasks = createInitialTaskSource();
  ganttConfig = TASK_EDITOR_GANTT_CONFIG;
  calendars = [STANDARD_CALENDAR];
  dependencies = SHOWCASE_DEPENDENCIES;
  resources = SHOWCASE_RESOURCES;
  assignments = SHOWCASE_ASSIGNMENTS;
  taskEditorDialog = TASK_EDITOR_DIALOG_CONFIG;

  ngAfterViewInit(): void {
    defineGanttToolbar(this.toolbarRef.nativeElement, {
      grid: this.gridRef.nativeElement,
      columns: SHOWCASE_TOOLBAR_COLUMNS,
      controls: {
        export: false,
        baseline: false,
      },
    });
  }
}
Project Data ts
/**
 * Shared project data for all Gantt documentation examples.
 * Uses the "Website Redesign" project — simple enough to follow,
 * rich enough to show hierarchy, milestones, dependencies, and baselines.
 */
import type {
  ColumnRegular,
} from '@revolist/revogrid';
import type {
  TaskEntity,
  DependencyEntity,
  CalendarEntity,
  ResourceEntity,
  AssignmentEntity,
  BaselineSnapshot,
  GanttPluginConfig,
} from '@revolist/revogrid-enterprise';
import { createDefaultTaskTableColumn } from '@revolist/revogrid-enterprise';

// ─── IDs ─────────────────────────────────────────────────────────────────────
export const PROJECT_ID = 'project-web-redesign';
export const CALENDAR_ID = 'cal-standard';
export const CALENDAR_US_ID = 'cal-us';

// ─── Base config (override per example) ──────────────────────────────────────
export const GANTT_BASE_CONFIG = {
  id: PROJECT_ID,
  name: 'Website Redesign',
  version: '1',
  currency: 'USD',
  timeZone: 'UTC',
  primaryCalendarId: CALENDAR_ID,
  updatedAt: '2026-04-06T00:00:00Z',
  zoomPreset: 'week' as const,
};

// ─── Calendars ────────────────────────────────────────────────────────────────
export const STANDARD_CALENDAR: CalendarEntity = {
  id: CALENDAR_ID,
  name: 'Standard',
  timeZone: 'UTC',
  workingDays: [1, 2, 3, 4, 5],
  holidays: [],
  hoursPerDay: 8,
};

export const US_CALENDAR: CalendarEntity = {
  id: CALENDAR_US_ID,
  name: 'US Standard',
  timeZone: 'America/New_York',
  workingDays: [1, 2, 3, 4, 5],
  holidays: ['2026-05-25', '2026-07-04'],
  hoursPerDay: 8,
};

// ─── Tasks ────────────────────────────────────────────────────────────────────
export const TASKS: TaskEntity[] = [
  {
    id: 't1', projectId: PROJECT_ID, parentId: null,
    wbsCode: '1', name: 'Design', type: 'summary', status: 'in-progress',
    startDate: '2026-04-06', endDate: '2026-04-24', durationDays: 15,
    progressPercent: 60, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't2', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.1', name: 'Wireframes', type: 'task', status: 'done',
    startDate: '2026-04-06', endDate: '2026-04-10', durationDays: 5,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't3', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.2', name: 'Design Review', type: 'milestone', status: 'done',
    startDate: '2026-04-10', endDate: '2026-04-10', durationDays: 0,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
  },
  {
    id: 't4', projectId: PROJECT_ID, parentId: 't1',
    wbsCode: '1.3', name: 'Visual Design', type: 'task', status: 'in-progress',
    startDate: '2026-04-13', endDate: '2026-04-24', durationDays: 10,
    progressPercent: 40, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't5', projectId: PROJECT_ID, parentId: null,
    wbsCode: '2', name: 'Development', type: 'summary', status: 'not-started',
    startDate: '2026-04-27', endDate: '2026-05-20', durationDays: 18,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: false, tags: [],
  },
  {
    id: 't6', projectId: PROJECT_ID, parentId: 't5',
    wbsCode: '2.1', name: 'Frontend', type: 'task', status: 'not-started',
    startDate: '2026-04-27', endDate: '2026-05-13', durationDays: 13,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't7', projectId: PROJECT_ID, parentId: 't5',
    wbsCode: '2.2', name: 'Backend', type: 'task', status: 'not-started',
    startDate: '2026-04-27', endDate: '2026-05-20', durationDays: 18,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
  },
  {
    id: 't8', projectId: PROJECT_ID, parentId: null,
    wbsCode: '3', name: 'Launch', type: 'milestone', status: 'not-started',
    startDate: '2026-05-20', endDate: '2026-05-20', durationDays: 0,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: ['milestone'],
  },
];

/** Tasks with Visual Design slipped +3 days — used in the baseline example. */
export const TASKS_SLIPPED: TaskEntity[] = TASKS.map((task) => {
  if (task.id === 't4') {
    return { ...task, endDate: '2026-04-27', durationDays: 13, progressPercent: 30 };
  }
  if (task.id === 't1') {
    return { ...task, endDate: '2026-04-27', durationDays: 18, progressPercent: 50 };
  }
  if (task.id === 't5') {
    return { ...task, startDate: '2026-04-28', endDate: '2026-05-21' };
  }
  if (task.id === 't6') {
    return { ...task, startDate: '2026-04-28', endDate: '2026-05-14' };
  }
  if (task.id === 't7') {
    return { ...task, startDate: '2026-04-28', endDate: '2026-05-21' };
  }
  if (task.id === 't8') {
    return { ...task, startDate: '2026-05-21', endDate: '2026-05-21' };
  }
  return task;
});

/** Tasks with scheduling constraints — used in the scheduling example. */
export const TASKS_SCHEDULED: TaskEntity[] = TASKS.map((task) => {
  if (task.id === 't6') {
    return {
      ...task, startDate: '2026-05-04', endDate: '2026-05-20',
      constraintType: 'start-no-earlier-than', constraintDate: '2026-05-04',
    };
  }
  if (task.id === 't7') {
    return { ...task, durationDays: 24, endDate: '2026-05-28', deadlineDate: '2026-05-22' };
  }
  if (task.id === 't8') {
    return { ...task, startDate: '2026-05-28', endDate: '2026-05-28' };
  }
  return task;
});

// ─── Dependencies ─────────────────────────────────────────────────────────────
export const DEPENDENCIES: DependencyEntity[] = [
  { id: 'd1', predecessorTaskId: 't2', successorTaskId: 't3', type: 'finish-to-start', lagDays: 0 },
  { id: 'd2', predecessorTaskId: 't3', successorTaskId: 't4', type: 'finish-to-start', lagDays: 1 },
  { id: 'd3', predecessorTaskId: 't4', successorTaskId: 't6', type: 'finish-to-start', lagDays: 1 },
  { id: 'd4', predecessorTaskId: 't4', successorTaskId: 't7', type: 'finish-to-start', lagDays: 1 },
  { id: 'd5', predecessorTaskId: 't6', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
  { id: 'd6', predecessorTaskId: 't7', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
];

// ─── Resources & Assignments ──────────────────────────────────────────────────
export const RESOURCES: ResourceEntity[] = [
  { id: 'r1', name: 'Alice Chen', role: 'Designer', calendarId: CALENDAR_ID, allocationCapacity: 1, hourlyCost: 95 },
  { id: 'r2', name: 'Bob Kim', role: 'Frontend Dev', calendarId: CALENDAR_ID, allocationCapacity: 1, hourlyCost: 110 },
  { id: 'r3', name: 'Carla Díaz', role: 'Backend Dev', calendarId: CALENDAR_ID, allocationCapacity: 1, hourlyCost: 105 },
];

export const ASSIGNMENTS: AssignmentEntity[] = [
  { id: 'a1', taskId: 't2', resourceId: 'r1', allocationUnits: 1, responsibility: 'assigned' },
  { id: 'a2', taskId: 't4', resourceId: 'r1', allocationUnits: 1, responsibility: 'assigned' },
  { id: 'a3', taskId: 't6', resourceId: 'r2', allocationUnits: 1, responsibility: 'assigned' },
  { id: 'a4', taskId: 't7', resourceId: 'r3', allocationUnits: 1, responsibility: 'assigned' },
];

// ─── Baseline ─────────────────────────────────────────────────────────────────
export const BASELINES: BaselineSnapshot[] = [
  {
    id: 'baseline-approved',
    name: 'Approved Plan',
    capturedAt: '2026-04-01T08:00:00Z',
    tasks: [
      { taskId: 't1', startDate: '2026-04-06', endDate: '2026-04-24', durationDays: 15, progressPercent: 0 },
      { taskId: 't2', startDate: '2026-04-06', endDate: '2026-04-10', durationDays: 5,  progressPercent: 0 },
      { taskId: 't3', startDate: '2026-04-10', endDate: '2026-04-10', durationDays: 0,  progressPercent: 0 },
      { taskId: 't4', startDate: '2026-04-13', endDate: '2026-04-24', durationDays: 10, progressPercent: 0 },
      { taskId: 't5', startDate: '2026-04-27', endDate: '2026-05-20', durationDays: 18, progressPercent: 0 },
      { taskId: 't6', startDate: '2026-04-27', endDate: '2026-05-13', durationDays: 13, progressPercent: 0 },
      { taskId: 't7', startDate: '2026-04-27', endDate: '2026-05-20', durationDays: 18, progressPercent: 0 },
      { taskId: 't8', startDate: '2026-05-20', endDate: '2026-05-20', durationDays: 0,  progressPercent: 0 },
    ],
  },
];

// ─── Default columns (basic examples) ────────────────────────────────────────
export const DEFAULT_COLUMNS = [
  createDefaultTaskTableColumn('wbs'),
  createDefaultTaskTableColumn('name'),
  createDefaultTaskTableColumn('assignees'),
  createDefaultTaskTableColumn('cost' as any),
  createDefaultTaskTableColumn('startDate'),
  createDefaultTaskTableColumn('endDate'),
  createDefaultTaskTableColumn('duration'),
  createDefaultTaskTableColumn('percentDone'),
].filter(Boolean);

// ─── Showcase column options (all 10, with visibility flags) ─────────────────
export const SHOWCASE_COLUMN_OPTIONS = [
  { prop: 'wbs',          label: 'WBS',          defaultVisible: true  },
  { prop: 'name',         label: 'Name',         defaultVisible: true  },
  { prop: 'assignees',    label: 'Assignees',    defaultVisible: true  },
  { prop: 'cost',         label: 'Cost',         defaultVisible: true  },
  { prop: 'startDate',    label: 'Start Date',   defaultVisible: true  },
  { prop: 'endDate',      label: 'End Date',     defaultVisible: false },
  { prop: 'duration',     label: 'Duration',     defaultVisible: true  },
  { prop: 'percentDone',  label: '% Done',       defaultVisible: false },
  { prop: 'predecessors', label: 'Predecessors', defaultVisible: false },
  { prop: 'successors',   label: 'Successors',   defaultVisible: false },
  { prop: 'status',       label: 'Status',       defaultVisible: true  },
] as const;

export const SHOWCASE_TOOLBAR_COLUMNS = [
  { prop: 'name', label: 'Task', visible: true },
  { prop: 'assignees', label: 'Assignees', visible: true },
  { prop: 'cost', label: 'Cost', visible: true },
  { prop: 'startDate', label: 'Start', visible: true },
  { prop: 'duration', label: 'Dur', visible: true },
];

export type ShowcaseColumnProp = (typeof SHOWCASE_COLUMN_OPTIONS)[number]['prop'];

/** All 10 showcase columns as RevoGrid column definitions. */
export const SHOWCASE_COLUMNS = SHOWCASE_COLUMN_OPTIONS
  .map((c) => createDefaultTaskTableColumn(c.prop as any))
  .filter((c): c is NonNullable<typeof c> => Boolean(c));

/** Props that are hidden by default in the showcase. */
export const SHOWCASE_DEFAULT_HIDDEN: string[] = SHOWCASE_COLUMN_OPTIONS
  .filter((c) => !c.defaultVisible)
  .map((c) => c.prop as string);

type ShowcaseTaskEntity = TaskEntity & {
  estimatedCost?: number;
};

export const SHOWCASE_PROJECT_ID = 'project-launch-saas';

export const SHOWCASE_GANTT_CONFIG = {
  ...GANTT_BASE_CONFIG,
  id: SHOWCASE_PROJECT_ID,
  name: 'Launch SaaS Product',
  updatedAt: '2026-04-28T10:00:00Z',
  zoomPreset: 'day-week' as const,
  allowTaskCreate: true,
  visuals: {
    showBaseline: true,
    showCriticalPath: true,
    showTaskLabels: true,
    shadeNonWorkingTime: true,
    showTodayLine: true,
    milestoneLines: [
      { id: 'public-launch', date: '2026-05-23', label: 'Launch', color: '#facc15' },
    ],
  },
  scheduling: {
    excludeHolidaysFromDuration: true,
  },
} satisfies GanttPluginConfig;

export const SHOWCASE_TASKS: ShowcaseTaskEntity[] = [
  {
    id: 'launch', projectId: SHOWCASE_PROJECT_ID, parentId: null,
    wbsCode: '', name: 'Launch SaaS Product', type: 'summary', status: 'blocked',
    startDate: '2026-04-06', endDate: '2026-05-23', durationDays: 47,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: [],
    estimatedCost: 248400,
  },
  {
    id: 'backend', projectId: SHOWCASE_PROJECT_ID, parentId: 'launch',
    wbsCode: '1', name: 'Backend Setup', type: 'summary', status: 'blocked',
    startDate: '2026-04-06', endDate: '2026-04-18', durationDays: 12,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: true, tags: ['Backend'],
    estimatedCost: 32400,
  },
  {
    id: 'design', projectId: SHOWCASE_PROJECT_ID, parentId: 'launch',
    wbsCode: '2', name: 'Design', type: 'summary', status: 'blocked',
    startDate: '2026-04-06', endDate: '2026-04-24', durationDays: 18,
    progressPercent: 97, calendarId: CALENDAR_ID, isCritical: true, tags: ['Design'],
    estimatedCost: 14800,
  },
  {
    id: 'frontend', projectId: SHOWCASE_PROJECT_ID, parentId: 'launch',
    wbsCode: '3', name: 'Frontend Development', type: 'summary', status: 'blocked',
    startDate: '2026-04-14', endDate: '2026-05-10', durationDays: 24,
    progressPercent: 50, calendarId: CALENDAR_ID, isCritical: true, tags: ['Frontend'],
    estimatedCost: 38600,
  },
  {
    id: 'devops', projectId: SHOWCASE_PROJECT_ID, parentId: 'launch',
    wbsCode: '4', name: 'DevOps', type: 'summary', status: 'blocked',
    startDate: '2026-04-10', endDate: '2026-05-12', durationDays: 32,
    progressPercent: 85, calendarId: CALENDAR_ID, isCritical: true, tags: ['DevOps'],
    estimatedCost: 21800,
  },
  {
    id: 'iac', projectId: SHOWCASE_PROJECT_ID, parentId: 'devops',
    wbsCode: '4.1', name: 'Infrastructure as Code', type: 'task', status: 'done',
    startDate: '2026-04-10', endDate: '2026-04-18', durationDays: 8,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: false, tags: ['DevOps'],
    estimatedCost: 7800,
  },
  {
    id: 'ci-cd', projectId: SHOWCASE_PROJECT_ID, parentId: 'devops',
    wbsCode: '4.2', name: 'CI / CD Pipeline', type: 'task', status: 'done',
    startDate: '2026-04-16', endDate: '2026-04-24', durationDays: 8,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: false, tags: ['DevOps'],
    estimatedCost: 5200,
  },
  {
    id: 'monitoring', projectId: SHOWCASE_PROJECT_ID, parentId: 'devops',
    wbsCode: '4.3', name: 'Monitoring & Logging', type: 'task', status: 'blocked',
    startDate: '2026-04-22', endDate: '2026-04-30', durationDays: 8,
    progressPercent: 55, calendarId: CALENDAR_ID, isCritical: false, tags: ['DevOps'],
    estimatedCost: 4400,
  },
  {
    id: 'prod-deploy', projectId: SHOWCASE_PROJECT_ID, parentId: 'launch',
    wbsCode: '5', name: 'Production Deploy', type: 'milestone', status: 'not-started',
    startDate: '2026-05-12', endDate: '2026-05-12', durationDays: 0,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: ['Milestone'],
  },
  {
    id: 'qa', projectId: SHOWCASE_PROJECT_ID, parentId: 'launch',
    wbsCode: '6', name: 'QA', type: 'summary', status: 'blocked',
    startDate: '2026-04-20', endDate: '2026-05-18', durationDays: 28,
    progressPercent: 32, calendarId: CALENDAR_ID, isCritical: true, tags: ['QA'],
    estimatedCost: 12200,
  },
  {
    id: 'test-plan', projectId: SHOWCASE_PROJECT_ID, parentId: 'qa',
    wbsCode: '6.1', name: 'Test Plan', type: 'task', status: 'done',
    startDate: '2026-04-20', endDate: '2026-04-24', durationDays: 4,
    progressPercent: 100, calendarId: CALENDAR_ID, isCritical: false, tags: ['QA'],
    estimatedCost: 2800,
  },
  {
    id: 'unit-tests', projectId: SHOWCASE_PROJECT_ID, parentId: 'qa',
    wbsCode: '6.2', name: 'Unit Tests', type: 'task', status: 'in-progress',
    startDate: '2026-04-24', endDate: '2026-05-04', durationDays: 10,
    progressPercent: 62, calendarId: CALENDAR_ID, isCritical: false, tags: ['QA'],
    estimatedCost: 4200,
  },
  {
    id: 'integration-tests', projectId: SHOWCASE_PROJECT_ID, parentId: 'qa',
    wbsCode: '6.3', name: 'Integration Tests', type: 'task', status: 'not-started',
    startDate: '2026-05-02', endDate: '2026-05-12', durationDays: 10,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: false, tags: ['QA'],
    estimatedCost: 3400,
  },
  {
    id: 'uat', projectId: SHOWCASE_PROJECT_ID, parentId: 'qa',
    wbsCode: '6.4', name: 'User Acceptance Test', type: 'task', status: 'not-started',
    startDate: '2026-05-10', endDate: '2026-05-18', durationDays: 8,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: false, tags: ['QA'],
    estimatedCost: 1800,
  },
  {
    id: 'security', projectId: SHOWCASE_PROJECT_ID, parentId: 'launch',
    wbsCode: '7', name: 'Security', type: 'summary', status: 'not-started',
    startDate: '2026-05-06', endDate: '2026-05-21', durationDays: 15,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: ['Security'],
    estimatedCost: 7200,
  },
  {
    id: 'pentest', projectId: SHOWCASE_PROJECT_ID, parentId: 'security',
    wbsCode: '7.1', name: 'Penetration Testing', type: 'task', status: 'not-started',
    startDate: '2026-05-06', endDate: '2026-05-14', durationDays: 8,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: false, tags: ['Security'],
    estimatedCost: 4800,
  },
  {
    id: 'audit', projectId: SHOWCASE_PROJECT_ID, parentId: 'security',
    wbsCode: '7.2', name: 'Audit & Compliance', type: 'task', status: 'not-started',
    startDate: '2026-05-12', endDate: '2026-05-21', durationDays: 9,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: false, tags: ['Security'],
    estimatedCost: 2400,
  },
  {
    id: 'public-launch', projectId: SHOWCASE_PROJECT_ID, parentId: 'launch',
    wbsCode: '8', name: 'Public Launch', type: 'milestone', status: 'not-started',
    startDate: '2026-05-23', endDate: '2026-05-23', durationDays: 0,
    progressPercent: 0, calendarId: CALENDAR_ID, isCritical: true, tags: ['Milestone'],
  },
];

export const SHOWCASE_DEPENDENCIES: DependencyEntity[] = [
  { id: 'sd-1', predecessorTaskId: 'iac', successorTaskId: 'ci-cd', type: 'finish-to-start', lagDays: 0 },
  { id: 'sd-2', predecessorTaskId: 'ci-cd', successorTaskId: 'monitoring', type: 'finish-to-start', lagDays: 0 },
  { id: 'sd-3', predecessorTaskId: 'test-plan', successorTaskId: 'unit-tests', type: 'finish-to-start', lagDays: 0 },
  { id: 'sd-4', predecessorTaskId: 'unit-tests', successorTaskId: 'integration-tests', type: 'finish-to-start', lagDays: 0 },
  { id: 'sd-5', predecessorTaskId: 'integration-tests', successorTaskId: 'uat', type: 'finish-to-start', lagDays: 0 },
  { id: 'sd-6', predecessorTaskId: 'audit', successorTaskId: 'public-launch', type: 'finish-to-start', lagDays: 0 },
];

export const SHOWCASE_RESOURCES: ResourceEntity[] = [
  { id: 'nk', name: 'Nina Kim', role: 'Platform Engineer', calendarId: CALENDAR_ID, allocationCapacity: 1, hourlyCost: 125 },
  { id: 'rp', name: 'Ravi Patel', role: 'QA Lead', calendarId: CALENDAR_ID, allocationCapacity: 1, hourlyCost: 100 },
  { id: 'jd', name: 'Jordan Diaz', role: 'Automation Engineer', calendarId: CALENDAR_ID, allocationCapacity: 1, hourlyCost: 95 },
  { id: 'sr', name: 'Sam Rivera', role: 'Product Owner', calendarId: CALENDAR_ID, allocationCapacity: 1, hourlyCost: 90 },
  { id: 'vk', name: 'Vera Khan', role: 'Security Engineer', calendarId: CALENDAR_ID, allocationCapacity: 1, hourlyCost: 115 },
];

export const SHOWCASE_ASSIGNMENTS: AssignmentEntity[] = [
  { id: 'sa-1', taskId: 'iac', resourceId: 'nk', allocationUnits: 1, responsibility: 'Owner' },
  { id: 'sa-2', taskId: 'ci-cd', resourceId: 'nk', allocationUnits: 1, responsibility: 'Owner' },
  { id: 'sa-3', taskId: 'monitoring', resourceId: 'nk', allocationUnits: 1, responsibility: 'Owner' },
  { id: 'sa-4', taskId: 'test-plan', resourceId: 'rp', allocationUnits: 1, responsibility: 'Owner' },
  { id: 'sa-5', taskId: 'unit-tests', resourceId: 'rp', allocationUnits: 1, responsibility: 'Lead' },
  { id: 'sa-6', taskId: 'unit-tests', resourceId: 'jd', allocationUnits: 1, responsibility: 'Automation' },
  { id: 'sa-7', taskId: 'integration-tests', resourceId: 'rp', allocationUnits: 1, responsibility: 'Owner' },
  { id: 'sa-8', taskId: 'uat', resourceId: 'rp', allocationUnits: 1, responsibility: 'Lead' },
  { id: 'sa-9', taskId: 'uat', resourceId: 'sr', allocationUnits: 1, responsibility: 'Signoff' },
  { id: 'sa-10', taskId: 'pentest', resourceId: 'vk', allocationUnits: 1, responsibility: 'Owner' },
  { id: 'sa-11', taskId: 'audit', resourceId: 'vk', allocationUnits: 1, responsibility: 'Lead' },
  { id: 'sa-12', taskId: 'audit', resourceId: 'jd', allocationUnits: 1, responsibility: 'Compliance' },
];

export const SHOWCASE_BASELINES: BaselineSnapshot[] = [
  {
    id: 'showcase-approved',
    name: 'Approved launch plan',
    capturedAt: '2026-04-01T08:00:00Z',
    tasks: SHOWCASE_TASKS.map((task) => ({
      taskId: task.id,
      startDate: task.startDate,
      endDate: task.endDate,
      durationDays: task.durationDays,
      progressPercent: 0,
    })),
  },
];

const SHOWCASE_STATUS_COLOR: Record<string, string> = {
  'not-started': '#525866',
  'in-progress': '#f59e0b',
  done: '#39d07d',
  blocked: '#ff4d4f',
};

const SHOWCASE_TAG_COLORS: Record<string, { color: string; bg: string; border: string }> = {
  Backend: { color: '#ff66b3', bg: 'rgba(255, 102, 179, 0.14)', border: 'rgba(255, 102, 179, 0.35)' },
  Design: { color: '#a78bfa', bg: 'rgba(167, 139, 250, 0.14)', border: 'rgba(167, 139, 250, 0.36)' },
  Frontend: { color: '#34d399', bg: 'rgba(52, 211, 153, 0.14)', border: 'rgba(52, 211, 153, 0.36)' },
  DevOps: { color: '#fb923c', bg: 'rgba(251, 146, 60, 0.14)', border: 'rgba(251, 146, 60, 0.36)' },
  QA: { color: '#60a5fa', bg: 'rgba(96, 165, 250, 0.14)', border: 'rgba(96, 165, 250, 0.36)' },
  Security: { color: '#facc15', bg: 'rgba(250, 204, 21, 0.13)', border: 'rgba(250, 204, 21, 0.36)' },
};

function renderShowcaseNameCell(h: Parameters<Required<ColumnRegular>['cellTemplate']>[0], model: any) {
  const tag = model.tags?.[0];
  const tagStyle = tag && SHOWCASE_TAG_COLORS[tag]
    ? {
        '--showcase-tag-color': SHOWCASE_TAG_COLORS[tag].color,
        '--showcase-tag-bg': SHOWCASE_TAG_COLORS[tag].bg,
        '--showcase-tag-border': SHOWCASE_TAG_COLORS[tag].border,
      }
    : {};

  return h('div', { class: { 'gantt-showcase-task-cell': true } }, [
    h('span', {
      class: {
        'gantt-showcase-status-dot': true,
        'gantt-showcase-status-dot--milestone': model.taskKind === 'milestone',
      },
      style: { '--showcase-status-color': SHOWCASE_STATUS_COLOR[model.statusKey ?? 'not-started'] ?? SHOWCASE_STATUS_COLOR['not-started'] },
    }),
    h('span', { class: { 'gantt-showcase-task-name': true }, title: model.name }, model.name),
    tag && SHOWCASE_TAG_COLORS[tag]
      ? h('span', { class: { 'gantt-showcase-tag': true }, style: tagStyle }, tag)
      : null,
  ]);
}

function showcaseCellProperties({ model }: any) {
  return {
    class: {
      'gantt-showcase-cell--root': model.id === 'launch',
      'gantt-showcase-cell--summary': model.taskKind === 'summary',
    },
  };
}

export const SHOWCASE_COLUMNS_POLISHED: ColumnRegular[] = [
  {
    ...createDefaultTaskTableColumn('name'),
    name: 'Task',
    size: 292,
    cellTemplate: (h, { model }) => renderShowcaseNameCell(h, model),
    cellProperties: showcaseCellProperties,
  },
  {
    ...createDefaultTaskTableColumn('assignees'),
    size: 102,
    cellProperties: showcaseCellProperties,
  },
  {
    ...createDefaultTaskTableColumn('cost' as any),
    size: 114,
    cellProperties: showcaseCellProperties,
  },
  {
    ...createDefaultTaskTableColumn('startDate'),
    name: 'Start',
    size: 116,
    cellProperties: showcaseCellProperties,
  },
  {
    ...createDefaultTaskTableColumn('duration'),
    name: 'Dur',
    size: 90,
    cellProperties: showcaseCellProperties,
  },
];

type ShowcaseTaskColor = { barColor: string; progressColor: string; textColor?: string };

export const GANTT_SHOWCASE_DEFAULT_TASK_COLOR: ShowcaseTaskColor = {
  barColor: '#64748b',
  progressColor: '#94a3b8',
  textColor: '#ffffff',
};

export const GANTT_SHOWCASE_TASK_COLORS: Record<string, ShowcaseTaskColor> = {
  launch: { barColor: '#a5a7b0', progressColor: '#8f929d', textColor: '#f8fafc' },
  backend: { barColor: '#ff77d2', progressColor: '#f43f9e' },
  design: { barColor: '#d9b4ff', progressColor: '#a78bfa', textColor: '#25143f' },
  frontend: { barColor: '#5eead4', progressColor: '#34d399', textColor: '#092f25' },
  devops: { barColor: '#ffd45a', progressColor: '#fb923c', textColor: '#2d1f02' },
  iac: { barColor: '#b66b2e', progressColor: '#f97316' },
  'ci-cd': { barColor: '#b66b2e', progressColor: '#f97316' },
  monitoring: { barColor: '#5a2c12', progressColor: '#f97316' },
  'prod-deploy': { barColor: '#facc15', progressColor: '#facc15', textColor: '#18181b' },
  qa: { barColor: '#60a5fa', progressColor: '#7dd3fc', textColor: '#061633' },
  'test-plan': { barColor: '#4d76b2', progressColor: '#7dd3fc' },
  'unit-tests': { barColor: '#1e3558', progressColor: '#4f7dbd' },
  'integration-tests': { barColor: '#102441', progressColor: '#60a5fa' },
  uat: { barColor: '#102441', progressColor: '#60a5fa' },
  security: { barColor: '#facc15', progressColor: '#eab308', textColor: '#18181b' },
  pentest: { barColor: '#2f2610', progressColor: '#facc15' },
  audit: { barColor: '#2f2610', progressColor: '#facc15' },
  'public-launch': { barColor: '#facc15', progressColor: '#facc15', textColor: '#18181b' },
};

export function getShowcaseTaskBarColor({ row }: any) {
  const assigneeCount = row.assigneeDetails?.length ?? 0;

  return {
    ...GANTT_SHOWCASE_DEFAULT_TASK_COLOR,
    ...GANTT_SHOWCASE_TASK_COLORS[row.id],
    ...(assigneeCount && row.taskKind !== 'summary' ? { className: 'gantt-bar--with-assignee' } : {}),
  };
}

export function renderShowcaseTaskBarContent({ h, row, defaultContent }: any) {
  if (!row.assigneeDetails?.length || row.taskKind === 'summary') {
    return defaultContent;
  }

  return [
    ...defaultContent,
    h('span', { class: { 'gantt-bar__assignee-badge': true } }, row.assigneeDetails[0].initials),
  ];
}

// ─── Typed config builders ────────────────────────────────────────────────────
export function makeGanttConfig(overrides: Partial<GanttPluginConfig> = {}): GanttPluginConfig {
  return { ...GANTT_BASE_CONFIG, ...overrides } as GanttPluginConfig;
}

The task editor demo opens the editor from the Gantt row context menu and includes the packaged Gantt toolbar:

  • The grid renders normal Gantt tasks, dependencies, calendars, resources, and assignments.
  • Right-click a task row and choose Edit task….
  • Use either row context-menu Add task or toolbar Task to create a task and open the same editor for the new row.
  • The auto-installed task editor dialog adds the row-menu edit item and owns the native HTML <dialog>.
  • New tasks open in the editor by default after the Gantt plugin emits its post-create lifecycle event. Set ganttTaskEditorDialog.openOnCreate to false when task creation should stay silent.
  • The dialog renders a tabbed task information UI from TASK_EDITOR_FIELD_SCHEMA.
  • Details contains core schedule fields, constraints, notes, and manual scheduling.
  • Advanced contains task type, effort-driven scheduling, work, remaining duration, actual dates, inactive state, leveling priority, can-level, and leveling delay.
  • Predecessors and Successors edit dependency target, dependency type, and lead/lag days.
  • Assignments renders the resource assignment picker when ganttResources and ganttAssignments are available.
  • The patch preview calls normalizeTaskEditorSubmit(task, values) on every edit.
  • The submit button applies the returned patch through the Gantt runtime/provider edit path, updates grid.ganttAssignments and grid.ganttDependencies, and refreshes the project.
grid.plugins = [GanttPlugin];
grid.ganttTaskEditorDialog = {
title: 'Task details',
description: 'Update task fields and save the changes to the project schedule.',
applyLabel: 'Save changes',
localeText: {
tabs: {
assignments: 'Team',
},
fields: {
effortMode: 'Task type',
},
resourcesSearch: {
placeholder: 'Find a resource',
},
},
};
const values = createTaskEditorFormValues(task, resources, assignments);
const result = normalizeTaskEditorSubmit(task, values);
if (result.ok) {
updateTask(task.id, result.patch);
}

The schema includes editable task fields such as name, startDate, endDate, durationDays, progressPercent, constraints, deadlines, actual dates, work, effort mode, effort-driven scheduling, inactive state, leveling controls, manual scheduling, and notes. It also includes display fields such as resourceLabels.

Date fields open as date-only controls by default. Use the field-level Time toggle in the packaged dialog when you need minute-level scheduling; the dialog stores the submitted value as a UTC ISO datetime.

Set gantt.dateFormats.editor when the packaged dialog should show custom date text instead of native date inputs. In that mode, the dialog uses a text input, calls your parser as the user types, and still submits ISO dates to normalizeTaskEditorSubmit() and the Gantt mutation services:

grid.gantt = {
...project,
dateFormats: {
locale: 'en-GB',
timeZone: 'UTC',
table: { day: '2-digit', month: '2-digit', year: 'numeric' },
tooltip: { day: '2-digit', month: 'short', year: 'numeric' },
editor: {
options: { day: '2-digit', month: '2-digit', year: 'numeric' },
parser(value) {
const match = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value.trim());
return match ? `${match[3]}-${match[2]}-${match[1]}` : null;
},
},
},
};

Custom date formats are presentation and input helpers only. Keep persisted tasks as YYYY-MM-DD dates or UTC datetimes ending in Z.

resourceLabels is read-only in the schema helper because TaskUpdate does not own assignment rows. The packaged dialog upgrades that field into a resource assignment picker and writes the selected resources to grid.ganttAssignments. Set ganttTaskEditorDialog.resources to false when you want the field to stay read-only.

Entity panels include search controls: resource assignment searches by resource name and role, while predecessor and successor panels search task names. Resource lists, task dropdowns, dependency type dropdowns, and schema-backed select options are sorted A-Z by their displayed labels.

Use ganttTaskEditorDialog.localeText to translate or customize packaged dialog text. It covers tabs, field labels, field option labels, dependency type labels, empty states, search placeholders, switch labels, validation headings, save errors, and context-menu text. Existing top-level title, description, applyLabel, resetLabel, closeLabel, and menuItemName still work as direct overrides for those common labels.

Use ganttTaskEditorDialog.openOnCreate = false to keep row context-menu or toolbar task creation from opening the dialog. The Gantt row context menu still keeps the default Add task action when task creation is allowed, and the editor plugin prepends its Edit task… item while preserving custom rowContextMenu or contextMenu items and resolve behavior. Set gantt.contextMenu = false to opt out of generated Gantt menu items, or set ganttTaskEditorDialog.contextMenu when only the packaged editor menu item should change.

normalizeTaskEditorSubmit() validates values before returning a patch:

  • name cannot be empty.
  • Date values must be ISO dates like 2026-05-12 or UTC datetimes ending in Z.
  • endDate cannot be earlier than startDate.
  • Numeric fields must be non-negative.
  • progressPercent is clamped to 0..100.
  • Unsupported constraint values return field errors.

If both startDate and endDate are present, the helper recalculates durationDays from the date range. If values are unchanged, the default result omits them from the patch.

In production, keep the same flow but replace the demo’s local source update with your persistence and project-state update path:

async function submitTaskEditor(task, values) {
const result = normalizeTaskEditorSubmit(task, values);
if (!result.ok) {
renderFieldErrors(result.errors);
return;
}
await api.updateTask(task.id, result.patch);
grid.source = grid.source.map((row) =>
row.id === task.id ? { ...row, ...result.patch } : row,
);
}

The Gantt plugin still owns timeline projection and scheduling when the refreshed task source is applied.

The packaged dialog defaults to applying task field patches through the Gantt runtime/provider edit path and replacing the edited task’s assignment rows in grid.ganttAssignments. This keeps task editor saves visible to HistoryPlugin instead of bypassing grid edit tracking with direct grid.source replacement. To persist edits first, provide ganttTaskEditorDialog.onSubmit; return false from that callback when your application has already applied the task and assignment updates and the plugin should skip the default patch.

Dependency tabs also patch grid.ganttDependencies by replacing links that involve the edited task. Existing dependency ids are preserved, new rows get deterministic ids, and lag supports negative lead or positive lag days.

Set preview: true only for diagnostics or documentation when you want to show the generated TaskUpdate JSON.