Read-Only Mode
Live example
Section titled “Live example”Enable Read only, then try changing task dates or dragging a bar. The project remains inspectable: scroll, zoom, expand rows, and inspect dependencies. Disable Read only to edit again.
Source code
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(); };
}
<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>
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>;
}
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 = ''; }
}
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.';
}
Read-only mode turns the packaged Gantt editing surface into an inspection surface. Users can still view the task table and timeline, navigate, search, filter, sort, zoom, and use non-mutating UI, but packaged project mutations are rejected.
Enable it on the Gantt config:
grid.gantt = { id: 'project-1', name: 'Project Roadmap', version: '1', currency: 'USD', timeZone: 'UTC', primaryCalendarId: 'standard', updatedAt: new Date().toISOString(), readOnly: true,};What It Blocks
Section titled “What It Blocks”readOnly: true blocks mutations routed through the packaged Gantt services:
- Task changes: move, resize, split, progress edits, inline task-field edits, task creation, indent, outdent, and delete.
- Dependency changes: create, update, delete, and predecessor/successor field replacement.
- Assignment changes: assignee/resource field updates and assignment replacement.
Blocked mutations return a failure result instead of updating the stores. Task changes return readonly-task; dependency and assignment changes return readonly-project.
What Still Works
Section titled “What Still Works”Read-only mode does not turn the grid into a static screenshot. Non-mutating workflows remain available:
- Timeline rendering, task bars, dependencies, baselines, critical-path visuals, and diagnostics.
- Tree expand/collapse, search, filtering, sorting, scrolling, and zoom.
- External application-controlled state replacement, such as loading a different project snapshot or changing the Gantt config.
The built-in Gantt context menu also hides mutating row actions when the project is read-only.
Difference From Grid Readonly
Section titled “Difference From Grid Readonly”gantt.readOnly is a Gantt project mutation guard. It protects the packaged task, dependency, and assignment mutation paths.
The RevoGrid readonly property is a grid-level editing setting. Use it when you also want to prevent generic cell editing behavior outside Gantt’s mutation services.
grid.readonly = true;grid.gantt = { ...projectConfig, readOnly: true,};