Skip to content

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.

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

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 methodConfiguration and behavior
GanttQueryOptionsLoadQueryOptions<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.
GanttProjectDataproject: ProjectSnapshot, optional opaque string revision, taskRows, fields. project.version remains the schema version.
taskRows / fieldsSupply 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.

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