Skip to content

Locked Tasks

Compare the gray locked task with its editable neighbors. Try editing or dragging the locked task, then enable Read only to protect the whole project.

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

import { defineGanttToolbar, GanttPlugin, type ProjectSnapshot } from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  SHOWCASE_ASSIGNMENTS, SHOWCASE_BASELINES, SHOWCASE_COLUMNS_WITH_ROW_SELECT, SHOWCASE_DEPENDENCIES,
  SHOWCASE_RESOURCES, SHOWCASE_TOOLBAR_COLUMNS, STANDARD_CALENDAR,
} from './shared/gantt-project-data';
import { createProductionGanttConfig, createProductionTasks, createProductionPersistence, productionError, productionSourceRows } from './GanttProductionPatternsShared';
import './gantt-production-patterns.css';

export function load(parentSelector: string) {
  const parent = document.querySelector(parentSelector);
  if (!parent) return () => {};
  const root = document.createElement('section');
  const controls = document.createElement('div');
  const status = document.createElement('p');
  const toolbar = document.createElement('div');
  const grid = document.createElement('revo-grid');
  const persistence = createProductionPersistence();
  let readOnly = false;
  let localized = false;
  let large = false;
  let project: ProjectSnapshot | null = null;
  const applyConfig = () => { grid.gantt = createProductionGanttConfig(readOnly, localized, project); };
  const applyProject = () => {
    applyConfig();
    grid.ganttDependencies = project?.dependencies ?? (large ? [] : SHOWCASE_DEPENDENCIES);
    grid.ganttCalendars = project?.calendars ?? [STANDARD_CALENDAR];
    grid.ganttResources = project?.resources ?? SHOWCASE_RESOURCES;
    grid.ganttAssignments = project?.assignments ?? (large ? [] : SHOWCASE_ASSIGNMENTS);
    grid.ganttBaselines = project?.baselines ?? (large ? [] : SHOWCASE_BASELINES);
    grid.source = project ? productionSourceRows(project) : createProductionTasks(large);
  };
  const toggle = (label: string, update: (value: boolean) => void) => {
    const element = document.createElement('label');
    const input = document.createElement('input');
    input.type = 'checkbox';
    input.addEventListener('change', () => update(input.checked));
    element.append(input, document.createTextNode(label));
    return { element, input };
  };
  const run = async (action: () => unknown | Promise<unknown>) => {
    try { await action(); } catch (error) { status.textContent = productionError(error); }
  };
  const button = (label: string, action: () => unknown | Promise<unknown>) => {
    const element = document.createElement('button');
    element.type = 'button';
    element.textContent = label;
    element.addEventListener('click', () => { void run(action); });
    return element;
  };
  root.className = 'gantt-production-demo';
  controls.className = 'gantt-production-controls';
  status.className = 'gantt-production-status';
  status.setAttribute('role', 'status');
  status.textContent = 'Save and restore use browser memory only; no server is contacted.';
  toolbar.className = 'gantt-production-toolbar';
  grid.className = 'gantt-production-grid';
  grid.theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  grid.hideAttribution = true;
  grid.plugins = [GanttPlugin, ExportExcelPlugin, RowSelectPlugin];
  grid.rowSelect = { rowOrder: true };
  grid.columns = [...SHOWCASE_COLUMNS_WITH_ROW_SELECT];
  const largeToggle = toggle('750 tasks', (value) => { large = value; project = null; applyProject(); });
  const restore = button('Restore saved project', async () => {
    project = await persistence.load();
    large = false;
    largeToggle.input.checked = false;
    applyProject();
    status.textContent = 'Restored the saved project, including tasks and project collections.';
  });
  restore.disabled = true;
  const importLabel = document.createElement('label');
  importLabel.className = 'gantt-production-file';
  importLabel.textContent = 'Import JSON';
  const fileInput = document.createElement('input');
  fileInput.type = 'file';
  fileInput.accept = '.json,application/json';
  fileInput.setAttribute('aria-label', 'Import JSON');
  fileInput.addEventListener('change', async () => {
    const file = fileInput.files?.[0];
    if (file) await run(async () => {
      project = await persistence.importFile(file);
      large = false;
      largeToggle.input.checked = false;
      applyProject();
      status.textContent = `Imported ${project.tasks.length} tasks from ${file.name}.`;
    });
    fileInput.value = '';
  });
  importLabel.append(fileInput);
  controls.append(
    toggle('Read only', (value) => { readOnly = value; applyConfig(); }).element,
    toggle('Español dates', (value) => { localized = value; applyConfig(); }).element,
    largeToggle.element,
    importLabel,
    button('Export JSON', () => persistence.exportFile(grid)),
    button('Save in memory', async () => {
      await persistence.save(grid);
      restore.disabled = false;
      status.textContent = 'Saved the current edited project in browser memory.';
    }),
    restore,
    button('Print / PDF', () => persistence.print(grid)),
  );
  root.append(controls, status, toolbar, grid);
  parent.appendChild(root);
  applyProject();
  defineGanttToolbar(toolbar, { grid, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true } });
  return () => { toolbar.textContent = ''; grid.remove(); root.remove(); };
}
Vuevue
<template>
  <section class="gantt-production-demo">
    <div class="gantt-production-controls">
      <label><input v-model="readOnly" type="checkbox" />Read only</label>
      <label><input v-model="localized" type="checkbox" />Español dates</label>
      <label><input v-model="large" type="checkbox" @change="project = null" />750 tasks</label>
      <label class="gantt-production-file">Import JSON<input type="file" accept=".json,application/json" aria-label="Import JSON" @change="importJson" /></label>
      <button type="button" @click="run(() => persistence.exportFile(getGrid()))">Export JSON</button>
      <button type="button" @click="save">Save in memory</button>
      <button type="button" :disabled="!hasSaved" @click="restore">Restore saved project</button>
      <button type="button" @click="run(() => persistence.print(getGrid()))">Print / PDF</button>
    </div>
    <p class="gantt-production-status" role="status">{{ status }}</p>
    <div ref="toolbarRef" class="gantt-production-toolbar"></div>
    <RevoGrid ref="gridRef" class="gantt-production-grid" hide-attribution :theme="isDark ? 'darkCompact' : 'compact'"
      :plugins="plugins" :row-select.prop="rowSelect" :source="tasks" :columns="columns" :gantt.prop="ganttConfig"
      :gantt-dependencies.prop="dependencies" :gantt-calendars.prop="calendars" :gantt-resources.prop="resources"
      :gantt-assignments.prop="assignments" :gantt-baselines.prop="baselines" />
  </section>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { defineGanttToolbar, GanttPlugin, type ProjectSnapshot } from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';
import { SHOWCASE_ASSIGNMENTS, SHOWCASE_BASELINES, SHOWCASE_COLUMNS_WITH_ROW_SELECT, SHOWCASE_DEPENDENCIES, SHOWCASE_RESOURCES, SHOWCASE_TOOLBAR_COLUMNS, STANDARD_CALENDAR } from './shared/gantt-project-data';
import { createProductionGanttConfig, createProductionTasks, createProductionPersistence, productionError, productionSourceRows } from './GanttProductionPatternsShared';
import './gantt-production-patterns.css';
const { isDark } = currentThemeVue();
const plugins = [GanttPlugin, ExportExcelPlugin, RowSelectPlugin];
const rowSelect = { rowOrder: true };
const columns = SHOWCASE_COLUMNS_WITH_ROW_SELECT;
const readOnly = ref(false);
const localized = ref(false);
const large = ref(false);
const project = ref<ProjectSnapshot | null>(null);
const status = ref('Save and restore use browser memory only; no server is contacted.');
const hasSaved = ref(false);
const persistence = createProductionPersistence();
const tasks = computed(() => project.value ? productionSourceRows(project.value) : createProductionTasks(large.value));
const dependencies = computed(() => project.value?.dependencies ?? (large.value ? [] : SHOWCASE_DEPENDENCIES));
const calendars = computed(() => project.value?.calendars ?? [STANDARD_CALENDAR]);
const resources = computed(() => project.value?.resources ?? SHOWCASE_RESOURCES);
const assignments = computed(() => project.value?.assignments ?? (large.value ? [] : SHOWCASE_ASSIGNMENTS));
const baselines = computed(() => project.value?.baselines ?? (large.value ? [] : SHOWCASE_BASELINES));
const ganttConfig = computed(() => createProductionGanttConfig(readOnly.value, localized.value, project.value));
const gridRef = ref<InstanceType<typeof RevoGrid> | HTMLRevoGridElement | null>(null);
const toolbarRef = ref<HTMLElement | null>(null);
let frame = 0;
function getGrid(): HTMLRevoGridElement {
  const value = gridRef.value as (InstanceType<typeof RevoGrid> & { $el?: HTMLRevoGridElement }) | HTMLRevoGridElement | null;
  const grid = value && '$el' in value ? value.$el : value;
  if (!(grid instanceof HTMLElement)) throw new Error('The grid is still loading.');
  return grid as HTMLRevoGridElement;
}
async function run(action: () => unknown | Promise<unknown>) {
  try { await action(); } catch (error) { status.value = productionError(error); }
}
async function save() { await run(async () => { await persistence.save(getGrid()); hasSaved.value = true; status.value = 'Saved the current edited project in browser memory.'; }); }
async function restore() { await run(async () => { project.value = await persistence.load(); large.value = false; status.value = 'Restored the saved project, including tasks and project collections.'; }); }
async function importJson(event: Event) {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  if (file) await run(async () => { project.value = await persistence.importFile(file); large.value = false; status.value = `Imported ${project.value.tasks.length} tasks from ${file.name}.`; });
  input.value = '';
}
function mountToolbar() {
  let grid: HTMLRevoGridElement;
  try { grid = getGrid(); } catch { frame = requestAnimationFrame(mountToolbar); return; }
  if (toolbarRef.value) defineGanttToolbar(toolbarRef.value, { grid, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true } });
}
onMounted(async () => { await nextTick(); mountToolbar(); });
onBeforeUnmount(() => { cancelAnimationFrame(frame); if (toolbarRef.value) toolbarRef.value.textContent = ''; });
</script>
Reacttsx
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { defineGanttToolbar, GanttPlugin, type ProjectSnapshot } from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  SHOWCASE_ASSIGNMENTS, SHOWCASE_BASELINES, SHOWCASE_COLUMNS_WITH_ROW_SELECT, SHOWCASE_DEPENDENCIES,
  SHOWCASE_RESOURCES, SHOWCASE_TOOLBAR_COLUMNS, STANDARD_CALENDAR,
} from './shared/gantt-project-data';
import { createProductionGanttConfig, createProductionTasks, createProductionPersistence, productionError, productionSourceRows } from './GanttProductionPatternsShared';
import './gantt-production-patterns.css';

export default function GanttProductionPatterns() {
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const toolbarRef = useRef<HTMLDivElement>(null);
  const [readOnly, setReadOnly] = useState(false);
  const [localized, setLocalized] = useState(false);
  const [large, setLarge] = useState(false);
  const [project, setProject] = useState<ProjectSnapshot | null>(null);
  const [status, setStatus] = useState('Save and restore use browser memory only; no server is contacted.');
  const [hasSaved, setHasSaved] = useState(false);
  const persistence = useMemo(() => createProductionPersistence(), []);
  const plugins = useMemo(() => [GanttPlugin, ExportExcelPlugin, RowSelectPlugin], []);
  const rowSelect = useMemo(() => ({ rowOrder: true }), []);
  const columns = useMemo(() => SHOWCASE_COLUMNS_WITH_ROW_SELECT, []);
  const tasks = useMemo(() => project ? productionSourceRows(project) : createProductionTasks(large), [project, large]);
  const gantt = useMemo(() => createProductionGanttConfig(readOnly, localized, project), [readOnly, localized, project]);
  const dependencies = useMemo(() => project?.dependencies ?? (large ? [] : SHOWCASE_DEPENDENCIES), [project, large]);
  const calendars = useMemo(() => project?.calendars ?? [STANDARD_CALENDAR], [project]);
  const resources = useMemo(() => project?.resources ?? SHOWCASE_RESOURCES, [project]);
  const assignments = useMemo(() => project?.assignments ?? (large ? [] : SHOWCASE_ASSIGNMENTS), [project, large]);
  const baselines = useMemo(() => project?.baselines ?? (large ? [] : SHOWCASE_BASELINES), [project, large]);
  function getGrid(): HTMLRevoGridElement {
    if (!gridRef.current) throw new Error('The grid is still loading.');
    return gridRef.current;
  }
  async function run(action: () => unknown | Promise<unknown>) {
    try { await action(); } catch (error) { setStatus(productionError(error)); }
  }
  async function save() {
    await run(async () => {
      await persistence.save(getGrid());
      setHasSaved(true);
      setStatus('Saved the current edited project in browser memory.');
    });
  }
  async function restore() {
    await run(async () => {
      setProject(await persistence.load());
      setLarge(false);
      setStatus('Restored the saved project, including tasks and project collections.');
    });
  }
  async function importJson(event: React.ChangeEvent<HTMLInputElement>) {
    const input = event.currentTarget;
    const file = input.files?.[0];
    if (file) await run(async () => {
      const imported = await persistence.importFile(file);
      setProject(imported);
      setLarge(false);
      setStatus(`Imported ${imported.tasks.length} tasks from ${file.name}.`);
    });
    input.value = '';
  }
  useEffect(() => {
    const toolbar = toolbarRef.current;
    if (!toolbar || !gridRef.current) return;
    defineGanttToolbar(toolbar, { grid: gridRef.current, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true } });
    return () => { toolbar.textContent = ''; };
  }, []);
  return <section className="gantt-production-demo">
    <div className="gantt-production-controls">
      <label><input type="checkbox" checked={readOnly} onChange={(event) => setReadOnly(event.currentTarget.checked)} />Read only</label>
      <label><input type="checkbox" checked={localized} onChange={(event) => setLocalized(event.currentTarget.checked)} />Español dates</label>
      <label><input type="checkbox" checked={large} onChange={(event) => { setLarge(event.currentTarget.checked); setProject(null); }} />750 tasks</label>
      <label className="gantt-production-file">Import JSON<input type="file" accept=".json,application/json" aria-label="Import JSON" onChange={importJson} /></label>
      <button type="button" onClick={() => { void run(() => persistence.exportFile(getGrid())); }}>Export JSON</button>
      <button type="button" onClick={save}>Save in memory</button>
      <button type="button" disabled={!hasSaved} onClick={restore}>Restore saved project</button>
      <button type="button" onClick={() => { void run(() => persistence.print(getGrid())); }}>Print / PDF</button>
    </div>
    <p className="gantt-production-status" role="status">{status}</p>
    <div ref={toolbarRef} className="gantt-production-toolbar" />
    <RevoGrid ref={gridRef} className="gantt-production-grid" theme={currentTheme().isDark() ? 'darkCompact' : 'compact'} hideAttribution
      plugins={plugins} rowSelect={rowSelect} source={tasks} columns={columns} gantt={gantt}
      ganttDependencies={dependencies} ganttCalendars={calendars} ganttResources={resources}
      ganttAssignments={assignments} ganttBaselines={baselines} />
  </section>;
}
Angularts
import { ChangeDetectorRef, inject, AfterViewInit, Component, ElementRef, NO_ERRORS_SCHEMA, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { defineGanttToolbar, GanttPlugin, type ProjectSnapshot } from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { SHOWCASE_ASSIGNMENTS, SHOWCASE_BASELINES, SHOWCASE_COLUMNS_WITH_ROW_SELECT, SHOWCASE_DEPENDENCIES, SHOWCASE_RESOURCES, SHOWCASE_TOOLBAR_COLUMNS, STANDARD_CALENDAR } from './shared/gantt-project-data';
import { createProductionGanttConfig, createProductionTasks, createProductionPersistence, productionError, productionSourceRows } from './GanttProductionPatternsShared';

@Component({
  selector: 'gantt-production-patterns-grid', standalone: true, schemas: [NO_ERRORS_SCHEMA], imports: [RevoGrid],
  styleUrls: ['./gantt-production-patterns.css'], encapsulation: ViewEncapsulation.None,
  template: `<section class="gantt-production-demo">
    <div class="gantt-production-controls">
      <label><input type="checkbox" [checked]="readOnly" (change)="setReadOnly($any($event.target).checked)" />Read only</label>
      <label><input type="checkbox" [checked]="localized" (change)="setLocalized($any($event.target).checked)" />Español dates</label>
      <label><input type="checkbox" [checked]="large" (change)="setLarge($any($event.target).checked)" />750 tasks</label>
      <label class="gantt-production-file">Import JSON<input type="file" accept=".json,application/json" aria-label="Import JSON" (change)="importJson($event)" /></label>
      <button type="button" (click)="exportJson()">Export JSON</button>
      <button type="button" (click)="save()">Save in memory</button>
      <button type="button" [disabled]="!hasSaved" (click)="restore()">Restore saved project</button>
      <button type="button" (click)="print()">Print / PDF</button>
    </div>
    <p class="gantt-production-status" role="status">{{ status }}</p>
    <div #toolbar class="gantt-production-toolbar"></div>
    <revo-grid #grid class="gantt-production-grid" [theme]="theme" [hideAttribution]="true" [plugins]="plugins"
      [rowSelect]="rowSelect" [source]="tasks" [columns]="columns" [gantt]="ganttConfig" [ganttDependencies]="dependencies"
      [ganttCalendars]="calendars" [ganttResources]="resources" [ganttAssignments]="assignments" [ganttBaselines]="baselines"></revo-grid>
  </section>`,
})
export class GanttProductionPatternsGridComponent implements AfterViewInit, OnDestroy {
  private readonly changeDetector = inject(ChangeDetectorRef);
  @ViewChild('grid', { read: ElementRef }) gridRef!: ElementRef<HTMLRevoGridElement>;
  @ViewChild('toolbar', { read: ElementRef }) toolbarRef!: ElementRef<HTMLElement>;
  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  plugins = [GanttPlugin, ExportExcelPlugin, RowSelectPlugin]; rowSelect = { rowOrder: true }; columns = SHOWCASE_COLUMNS_WITH_ROW_SELECT;
  calendars = [STANDARD_CALENDAR]; resources = [...SHOWCASE_RESOURCES]; assignments = [...SHOWCASE_ASSIGNMENTS]; baselines = [...SHOWCASE_BASELINES];
  readOnly = false; localized = false; large = false; tasks = createProductionTasks(); dependencies = SHOWCASE_DEPENDENCIES;
  project: ProjectSnapshot | null = null;
  status = 'Save and restore use browser memory only; no server is contacted.';
  hasSaved = false;
  private persistence = createProductionPersistence();
  ganttConfig = createProductionGanttConfig();
  private refresh() { this.ganttConfig = createProductionGanttConfig(this.readOnly, this.localized, this.project); }
  private refreshProject() {
    this.tasks = this.project ? productionSourceRows(this.project) : createProductionTasks(this.large);
    this.dependencies = [...(this.project?.dependencies ?? (this.large ? [] : SHOWCASE_DEPENDENCIES))];
    this.calendars = [...(this.project?.calendars ?? [STANDARD_CALENDAR])];
    this.resources = [...(this.project?.resources ?? SHOWCASE_RESOURCES)];
    this.assignments = [...(this.project?.assignments ?? (this.large ? [] : SHOWCASE_ASSIGNMENTS))];
    this.baselines = [...(this.project?.baselines ?? (this.large ? [] : SHOWCASE_BASELINES))];
    this.refresh();
  }
  private getGrid(): HTMLRevoGridElement {
    const grid = this.gridRef?.nativeElement;
    if (!grid) throw new Error('The grid is still loading.');
    return grid;
  }
  private async run(action: () => unknown | Promise<unknown>) {
    try { await action(); } catch (error) { this.status = productionError(error); }
    finally { this.changeDetector.markForCheck(); }
  }
  setReadOnly(value: boolean) { this.readOnly = value; this.refresh(); }
  setLocalized(value: boolean) { this.localized = value; this.refresh(); }
  setLarge(value: boolean) { this.large = value; this.project = null; this.refreshProject(); }
  exportJson() { return this.run(() => this.persistence.exportFile(this.getGrid())); }
  print() { return this.run(() => this.persistence.print(this.getGrid())); }
  save() {
    return this.run(async () => {
      await this.persistence.save(this.getGrid()); this.hasSaved = true;
      this.status = 'Saved the current edited project in browser memory.';
    });
  }
  restore() {
    return this.run(async () => {
      this.project = await this.persistence.load(); this.large = false; this.refreshProject();
      this.status = 'Restored the saved project, including tasks and project collections.';
    });
  }
  async importJson(event: Event) {
    const input = event.target as HTMLInputElement;
    const file = input.files?.[0];
    if (file) await this.run(async () => {
      this.project = await this.persistence.importFile(file); this.large = false; this.refreshProject();
      this.status = `Imported ${this.project.tasks.length} tasks from ${file.name}.`;
    });
    input.value = '';
  }
  ngAfterViewInit() { defineGanttToolbar(this.toolbarRef.nativeElement, { grid: this.gridRef.nativeElement, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true } }); }
  ngOnDestroy() { if (this.toolbarRef?.nativeElement) this.toolbarRef.nativeElement.textContent = ''; }
}
Sharedts
import { exportProjectSnapshot, parseProjectSnapshotJson, type GanttPluginConfig, type GanttTaskSourceRow, type ISODateString, type ProjectSnapshot } from '@revolist/gantt';
import { getGanttScheduleSnapshot } from './recipes/current-schedule-snapshot';
import { ProjectSnapshotRestAdapter } from './recipes/rest-adapter';
import { printGanttContainer } from './recipes/print-pdf-recipe';
import {
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_BASELINES,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_GANTT_CONFIG,
  SHOWCASE_RESOURCES,
  SHOWCASE_TASKS,
  STANDARD_CALENDAR,
} from './shared/gantt-project-data';

export function createProductionGanttConfig(readOnly = false, localized = false, project?: ProjectSnapshot | null): GanttPluginConfig {
  return {
    ...SHOWCASE_GANTT_CONFIG,
    ...(project ? { id: project.id, name: project.name, version: project.version, currency: project.currency, timeZone: project.timeZone, primaryCalendarId: project.primaryCalendarId, updatedAt: project.updatedAt, durationSettings: project.durationSettings, calendars: project.calendars } : {}),
    allowTaskCreate: true,
    taskCreateRow: true,
    readOnly,
    labels: localized ? {
      taskMode: { auto: 'Automática', manual: 'Manual' },
      workflowStatus: { 'not-started': 'Sin iniciar', 'in-progress': 'En curso', done: 'Finalizada', blocked: 'Bloqueada' },
    } : undefined,
    dateFormats: localized ? {
      locale: 'es-ES',
      table: { dateStyle: 'medium' },
      tooltip: { dateStyle: 'long' },
    } : undefined,
    visuals: {
      ...SHOWCASE_GANTT_CONFIG.visuals,
      showBaseline: true,
      showTaskLabels: true,
      timeRanges: [{ id: 'release-window', startDate: '2026-02-16', endDate: '2026-02-20', label: 'Release window', color: '#8b5cf633' }],
      milestoneLines: [{ id: 'go-live', date: '2026-02-27', label: 'Go live', color: '#e11d48' }],
      taskMarkerHook: (task) => task.deadlineDate ? [{ id: `deadline-${task.id}`, date: task.deadlineDate, label: 'Deadline' }] : [],
      taskBarColorHook: ({ row }) => row.id === SHOWCASE_TASKS[1].id ? { barColor: '#64748b', className: 'gantt-production-locked' } : undefined,
      taskTooltipFields: ['startDate', 'endDate', 'duration', 'percentDone', 'assignees'],
    },
  } as GanttPluginConfig;
}

export function createProductionTasks(large = false): GanttTaskSourceRow[] {
  if (!large) {
    return SHOWCASE_TASKS.map((task, index) => ({ ...task, ...(index === 1 ? { locked: true } : {}) })) as GanttTaskSourceRow[];
  }

  return Array.from({ length: 750 }, (_, index) => {
    const day = 2 + (index % 20);
    return {
      id: `large-${index + 1}`,
      name: `Virtualized task ${index + 1}`,
      startDate: `2026-02-${String(day).padStart(2, '0')}` as ISODateString,
      duration: 1 + (index % 5),
      durationUnit: 'day',
      percentDone: (index * 7) % 101,
      taskMode: 'manual',
      locked: index % 17 === 0,
    } satisfies GanttTaskSourceRow;
  });
}

export function productionSourceRows(project: ProjectSnapshot): GanttTaskSourceRow[] {
  return project.tasks.map((task) => ({
    ...task,
    durationUnit: task.durationUnit ?? 'hour',
    percentDone: task.progressPercent,
    taskMode: task.manuallyScheduled ? 'manual' : 'auto',
  }));
}

export async function getProductionProject(grid: HTMLRevoGridElement): Promise<ProjectSnapshot> {
  return getGanttScheduleSnapshot(grid);
}

export function downloadProductionSnapshot(project: ProjectSnapshot): void {
  const href = URL.createObjectURL(new Blob([exportProjectSnapshot(project, { space: 2 })], { type: 'application/json' }));
  const link = document.createElement('a');
  link.href = href;
  link.download = 'gantt-project.json';
  link.click();
  setTimeout(() => URL.revokeObjectURL(href), 0);
}

/** A real REST recipe with an injected browser-memory transport; no server is contacted. */
export function createProductionPersistence() {
  let savedJson: string | undefined;
  const adapter = new ProjectSnapshotRestAdapter({
    url: '/example/project',
    fetchImpl: (async (_input, init) => {
      if (init?.method === 'PUT') {
        savedJson = String(init.body);
        return new Response(null, { status: 204 });
      }
      return new Response(savedJson ?? 'No saved project', { status: savedJson ? 200 : 404 });
    }) as typeof fetch,
  });
  return {
    save: async (grid: HTMLRevoGridElement) => adapter.save(await getProductionProject(grid)),
    load: () => adapter.load(),
    importFile: async (file: Pick<File, 'text'>): Promise<ProjectSnapshot> => {
      const parsed = parseProjectSnapshotJson(await file.text());
      if (parsed.ok === false) throw new Error(parsed.error);
      return parsed.project;
    },
    exportFile: async (grid: HTMLRevoGridElement) => downloadProductionSnapshot(await getProductionProject(grid)),
    print: (grid: HTMLRevoGridElement) => {
      const result = printGanttContainer(grid.parentElement, { cleanupDelayMs: 60_000, hideSelectors: ['.gantt-production-controls', '.gantt-production-status', '.gantt-production-toolbar', '.gantt-toolbar', '.rgContextMenu'], metadata: { title: grid.gantt?.name ?? 'Project schedule' } });
      if (!result.ok) throw new Error('The print preview could not be opened.');
    },
  };
}

export function productionError(error: unknown): string {
  return error instanceof Error ? error.message : 'The operation failed.';
}

Locked tasks let an application protect individual tasks after approval, baseline capture, phase closure, or any other workflow decision. A locked task remains visible in the task table and timeline, but packaged Gantt task mutations are rejected for that task.

Set locked: true on the flat task source row:

const tasks = [
{
id: 'design-signoff',
parentId: null,
name: 'Design sign-off',
type: 'milestone',
workflowStatus: 'done',
startDate: '2026-04-15',
endDate: '2026-04-15',
duration: 1,
percentDone: 100,
calendarId: 'standard',
tags: ['approved'],
locked: true,
},
];

locked: true blocks packaged task mutations for the locked task:

  • Timeline task movement and resizing.
  • Split creation, movement, and removal.
  • Progress updates.
  • Inline task-field edits routed through the Gantt task mutation service.
  • Indent and outdent actions.
  • Child task creation under a locked parent.
  • Delete when the selected task subtree contains a locked task.

Blocked task mutations return readonly-task and leave task data unchanged.

Locking a task does not remove it from scheduling, rendering, or inspection flows:

  • The task still renders in the table and timeline.
  • Scheduler recalculation can still read the task and include it in dependencies, critical path, baselines, and diagnostics.
  • Search, filter, sort, expand/collapse, scroll, and zoom remain available.
  • Non-locked tasks continue to edit normally.

The source-row locked field is task-level protection, not a complete permission or checkout system.

Dependency changes are not globally disabled by locking a task. Assignment/resource edits are also handled by their own mutation service. Use gantt.readOnly when the whole project should reject packaged task, dependency, and assignment mutations.

For application-specific policy, pair locked tasks with cancelable Gantt events or validation recipes.

grid.addEventListener('gantt-before-task-change', (event) => {
if (lockedTaskIds.has(event.detail.taskId)) {
event.preventDefault();
}
});

Use the status indicator helpers when you want to show a lock label, class name, icon, or tooltip in a custom renderer.

const status = createTaskStatusIndicators({
taskId: row.id,
taskLocked: row.locked === true,
});
renderIndicatorCell({
className: status.classNames.join(' '),
title: status.label,
indicators: status.indicators,
});

The helper returns metadata only, so the host application can choose the exact icon and placement.

The built-in Gantt row context menu hides hierarchy actions when the selected task is locked. Mutating row actions are also guarded by the task mutation service, so direct calls still return readonly-task.