Skip to content

Gantt Examples

The example browser reads the shared demo catalog directly. Every registered Gantt demo and framework variant appears here automatically; there is no separate manual inventory to maintain.

Source code
src/components/gantt/gantt-showcase/GanttShowcase.ts
import '@revolist/ui/styles.scss';
import { createControl } from '@revolist/ui';
import './gantt-showcase.scss';
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();
import { GanttPlugin } from '@revolist/gantt';
import { ExportExcelPlugin, RowStatusPlugin } from '@revolist/revogrid-pro';
import {
STANDARD_CALENDAR,
SHOWCASE_ASSIGNMENTS,
SHOWCASE_BASELINES,
SHOWCASE_COLUMNS_WITH_COMPLETION,
SHOWCASE_DEFAULT_HIDDEN,
SHOWCASE_DEPENDENCIES,
SHOWCASE_GANTT_CONFIG,
SHOWCASE_RESOURCES,
SHOWCASE_TASKS,
renderShowcaseTaskBarColor,
renderShowcaseTaskBarContent,
} from '../shared/gantt-project-data';
import { currentTheme } from '../../composables/useRandomData';
// ─── Entry point ──────────────────────────────────────────────────────────────
export function load(parentSelector: string): void {
const parent = document.querySelector(parentSelector);
if (!parent) return;
const darkTheme = currentTheme().isDark();
const container = document.createElement('div');
container.className = `gantt-showcase-shell grow h-full ${darkTheme ? 'gantt-showcase-shell--dark' : 'gantt-showcase-shell--light'}`;
parent.appendChild(container);
const grid = document.createElement('revo-grid') as HTMLRevoGridElement;
const controls = document.createElement('div');
controls.className = 'gantt-showcase-controls';
container.appendChild(controls);
grid.theme = darkTheme ? 'darkCompact' : 'compact';
grid.readonly = false;
grid.range = true;
grid.resize = true;
grid.rowSize = 42;
grid.rowHeaders = false;
grid.hideAttribution = true;
grid.autoSizeColumn = false;
grid.classList.add('gantt-showcase-grid');
grid.plugins = [GanttPlugin, ExportExcelPlugin, RowStatusPlugin];
grid.hideColumns = [...SHOWCASE_DEFAULT_HIDDEN];
grid.columns = [...SHOWCASE_COLUMNS_WITH_COMPLETION];
grid.source = [...SHOWCASE_TASKS];
grid.ganttDependencies = [...SHOWCASE_DEPENDENCIES];
grid.ganttCalendars = [{ ...STANDARD_CALENDAR }];
grid.ganttResources = [...SHOWCASE_RESOURCES];
grid.ganttAssignments = [...SHOWCASE_ASSIGNMENTS];
grid.ganttBaselines = [...SHOWCASE_BASELINES];
let showCriticalPath = Boolean(
SHOWCASE_GANTT_CONFIG.visuals.showCriticalPath,
);
let showBaseline = false;
function applyGanttConfig() {
grid.gantt = {
...SHOWCASE_GANTT_CONFIG,
visuals: {
...SHOWCASE_GANTT_CONFIG.visuals,
showCriticalPath,
showBaseline,
taskBarColorHook: renderShowcaseTaskBarColor,
taskBarContentHook: renderShowcaseTaskBarContent,
},
} as typeof grid.gantt;
}
function createToggle(
label: string,
checked: () => boolean,
onChange: (value: boolean) => void,
) {
const control = createControl(document, 'button', { class: 'rv-btn-sm' });
control.textContent = label;
const sync = () => {
control.setAttribute('aria-pressed', String(checked()));
control.classList.toggle('rv-btn-primary', checked());
};
control.addEventListener('click', () => {
onChange(!checked());
applyGanttConfig();
sync();
});
sync();
return control;
}
controls.append(
createToggle(
'Critical path',
() => showCriticalPath,
(value) => {
showCriticalPath = value;
},
),
createToggle(
'Baselines',
() => showBaseline,
(value) => {
showBaseline = value;
},
),
);
applyGanttConfig();
container.appendChild(grid);
}

20 of 20 Gantt examples

Planning

Remote Gantt preview
TreePerformanceServer-side

Remote Gantt

Load server-scheduled task hierarchies with native Query, sibling paging and revision-consistent dependencies.

Source files
  • src/index.ts
  • src/gantt.remote.utils.ts
  • src/gantt.remote.bootstrap.ts
  • src/backend/seed.ts
  • src/backend/service.ts
  • src/backend/module.ts
  • tree-remote/backend/contracts.ts
  • tree-remote/backend/seed.ts
  • tree-remote/backend/service.ts
  • composables/makeData.ts
  • composables/useRandomData.ts
Gantt Microsoft Project preview
Microsoft ProjectImportExport

Microsoft Project

Import Microsoft Project XML or MPP14, edit the schedule, inspect conversion warnings, and export the current project as XML.

Source files
  • src/index.ts
  • src/gantt-microsoft-project-data.ts
  • src/gantt-microsoft-project.css
  • src/recipes/current-schedule-snapshot.ts
Gantt Feature Recipes preview
ExamplesValidationContext Menu

Feature Recipes

Runnable example of the application-owned Gantt example recipes for column presets, context menus, validation, editor forms, and task status indicators.

Source files
  • src/index.ts
  • src/GanttExampleRecipesShared.ts
  • src/gantt-example-recipes.css
  • src/shared/gantt-project-data.ts
  • src/shared/gantt-project-base-data.ts
  • src/shared/gantt-showcase-data.ts
  • src/shared/gantt-showcase-columns.ts
  • src/shared/gantt-showcase-columns.css
  • src/shared/gantt-showcase-icons.ts
  • src/recipes/current-schedule-snapshot.ts
  • src/recipes/index.ts
  • src/recipes/print-pdf-recipe.css
  • gantt/features/context-menu/context-menu-extensions.ts
  • src/recipes/feature-helper-examples.ts
  • src/recipes/graphql-adapter.ts
  • src/recipes/grid-projection-examples.ts
  • src/recipes/persistence-examples.ts
  • src/recipes/postgres-persistence.ts
  • src/recipes/print-pdf-recipe.ts
  • src/recipes/rest-adapter.ts
  • src/recipes/validation-recipes.ts
Gantt Task Editor Form preview
EditorFormsTaskUpdate

Task Editor Form

Standalone task editor UI built from the exported Gantt field schema and submit normalizer.

Source files
  • src/index.ts
Gantt (Production Patterns) preview
HistoryRead OnlyLocalizationCustomization

Production Patterns

Try undo/redo, read-only and locked tasks, localized dates, custom visuals, JSON/Excel export, and a 750-task virtualized project.

Source files
  • src/index.ts
  • src/GanttProductionPatternsShared.ts
  • src/gantt-production-patterns.css
  • src/shared/gantt-project-data.ts
  • src/shared/gantt-project-base-data.ts
  • src/shared/gantt-showcase-data.ts
  • src/shared/gantt-showcase-columns.ts
  • src/shared/gantt-showcase-columns.css
  • src/shared/gantt-showcase-icons.ts
  • src/recipes/current-schedule-snapshot.ts
  • src/recipes/index.ts
  • src/recipes/print-pdf-recipe.css
  • gantt/features/context-menu/context-menu-extensions.ts
  • src/recipes/feature-helper-examples.ts
  • src/recipes/graphql-adapter.ts
  • src/recipes/grid-projection-examples.ts
  • src/recipes/persistence-examples.ts
  • src/recipes/postgres-persistence.ts
  • src/recipes/print-pdf-recipe.ts
  • src/recipes/rest-adapter.ts
  • src/recipes/validation-recipes.ts
Gantt (Full Showcase) preview
ShowcaseFull Feature

Full Showcase

Comprehensive Gantt implementation featuring toolbars, resources, and assignments.

Source files
  • src/index.ts
  • shared/gantt-project-data.ts
  • shared/gantt-project-base-data.ts
  • shared/gantt-showcase-data.ts
  • shared/gantt-showcase-columns.ts
  • shared/gantt-showcase-columns.css
  • shared/gantt-showcase-icons.ts
  • src/gantt-showcase.scss

Scheduling

Gantt Chart (Basic) preview
Project ManagementScheduling

Basic

A powerful Gantt chart plugin for project scheduling and task management.

Source files
  • src/index.ts
Gantt Chart (10,000 Tasks) preview
Big DataPerformanceDependencies

10,000 Tasks

Test virtual scrolling and task editing with 10,000 tasks and 19,796 dependencies across three months.

Source files
  • src/index.ts
Gantt Chart (Twenty-Year Timeline) preview
Big DataPerformanceDependenciesZoom

Twenty-Year Timeline

Test responsive horizontal scrolling, zooming, and task editing with 100 tasks and 194 dependencies across twenty years.

Source files
  • src/index.ts
Gantt (Scheduling) preview
SchedulingAutomation

Scheduling

Advanced scheduling features including auto-scheduling and constraint handling.

Source files
  • src/index.ts
Gantt (Advanced Dependencies) preview
DependenciesLead Lag

Advanced Dependencies

Finish-start, start-start, finish-finish, and start-finish links with lag, lead, and editable dependency columns.

Source files
  • src/index.ts
  • src/gantt-advanced-data.ts
  • src/GanttAdvancedVanillaDemo.ts
Gantt (Constraints And Deadlines) preview
ConstraintsDeadlines

Constraints And Deadlines

Constraint windows, hard constraints, deadline indicators, and scheduling warnings.

Source files
  • src/index.ts
  • src/gantt-advanced-data.ts
  • src/GanttAdvancedVanillaDemo.ts
Gantt (Project Finish ALAP) preview
ALAPScheduling

Project Finish ALAP

Backward scheduling from a project finish anchor with successor-driven task placement.

Source files
  • src/index.ts
  • src/gantt-advanced-data.ts
  • src/GanttAdvancedVanillaDemo.ts

Resources & Work

Gantt (Resource Planning) preview
ResourcesScheduling

Resource Planning

Resource assignments, allocation units, cost, over-allocation warnings, and deterministic auto-leveling.

Source files
  • src/index.ts
  • src/gantt-advanced-data.ts
  • src/GanttAdvancedVanillaDemo.ts
  • src/gantt-resource-planning-toolbar.ts
Gantt (Scheduler Features) preview
SchedulingLead LagWork

Scheduler Features

A single scheduler-focused example showing dependency lag, dependency lead, and fixed-duration, fixed-work, and fixed-units effort modes.

Source files
  • src/index.ts
  • src/gantt-advanced-data.ts
  • src/GanttAdvancedVanillaDemo.ts
Gantt (Progress And Work) preview
ProgressWork

Progress And Work

Fixed-duration, fixed-work, fixed-units, actuals, remaining duration, and split-range scheduling.

Source files
  • src/index.ts
  • src/gantt-advanced-data.ts
  • src/GanttAdvancedVanillaDemo.ts
Gantt (Split Tasks) preview
Split TasksScheduling

Split Tasks

Pause and resume work within task spans using split ranges while preserving planned working duration.

Source files
  • src/index.ts
  • src/gantt-advanced-data.ts
  • src/GanttAdvancedVanillaDemo.ts

Analysis & Optimization

Gantt Chart (Baselines) preview
BaselinesProject Management

Baselines

Compare current project progress against approved baselines in real-time.

Source files
  • src/index.ts
Gantt (Critical Path) preview
Critical PathOptimization

Critical Path

Highlight tasks on the critical path to identify project bottlenecks.

Source files
  • src/index.ts
Gantt (Critical Path Analysis) preview
Critical PathSlack

Critical Path Analysis

Show early/late dates, total slack, and noncritical parallel work with positive float.

Source files
  • src/index.ts
  • src/gantt-advanced-data.ts
  • src/GanttAdvancedVanillaDemo.ts

These helpers live in the examples workspace. Copy the relevant recipes files beside your application example and use relative imports; they are not exports of @revolist/gantt.

Application recipe sources

Open the runnable feature-recipes demo for the rendered behavior, or expand any helper to inspect the copyable application source inline.

Context menuGrid actions

Context menu extensions

Duplicate, delete, assign-resource, and export actions for Gantt row menus.

packages/gantt/src/gantt/features/context-menu/context-menu-extensions.ts
Open preview
Source code
gantt/features/context-menu/context-menu-extensions.ts
/**
* @packageDocumentation
* Helpers for extending the Gantt context menu through RevoGrid Pro.
*
* The helpers create task-aware context-menu items for duplicate, delete,
* assign-resource, and export actions. They keep persistence and permission
* decisions in the host application while providing a consistent Gantt context
* object with the focused task, grid element, providers, selected range, and
* close callback.
*/
import exportIcon from '@fortawesome/fontawesome-free/svgs/solid/file-export.svg?raw';
import type { Cell, PluginProviders, RangeArea } from '@revolist/revogrid';
import {
EXCEL_EXPORT_EVENT,
type ContextMenuItem,
type ExportExcelEvent,
} from '@revolist/revogrid-pro';
import type { TaskEntity, TaskId } from '../../core';
import { GANTT_TASK_CREATE_EVENT } from '../../grid/gantt-events';
import type { GanttCreateTaskOptions } from '../../grid/gantt-plugin.types';
type ContextMenuActionContext = NonNullable<
Parameters<NonNullable<ContextMenuItem['action']>>[4]
>;
export interface GanttContextMenuExtensionContext {
readonly event: MouseEvent;
readonly focused?: Cell | null;
readonly range?: RangeArea | null;
readonly close?: () => void;
readonly revogrid: HTMLRevoGridElement;
readonly providers: PluginProviders;
readonly taskId: TaskId | null;
readonly task: Partial<TaskEntity> | null;
}
export interface GanttContextMenuBaseItemOptions {
readonly name?: ContextMenuItem['name'];
readonly class?: string;
readonly rowClass?: ContextMenuItem['rowClass'];
readonly icon?: string;
readonly hidden?: ContextMenuItem['hidden'];
readonly keepOpen?: boolean;
}
export interface DuplicateTaskMenuItemOptions extends GanttContextMenuBaseItemOptions {
readonly copyName?: (
task: Partial<TaskEntity>,
context: GanttContextMenuExtensionContext,
) => string;
readonly createTaskOptions?: (
context: GanttContextMenuExtensionContext,
) => GanttCreateTaskOptions | null | undefined;
}
export interface DeleteTaskMenuItemOptions extends GanttContextMenuBaseItemOptions {
readonly onDeleteTask: (context: GanttContextMenuExtensionContext) => void;
readonly confirm?: (context: GanttContextMenuExtensionContext) => boolean;
}
export interface AssignResourceMenuItemOptions extends GanttContextMenuBaseItemOptions {
readonly onAssignResource: (
context: GanttContextMenuExtensionContext,
) => void;
}
export interface ExportMenuItemOptions extends GanttContextMenuBaseItemOptions {
readonly exportOptions?:
| ExportExcelEvent
| ((
context: GanttContextMenuExtensionContext,
) => ExportExcelEvent | undefined);
}
export function createDuplicateTaskMenuItem(
options: DuplicateTaskMenuItemOptions = {},
): ContextMenuItem {
return createMenuItem(options, {
name: options.name ?? 'Duplicate',
action: (context) => {
const createOptions =
options.createTaskOptions?.(context) ??
createDuplicateTaskOptions(context, options.copyName);
if (!createOptions) {
return;
}
dispatchGridEvent(
context.revogrid,
GANTT_TASK_CREATE_EVENT,
createOptions,
);
},
});
}
export function createDeleteTaskMenuItem(
options: DeleteTaskMenuItemOptions,
): ContextMenuItem {
return createMenuItem(options, {
name: options.name ?? 'Delete',
action: (context) => {
if (!context.taskId) {
return;
}
if (options.confirm && !options.confirm(context)) {
return;
}
options.onDeleteTask(context);
},
});
}
export function createAssignResourceMenuItem(
options: AssignResourceMenuItemOptions,
): ContextMenuItem {
return createMenuItem(options, {
name: options.name ?? 'Assign resource',
action: (context) => {
if (!context.taskId) {
return;
}
options.onAssignResource(context);
},
});
}
export function createExportMenuItem(
options: ExportMenuItemOptions = {},
): ContextMenuItem {
return {
...createMenuItem(
{ ...options, icon: options.icon ?? exportIcon },
{
name: options.name ?? 'Export Gantt',
action: (context) => {
const detail =
typeof options.exportOptions === 'function'
? options.exportOptions(context)
: options.exportOptions;
dispatchGridEvent(
context.revogrid,
EXCEL_EXPORT_EVENT,
detail ?? { sheetName: 'Gantt' },
);
},
},
),
title: typeof options.name === 'string' ? options.name : 'Export Gantt',
};
}
export function createGanttContextMenuExtensionItems(options: {
readonly duplicate?: DuplicateTaskMenuItemOptions | false;
readonly delete?: DeleteTaskMenuItemOptions | false;
readonly assignResource?: AssignResourceMenuItemOptions | false;
readonly export?: ExportMenuItemOptions | false;
}): ContextMenuItem[] {
return [
options.duplicate === false
? null
: createDuplicateTaskMenuItem(options.duplicate),
options.delete === false || !options.delete
? null
: createDeleteTaskMenuItem(options.delete),
options.assignResource === false || !options.assignResource
? null
: createAssignResourceMenuItem(options.assignResource),
options.export === false ? null : createExportMenuItem(options.export),
].filter((item): item is ContextMenuItem => item !== null);
}
function createMenuItem(
options: GanttContextMenuBaseItemOptions,
defaults: {
readonly name: ContextMenuItem['name'];
readonly action: (context: GanttContextMenuExtensionContext) => void;
},
): ContextMenuItem {
return {
name: defaults.name,
class: options.class,
rowClass: options.rowClass,
icon: options.icon,
hidden: options.hidden,
keepOpen: options.keepOpen,
action: (event, focused, range, close, actionContext) => {
const context = createExtensionContext(
event,
focused,
range,
close,
actionContext,
);
if (!context) {
return;
}
defaults.action(context);
},
};
}
function createExtensionContext(
event: MouseEvent,
focused: Cell | null | undefined,
range: RangeArea | null | undefined,
close: (() => void) | undefined,
actionContext: ContextMenuActionContext | undefined,
): GanttContextMenuExtensionContext | null {
if (!actionContext) {
return null;
}
const task = getFocusedTask(focused, actionContext.providers);
return {
event,
focused,
range,
close,
revogrid: actionContext.revogrid,
providers: actionContext.providers,
taskId: typeof task?.id === 'string' ? task.id : null,
task,
};
}
function getFocusedTask(
focused: Cell | null | undefined,
providers: PluginProviders,
): Partial<TaskEntity> | null {
if (!focused) {
return null;
}
const model = providers.data.getModel?.(focused.y, 'rgRow') as
| Partial<TaskEntity>
| undefined;
return typeof model?.id === 'string' ? model : null;
}
function createDuplicateTaskOptions(
context: GanttContextMenuExtensionContext,
copyName?: (
task: Partial<TaskEntity>,
context: GanttContextMenuExtensionContext,
) => string,
): GanttCreateTaskOptions | null {
if (!context.taskId || !context.task) {
return null;
}
return {
parentId: context.task.parentId,
insertAfterTaskId: context.taskId,
startDate: context.task.startDate,
endDate: context.task.endDate,
name:
copyName?.(context.task, context) ??
`${context.task.name ?? 'Task'} copy`,
};
}
function dispatchGridEvent<T>(
revogrid: HTMLRevoGridElement,
eventName: string,
detail: T,
): void {
revogrid.dispatchEvent(
new CustomEvent(eventName, {
detail,
bubbles: true,
}),
);
}
EditingDiagnostics

Feature helper examples

Role editability, task editor fields, popover models, and diagnostics summaries.

examples/components/src/components/gantt/recipes/feature-helper-examples.ts
Open preview
Source code
src/gantt/recipes/feature-helper-examples.ts
/**
* @packageDocumentation
* Grouped example API for Gantt editing, validation, diagnostics, and popovers.
*
* These recipes show how to combine the small quick-win helpers into
* application-facing wiring: role-based before-change guards, custom task
* editor forms, task detail popover models, dependency validation summaries,
* resource over-allocation summaries, and composed task validation handlers.
*/
import {
GANTT_BEFORE_ASSIGNMENT_CHANGE_EVENT,
GANTT_BEFORE_DEPENDENCY_CHANGE_EVENT,
GANTT_BEFORE_TASK_CHANGE_EVENT,
type GanttBeforeAssignmentChangeDetail,
type GanttBeforeDependencyChangeDetail,
type GanttBeforeTaskChangeDetail,
} from '@revolist/gantt';
import type {
AssignmentEntity,
ResourceEntity,
TaskEntity,
TaskId,
TaskUpdate,
} from '@revolist/gantt';
import type { SchedulerConflict, SchedulerIssue } from '@revolist/gantt';
import type { ResourcePlanningRowData, GanttGridRow } from '@revolist/gantt';
import {
createGanttRoleEditabilityHelpers,
type GanttBeforeChangeDetailByEventName,
type GanttBeforeChangeEventName,
type GanttRoleEditabilityContext,
type GanttRoleEditabilityPolicy,
} from '@revolist/gantt';
import {
TASK_EDITOR_FIELD_SCHEMA,
createTaskEditorFormValues,
normalizeTaskEditorSubmit,
type NormalizeTaskEditorSubmitOptions,
type TaskEditorFieldId,
type TaskEditorFieldSchema,
type TaskEditorFormValues,
type TaskEditorSubmitResult,
} from '@revolist/gantt';
import {
createTaskDetailPopoverModel,
type TaskDetailPopoverModel,
} from '@revolist/gantt';
import {
buildDependencyValidationSummary,
type DependencyValidationSummary,
type DependencyValidationSummaryInput,
} from '@revolist/gantt';
import {
buildResourceOverallocationSummary,
type ResourceOverallocationSummary,
type ResourceOverallocationSummaryInput,
type ResourceOverallocationLoadSummary,
} from '@revolist/gantt';
import {
createApprovalGateValidator,
createForbiddenDateRangeValidator,
createGanttBeforeTaskChangeValidationHandler,
createLockedPhaseValidator,
createRoleGuardValidator,
validateGanttBeforeTaskChange,
type GanttBeforeTaskChangeValidator,
type GanttDateRange,
type GanttValidationContext,
type GanttValidationDecision,
type GanttValidationReject,
} from './validation-recipes';
export interface GanttExampleCancelableEvent<TDetail> {
readonly detail: TDetail;
preventDefault(): void;
}
export interface GanttRoleBasedEditabilityRecipeOptions {
readonly policy?: GanttRoleEditabilityPolicy;
readonly getRoleContext: () => GanttRoleEditabilityContext;
readonly onDeny?: (
eventName: GanttBeforeChangeEventName,
detail: GanttBeforeChangeDetailByEventName[GanttBeforeChangeEventName],
) => void;
}
export interface GanttRoleBasedEditabilityRecipe {
readonly policy: GanttRoleEditabilityPolicy;
readonly handlers: {
readonly [GANTT_BEFORE_TASK_CHANGE_EVENT]: (
event: GanttExampleCancelableEvent<GanttBeforeTaskChangeDetail>,
) => void;
readonly [GANTT_BEFORE_DEPENDENCY_CHANGE_EVENT]: (
event: GanttExampleCancelableEvent<GanttBeforeDependencyChangeDetail>,
) => void;
readonly [GANTT_BEFORE_ASSIGNMENT_CHANGE_EVENT]: (
event: GanttExampleCancelableEvent<GanttBeforeAssignmentChangeDetail>,
) => void;
};
readonly canEdit: <TEventName extends GanttBeforeChangeEventName>(
eventName: TEventName,
detail: Pick<GanttBeforeChangeDetailByEventName[TEventName], 'action'>,
) => boolean;
}
export interface CustomTaskEditorRecipeOptions {
readonly task: TaskEntity;
readonly resources?: readonly ResourceEntity[];
readonly assignments?: readonly AssignmentEntity[];
readonly fieldIds?: readonly TaskEditorFieldId[];
readonly includeUnchanged?: boolean;
readonly onSubmitPatch?: (patch: TaskUpdate) => void;
}
export interface CustomTaskEditorRecipe {
readonly fields: readonly TaskEditorFieldSchema[];
readonly initialValues: TaskEditorFormValues;
readonly submit: (
values: TaskEditorFormValues,
options?: NormalizeTaskEditorSubmitOptions,
) => TaskEditorSubmitResult;
}
export interface TaskDetailPopoverRecipeOptions {
readonly row: GanttGridRow;
readonly task?: TaskEntity;
readonly resources?: readonly ResourceEntity[];
readonly assignments?: readonly AssignmentEntity[];
}
export interface TaskDetailPopoverRecipe {
readonly model: TaskDetailPopoverModel;
readonly sectionTitles: readonly string[];
readonly warningMessages: readonly string[];
}
export interface DependencyValidationSummaryRecipe {
readonly summary: DependencyValidationSummary;
readonly hasBlockingConflicts: boolean;
readonly badgeLabel: string;
readonly focusTaskIds: readonly TaskId[];
}
export interface ResourceOverallocationSummaryRecipe {
readonly summary: ResourceOverallocationSummary;
readonly hasOverallocations: boolean;
readonly badgeLabel: string;
readonly worstResourceId: string | null;
}
export interface CustomValidationRecipeOptions {
readonly context: GanttValidationContext | (() => GanttValidationContext);
readonly rolePolicy?: GanttRoleEditabilityPolicy;
readonly freezeWindows?: readonly GanttDateRange[];
readonly lockedPhaseIds?: readonly string[];
readonly phaseByTaskId?: Readonly<Record<TaskId, string>>;
readonly validators?: readonly GanttBeforeTaskChangeValidator<GanttValidationContext>[];
readonly onReject?: (rejection: GanttValidationReject) => void;
}
export interface CustomValidationRecipe {
readonly validators: readonly GanttBeforeTaskChangeValidator<GanttValidationContext>[];
readonly handler: (
event: GanttExampleCancelableEvent<GanttBeforeTaskChangeDetail>,
) => void;
readonly validate: (
detail: GanttBeforeTaskChangeDetail,
) => GanttValidationDecision;
}
const DEFAULT_ROLE_POLICY: GanttRoleEditabilityPolicy = {
roles: {
viewer: [],
planner: ['task:*', 'dependency:create', 'dependency:delete'],
resourceManager: ['assignment:edit'],
admin: ['*'],
},
};
const DEFAULT_EDITOR_FIELDS: readonly TaskEditorFieldId[] = [
'name',
'startDate',
'endDate',
'duration',
'progressPercent',
'constraintType',
'constraintDate',
'deadlineDate',
'resourceLabels',
'notes',
];
/**
* Recipe: attach these handlers to the three cancelable Gantt before-change events.
* The app owns role lookup; the helper only turns roles into deterministic decisions.
*/
export function createRoleBasedEditabilityRecipe(
options: GanttRoleBasedEditabilityRecipeOptions,
): GanttRoleBasedEditabilityRecipe {
const policy = options.policy ?? DEFAULT_ROLE_POLICY;
const editability = createGanttRoleEditabilityHelpers(policy);
function handle<TEventName extends GanttBeforeChangeEventName>(
eventName: TEventName,
event: GanttExampleCancelableEvent<
GanttBeforeChangeDetailByEventName[TEventName]
>,
): void {
if (editability.decide(eventName, event.detail, options.getRoleContext())) {
return;
}
event.preventDefault();
options.onDeny?.(eventName, event.detail);
}
return {
policy,
handlers: {
[GANTT_BEFORE_TASK_CHANGE_EVENT]: (event) =>
handle(GANTT_BEFORE_TASK_CHANGE_EVENT, event),
[GANTT_BEFORE_DEPENDENCY_CHANGE_EVENT]: (event) =>
handle(GANTT_BEFORE_DEPENDENCY_CHANGE_EVENT, event),
[GANTT_BEFORE_ASSIGNMENT_CHANGE_EVENT]: (event) =>
handle(GANTT_BEFORE_ASSIGNMENT_CHANGE_EVENT, event),
},
canEdit: (eventName, detail) =>
editability.decide(eventName, detail, options.getRoleContext()),
};
}
/**
* Recipe: use the schema to render any UI form, then normalize submitted values
* into the task mutation patch expected by Gantt task services.
*/
export function createCustomTaskEditorFormRecipe(
options: CustomTaskEditorRecipeOptions,
): CustomTaskEditorRecipe {
const fieldIds = new Set(options.fieldIds ?? DEFAULT_EDITOR_FIELDS);
const fields = TASK_EDITOR_FIELD_SCHEMA.filter((field) =>
fieldIds.has(field.id),
);
const initialValues = createTaskEditorFormValues(
options.task,
options.resources,
options.assignments,
);
return {
fields,
initialValues,
submit: (values, submitOptions) => {
const result = normalizeTaskEditorSubmit(options.task, values, {
includeUnchanged:
submitOptions?.includeUnchanged ?? options.includeUnchanged,
});
if (result.ok) {
options.onSubmitPatch?.(result.patch);
}
return result;
},
};
}
/**
* Recipe: resolve the presentation-free popover model at hover/focus time and
* let the framework render sections, fields, warnings, and resource assignments.
*/
export function createTaskDetailPopoverRecipe(
options: TaskDetailPopoverRecipeOptions,
): TaskDetailPopoverRecipe {
const model = createTaskDetailPopoverModel(options);
const warnings =
model.sections
.find((section) => section.id === 'warnings')
?.fields.flatMap((field) =>
Array.isArray(field.value) ? field.value : [field.displayValue],
)
.filter(Boolean) ?? [];
return {
model,
sectionTitles: model.sections.map((section) => section.title),
warningMessages: warnings,
};
}
/**
* Recipe: combine scheduler diagnostics and projected row warnings into one
* compact dependency-health object for side panels, badges, or command palettes.
*/
export function createDependencyValidationSummaryRecipe(
input: DependencyValidationSummaryInput,
): DependencyValidationSummaryRecipe {
const summary = buildDependencyValidationSummary(input);
const focusTaskIds = [
...new Set(
summary.groups.flatMap((group) =>
group.items.flatMap((item) => item.relatedTaskIds),
),
),
];
return {
summary,
hasBlockingConflicts: summary.conflictCount > 0,
badgeLabel:
summary.totalCount === 0
? 'No schedule issues'
: `${summary.totalCount} schedule issues`,
focusTaskIds,
};
}
/**
* Recipe: feed either projected resource rows, raw load summaries, or scheduler
* conflicts into a summary object that a resource dashboard can render.
*/
export function createResourceOverallocationSummaryRecipe(
input: ResourceOverallocationSummaryInput,
): ResourceOverallocationSummaryRecipe {
const summary = buildResourceOverallocationSummary(input);
return {
summary,
hasOverallocations: summary.totalCount > 0,
badgeLabel:
summary.totalCount === 0
? 'No over-allocation'
: `${summary.totalCount} over-allocation windows`,
worstResourceId: summary.groups[0]?.resourceId ?? null,
};
}
/**
* Recipe: compose custom before-task-change validation from small validators.
* Add this handler to `gantt-before-task-change` and render `onReject` however
* the app reports validation failures.
*/
export function createCustomValidationRecipe(
options: CustomValidationRecipeOptions,
): CustomValidationRecipe {
const configuredValidators: GanttBeforeTaskChangeValidator<GanttValidationContext>[] =
[];
if (options.rolePolicy) {
configuredValidators.push(
createRoleGuardValidator({ policy: options.rolePolicy }),
);
}
if (options.freezeWindows?.length) {
configuredValidators.push(
createForbiddenDateRangeValidator({ ranges: options.freezeWindows }),
);
}
if (options.lockedPhaseIds?.length) {
configuredValidators.push(
createLockedPhaseValidator({
lockedPhaseIds: options.lockedPhaseIds,
getPhaseId: (detail) =>
detail.taskId ? options.phaseByTaskId?.[detail.taskId] : null,
}),
);
}
configuredValidators.push(createApprovalGateValidator());
configuredValidators.push(...(options.validators ?? []));
const resolveContext = (): GanttValidationContext =>
typeof options.context === 'function'
? (options.context as () => GanttValidationContext)()
: options.context;
return {
validators: configuredValidators,
handler: (event) => {
createGanttBeforeTaskChangeValidationHandler({
context: resolveContext,
validators: configuredValidators,
onReject: options.onReject,
})(event as CustomEvent<GanttBeforeTaskChangeDetail>);
},
validate: (detail) =>
validateGanttBeforeTaskChange(
detail,
resolveContext(),
configuredValidators,
),
};
}
export function createResourceLoadSummaryExample(
resourceId: string,
resourceName: string,
planning: ResourcePlanningRowData,
): ResourceOverallocationLoadSummary {
return {
resourceId,
resourceName,
planning,
};
}
export function createSchedulerDiagnosticsExample(
conflicts: readonly SchedulerConflict[] = [],
issues: readonly SchedulerIssue[] = [],
): DependencyValidationSummaryInput {
return {
conflicts,
issues,
};
}
GraphQLPersistence

GraphQL adapter

Configurable project snapshot load/save operations over a GraphQL endpoint.

examples/components/src/components/gantt/recipes/graphql-adapter.ts
Open preview
Source code
src/gantt/recipes/graphql-adapter.ts
/**
* @packageDocumentation
* GraphQL persistence adapter example for Gantt project snapshots.
*
* The adapter wraps configurable GraphQL load and save operations around the
* JSON project snapshot helpers. It accepts an injected `fetch` implementation
* for tests, SSR, and framework adapters, and converts HTTP, GraphQL, response,
* network, and snapshot-validation failures into typed adapter errors.
*/
import type { ProjectSnapshot } from '@revolist/gantt';
import {
exportProjectSnapshot,
parseProjectSnapshotJson,
} from '@revolist/gantt';
export type ProjectSnapshotGraphqlErrorCode =
| 'GRAPHQL_ERROR'
| 'HTTP_ERROR'
| 'INVALID_RESPONSE'
| 'INVALID_SNAPSHOT'
| 'NETWORK_ERROR';
export interface ProjectSnapshotGraphqlErrorDetails {
readonly status?: number;
readonly payload?: unknown;
readonly cause?: unknown;
}
export class ProjectSnapshotGraphqlError extends Error {
constructor(
public readonly code: ProjectSnapshotGraphqlErrorCode,
message: string,
public readonly details: ProjectSnapshotGraphqlErrorDetails = {},
) {
super(message);
this.name = 'ProjectSnapshotGraphqlError';
}
}
export interface ProjectSnapshotGraphqlAdapterOptions {
/** GraphQL HTTP endpoint. */
readonly url: string;
/** Optional headers for authentication, tenancy, or content negotiation. */
readonly headers?: Readonly<Record<string, string>>;
/** Custom fetch implementation for tests, SSR, or framework adapters. */
readonly fetchImpl?: typeof fetch;
/** JSON indentation used for readable save payloads. */
readonly space?: number | string;
/** Query used by load(). Must return a serialized project snapshot string. */
readonly loadQuery?: string;
/** Mutation used by save(). Receives the serialized snapshot in `$snapshot`. */
readonly saveMutation?: string;
/** Response data field read by load(). */
readonly loadField?: string;
/** Response data field checked by save(). */
readonly saveField?: string;
}
export interface ProjectSnapshotGraphqlRequestOptions {
readonly signal?: AbortSignal;
readonly variables?: Readonly<Record<string, unknown>>;
}
interface GraphqlResponsePayload {
readonly data?: Record<string, unknown> | null;
readonly errors?: unknown;
}
const DEFAULT_LOAD_QUERY = `
query LoadProjectSnapshot {
projectSnapshot
}
`;
const DEFAULT_SAVE_MUTATION = `
mutation SaveProjectSnapshot($snapshot: String!) {
saveProjectSnapshot(snapshot: $snapshot)
}
`;
/**
* Framework-neutral example for persisting project snapshots through GraphQL.
*/
export class ProjectSnapshotGraphqlAdapter {
private readonly fetchImpl: typeof fetch;
constructor(private readonly options: ProjectSnapshotGraphqlAdapterOptions) {
this.fetchImpl = options.fetchImpl ?? fetch;
}
async load(
request: ProjectSnapshotGraphqlRequestOptions = {},
): Promise<ProjectSnapshot> {
const payload = await this.execute({
query: this.options.loadQuery ?? DEFAULT_LOAD_QUERY,
variables: request.variables,
signal: request.signal,
});
const field = this.options.loadField ?? 'projectSnapshot';
const snapshotJson = payload.data?.[field];
if (typeof snapshotJson !== 'string') {
throw new ProjectSnapshotGraphqlError(
'INVALID_RESPONSE',
'Project snapshot GraphQL response did not include a serialized snapshot.',
{ payload },
);
}
const parsed = parseProjectSnapshotJson(snapshotJson);
if (parsed.ok === false) {
throw new ProjectSnapshotGraphqlError('INVALID_SNAPSHOT', parsed.error, {
payload: snapshotJson,
});
}
return parsed.project;
}
async save(
project: ProjectSnapshot,
request: ProjectSnapshotGraphqlRequestOptions = {},
): Promise<void> {
const snapshot = exportProjectSnapshot(project, {
space: this.options.space,
});
const payload = await this.execute({
query: this.options.saveMutation ?? DEFAULT_SAVE_MUTATION,
variables: {
...request.variables,
snapshot,
},
signal: request.signal,
});
const field = this.options.saveField ?? 'saveProjectSnapshot';
if (
payload.data &&
field in payload.data &&
payload.data[field] === false
) {
throw new ProjectSnapshotGraphqlError(
'INVALID_RESPONSE',
'Project snapshot GraphQL save operation was rejected.',
{ payload },
);
}
}
private async execute(request: {
readonly query: string;
readonly variables?: Readonly<Record<string, unknown>>;
readonly signal?: AbortSignal;
}): Promise<GraphqlResponsePayload> {
const response = await this.request(() =>
this.fetchImpl(this.options.url, {
method: 'POST',
headers: {
'content-type': 'application/json',
...this.options.headers,
},
body: JSON.stringify({
query: request.query,
variables: request.variables ?? {},
}),
signal: request.signal,
}),
);
const payload = await this.readPayload(response);
if (!response.ok) {
throw new ProjectSnapshotGraphqlError(
'HTTP_ERROR',
'Project snapshot GraphQL request returned an error.',
{
status: response.status,
payload,
},
);
}
if (payload.errors) {
throw new ProjectSnapshotGraphqlError(
'GRAPHQL_ERROR',
'Project snapshot GraphQL operation returned errors.',
{ payload },
);
}
return payload;
}
private async request(run: () => Promise<Response>): Promise<Response> {
try {
return await run();
} catch (error) {
throw new ProjectSnapshotGraphqlError(
'NETWORK_ERROR',
'Project snapshot GraphQL request failed.',
{
cause: error,
},
);
}
}
private async readPayload(
response: Response,
): Promise<GraphqlResponsePayload> {
const text = await response.text();
if (!text) {
return {};
}
try {
const payload = JSON.parse(text) as GraphqlResponsePayload;
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('GraphQL response must be a JSON object.');
}
return payload;
} catch (error) {
throw new ProjectSnapshotGraphqlError(
'INVALID_RESPONSE',
'Project snapshot GraphQL response could not be parsed.',
{
status: response.status,
payload: text,
cause: error,
},
);
}
}
}
GridExport

Grid projection examples

Column presets, Excel row mapping, status indicators, and menu composition.

examples/components/src/components/gantt/recipes/grid-projection-examples.ts
Open preview
Source code
src/gantt/recipes/grid-projection-examples.ts
/**
* @packageDocumentation
* Grouped example API for grid, export, rendering, and context-menu quick wins.
*
* The recipes demonstrate how an application can map projected Gantt rows to
* spreadsheet-safe exports, choose packaged Gantt column presets, derive
* read-only/locked task indicator metadata, and compose context-menu actions
* without depending on internal projection details.
*/
import type { ColumnRegular } from '@revolist/revogrid';
import type { ContextMenuItem } from '@revolist/revogrid-pro';
import type { ProjectSnapshot, TaskId } from '@revolist/gantt';
import {
mapGanttRowsToExcelExport,
type GanttExcelExportRow,
type GanttExcelMappingOptions,
} from '@revolist/gantt';
import {
printGanttContainer,
type GanttPrintDateRange,
type GanttPrintOptions,
type GanttPrintResult,
} from './print-pdf-recipe';
import {
createGanttColumnPreset,
getGanttColumnPresetProps,
type GanttColumnPresetName,
type GanttColumnPresetProp,
} from '@revolist/gantt';
import {
exportGanttExcel,
type GanttExcelExportOptions,
} from '@revolist/gantt';
import {
createTaskStatusIndicators,
type TaskStatusIndicatorProjection,
} from '@revolist/gantt';
import type { GanttGridRow } from '@revolist/gantt';
import {
createGanttContextMenuExtensionItems,
type AssignResourceMenuItemOptions,
type DeleteTaskMenuItemOptions,
type DuplicateTaskMenuItemOptions,
type ExportMenuItemOptions,
type GanttContextMenuExtensionContext,
} from '@revolist/gantt';
export interface GanttExcelExportRecipeOptions extends GanttExcelMappingOptions {
readonly rows: readonly GanttGridRow[];
readonly project: ProjectSnapshot;
}
export interface GanttExcelExportRecipe {
readonly rows: readonly GanttExcelExportRow[];
readonly fieldNames: readonly (keyof GanttExcelExportRow)[];
}
export interface GanttPdfExcelReportingRecipeOptions {
readonly grid: HTMLRevoGridElement;
readonly target: Element | null | undefined;
readonly document?: Document;
readonly window?: GanttPrintOptions['window'];
readonly now?: () => Date | string;
readonly pdf?: Omit<GanttPrintOptions, 'document' | 'window' | 'metadata'>;
readonly excel?: GanttExcelExportOptions;
}
export interface GanttPdfExcelReportingExportOptions {
readonly range?: GanttPrintDateRange;
readonly excel?: GanttExcelExportOptions;
}
export interface GanttPdfExcelReportingRecipe {
exportPdf(
project: ProjectSnapshot,
range?: GanttPrintDateRange,
): GanttPrintResult;
exportExcel(
options?: GanttExcelExportOptions,
): CustomEvent<GanttExcelExportOptions>;
exportReport(
project: ProjectSnapshot,
options?: GanttPdfExcelReportingExportOptions,
): {
readonly pdf: GanttPrintResult;
readonly excel: CustomEvent<GanttExcelExportOptions>;
};
}
export interface GanttColumnPresetRecipeOptions {
readonly preset: GanttColumnPresetName;
readonly extraColumns?: readonly ColumnRegular[];
}
export interface GanttColumnPresetRecipe {
readonly preset: GanttColumnPresetName;
readonly presetProps: readonly GanttColumnPresetProp[];
readonly columns: readonly ColumnRegular[];
}
export interface GanttTaskStatusIndicatorRecipeOptions {
readonly rows: readonly Pick<GanttGridRow, 'id'>[];
readonly projectReadOnly?: boolean;
readonly lockedTaskIds?: ReadonlySet<TaskId> | readonly TaskId[];
readonly labels?: Parameters<typeof createTaskStatusIndicators>[0]['labels'];
readonly classNames?: Parameters<
typeof createTaskStatusIndicators
>[0]['classNames'];
}
export interface GanttTaskStatusIndicatorRecipeRow {
readonly taskId: TaskId;
readonly status: TaskStatusIndicatorProjection;
readonly className: string;
readonly title: string;
readonly ariaLabel: string | undefined;
}
export interface GanttContextMenuQuickWinRecipeOptions {
readonly duplicate?: DuplicateTaskMenuItemOptions | false;
readonly delete?: DeleteTaskMenuItemOptions | false;
readonly assignResource?: AssignResourceMenuItemOptions | false;
readonly export?: ExportMenuItemOptions | false;
}
const EMPTY_EXCEL_FIELD_NAMES = Object.freeze(
[],
) as readonly (keyof GanttExcelExportRow)[];
/**
* Framework-neutral Excel recipe for customers that export the projected Gantt
* task table instead of serializing renderer-only timeline fields.
*/
export function createGanttExcelExportRecipe(
options: GanttExcelExportRecipeOptions,
): GanttExcelExportRecipe {
const rows = mapGanttRowsToExcelExport(options.rows, options.project, {
includeResourceRows: options.includeResourceRows,
});
return {
rows,
fieldNames: rows[0]
? (Object.keys(rows[0]) as (keyof GanttExcelExportRow)[])
: EMPTY_EXCEL_FIELD_NAMES,
};
}
/**
* Framework-neutral reporting recipe for toolbars that offer both stakeholder
* PDF reports and spreadsheet handoff from the same Gantt grid.
*/
export function createGanttPdfExcelReportingRecipe(
options: GanttPdfExcelReportingRecipeOptions,
): GanttPdfExcelReportingRecipe {
const exportPdf = (
project: ProjectSnapshot,
range?: GanttPrintDateRange,
): GanttPrintResult =>
printGanttContainer(options.target, {
...options.pdf,
document: options.document,
window: options.window,
metadata: {
title: project.name,
subtitle: `${project.tasks.length} tasks | ${project.resources.length} resources`,
range,
printedAt: options.now?.() ?? new Date(),
},
});
const exportExcel = (
excelOptions?: GanttExcelExportOptions,
): CustomEvent<GanttExcelExportOptions> =>
exportGanttExcel(options.grid, {
...options.excel,
...excelOptions,
});
return {
exportPdf,
exportExcel,
exportReport(project, reportOptions = {}) {
return {
pdf: exportPdf(project, reportOptions.range),
excel: exportExcel(reportOptions.excel),
};
},
};
}
/**
* Framework-neutral column recipe for switching between Gantt-aware presets and
* customer-defined columns before assigning the result to `grid.columns`.
*/
export function createGanttColumnPresetRecipe(
options: GanttColumnPresetRecipeOptions,
): GanttColumnPresetRecipe {
const presetColumns = createGanttColumnPreset(options.preset);
return {
preset: options.preset,
presetProps: getGanttColumnPresetProps(options.preset),
columns: [...presetColumns, ...(options.extraColumns ?? [])],
};
}
/**
* Framework-neutral visual-indicator recipe for read-only projects and locked
* tasks. The returned class/title values can be bound to a custom cell renderer
* or status column without changing the projected task row.
*/
export function createGanttTaskStatusIndicatorRecipe(
options: GanttTaskStatusIndicatorRecipeOptions,
): readonly GanttTaskStatusIndicatorRecipeRow[] {
const lockedTaskIds = toTaskIdSet(options.lockedTaskIds);
return options.rows.map((row) => {
const status = createTaskStatusIndicators({
taskId: row.id,
projectReadOnly: options.projectReadOnly,
taskLocked: lockedTaskIds.has(row.id),
labels: options.labels,
classNames: options.classNames,
});
return {
taskId: row.id,
status,
className: status.classNames.join(' '),
title: status.label,
ariaLabel: status.label || undefined,
};
});
}
/**
* Framework-neutral context-menu recipe for composing the Gantt quick actions
* with RevoGrid Pro's context-menu plugin.
*/
export function createGanttContextMenuQuickWinRecipe(
options: GanttContextMenuQuickWinRecipeOptions,
): readonly ContextMenuItem[] {
return createGanttContextMenuExtensionItems(options);
}
export function createTaskNameExcelExportMenuItem(
workbookName: string,
): ContextMenuItem {
return createGanttContextMenuQuickWinRecipe({
duplicate: false,
delete: false,
assignResource: false,
export: {
exportOptions: (context: GanttContextMenuExtensionContext) => ({
sheetName: context.task?.name ?? 'Gantt',
workbookName,
}),
},
})[0];
}
function toTaskIdSet(
taskIds: ReadonlySet<TaskId> | readonly TaskId[] | undefined,
): ReadonlySet<TaskId> {
if (!taskIds) {
return new Set();
}
return taskIds instanceof Set ? taskIds : new Set(taskIds);
}
PersistenceRecipes

Persistence examples

Grouped recipes for REST, GraphQL, PostgreSQL, JSON snapshots, print/PDF, and framework metadata.

examples/components/src/components/gantt/recipes/persistence-examples.ts
Open preview
Source code
src/gantt/recipes/persistence-examples.ts
/**
* @packageDocumentation
* Grouped example API for Gantt persistence, reporting, and framework metadata.
*
* The recipes wrap REST, GraphQL, Supabase/PostgreSQL, JSON snapshot, and print
* helpers with injected state and transport dependencies. They are designed as
* copyable integration patterns for applications that own the backend, file
* picker, print shell, or framework-specific demo catalog.
*/
import type { ProjectSnapshot } from '@revolist/gantt';
import {
cloneProjectSnapshot,
exportProjectSnapshot,
parseProjectSnapshotJson,
type ProjectSnapshotParseResult,
} from '@revolist/gantt';
import {
ProjectSnapshotGraphqlAdapter,
type ProjectSnapshotGraphqlAdapterOptions,
type ProjectSnapshotGraphqlRequestOptions,
} from './graphql-adapter';
import {
buildCreateProjectSnapshotTableSql,
buildLoadProjectSnapshotSql,
buildSaveProjectSnapshotSql,
ProjectSnapshotSupabaseAdapter,
type PostgresStatement,
type ProjectSnapshotPostgresOptions,
type ProjectSnapshotPostgresRow,
type SupabaseLikeClient,
} from './postgres-persistence';
import {
printGanttContainer,
type GanttPrintDateRange,
type GanttPrintOptions,
type GanttPrintResult,
} from './print-pdf-recipe';
import {
ProjectSnapshotRestAdapter,
type ProjectSnapshotRestAdapterOptions,
type ProjectSnapshotRestRequestOptions,
} from './rest-adapter';
export type GanttPersistenceRecipeId =
| 'rest-project-snapshot'
| 'graphql-project-snapshot'
| 'supabase-postgres-project-snapshot'
| 'postgres-sql-project-snapshot'
| 'print-pdf-export'
| 'json-snapshot-helpers'
| 'portal-svelte-gantt'
| 'portal-resource-filter';
export type GanttPersistenceRecipeChannel =
| 'persistence'
| 'export'
| 'reporting'
| 'framework';
export type GanttPersistenceRecipeFramework =
| 'framework-neutral'
| 'svelte'
| 'vanilla';
export interface GanttPersistenceRecipeMetadata {
readonly id: GanttPersistenceRecipeId;
readonly title: string;
readonly channel: GanttPersistenceRecipeChannel;
readonly frameworks: readonly GanttPersistenceRecipeFramework[];
readonly sourceFiles: readonly string[];
readonly dependencies: readonly string[];
readonly summary: string;
}
export interface ProjectSnapshotState {
getProject(): ProjectSnapshot;
setProject(project: ProjectSnapshot): void;
}
export interface ProjectSnapshotPersistenceRecipe {
readonly metadata: GanttPersistenceRecipeMetadata;
load(
request?:
| ProjectSnapshotRestRequestOptions
| ProjectSnapshotGraphqlRequestOptions,
): Promise<ProjectSnapshot>;
save(
request?:
| ProjectSnapshotRestRequestOptions
| ProjectSnapshotGraphqlRequestOptions,
): Promise<void>;
}
export interface ProjectSnapshotStorageRecipe {
readonly metadata: GanttPersistenceRecipeMetadata;
load(projectId: string): Promise<ProjectSnapshot | null>;
save(): Promise<ProjectSnapshot>;
}
export interface ProjectSnapshotSqlExecutor {
query<T>(
statement: PostgresStatement,
): Promise<{ readonly rows: readonly T[] }>;
}
export interface ProjectSnapshotPostgresRecipe {
readonly metadata: GanttPersistenceRecipeMetadata;
createTable(): Promise<void>;
load(projectId: string): Promise<ProjectSnapshot | null>;
save(): Promise<ProjectSnapshot>;
buildReportingStatement(projectId: string): PostgresStatement;
}
export interface GanttPrintPdfRecipeOptions {
readonly target: Element | null | undefined;
readonly document?: Document;
readonly window?: GanttPrintOptions['window'];
readonly now?: () => Date | string;
}
export interface GanttPrintPdfRecipe {
readonly metadata: GanttPersistenceRecipeMetadata;
exportProject(
project: ProjectSnapshot,
range?: GanttPrintDateRange,
): GanttPrintResult;
}
export interface ProjectSnapshotJsonRecipeOptions {
readonly readText?: () => string | Promise<string>;
readonly writeText?: (
fileName: string,
contents: string,
) => void | Promise<void>;
readonly fileName?: (project: ProjectSnapshot) => string;
}
export interface ProjectSnapshotJsonRecipe {
readonly metadata: GanttPersistenceRecipeMetadata;
clone(project: ProjectSnapshot): ProjectSnapshot;
export(project: ProjectSnapshot, space?: number | string): string;
parse(json: string): ProjectSnapshotParseResult;
load(): Promise<ProjectSnapshot>;
save(project: ProjectSnapshot, space?: number | string): Promise<string>;
}
export const GANTT_PERSISTENCE_QUICK_WIN_EXAMPLES: readonly GanttPersistenceRecipeMetadata[] =
[
{
id: 'rest-project-snapshot',
title: 'REST project snapshot persistence',
channel: 'persistence',
frameworks: ['framework-neutral'],
sourceFiles: [
'examples/components/src/components/gantt/recipes/rest-adapter.ts',
'packages/gantt/src/gantt/core/project-snapshot-json.ts',
],
dependencies: ['fetch'],
summary:
'Loads and saves a serialized ProjectSnapshot through an injected fetch implementation.',
},
{
id: 'graphql-project-snapshot',
title: 'GraphQL project snapshot persistence',
channel: 'persistence',
frameworks: ['framework-neutral'],
sourceFiles: [
'examples/components/src/components/gantt/recipes/graphql-adapter.ts',
'packages/gantt/src/gantt/core/project-snapshot-json.ts',
],
dependencies: ['fetch'],
summary:
'Uses configurable GraphQL operations and variables to load and save ProjectSnapshot JSON.',
},
{
id: 'supabase-postgres-project-snapshot',
title: 'Supabase/PostgreSQL project snapshot persistence',
channel: 'persistence',
frameworks: ['framework-neutral'],
sourceFiles: [
'examples/components/src/components/gantt/recipes/postgres-persistence.ts',
'packages/gantt/src/gantt/core/project-snapshot-json.ts',
],
dependencies: ['Supabase-like client'],
summary:
'Persists JSONB snapshots with a minimal Supabase-style client shape.',
},
{
id: 'postgres-sql-project-snapshot',
title: 'PostgreSQL SQL project snapshot persistence',
channel: 'reporting',
frameworks: ['framework-neutral'],
sourceFiles: [
'examples/components/src/components/gantt/recipes/postgres-persistence.ts',
],
dependencies: ['PostgreSQL query executor'],
summary:
'Builds parameterized DDL, load, save, and reporting SQL without owning the database client.',
},
{
id: 'print-pdf-export',
title: 'Print/PDF export',
channel: 'export',
frameworks: ['framework-neutral', 'vanilla'],
sourceFiles: [
'examples/components/src/components/gantt/recipes/print-pdf-recipe.ts',
],
dependencies: ['DOM print API'],
summary:
'Prepares a printable Gantt container that users can route to browser print or Save as PDF.',
},
{
id: 'json-snapshot-helpers',
title: 'JSON snapshot helpers',
channel: 'persistence',
frameworks: ['framework-neutral'],
sourceFiles: ['packages/gantt/src/gantt/core/project-snapshot-json.ts'],
dependencies: ['JSON'],
summary:
'Clones, serializes, parses, imports, and exports ProjectSnapshot payloads.',
},
{
id: 'portal-svelte-gantt',
title: 'Portal Svelte Gantt demo',
channel: 'framework',
frameworks: ['svelte'],
sourceFiles: [
'apps/portal/src/components/gantt/GanttSvelteExample.svelte',
'apps/portal/src/components/gantt/GanttSvelteExample.md',
],
dependencies: ['@revolist/svelte-datagrid'],
summary:
'Existing portal reference showing Gantt usage through the Svelte wrapper.',
},
{
id: 'portal-resource-filter',
title: 'Portal resource filter demo',
channel: 'framework',
frameworks: ['framework-neutral', 'vanilla'],
sourceFiles: [
'apps/portal/src/components/gantt/GanttResourceFilterExample.ts',
],
dependencies: ['resourceFilterIds'],
summary:
'Existing portal reference for filtering projected Gantt rows by assigned resource IDs.',
},
];
export function createRestProjectPersistenceRecipe(
options: ProjectSnapshotRestAdapterOptions,
state: ProjectSnapshotState,
): ProjectSnapshotPersistenceRecipe {
const adapter = new ProjectSnapshotRestAdapter(options);
return {
metadata: getPersistenceRecipeMetadata('rest-project-snapshot'),
async load(request) {
const project = await adapter.load(
request as ProjectSnapshotRestRequestOptions | undefined,
);
state.setProject(project);
return project;
},
async save(request) {
await adapter.save(
state.getProject(),
request as ProjectSnapshotRestRequestOptions | undefined,
);
},
};
}
export function createGraphqlProjectPersistenceRecipe(
options: ProjectSnapshotGraphqlAdapterOptions,
state: ProjectSnapshotState,
): ProjectSnapshotPersistenceRecipe {
const adapter = new ProjectSnapshotGraphqlAdapter(options);
return {
metadata: getPersistenceRecipeMetadata('graphql-project-snapshot'),
async load(request) {
const project = await adapter.load(
request as ProjectSnapshotGraphqlRequestOptions | undefined,
);
state.setProject(project);
return project;
},
async save(request) {
await adapter.save(
state.getProject(),
request as ProjectSnapshotGraphqlRequestOptions | undefined,
);
},
};
}
export function createSupabasePostgresPersistenceRecipe(
client: SupabaseLikeClient<ProjectSnapshotPostgresRow>,
state: ProjectSnapshotState,
options: ProjectSnapshotPostgresOptions = {},
): ProjectSnapshotStorageRecipe {
const adapter = new ProjectSnapshotSupabaseAdapter(client, options);
return {
metadata: getPersistenceRecipeMetadata(
'supabase-postgres-project-snapshot',
),
async load(projectId) {
const project = await adapter.load(projectId);
if (project) {
state.setProject(project);
}
return project;
},
async save() {
const project = await adapter.save(state.getProject());
state.setProject(project);
return project;
},
};
}
export function createPostgresSqlPersistenceRecipe(
executor: ProjectSnapshotSqlExecutor,
state: ProjectSnapshotState,
options: ProjectSnapshotPostgresOptions = {},
): ProjectSnapshotPostgresRecipe {
return {
metadata: getPersistenceRecipeMetadata('postgres-sql-project-snapshot'),
async createTable() {
await executor.query(buildCreateProjectSnapshotTableSql(options));
},
async load(projectId) {
const result = await executor.query<{ readonly snapshot: unknown }>(
buildLoadProjectSnapshotSql(projectId, options),
);
const snapshot = result.rows[0]?.snapshot;
if (snapshot === undefined) {
return null;
}
const parsed = parseProjectSnapshotJson(
typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot),
);
if (parsed.ok === false) {
throw new Error(parsed.error);
}
state.setProject(parsed.project);
return parsed.project;
},
async save() {
const project = state.getProject();
const result = await executor.query<ProjectSnapshotPostgresRow>(
buildSaveProjectSnapshotSql(project, options),
);
const persisted = result.rows[0]?.snapshot ?? project;
const parsed = parseProjectSnapshotJson(
typeof persisted === 'string' ? persisted : JSON.stringify(persisted),
);
if (parsed.ok === false) {
throw new Error(parsed.error);
}
state.setProject(parsed.project);
return parsed.project;
},
buildReportingStatement(projectId) {
return {
text: [
'select',
" snapshot->>'id' as project_id,",
" snapshot->>'name' as project_name,",
" jsonb_array_length(snapshot->'tasks') as task_count,",
" jsonb_array_length(snapshot->'resources') as resource_count,",
" jsonb_array_length(snapshot->'dependencies') as dependency_count",
`from ${quoteQualifiedIdentifier(options.tableName ?? 'gantt_project_snapshots')}`,
'where project_id = $1;',
].join('\n'),
values: [projectId],
};
},
};
}
export function createPrintPdfExportRecipe(
options: GanttPrintPdfRecipeOptions,
): GanttPrintPdfRecipe {
return {
metadata: getPersistenceRecipeMetadata('print-pdf-export'),
exportProject(project, range) {
return printGanttContainer(options.target, {
document: options.document,
window: options.window,
metadata: {
title: project.name,
subtitle: `${project.tasks.length} tasks | ${project.resources.length} resources`,
range,
printedAt: options.now?.() ?? new Date(),
},
});
},
};
}
export function createJsonSnapshotRecipe(
options: ProjectSnapshotJsonRecipeOptions = {},
): ProjectSnapshotJsonRecipe {
return {
metadata: getPersistenceRecipeMetadata('json-snapshot-helpers'),
clone: cloneProjectSnapshot,
export(project, space) {
return exportProjectSnapshot(project, { space });
},
parse: parseProjectSnapshotJson,
async load() {
if (!options.readText) {
throw new Error(
'Project snapshot JSON load requires an injected readText function.',
);
}
const parsed = parseProjectSnapshotJson(await options.readText());
if (parsed.ok === false) {
throw new Error(parsed.error);
}
return parsed.project;
},
async save(project, space) {
if (!options.writeText) {
throw new Error(
'Project snapshot JSON save requires an injected writeText function.',
);
}
const contents = exportProjectSnapshot(project, { space });
const fileName =
options.fileName?.(project) ?? `${project.id}-gantt-project.json`;
await options.writeText(fileName, contents);
return contents;
},
};
}
export function getPersistenceRecipeMetadata(
id: GanttPersistenceRecipeId,
): GanttPersistenceRecipeMetadata {
const metadata = GANTT_PERSISTENCE_QUICK_WIN_EXAMPLES.find(
(example) => example.id === id,
);
if (!metadata) {
throw new Error(`Unknown Gantt persistence recipe "${id}".`);
}
return metadata;
}
function quoteQualifiedIdentifier(identifier: string): string {
const parts = identifier.split('.');
if (parts.some((part) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(part))) {
throw new Error(
`PostgreSQL identifier "${identifier}" is not supported by this example.`,
);
}
return parts.map((part) => `"${part}"`).join('.');
}
PostgreSQLSupabase

PostgreSQL persistence

SQL builders and a Supabase-style adapter for project snapshot storage.

examples/components/src/components/gantt/recipes/postgres-persistence.ts
Open preview
Source code
src/gantt/recipes/postgres-persistence.ts
/**
* @packageDocumentation
* PostgreSQL and Supabase-style persistence example for Gantt project snapshots.
*
* The module provides parameterized SQL builders for a JSONB snapshot table and
* a small Supabase-like adapter shape. It intentionally avoids importing a
* concrete database client so applications can use the same snapshot contract
* with Supabase, node-postgres, serverless SQL clients, or tests.
*/
import type { ProjectSnapshot } from '@revolist/gantt';
import {
exportProjectSnapshot,
parseProjectSnapshotJson,
} from '@revolist/gantt';
export const PROJECT_SNAPSHOT_TABLE = 'gantt_project_snapshots';
export interface PostgresStatement {
readonly text: string;
readonly values: readonly unknown[];
}
export interface ProjectSnapshotPostgresOptions {
readonly tableName?: string;
}
export interface ProjectSnapshotPostgresRow {
readonly project_id: string;
readonly version: string;
readonly snapshot: unknown;
readonly updated_at: string;
}
export type ProjectSnapshotPostgresErrorCode =
| 'INVALID_IDENTIFIER'
| 'INVALID_SNAPSHOT'
| 'QUERY_ERROR';
export interface ProjectSnapshotPostgresErrorDetails {
readonly cause?: unknown;
readonly payload?: unknown;
}
export class ProjectSnapshotPostgresError extends Error {
constructor(
public readonly code: ProjectSnapshotPostgresErrorCode,
message: string,
public readonly details: ProjectSnapshotPostgresErrorDetails = {},
) {
super(message);
this.name = 'ProjectSnapshotPostgresError';
}
}
export interface SupabaseLikeError {
readonly message: string;
readonly code?: string;
readonly details?: string;
readonly hint?: string;
}
export interface SupabaseLikeResult<T> {
readonly data: T;
readonly error: SupabaseLikeError | null;
}
export interface SupabaseLikeMaybeSingleQuery<T> {
maybeSingle(): Promise<SupabaseLikeResult<T | null>>;
}
export interface SupabaseLikeSingleQuery<T> {
single(): Promise<SupabaseLikeResult<T>>;
}
export interface SupabaseLikeFilterBuilder<T> {
eq(column: string, value: unknown): SupabaseLikeMaybeSingleQuery<T>;
}
export interface SupabaseLikeUpsertBuilder<T> {
select(columns?: string): SupabaseLikeSingleQuery<T>;
}
export interface SupabaseLikeTable<T> {
select(columns?: string): SupabaseLikeFilterBuilder<T>;
upsert(
row: T,
options?: { readonly onConflict?: string },
): SupabaseLikeUpsertBuilder<T>;
}
export interface SupabaseLikeClient<T> {
from(table: string): SupabaseLikeTable<T>;
}
/**
* SQL shape for storing one JSONB project snapshot per project id.
*/
export function buildCreateProjectSnapshotTableSql(
options: ProjectSnapshotPostgresOptions = {},
): PostgresStatement {
const table = quoteQualifiedIdentifier(
options.tableName ?? PROJECT_SNAPSHOT_TABLE,
);
return {
text: [
`create table if not exists ${table} (`,
' project_id text primary key,',
' version text not null,',
' snapshot jsonb not null,',
' updated_at timestamptz not null,',
" constraint gantt_project_snapshots_snapshot_object check (jsonb_typeof(snapshot) = 'object')",
');',
].join('\n'),
values: [],
};
}
export function buildLoadProjectSnapshotSql(
projectId: string,
options: ProjectSnapshotPostgresOptions = {},
): PostgresStatement {
const table = quoteQualifiedIdentifier(
options.tableName ?? PROJECT_SNAPSHOT_TABLE,
);
return {
text: `select snapshot from ${table} where project_id = $1 limit 1;`,
values: [projectId],
};
}
export function buildSaveProjectSnapshotSql(
project: ProjectSnapshot,
options: ProjectSnapshotPostgresOptions = {},
): PostgresStatement {
const table = quoteQualifiedIdentifier(
options.tableName ?? PROJECT_SNAPSHOT_TABLE,
);
return {
text: [
`insert into ${table} (project_id, version, snapshot, updated_at)`,
'values ($1, $2, $3::jsonb, $4::timestamptz)',
'on conflict (project_id) do update set',
' version = excluded.version,',
' snapshot = excluded.snapshot,',
' updated_at = excluded.updated_at',
'returning project_id, version, snapshot, updated_at;',
].join('\n'),
values: [
project.id,
project.version,
exportProjectSnapshot(project),
project.updatedAt,
],
};
}
/**
* Framework-neutral Supabase-style example for storing ProjectSnapshot JSON.
* The client shape is intentionally local and minimal, so no Supabase package is
* required by the Gantt package or by consumers using another data client.
*/
export class ProjectSnapshotSupabaseAdapter {
constructor(
private readonly client: SupabaseLikeClient<ProjectSnapshotPostgresRow>,
private readonly options: ProjectSnapshotPostgresOptions = {},
) {}
async load(projectId: string): Promise<ProjectSnapshot | null> {
const result = await this.client
.from(this.tableName)
.select('snapshot')
.eq('project_id', projectId)
.maybeSingle();
if (result.error) {
throw createQueryError(result.error);
}
if (!result.data) {
return null;
}
return parseSnapshotPayload(result.data.snapshot);
}
async save(project: ProjectSnapshot): Promise<ProjectSnapshot> {
const result = await this.client
.from(this.tableName)
.upsert(toProjectSnapshotRow(project), { onConflict: 'project_id' })
.select('project_id, version, snapshot, updated_at')
.single();
if (result.error) {
throw createQueryError(result.error);
}
return parseSnapshotPayload(result.data.snapshot);
}
private get tableName(): string {
return this.options.tableName ?? PROJECT_SNAPSHOT_TABLE;
}
}
export function toProjectSnapshotRow(
project: ProjectSnapshot,
): ProjectSnapshotPostgresRow {
return {
project_id: project.id,
version: project.version,
snapshot: JSON.parse(exportProjectSnapshot(project)) as ProjectSnapshot,
updated_at: project.updatedAt,
};
}
function parseSnapshotPayload(payload: unknown): ProjectSnapshot {
const json = typeof payload === 'string' ? payload : JSON.stringify(payload);
const parsed = parseProjectSnapshotJson(json);
if (parsed.ok === false) {
throw new ProjectSnapshotPostgresError('INVALID_SNAPSHOT', parsed.error, {
payload,
});
}
return parsed.project;
}
function createQueryError(
error: SupabaseLikeError,
): ProjectSnapshotPostgresError {
return new ProjectSnapshotPostgresError('QUERY_ERROR', error.message, {
cause: error,
});
}
function quoteQualifiedIdentifier(identifier: string): string {
const parts = identifier.split('.');
if (parts.some((part) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(part))) {
throw new ProjectSnapshotPostgresError(
'INVALID_IDENTIFIER',
`PostgreSQL identifier "${identifier}" is not supported by this example.`,
{ payload: identifier },
);
}
return parts.map((part) => `"${part}"`).join('.');
}
PrintPDF

Print/PDF recipe

DOM print preparation for browser print and Save as PDF workflows.

examples/components/src/components/gantt/recipes/print-pdf-recipe.ts
Open preview
Source code
src/gantt/recipes/print-pdf-recipe.ts
/**
* @packageDocumentation
* Print and Save-as-PDF recipe API for Gantt reporting.
*
* The helpers prepare a target Gantt container for browser printing, attach
* optional report metadata, call an injected `window.print()` implementation,
* and clean up after printing. The browser print dialog remains the PDF engine,
* which keeps this recipe dependency-free and suitable for stakeholder reports.
*/
type ISODateString = import('@revolist/gantt').TaskEntity['startDate'];
import './print-pdf-recipe.css';
export type GanttPrintOrientation = 'portrait' | 'landscape';
export interface GanttPrintDateRange {
readonly startDate?: ISODateString;
readonly endDate?: ISODateString;
readonly label?: string;
}
export interface GanttPrintableMetadataOptions {
readonly title?: string;
readonly subtitle?: string;
readonly range?: GanttPrintDateRange;
readonly printedAt?: Date | string;
}
export interface GanttPrintableMetadata {
readonly title: string;
readonly subtitle?: string;
readonly range?: GanttPrintDateRange;
readonly rangeLabel?: string;
readonly printedAt?: string;
}
export interface GanttPrintStyleOptions {
readonly pageSize?: string;
readonly orientation?: GanttPrintOrientation;
readonly margin?: string;
readonly printRootClass?: string;
readonly printHeaderClass?: string;
readonly hideSelectors?: readonly string[];
readonly preserveColor?: boolean;
}
export interface GanttPrintOptions extends GanttPrintStyleOptions {
readonly metadata?: GanttPrintableMetadataOptions;
readonly document?: Document;
readonly window?: GanttPrintWindow;
readonly cleanupDelayMs?: number;
}
export interface GanttPrintWindow {
print(): void;
removeEventListener?(type: 'afterprint', listener: () => void): void;
addEventListener?(
type: 'afterprint',
listener: () => void,
options?: AddEventListenerOptions,
): void;
}
export interface GanttPrintResult {
readonly ok: boolean;
readonly reason?:
| 'missing-target'
| 'missing-document'
| 'missing-window-print'
| 'print-failed';
readonly error?: unknown;
}
interface NormalizedGanttPrintOptions {
readonly pageSize: string;
readonly orientation: GanttPrintOrientation;
readonly margin: string;
readonly printRootClass: string;
readonly printHeaderClass: string;
readonly hideSelectors: readonly string[];
readonly preserveColor: boolean;
}
const DEFAULT_PRINT_OPTIONS: NormalizedGanttPrintOptions = {
pageSize: 'A4',
orientation: 'landscape',
margin: '12mm',
printRootClass: 'revo-gantt-print-root',
printHeaderClass: 'revo-gantt-print-header',
hideSelectors: [
'[data-gantt-print-hidden]',
'.gantt-toolbar',
'.rgContextMenu',
],
preserveColor: true,
};
export function createGanttPrintOptions(
options: GanttPrintStyleOptions = {},
): NormalizedGanttPrintOptions {
return {
pageSize: options.pageSize ?? DEFAULT_PRINT_OPTIONS.pageSize,
orientation: options.orientation ?? DEFAULT_PRINT_OPTIONS.orientation,
margin: options.margin ?? DEFAULT_PRINT_OPTIONS.margin,
printRootClass:
options.printRootClass ?? DEFAULT_PRINT_OPTIONS.printRootClass,
printHeaderClass:
options.printHeaderClass ?? DEFAULT_PRINT_OPTIONS.printHeaderClass,
hideSelectors: options.hideSelectors ?? DEFAULT_PRINT_OPTIONS.hideSelectors,
preserveColor: options.preserveColor ?? DEFAULT_PRINT_OPTIONS.preserveColor,
};
}
export function createGanttPrintStyles(
options: GanttPrintStyleOptions = {},
): string {
const normalized = createGanttPrintOptions(options);
const hideRule =
normalized.hideSelectors.length > 0
? `${normalized.hideSelectors.join(', ')} { display: none !important; }`
: '';
const colorRule = normalized.preserveColor
? 'print-color-adjust: exact; -webkit-print-color-adjust: exact;'
: '';
return [
'@media print {',
` @page { size: ${normalized.pageSize} ${normalized.orientation}; margin: ${normalized.margin}; }`,
` body.gantt-print-active { ${colorRule} }`,
` body.gantt-print-active:not(.${normalized.printRootClass}) > *:not(.${normalized.printRootClass}):not(.revo-gantt-print-ancestor), body.gantt-print-active .revo-gantt-print-ancestor > *:not(.${normalized.printRootClass}):not(.revo-gantt-print-ancestor) { display: none !important; }`,
' body.gantt-print-active .revo-gantt-print-ancestor { display: block !important; height: auto !important; overflow: visible !important; }',
` .${normalized.printRootClass} { display: block !important; width: 100% !important; overflow: visible !important; }`,
` .${normalized.printRootClass} revo-grid { height: var(--gantt-print-height, 500px) !important; overflow: visible !important; }`,
` .${normalized.printRootClass} .rgRoot { height: 100% !important; overflow: visible !important; }`,
` .${normalized.printHeaderClass} { margin: 0 0 12px; break-after: avoid; }`,
` .${normalized.printHeaderClass} h1 { margin: 0 0 4px; font-size: 18pt; line-height: 1.2; }`,
` .${normalized.printHeaderClass} p { margin: 0; font-size: 9pt; color: #4b5563; }`,
hideRule ? ` ${hideRule}` : '',
'}',
]
.filter(Boolean)
.join('\n');
}
export function createPrintableGanttMetadata(
options: GanttPrintableMetadataOptions = {},
): GanttPrintableMetadata {
const rangeLabel = formatGanttPrintRange(options.range);
const printedAt =
options.printedAt instanceof Date
? options.printedAt.toISOString()
: options.printedAt;
return {
title: normalizeTitle(options.title),
subtitle: normalizeOptionalText(options.subtitle),
range: options.range,
rangeLabel,
printedAt,
};
}
export function formatGanttPrintRange(
range: GanttPrintDateRange | undefined,
): string | undefined {
if (!range) {
return undefined;
}
if (range.label) {
return range.label;
}
if (range.startDate && range.endDate) {
return `${range.startDate} - ${range.endDate}`;
}
return range.startDate ?? range.endDate;
}
export function printGanttContainer(
target: Element | null | undefined,
options: GanttPrintOptions = {},
): GanttPrintResult {
if (!target) {
return { ok: false, reason: 'missing-target' };
}
const ownerDocument = options.document ?? target.ownerDocument;
if (!ownerDocument) {
return { ok: false, reason: 'missing-document' };
}
const printWindow = options.window ?? ownerDocument.defaultView;
if (!printWindow || typeof printWindow.print !== 'function') {
return { ok: false, reason: 'missing-window-print' };
}
const normalized = createGanttPrintOptions(options);
const cleanup = prepareGanttPrintDom(
target,
ownerDocument,
options.metadata,
normalized,
);
let cleanupTimer: number | undefined;
const cleanupOnce = createOnce(() => {
printWindow.removeEventListener?.('afterprint', cleanupOnce);
if (cleanupTimer !== undefined)
ownerDocument.defaultView?.clearTimeout(cleanupTimer);
cleanup();
});
try {
printWindow.addEventListener?.('afterprint', cleanupOnce, {
once: true,
} as AddEventListenerOptions);
if (options.cleanupDelayMs !== undefined) {
cleanupTimer = ownerDocument.defaultView?.setTimeout(
cleanupOnce,
options.cleanupDelayMs,
);
}
printWindow.print();
if (options.cleanupDelayMs === undefined) cleanupOnce();
return { ok: true };
} catch (error) {
cleanupOnce();
return { ok: false, reason: 'print-failed', error };
}
}
function prepareGanttPrintDom(
target: Element,
ownerDocument: Document,
metadataOptions: GanttPrintableMetadataOptions | undefined,
options: NormalizedGanttPrintOptions,
): () => void {
// Virtualized rows need a definite viewport height when print ancestors become
// auto-sized. Capture the current viewport, then restore application styles.
const grids = [
...(target.matches('revo-grid') ? [target as HTMLElement] : []),
...Array.from(target.querySelectorAll<HTMLElement>('revo-grid')),
];
const gridHeights = grids.map((grid) => ({
grid,
previous: grid.style.getPropertyValue('--gantt-print-height'),
priority: grid.style.getPropertyPriority('--gantt-print-height'),
}));
for (const { grid } of gridHeights) {
const height = grid.getBoundingClientRect().height;
if (height > 0)
grid.style.setProperty('--gantt-print-height', `${height}px`);
}
const printStyle = ownerDocument.createElement('style');
printStyle.dataset.ganttPrintStyle = '';
printStyle.textContent = createGanttPrintStyles(options);
ownerDocument.head.appendChild(printStyle);
const body = ownerDocument.body;
const hadActiveClass = body.classList.contains('gantt-print-active');
const addedAncestors: Element[] = [];
for (
let ancestor = target.parentElement;
ancestor && ancestor !== body;
ancestor = ancestor.parentElement
) {
if (!ancestor.classList.contains('revo-gantt-print-ancestor')) {
ancestor.classList.add('revo-gantt-print-ancestor');
addedAncestors.push(ancestor);
}
}
const hadRootClass = target.classList.contains(options.printRootClass);
const hadDefaultRootClass = target.classList.contains(
DEFAULT_PRINT_OPTIONS.printRootClass,
);
const headerClasses = [
DEFAULT_PRINT_OPTIONS.printHeaderClass,
options.printHeaderClass,
].filter(
(className, index, classNames) => classNames.indexOf(className) === index,
);
const header = createPrintHeader(
ownerDocument,
createPrintableGanttMetadata(metadataOptions),
headerClasses.join(' '),
);
body.classList.add('gantt-print-active');
target.classList.add(DEFAULT_PRINT_OPTIONS.printRootClass);
target.classList.add(options.printRootClass);
target.insertBefore(header, target.firstChild);
return () => {
header.remove();
printStyle.remove();
for (const { grid, previous, priority } of gridHeights) {
if (previous)
grid.style.setProperty('--gantt-print-height', previous, priority);
else grid.style.removeProperty('--gantt-print-height');
}
if (!hadRootClass) {
target.classList.remove(options.printRootClass);
}
if (!hadDefaultRootClass) {
target.classList.remove(DEFAULT_PRINT_OPTIONS.printRootClass);
}
for (const ancestor of addedAncestors) {
ancestor.classList.remove('revo-gantt-print-ancestor');
}
if (!hadActiveClass) body.classList.remove('gantt-print-active');
};
}
function createPrintHeader(
ownerDocument: Document,
metadata: GanttPrintableMetadata,
headerClass: string,
): HTMLElement {
const header = ownerDocument.createElement('header');
const title = ownerDocument.createElement('h1');
const details = [
metadata.subtitle,
metadata.rangeLabel,
metadata.printedAt ? `Printed ${metadata.printedAt}` : undefined,
].filter((value): value is string => Boolean(value));
header.className = headerClass;
title.textContent = metadata.title;
header.appendChild(title);
if (details.length > 0) {
const paragraph = ownerDocument.createElement('p');
paragraph.textContent = details.join(' | ');
header.appendChild(paragraph);
}
return header;
}
function normalizeTitle(title: string | undefined): string {
const normalized = title?.trim();
return normalized || 'Gantt schedule';
}
function normalizeOptionalText(value: string | undefined): string | undefined {
const normalized = value?.trim();
return normalized || undefined;
}
function createOnce(callback: () => void): () => void {
let called = false;
return () => {
if (called) {
return;
}
called = true;
callback();
};
}
RESTPersistence

REST adapter

Fetch-based project snapshot persistence with injected request options.

examples/components/src/components/gantt/recipes/rest-adapter.ts
Open preview
Source code
src/gantt/recipes/rest-adapter.ts
/**
* @packageDocumentation
* REST persistence adapter example for Gantt project snapshots.
*
* The adapter loads and saves serialized `ProjectSnapshot` JSON through a
* configurable endpoint and injected `fetch` implementation. It is intended as
* a minimal backend integration pattern that keeps authentication headers,
* abort signals, transport, and storage ownership in the host application.
*/
import type { ProjectSnapshot } from '@revolist/gantt';
import {
exportProjectSnapshot,
parseProjectSnapshotJson,
} from '@revolist/gantt';
export type ProjectSnapshotRestErrorCode =
| 'HTTP_ERROR'
| 'INVALID_SNAPSHOT'
| 'NETWORK_ERROR';
export interface ProjectSnapshotRestErrorDetails {
readonly status?: number;
readonly payload?: unknown;
readonly cause?: unknown;
}
export class ProjectSnapshotRestError extends Error {
constructor(
public readonly code: ProjectSnapshotRestErrorCode,
message: string,
public readonly details: ProjectSnapshotRestErrorDetails = {},
) {
super(message);
this.name = 'ProjectSnapshotRestError';
}
}
export interface ProjectSnapshotRestAdapterOptions {
/** Endpoint that returns and accepts a serialized project snapshot. */
readonly url: string;
/** Optional headers for authentication, tenancy, or content negotiation. */
readonly headers?: Readonly<Record<string, string>>;
/** Custom fetch implementation for tests, SSR, or framework adapters. */
readonly fetchImpl?: typeof fetch;
/** JSON indentation used for readable save payloads. */
readonly space?: number | string;
}
export interface ProjectSnapshotRestRequestOptions {
readonly signal?: AbortSignal;
}
/**
* Framework-neutral example for persisting project snapshots through REST.
*/
export class ProjectSnapshotRestAdapter {
private readonly fetchImpl: typeof fetch;
constructor(private readonly options: ProjectSnapshotRestAdapterOptions) {
this.fetchImpl = options.fetchImpl ?? fetch;
}
async load(
request: ProjectSnapshotRestRequestOptions = {},
): Promise<ProjectSnapshot> {
const response = await this.request(() =>
this.fetchImpl(this.options.url, {
method: 'GET',
headers: this.options.headers,
signal: request.signal,
}),
);
await this.assertOk(response);
const json = await response.text();
const parsed = parseProjectSnapshotJson(json);
if (parsed.ok === false) {
throw new ProjectSnapshotRestError('INVALID_SNAPSHOT', parsed.error, {
status: response.status,
payload: json,
});
}
return parsed.project;
}
async save(
project: ProjectSnapshot,
request: ProjectSnapshotRestRequestOptions = {},
): Promise<void> {
const response = await this.request(() =>
this.fetchImpl(this.options.url, {
method: 'PUT',
headers: {
'content-type': 'application/json',
...this.options.headers,
},
body: exportProjectSnapshot(project, { space: this.options.space }),
signal: request.signal,
}),
);
await this.assertOk(response);
}
private async request(run: () => Promise<Response>): Promise<Response> {
try {
return await run();
} catch (error) {
throw new ProjectSnapshotRestError(
'NETWORK_ERROR',
'Project snapshot request failed.',
{
cause: error,
},
);
}
}
private async assertOk(response: Response): Promise<void> {
if (response.ok) {
return;
}
throw new ProjectSnapshotRestError(
'HTTP_ERROR',
'Project snapshot request returned an error.',
{
status: response.status,
payload: await readErrorPayload(response),
},
);
}
}
async function readErrorPayload(response: Response): Promise<unknown> {
const text = await response.text();
if (!text) {
return undefined;
}
try {
return JSON.parse(text);
} catch {
return text;
}
}
ValidationPermissions

Validation recipes

Forbidden dates, locked phases, approval gates, and role-guard checks.

examples/components/src/components/gantt/recipes/validation-recipes.ts
Open preview
Source code
src/gantt/recipes/validation-recipes.ts
/**
* @packageDocumentation
* Validation recipe API for cancelable Gantt task changes.
*
* The module provides small composable validators for role guards, forbidden
* date ranges, locked phases, and approval gates. Validators can be attached to
* `gantt-before-task-change` through a generated event handler or invoked
* directly before applying task mutations in non-DOM flows.
*/
import type { TaskEntity, TaskId } from '@revolist/gantt';
type ISODateString = import('@revolist/gantt').TaskEntity['startDate'];
import type {
GanttBeforeTaskChangeAction,
GanttBeforeTaskChangeDetail,
} from '@revolist/gantt';
import {
createGanttRoleEditabilityHelpers,
type GanttRoleEditabilityContext,
type GanttRoleEditabilityPolicy,
} from '@revolist/gantt';
export interface GanttValidationDecision {
readonly ok: boolean;
readonly code?: string;
readonly message?: string;
}
export type GanttBeforeTaskChangeValidator<TContext = GanttValidationContext> =
(
detail: GanttBeforeTaskChangeDetail,
context: TContext,
) => GanttValidationDecision | boolean;
export interface GanttValidationContext {
readonly getTaskById?: (taskId: TaskId) => TaskEntity | null | undefined;
readonly roleContext?: GanttRoleEditabilityContext;
readonly approvals?: GanttApprovalRegistry;
}
export interface GanttValidationReject {
readonly detail: GanttBeforeTaskChangeDetail;
readonly decision: GanttValidationDecision;
}
export interface GanttValidationHandlerOptions<TContext> {
readonly context: TContext | (() => TContext);
readonly validators: readonly GanttBeforeTaskChangeValidator<TContext>[];
readonly onReject?: (rejection: GanttValidationReject) => void;
}
export interface GanttDateRange {
readonly startDate: ISODateString;
readonly endDate: ISODateString;
readonly label?: string;
}
export interface ForbiddenDateRangeValidatorOptions {
readonly ranges: readonly GanttDateRange[];
readonly actions?: readonly GanttBeforeTaskChangeAction[];
readonly message?: (range: GanttDateRange) => string;
}
export interface LockedPhaseValidatorOptions<TContext> {
readonly lockedPhaseIds: readonly string[];
readonly getPhaseId: (
detail: GanttBeforeTaskChangeDetail,
context: TContext,
) => string | null | undefined;
readonly actions?: readonly GanttBeforeTaskChangeAction[];
readonly message?: (phaseId: string) => string;
}
export interface ApprovalRecord {
readonly approved: boolean;
readonly approvedBy?: string;
readonly expiresAt?: string;
}
export type GanttApprovalRegistry =
| Readonly<Record<TaskId, ApprovalRecord | undefined>>
| ((
detail: GanttBeforeTaskChangeDetail,
) => ApprovalRecord | null | undefined);
export interface ApprovalGateValidatorOptions<
TContext extends GanttValidationContext,
> {
readonly actions?: readonly GanttBeforeTaskChangeAction[];
readonly requiresApproval?: (
detail: GanttBeforeTaskChangeDetail,
context: TContext,
) => boolean;
readonly isApprovalValid?: (
approval: ApprovalRecord,
detail: GanttBeforeTaskChangeDetail,
context: TContext,
) => boolean;
readonly message?: string;
}
export interface RoleGuardValidatorOptions<
TContext extends GanttValidationContext,
> {
readonly policy: GanttRoleEditabilityPolicy;
readonly getRoleContext?: (
detail: GanttBeforeTaskChangeDetail,
context: TContext,
) => GanttRoleEditabilityContext;
readonly message?: string;
}
export function createGanttBeforeTaskChangeValidationHandler<TContext>(
options: GanttValidationHandlerOptions<TContext>,
): (event: CustomEvent<GanttBeforeTaskChangeDetail>) => void {
return (event) => {
const context =
typeof options.context === 'function'
? (options.context as () => TContext)()
: options.context;
const decision = validateGanttBeforeTaskChange(
event.detail,
context,
options.validators,
);
if (decision.ok) {
return;
}
event.preventDefault();
options.onReject?.({
detail: event.detail,
decision,
});
};
}
export function validateGanttBeforeTaskChange<TContext>(
detail: GanttBeforeTaskChangeDetail,
context: TContext,
validators: readonly GanttBeforeTaskChangeValidator<TContext>[],
): GanttValidationDecision {
for (const validator of validators) {
const decision = normalizeDecision(validator(detail, context));
if (!decision.ok) {
return decision;
}
}
return allow();
}
export function createForbiddenDateRangeValidator<
TContext extends GanttValidationContext,
>(
options: ForbiddenDateRangeValidatorOptions,
): GanttBeforeTaskChangeValidator<TContext> {
const actions = new Set(options.actions);
return (detail, context) => {
if (!matchesAction(actions, detail.action)) {
return allow();
}
const range = resolveProposedTaskRange(detail, context);
if (!range) {
return allow();
}
const forbiddenRange = options.ranges.find((candidate) =>
rangesOverlap(range, candidate),
);
if (!forbiddenRange) {
return allow();
}
return deny(
'forbidden-date-range',
options.message?.(forbiddenRange) ??
`Task changes cannot overlap ${forbiddenRange.label ?? 'the forbidden range'}.`,
);
};
}
export function createLockedPhaseValidator<TContext>(
options: LockedPhaseValidatorOptions<TContext>,
): GanttBeforeTaskChangeValidator<TContext> {
const actions = new Set(options.actions);
const lockedPhaseIds = new Set(options.lockedPhaseIds);
return (detail, context) => {
if (!matchesAction(actions, detail.action)) {
return allow();
}
const phaseId = options.getPhaseId(detail, context);
if (!phaseId || !lockedPhaseIds.has(phaseId)) {
return allow();
}
return deny(
'locked-phase',
options.message?.(phaseId) ?? `Phase "${phaseId}" is locked.`,
);
};
}
export function createApprovalGateValidator<
TContext extends GanttValidationContext,
>(
options: ApprovalGateValidatorOptions<TContext> = {},
): GanttBeforeTaskChangeValidator<TContext> {
const defaultActions: readonly GanttBeforeTaskChangeAction[] = [
'move',
'resize',
'progress',
];
const actions = new Set(options.actions ?? defaultActions);
return (detail, context) => {
if (!matchesAction(actions, detail.action)) {
return allow();
}
if (
options.requiresApproval &&
!options.requiresApproval(detail, context)
) {
return allow();
}
const approval = getApproval(detail, context.approvals);
const isApprovalValid = options.isApprovalValid ?? defaultApprovalCheck;
if (approval && isApprovalValid(approval, detail, context)) {
return allow();
}
return deny(
'approval-required',
options.message ?? 'Task change requires approval.',
);
};
}
export function createRoleGuardValidator<
TContext extends GanttValidationContext,
>(
options: RoleGuardValidatorOptions<TContext>,
): GanttBeforeTaskChangeValidator<TContext> {
const editability = createGanttRoleEditabilityHelpers(options.policy);
return (detail, context) => {
const roleContext = options.getRoleContext?.(detail, context) ??
context.roleContext ?? { roles: undefined };
return editability.canEditTask(detail, roleContext)
? allow()
: deny(
'role-denied',
options.message ?? 'Current role cannot edit this task.',
);
};
}
function resolveProposedTaskRange<TContext extends GanttValidationContext>(
detail: GanttBeforeTaskChangeDetail,
context: TContext,
): GanttDateRange | null {
const task = detail.taskId ? context.getTaskById?.(detail.taskId) : null;
const startDate =
detail.changes.startDate ??
detail.previousValues.startDate ??
task?.startDate;
const endDate =
detail.changes.endDate ?? detail.previousValues.endDate ?? task?.endDate;
return startDate && endDate ? { startDate, endDate } : null;
}
function rangesOverlap(left: GanttDateRange, right: GanttDateRange): boolean {
return left.startDate <= right.endDate && right.startDate <= left.endDate;
}
function matchesAction(
actions: ReadonlySet<GanttBeforeTaskChangeAction>,
action: GanttBeforeTaskChangeAction,
): boolean {
return actions.size === 0 || actions.has(action);
}
function getApproval(
detail: GanttBeforeTaskChangeDetail,
approvals: GanttApprovalRegistry | undefined,
): ApprovalRecord | null | undefined {
if (!approvals || !detail.taskId) {
return null;
}
return typeof approvals === 'function'
? approvals(detail)
: approvals[detail.taskId];
}
function defaultApprovalCheck(approval: ApprovalRecord): boolean {
if (!approval.approved) {
return false;
}
return !approval.expiresAt || Date.parse(approval.expiresAt) > Date.now();
}
function normalizeDecision(
decision: GanttValidationDecision | boolean,
): GanttValidationDecision {
return typeof decision === 'boolean' ? { ok: decision } : decision;
}
function allow(): GanttValidationDecision {
return { ok: true };
}
function deny(code: string, message: string): GanttValidationDecision {
return {
ok: false,
code,
message,
};
}