Queries and Commands
This page covers complete-project loading and persistence. For paged task hierarchies with application-owned editing and persistence, use server-side Gantt loading.
Live example
Section titled “Live example”The production example saves and restores complete project snapshots.
Source code
import { controlAttributes, createControl } from '@revolist/ui';import { defineCustomElements } from '@revolist/revogrid/loader';defineCustomElements();
import { defineGanttToolbar, GanttPlugin, GanttCalendarEditorPlugin, 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 = createControl(document, 'input', { type: 'checkbox' }); 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 = createControl(document, 'button', { type: 'button' }); element.type = 'button'; element.textContent = label; element.className = controlAttributes('button', { class: 'rv-btn' }).class; 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'); toolbar.className = 'gantt-production-toolbar'; grid.className = 'gantt-production-grid'; grid.theme = currentTheme().isDark() ? 'darkCompact' : 'compact'; grid.hideAttribution = true; grid.plugins = [GanttPlugin, GanttCalendarEditorPlugin, 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', 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 fileInput = createControl(document, 'input', { type: 'file' }); fileInput.type = 'file'; fileInput.hidden = true; fileInput.accept = '.json,application/json'; fileInput.setAttribute('aria-label', 'Import JSON file'); 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 = ''; }); const options = document.createElement('div'); options.className = 'gantt-production-options'; const actions = document.createElement('div'); actions.className = 'gantt-production-actions'; options.append( toggle('Read only', (value) => { readOnly = value; applyConfig(); }).element, toggle('Español dates', (value) => { localized = value; applyConfig(); }).element, largeToggle.element, ); actions.append( fileInput, button('Import JSON', () => fileInput.click()), button('JSON', () => persistence.exportFile(grid)), button('Save', async () => { await persistence.save(grid); restore.disabled = false; status.textContent = 'Saved the current edited project in browser memory.'; }), restore, button('Print', () => persistence.print(grid)), ); controls.append(options, actions); root.append(controls, status, toolbar, grid); parent.appendChild(root); applyProject(); defineGanttToolbar(toolbar, { grid, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true, calendars: true } }); return () => { toolbar.textContent = ''; grid.remove(); root.remove(); };}<template> <section class="gantt-production-demo"> <div class="gantt-production-controls"> <div class="gantt-production-options"> <label><input v-rv-control v-model="readOnly" type="checkbox" />Read only</label> <label><input v-rv-control v-model="localized" type="checkbox" />Español dates</label> <label><input v-rv-control v-model="large" type="checkbox" @change="project = null" />750 tasks</label> </div> <div class="gantt-production-actions"> <input v-rv-control ref="importInput" type="file" hidden accept=".json,application/json" aria-label="Import JSON file" @change="importJson" /> <button v-rv-control type="button" class="rv-btn" @click="importInput?.click()">Import JSON</button> <button v-rv-control type="button" class="rv-btn" @click="run(() => persistence.exportFile(getGrid()))">JSON</button> <button v-rv-control type="button" class="rv-btn" @click="save">Save</button> <button v-rv-control type="button" class="rv-btn" :disabled="!hasSaved" @click="restore">Restore</button> <button v-rv-control type="button" class="rv-btn" @click="run(() => persistence.print(getGrid()))">Print</button> </div> </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 { vRvControl } from '@revolist/ui/vue-control';import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';import RevoGrid from '@revolist/vue3-datagrid';import { defineGanttToolbar, GanttPlugin, GanttCalendarEditorPlugin, 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, GanttCalendarEditorPlugin, 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('');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 importInput = ref<HTMLInputElement | 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, calendars: true } });}onMounted(async () => { await nextTick(); mountToolbar(); });onBeforeUnmount(() => { cancelAnimationFrame(frame); if (toolbarRef.value) toolbarRef.value.textContent = ''; });</script>import { ButtonControl as UiButton, Input as UiInput } from '@revolist/ui/react-control';import React, { useEffect, useMemo, useRef, useState } from 'react';import { RevoGrid } from '@revolist/react-datagrid';import { defineGanttToolbar, GanttPlugin, GanttCalendarEditorPlugin, 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 importInput = useRef<HTMLInputElement>(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(''); const [hasSaved, setHasSaved] = useState(false); const persistence = useMemo(() => createProductionPersistence(), []); const plugins = useMemo(() => [GanttPlugin, GanttCalendarEditorPlugin, 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, calendars: true } }); return () => { toolbar.textContent = ''; }; }, []); return <section className="gantt-production-demo"> <div className="gantt-production-controls"> <div className="gantt-production-options"> <label><UiInput type="checkbox" checked={readOnly} onChange={(event) => setReadOnly(event.currentTarget.checked)} />Read only</label> <label><UiInput type="checkbox" checked={localized} onChange={(event) => setLocalized(event.currentTarget.checked)} />Español dates</label> <label><UiInput type="checkbox" checked={large} onChange={(event) => { setLarge(event.currentTarget.checked); setProject(null); }} />750 tasks</label> </div> <div className="gantt-production-actions"> <UiInput ref={importInput} type="file" hidden accept=".json,application/json" aria-label="Import JSON file" onChange={importJson} /> <UiButton type="button" className="rv-btn" onClick={() => importInput.current?.click()}>Import JSON</UiButton> <UiButton type="button" className="rv-btn" onClick={() => { void run(() => persistence.exportFile(getGrid())); }}>JSON</UiButton> <UiButton type="button" className="rv-btn" onClick={save}>Save</UiButton> <UiButton type="button" className="rv-btn" disabled={!hasSaved} onClick={restore}>Restore</UiButton> <UiButton type="button" className="rv-btn" onClick={() => { void run(() => persistence.print(getGrid())); }}>Print</UiButton> </div> </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>;}import { UiControlDirective } from '@revolist/ui/angular-control';import { ChangeDetectorRef, inject, AfterViewInit, Component, ElementRef, NO_ERRORS_SCHEMA, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core';import { RevoGrid } from '@revolist/angular-datagrid';import { defineGanttToolbar, GanttPlugin, GanttCalendarEditorPlugin, 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: [UiControlDirective, RevoGrid], styleUrls: ['./gantt-production-patterns.css'], encapsulation: ViewEncapsulation.None, template: `<section class="gantt-production-demo"> <div class="gantt-production-controls"> <div class="gantt-production-options"> <label><input rvControl type="checkbox" [checked]="readOnly" (change)="setReadOnly($any($event.target).checked)" />Read only</label> <label><input rvControl type="checkbox" [checked]="localized" (change)="setLocalized($any($event.target).checked)" />Español dates</label> <label><input rvControl type="checkbox" [checked]="large" (change)="setLarge($any($event.target).checked)" />750 tasks</label> </div> <div class="gantt-production-actions"> <input rvControl #importFile type="file" hidden accept=".json,application/json" aria-label="Import JSON file" (change)="importJson($event)" /> <button rvControl type="button" class="rv-btn" (click)="importFile.click()">Import JSON</button> <button rvControl type="button" class="rv-btn" (click)="exportJson()">JSON</button> <button rvControl type="button" class="rv-btn" (click)="save()">Save</button> <button rvControl type="button" class="rv-btn" [disabled]="!hasSaved" (click)="restore()">Restore</button> <button rvControl type="button" class="rv-btn" (click)="print()">Print</button> </div> </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, GanttCalendarEditorPlugin, 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 = ''; 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, calendars: true } }); } ngOnDestroy() { if (this.toolbarRef?.nativeElement) this.toolbarRef.nativeElement.textContent = ''; }}import { exportProjectSnapshot, parseProjectSnapshotJson, type GanttPluginConfig, type GanttTaskSourceRow, type ProjectSnapshot,} from '@revolist/gantt';type ISODateString = import('@revolist/gantt').TaskEntity['startDate'];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.';}Query integration
Section titled “Query integration”The whole-project Query adapter reads a complete authored project. A viewport, filtered task list or
scheduled projection is insufficient: dependencies, calendars, resources,
assignments and baselines must accompany all tasks. Existing REST/GraphQL
load(request) callbacks can return their unchanged ProjectSnapshot result.
Wrap them with createGanttQueryAdapter from @revolist/gantt and mount the
subscription with GanttPlugin.loadProject(adapter, request).
import { createGanttQueryAdapter } from '@revolist/gantt';import type { GanttQueryOptions } from '@revolist/gantt';import { createDataQueryContext } from '@revolist/revogrid-pro';
const context = createDataQueryContext();const source = context.source({ sourceId: 'project-api', schemaId: '1', tenantId: tenant.id, authorizationScope: session.permissionRevision,});const query: GanttQueryOptions = { source, resource: `gantt-project:${projectId}`, refetchInterval: 10_000, refetchOnWindowFocus: true, retry: 2,};const adapter = createGanttQueryAdapter(restAdapter, query);await gantt.loadProject(adapter, {});// A second grid can use this adapter, or another adapter with the same source// and semantic request. Each grid owns its own subscription.await otherGantt.loadProject(adapter, {});Wrapping an existing Gantt Query adapter without a new policy returns the same
adapter. Supplying an explicit policy (including false) applies it over the
original transport in a separate caller-owned adapter, without nesting caches.
Dispose a reused adapter only after all its consumers are finished; disposing an
adapter created with an explicit override leaves the original adapter alive.
Shared identity must include the project/endpoint. Set query.resource to a stable
project key for REST loaders whose URL/project ID lives in a closure and whose
request is {}. Two different projects must not share the same source, resource
and request. Tenant and authorization changes require a new source scope.
The request keeps transport-specific fields, such as GraphQL variables.
Cancellation is forwarded through the existing optional request.signal.
Supabase recipes whose load(projectId) takes a string can use a host wrapper:
{ load: ({ id }) => supabaseAdapter.load(id) }. Existing save(project, request)
recipes remain unchanged and application-owned; wrapping reads does not submit writes.
| Public type or method | Configuration and behavior |
|---|---|
GanttQueryOptions | LoadQueryOptions<GanttProjectData>: all native read options, defaults and callback types, source, resource, consistency, reconcile, onStatus. Pass false for direct uncached reads. |
GanttProjectLoader<Request> | load(request): Promise<ProjectSnapshot | GanttProjectData>. The complete frame is validated before entering shared cache. |
GanttProjectData | project: ProjectSnapshot, optional opaque string revision, taskRows, fields. project.version remains the schema version. |
taskRows / fields | Supply all task rows exactly once. Custom fields are preserved, canonical task values win mapped fields, and row order follows taskRows. Supply the field map when using custom IDs/titles. Omitting rows during same-ID hydration preserves existing matching custom rows and the configured field map. |
GanttQueryAdapter<Request> | load(request) is a one-shot read. observe(request, {onData,onStatus?}) returns ready, refresh(), dispose(). accepted() fences/refetches shared reads after a confirmed save. dispose() releases all adapter observers and its private context. |
GanttPlugin.getProjectData() | Detached authored frame with custom rows, field map and last hydrated server revision. Existing getProjectSnapshot() remains available. |
GanttPlugin.setProjectSnapshot(project, options?) | Atomic same-ID hydration. Options: taskRows, fields, revision, clearHistory (default true), expected (optional previously captured GanttProjectData). Invalid complete graphs or a mismatching expected frame throw before replacement. |
GanttPlugin.loadProject(adapter, request) | Retains the observer for polling/focus/reconnect. Replacing the load, clearing Gantt or destroying the grid detaches that grid’s observer. The caller still owns the adapter and shared context. |
Hydration stages the complete graph before publishing it and does not emit a
local edit command. Server hydration works for read-only projects. Background
polling updates clean views; a view with local edits keeps its authored state
until the application reconciles it. A confirmed canonical response can call
setProjectSnapshot with expected to protect later edits and
clearHistory: false to retain compensating undo/redo. Explicit hydration also
resets the clean polling baseline. A revision change alone never changes the
schema version or converts scheduled dates into authored dates.
Project payloads are JSON data; custom rows must be structured-cloneable. Normalize Map/Set fields into plain data for change tracking. Legacy host callbacks in rows continue to work locally, but those rows do not produce transport commands.
Destroy the grids before disposing an application-owned shared adapter/context. Detaching one grid leaves the other polling; detaching the last grid cancels its read and releases polling timers. See Gantt commands and existing integration recipes.
Command receipts
Section titled “Command receipts”@revolist/gantt exports GANTT_PROJECT_COMMIT_EVENT (gantt-project-commit),
GanttProjectCommit, createGanttCommandExecution and GanttCommandOptions.
The event captures the complete authored project and custom task rows after a
local transaction commits. operationId is fresh for every transaction;
historyReplay: true marks undo/redo as a new compensating write. Hydration,
unchanged transactions and rejected edits do not create commands.
createGanttCommandExecution(commit, options) captures commit.data in a shared
command envelope. resourceId is data.project.id, expectedRevision is
data.revision, and the command is the complete GanttProjectData. The schema
project.version is never a concurrency token. The full-project read and
hydration types describe the frame.
import { GANTT_PROJECT_COMMIT_EVENT, createGanttCommandExecution,} from '@revolist/gantt';
// The host owns a serial queue and conflict handling for full-project writes.// Retain execution handles when an outcome is unknown; recover before continuing.grid.addEventListener(GANTT_PROJECT_COMMIT_EVENT, ({ detail }) => { host.enqueueProjectCommit(detail);});
// Called by the host queue with a commit and its current authoritative base.const execution = createGanttCommandExecution(commit, { submit: (envelope, { signal }) => api.commitProject(envelope, signal), recover: (envelope, { signal }) => api.projectReceipt(envelope.operationId, signal), idempotent: true, // only when guaranteed by the backend source: adapter, applyAccepted: (canonical) => { gantt.setProjectSnapshot(canonical.project, { ...canonical, clearHistory: false, expected: commit.data, }); },});const receipt = await execution.execute();GanttCommandOptions has the shared command configuration types:
submit, recover, resume, idempotent, validateReceipt, applyAccepted,
source and onStatus. Accepted changes and conflict authoritative values
must be complete valid GanttProjectData for the same project. Their revision
must match the nonempty receipt revision. Invalid receipts remain unknown.
The server owns atomic revision comparison, authorization, durable receipts and
idempotency. There are no automatic writes or write retries.
When a newer local edit makes the expected guard fail, the server receipt stays
accepted and local reconciliation reports an error. The host must merge/rebase
those later edits before retrying reconcile() or advancing its queue. The
shared source hook runs after successful local reconciliation. If canonical
application is deferred, the host can call adapter.accepted() on the confirmed
accepted receipt to fence peer readers while it rebases the editing view. Before
submitting subsequent full-project commands, the host must choose their current
acknowledged revision and resolve conflicts; blindly submitting several captured
frames with the same revision is unsafe. The adapter does not add a second
optimistic ledger or automatically roll back history.
Legacy save(project, request) recipes still work. Their Promise<void> result
has no durable receipt: after a confirmed save, use adapter.accepted() to refresh
shared readers and explicitly hydrate/reconcile the editing view. See
Gantt persistence recipes.