Skip to content

Gantt Context Menu

Right-click a task to inspect the built-in actions and the custom duplicate, assignment-panel, and workbook actions. The assignment action emits an application event for a host-owned panel.

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

import { GanttPlugin } from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
import './gantt-example-recipes.css';
import {
  RECIPE_ASSIGNMENTS,
  RECIPE_CALENDARS,
  RECIPE_COLUMNS,
  RECIPE_DEPENDENCIES,
  RECIPE_GANTT_CONFIG,
  RECIPE_PANEL_ROWS,
  RECIPE_PANEL_SOURCE_REFERENCES,
  RECIPE_RESOURCES,
  RECIPE_ROW_CONTEXT_MENU,
  RECIPE_TASKS,
  attachRecipeValidation,
} from './GanttExampleRecipesShared';

const { isDark } = currentTheme();

function createPanel(): HTMLElement {
  const panel = document.createElement('aside');
  panel.className = 'gantt-recipe-panel';
  panel.innerHTML = `
    <div class="gantt-recipe-panel__header">
      <strong>Feature recipes</strong>
      <span>Live outputs from exported example helpers</span>
    </div>
    <dl class="gantt-recipe-panel__list">
      ${RECIPE_PANEL_ROWS.map((row) => `
        <div>
          <dt>${row.label}</dt>
          <dd>${row.value}</dd>
        </div>
      `).join('')}
    </dl>
    <div class="gantt-recipe-panel__sources">
      ${RECIPE_PANEL_SOURCE_REFERENCES.map((source) => `<code>${source}</code>`).join('')}
    </div>
  `;
  return panel;
}

export function load(parentSelector: string) {
  const parent = document.querySelector(parentSelector);
  if (!parent) {
    return () => {};
  }

  const root = document.createElement('section');
  root.className = 'gantt-recipe-demo';
  const grid = document.createElement('revo-grid');
  grid.className = 'gantt-recipe-grid';
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.hideAttribution = true;
  grid.plugins = [GanttPlugin];
  grid.columns = RECIPE_COLUMNS;
  grid.gantt = RECIPE_GANTT_CONFIG;
  grid.ganttDependencies = RECIPE_DEPENDENCIES;
  grid.ganttCalendars = RECIPE_CALENDARS;
  grid.ganttResources = RECIPE_RESOURCES;
  grid.ganttAssignments = RECIPE_ASSIGNMENTS;
  grid.rowContextMenu = RECIPE_ROW_CONTEXT_MENU;

  root.appendChild(grid);
  root.appendChild(createPanel());
  parent.appendChild(root);
  const detachValidation = attachRecipeValidation(grid);
  grid.source = RECIPE_TASKS;

  return () => {
    detachValidation();
    root.remove();
  };
}
Vuevue
<template>
  <section class="gantt-recipe-demo">
    <RevoGrid
      ref="gridRef"
      hide-attribution
      class="gantt-recipe-grid"
      :theme="isDark ? 'darkCompact' : 'compact'"
      :plugins="plugins"
      :source="tasks"
      :columns="columns"
      :gantt.prop="ganttConfig"
      :gantt-dependencies.prop="dependencies"
      :gantt-calendars.prop="calendars"
      :gantt-resources.prop="resources"
      :gantt-assignments.prop="assignments"
      :row-context-menu.prop="rowContextMenu"
    />
    <aside class="gantt-recipe-panel">
      <div class="gantt-recipe-panel__header">
        <strong>Feature recipes</strong>
        <span>Live outputs from exported example helpers</span>
      </div>
      <dl class="gantt-recipe-panel__list">
        <div v-for="row in panelRows" :key="row.label">
          <dt>{{ row.label }}</dt>
          <dd>{{ row.value }}</dd>
        </div>
      </dl>
      <div class="gantt-recipe-panel__sources">
        <code v-for="source in sourceReferences" :key="source">{{ source }}</code>
      </div>
    </aside>
  </section>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { GanttPlugin } from '@revolist/gantt';
import { currentThemeVue } from '../composables/useRandomData';
import {
  RECIPE_ASSIGNMENTS,
  RECIPE_CALENDARS,
  RECIPE_COLUMNS,
  RECIPE_DEPENDENCIES,
  RECIPE_GANTT_CONFIG,
  RECIPE_PANEL_ROWS,
  RECIPE_PANEL_SOURCE_REFERENCES,
  RECIPE_RESOURCES,
  RECIPE_ROW_CONTEXT_MENU,
  RECIPE_TASKS,
  attachRecipeValidation,
} from './GanttExampleRecipesShared';

const { isDark } = currentThemeVue();
const gridRef = ref<{ $el?: HTMLRevoGridElement } | null>(null);
const plugins = [GanttPlugin];
const tasks = ref(RECIPE_TASKS);
const columns = ref(RECIPE_COLUMNS);
const dependencies = ref(RECIPE_DEPENDENCIES);
const calendars = ref(RECIPE_CALENDARS);
const resources = ref(RECIPE_RESOURCES);
const assignments = ref(RECIPE_ASSIGNMENTS);
const ganttConfig = computed(() => RECIPE_GANTT_CONFIG);
const rowContextMenu = computed(() => RECIPE_ROW_CONTEXT_MENU);
const panelRows = RECIPE_PANEL_ROWS;
const sourceReferences = RECIPE_PANEL_SOURCE_REFERENCES;
let detachValidation: (() => void) | undefined;

onMounted(() => {
  const grid = gridRef.value?.$el;
  if (grid) {
    detachValidation = attachRecipeValidation(grid);
  }
});

onBeforeUnmount(() => {
  detachValidation?.();
});
</script>

<style src="./gantt-example-recipes.css"></style>
Reacttsx
import React, { useEffect, useMemo, useRef } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { GanttPlugin } from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
import {
  RECIPE_ASSIGNMENTS,
  RECIPE_CALENDARS,
  RECIPE_COLUMNS,
  RECIPE_DEPENDENCIES,
  RECIPE_GANTT_CONFIG,
  RECIPE_PANEL_ROWS,
  RECIPE_PANEL_SOURCE_REFERENCES,
  RECIPE_RESOURCES,
  RECIPE_ROW_CONTEXT_MENU,
  RECIPE_TASKS,
  attachRecipeValidation,
} from './GanttExampleRecipesShared';

const { isDark } = currentTheme();

export default function GanttExampleRecipes() {
  const gridRef = useRef<HTMLRevoGridElement | null>(null);
  const plugins = useMemo(() => [GanttPlugin], []);
  const columns = useMemo(() => RECIPE_COLUMNS, []);
  const gantt = useMemo(() => RECIPE_GANTT_CONFIG, []);
  const rowContextMenu = useMemo(() => RECIPE_ROW_CONTEXT_MENU, []);

  useEffect(() => {
    const grid = gridRef.current;
    return grid ? attachRecipeValidation(grid) : undefined;
  }, []);

  return (
    <section style={{
      display: 'grid',
      gridTemplateColumns: 'minmax(0, 1fr) 300px',
      gap: 16,
      minHeight: 540,
    }}>
      <RevoGrid
        ref={gridRef}
        style={{ minHeight: 540 }}
        theme={isDark() ? 'darkCompact' : 'compact'}
        hideAttribution
        plugins={plugins}
        source={RECIPE_TASKS}
        columns={columns}
        gantt={gantt}
        ganttDependencies={RECIPE_DEPENDENCIES}
        ganttCalendars={RECIPE_CALENDARS}
        ganttResources={RECIPE_RESOURCES}
        ganttAssignments={RECIPE_ASSIGNMENTS}
        rowContextMenu={rowContextMenu}
      />
      <aside style={{
        borderLeft: '1px solid #d8dee8',
        padding: '12px 0 12px 16px',
        font: '13px/1.4 system-ui, sans-serif',
      }}>
        <div style={{ display: 'grid', gap: 3, marginBottom: 14 }}>
          <strong>Feature recipes</strong>
          <span style={{ color: '#64748b' }}>Live outputs from exported example helpers</span>
        </div>
        <dl style={{ display: 'grid', gap: 10, margin: 0 }}>
          {RECIPE_PANEL_ROWS.map((row) => (
            <div key={row.label} style={{ display: 'grid', gap: 2 }}>
              <dt style={{ color: '#64748b' }}>{row.label}</dt>
              <dd style={{ margin: 0, fontWeight: 600 }}>{row.value}</dd>
            </div>
          ))}
        </dl>
        <div style={{ display: 'grid', gap: 5, marginTop: 18 }}>
          {RECIPE_PANEL_SOURCE_REFERENCES.map((source) => (
            <code key={source} style={{ whiteSpace: 'normal', fontSize: 11, color: '#475569' }}>{source}</code>
          ))}
        </div>
      </aside>
    </section>
  );
}
Angularts
import { Component, NO_ERRORS_SCHEMA, ViewChild, ViewEncapsulation, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RevoGrid } from '@revolist/angular-datagrid';
import { GanttPlugin } from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
import {
  RECIPE_ASSIGNMENTS,
  RECIPE_CALENDARS,
  RECIPE_COLUMNS,
  RECIPE_DEPENDENCIES,
  RECIPE_GANTT_CONFIG,
  RECIPE_PANEL_ROWS,
  RECIPE_PANEL_SOURCE_REFERENCES,
  RECIPE_RESOURCES,
  RECIPE_ROW_CONTEXT_MENU,
  RECIPE_TASKS,
  attachRecipeValidation,
} from './GanttExampleRecipesShared';

@Component({
  selector: 'gantt-example-recipes-grid',
  standalone: true,
  schemas: [NO_ERRORS_SCHEMA],
  imports: [CommonModule, RevoGrid],
  template: `
    <section class="gantt-recipe-demo">
      <revo-grid
        #grid
        class="gantt-recipe-grid"
        [theme]="theme"
        [hideAttribution]="true"
        [plugins]="plugins"
        [source]="tasks"
        [columns]="columns"
        [gantt]="ganttConfig"
        [ganttDependencies]="dependencies"
        [ganttCalendars]="calendars"
        [ganttResources]="resources"
        [ganttAssignments]="assignments"
        [rowContextMenu]="rowContextMenu"
      ></revo-grid>
      <aside class="gantt-recipe-panel">
        <div class="gantt-recipe-panel__header">
          <strong>Feature recipes</strong>
          <span>Live outputs from exported example helpers</span>
        </div>
        <dl class="gantt-recipe-panel__list">
          <div *ngFor="let row of panelRows">
            <dt>{{ row.label }}</dt>
            <dd>{{ row.value }}</dd>
          </div>
        </dl>
        <div class="gantt-recipe-panel__sources">
          <code *ngFor="let source of sourceReferences">{{ source }}</code>
        </div>
      </aside>
    </section>
  `,
  styleUrls: ['./gantt-example-recipes.css'],
  encapsulation: ViewEncapsulation.None,
})
export class GanttExampleRecipesGridComponent implements AfterViewInit, OnDestroy {
  @ViewChild('grid', { read: ElementRef }) grid?: ElementRef<HTMLRevoGridElement>;

  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  plugins = [GanttPlugin];
  tasks = RECIPE_TASKS;
  columns = RECIPE_COLUMNS;
  ganttConfig = RECIPE_GANTT_CONFIG;
  dependencies = RECIPE_DEPENDENCIES;
  calendars = RECIPE_CALENDARS;
  resources = RECIPE_RESOURCES;
  assignments = RECIPE_ASSIGNMENTS;
  rowContextMenu = RECIPE_ROW_CONTEXT_MENU;
  panelRows = RECIPE_PANEL_ROWS;
  sourceReferences = RECIPE_PANEL_SOURCE_REFERENCES;
  private detachValidation?: () => void;

  ngAfterViewInit(): void {
    const grid = this.grid?.nativeElement;
    if (grid) {
      this.detachValidation = attachRecipeValidation(grid);
    }
  }

  ngOnDestroy(): void {
    this.detachValidation?.();
  }
}
Sharedts
import type { ContextMenuConfig } from '@revolist/revogrid-pro';
import type {
  GanttBeforeTaskChangeDetail,
  GanttPluginConfig,
  GanttTaskSourceRow,
  TaskEntity,
} from '@revolist/gantt';
import {
  createCustomTaskEditorFormRecipe,
  createCustomValidationRecipe,
  createGanttColumnPresetRecipe,
  createGanttTaskStatusIndicatorRecipe,
} from './recipes';
import { createGanttContextMenuExtensionItems } from '@revolist/gantt';
import {
  ASSIGNMENTS,
  CALENDAR_ID,
  DEPENDENCIES,
  GANTT_BASE_CONFIG,
  RESOURCES,
  STANDARD_CALENDAR,
  TASKS,
} from './shared/gantt-project-data';

export const RECIPE_GANTT_CONFIG = {
  ...GANTT_BASE_CONFIG,
  id: 'project-gantt-recipes',
  name: 'Gantt Feature Recipes',
  primaryCalendarId: CALENDAR_ID,
  updatedAt: '2026-04-06T00:00:00Z',
  statusDate: '2026-04-06',
  zoomPreset: 'week-month' as const,
  allowTaskCreate: true,
  visuals: {
    showTaskLabels: true,
    showTodayLine: true,
    shadeNonWorkingTime: true,
  },
} satisfies GanttPluginConfig;

const lockedTaskIds = ['t7'];
const taskById = new Map(TASKS.map((task) => [task.id, task]));
function toRecipeTask(task: GanttTaskSourceRow | undefined): TaskEntity | undefined {
  if (!task) {
    return undefined;
  }

  return {
    ...task,
    projectId: RECIPE_GANTT_CONFIG.id,
    parentId: task.parentId ?? null,
    wbsCode: '',
    type: task.type ?? 'task',
    workflowStatus: task.workflowStatus ?? 'not-started',
    duration: task.duration ?? 0,
    progressPercent: task.percentDone ?? 0,
    calendarId: String(task.calendarId ?? CALENDAR_ID),
    isCritical: false,
    tags: Array.isArray(task.tags) ? task.tags : [],
  } as TaskEntity;
}
const columnPreset = createGanttColumnPresetRecipe({ preset: 'core' });
const editorRecipe = createCustomTaskEditorFormRecipe({
  task: toRecipeTask(taskById.get('t4'))!,
  resources: RESOURCES,
  assignments: ASSIGNMENTS,
  fieldIds: ['name', 'startDate', 'endDate', 'progressPercent', 'resourceLabels'],
});
const statusIndicators = createGanttTaskStatusIndicatorRecipe({
  rows: TASKS.map((task) => ({ id: task.id })),
  lockedTaskIds,
});
const validationRecipe = createCustomValidationRecipe({
  context: {
    getTaskById: (taskId) => toRecipeTask(taskById.get(taskId)),
    roleContext: { roles: ['planner'] },
  },
  rolePolicy: {
    roles: {
      planner: ['task:move', 'task:resize', 'task:progress'],
    },
  },
  freezeWindows: [
    { startDate: '2026-04-14', endDate: '2026-04-18', label: 'design freeze' },
  ],
});

const validationDetail: GanttBeforeTaskChangeDetail = {
  action: 'move',
  taskId: 't4',
  changes: {
    startDate: '2026-04-15',
    endDate: '2026-04-18',
  },
  previousValues: {
    startDate: '2026-04-13',
    endDate: '2026-04-24',
  },
};
const validationDecision = validationRecipe.validate(validationDetail);

export const RECIPE_COLUMNS = columnPreset.columns;
export const RECIPE_TASKS = TASKS.map((task) =>
  lockedTaskIds.includes(task.id) ? { ...task, locked: true } : task);
export const RECIPE_DEPENDENCIES = DEPENDENCIES;
export const RECIPE_CALENDARS = [STANDARD_CALENDAR];
export const RECIPE_RESOURCES = RESOURCES;
export const RECIPE_ASSIGNMENTS = ASSIGNMENTS;
export const RECIPE_ROW_CONTEXT_MENU: ContextMenuConfig = {
  items: [
    ...createGanttContextMenuExtensionItems({
      duplicate: {
        icon: 'fa-solid fa-copy',
        copyName: (task) => `${task.name ?? 'Task'} copy`,
      },
      delete: false,
      assignResource: {
        icon: 'fa-solid fa-user-plus',
        name: 'Open assignment panel',
        onAssignResource: (context) => {
          context.revogrid.dispatchEvent(new CustomEvent('gantt-recipe-action', {
            bubbles: true,
            detail: { action: 'assign-resource', taskId: context.taskId },
          }));
        },
      },
      export: {
        icon: 'fa-solid fa-file-excel',
        name: 'Export task workbook',
        exportOptions: (context) => ({
          sheetName: context.task?.name ?? 'Gantt',
          workbookName: 'gantt-recipes.xlsx',
        }),
      },
    }),
  ],
};

export const RECIPE_PANEL_ROWS = [
  {
    label: 'Column preset',
    value: `${columnPreset.presetProps.length} core fields`,
  },
  {
    label: 'Editor form',
    value: `${editorRecipe.fields.length} fields for ${editorRecipe.initialValues.name}`,
  },
  {
    label: 'Status indicators',
    value: `${statusIndicators.filter((row) => row.status.indicators.length > 0).length} protected rows`,
  },
  {
    label: 'Validation result',
    value: validationDecision.ok ? 'Allowed' : validationDecision.message ?? 'Blocked',
  },
  {
    label: 'Row menu extensions',
    value: `${RECIPE_ROW_CONTEXT_MENU.items.length} custom actions`,
  },
] as const;

export const RECIPE_PANEL_SOURCE_REFERENCES = [
  'packages/gantt/src/gantt/features/context-menu/context-menu-extensions.ts',
  'examples/components/src/components/gantt/recipes/feature-helper-examples.ts',
  'examples/components/src/components/gantt/recipes/grid-projection-examples.ts',
  'examples/components/src/components/gantt/recipes/validation-recipes.ts',
] as const;

export function attachRecipeValidation(grid: HTMLRevoGridElement): () => void {
  const handler = validationRecipe.handler as unknown as EventListener;
  grid.addEventListener('gantt-before-task-change', handler);
  return () => grid.removeEventListener('gantt-before-task-change', handler);
}

Gantt contributes task and timeline actions through the RevoGrid Pro DataGridContextMenuPlugin; it does not replace your rowContextMenu, columnContextMenu, or dataGridContextMenu configuration. Gantt-compatible shared grid commands are composed first, then dataGridContextMenu.getItems and application items can extend the result.

The integration is enabled by default for every GanttPlugin instance. Set gantt.contextMenu to false to opt out of generated Gantt row, column, and timeline menu items while leaving application-provided RevoGrid context menu configuration untouched:

grid.gantt = {
...project,
contextMenu: false,
};

You can also disable one generated surface at a time:

grid.gantt = {
...project,
contextMenu: {
row: false,
column: true,
},
};

Right-click a task-table row or task bar to open task actions:

  • Add
  • Convert to milestone
  • Indent
  • Outdent
  • Delete

The default actions dispatch Gantt events:

grid.addEventListener('gantt-task-create', (event) => {
const { parentId, insertAfterTaskId } = event.detail ?? {};
});
grid.addEventListener('gantt-task-indent', (event) => {
indentTask(event.detail?.taskId);
});
grid.addEventListener('gantt-task-outdent', (event) => {
outdentTask(event.detail?.taskId);
});
grid.addEventListener('gantt-task-delete', (event) => {
deleteTask(event.detail?.taskId);
});

Add uses the focused row or clicked task bar as the insertion anchor. When the anchor has descendants, Gantt inserts the new sibling after the full descendant block so the task tree stays valid. Context-menu creation names the row New task 1, New task 2, and so on, and does not open the task editor.

The generated row menu is suppressed on the pinned add-task draft row because that row is owned by the internal GanttAddTaskRowPlugin and is not a task entity. If you disable Gantt context-menu integration and provide your own rowContextMenu, use isGanttAddTaskRow() or the draft cell marker to skip that row. See Add Task Row.

The built-in Delete item dispatches the normal Gantt delete command, then Gantt emits the cancelable gantt-before-task-change event with detail.action === 'delete' immediately before it mutates any task. Call preventDefault() to keep the task (and its descendants) unchanged.

grid.addEventListener('gantt-before-task-change', (event) => {
if (event.detail.action !== 'delete') return;
const name = event.detail.previousValues.name ?? 'this task';
if (!window.confirm(`Delete ${name}?`)) {
event.preventDefault();
}
});

This protects deletion initiated from the default context-menu item and direct Gantt delete methods, so it is the preferred place for permission checks and a synchronous browser confirmation. It is called once per affected task; applications that allow bulk deletion should make their confirmation copy clear that descendants may also be removed.

gantt-before-task-change is synchronous. For an asynchronous application modal, prevent the original deletion, keep the selected task ids in application state, and call gantt.deleteTasks(taskIds) only after the user confirms. Guard the follow-up call so the same listener does not reopen the dialog, and recheck permissions before performing it.

Right-click main grid headers to open column actions:

  • Sort ascending
  • Sort descending
  • Filter
  • Hide column
  • Columns

Visibility follows the active column:

  • Sort actions appear only when the target column has sortable: true.
  • Filter appears only when the target column has filter configured.
  • Hide column is available for a leaf task-table column; Columns opens the standard column-management dialog.
  • If contextual filtering leaves no visible items, the menu is not opened.

Sort, filtering, hiding, and column management reuse the shared RevoGrid data-grid context-menu commands. Gantt removes incompatible generic commands such as row mutation, formatting, pinning, and schema operations.

Right-click the pinned Gantt timeline/canvas header to open timeline actions:

  • Zoom
  • Date range
  • Show today
  • Show flags when milestone lines are configured

Zoom controls are hidden when the current zoom level cannot move further in either direction. Date range changes the visible timeline range; the visual toggles remain open after changing their checked state.

The row menu respects packaged Gantt protection state:

  • gantt.readOnly: true hides mutating row actions.
  • Selected locked tasks hide hierarchy actions.
  • Mutation services still guard direct calls, so blocked task operations return readonly-task.

Use the normal RevoGrid Pro menu config surfaces. Gantt keeps your custom items and contributes its generated items without mutating those configurations.

grid.rowContextMenu = {
items: [
{
name: 'Open task details',
action: (_event, focused, _range, _close, context) => {
const task = context?.providers.data.getModel?.(focused?.y ?? 0, 'rgRow');
openTaskDetails(task?.id);
},
},
],
};

Gantt context menu customization has two layers:

  • gantt.contextMenu controls the generated Gantt row, column, and timeline actions.
  • rowContextMenu and columnContextMenu hold application menu config that should remain in the grid even when generated Gantt actions are disabled.

Use gantt.contextMenu = false when the application should fully own the menu:

grid.rowContextMenu = {
items: [
{
name: 'Open planning panel',
action: (_event, focused, _range, close, context) => {
const task = context?.providers.data.getModel?.(focused?.y ?? 0, 'rgRow');
openPlanningPanel(task?.id);
close();
},
},
],
};
grid.gantt = {
...project,
contextMenu: false,
};

Use partial opt-out when you want custom row actions but still want generated column and timeline actions:

grid.rowContextMenu = {
items: [
{
name: 'Assign owner',
action: (_event, focused, _range, close, context) => {
const task = context?.providers.data.getModel?.(focused?.y ?? 0, 'rgRow');
openOwnerPicker(task?.id);
close();
},
},
],
};
grid.gantt = {
...project,
contextMenu: {
row: false,
column: true,
},
};

Keep gantt.contextMenu enabled when you want to append app actions after the default Gantt actions:

grid.rowContextMenu = {
items: [
{
name: 'Show audit log',
action: (_event, focused, _range, close, context) => {
const task = context?.providers.data.getModel?.(focused?.y ?? 0, 'rgRow');
showTaskAuditLog(task?.id);
close();
},
},
],
};
grid.gantt = project;

You can also use a resolve function on the base RevoGrid menu config. Gantt preserves the resolver and the context menu plugin applies it to the composed menu:

grid.rowContextMenu = {
items: [
{ name: 'Open task details', action: openTaskDetailsFromMenu },
],
resolve(context) {
return context.target === 'row' ? undefined : null;
},
};

The packaged task editor item is controlled separately:

grid.ganttTaskEditorDialog = {
contextMenu: false,
};

Set ganttTaskEditorDialog.contextMenu to true when gantt.contextMenu is disabled but the application still wants the default Edit… item. Leave it unset to follow gantt.contextMenu.

For packaged extension items, use the built-in Gantt context-menu helpers:

import { createGanttContextMenuExtensionItems } from '@revolist/gantt';
const customItems = createGanttContextMenuExtensionItems({
duplicate: {
copyName: (task) => `${task.name ?? 'Task'} copy`,
},
assignResource: {
onAssignResource: (context) => {
if (context.taskId) {
openResourcePicker(context.taskId);
}
},
},
delete: false,
export: { name: 'Export task rows' },
});
grid.rowContextMenu = {
...grid.rowContextMenu,
items: [...(grid.rowContextMenu?.items ?? []), ...customItems],
};

rowContextMenu and columnContextMenu remain application-owned low-level menu configuration. Use dataGridContextMenu.getItems when you need to transform the Gantt-compatible shared grid commands: registered Gantt contributions run first, then this application resolver receives the filtered items.

Gantt registers and disposes its own contribution with the plugin lifecycle. It does not rewrite host menu properties, so application-provided items are not duplicated during Gantt configuration updates.

The packaged task editor follows gantt.contextMenu by default for its Edit… row-menu item. Set ganttTaskEditorDialog.contextMenu to false to disable only the editor item, or true to keep the editor item when generated Gantt menus are disabled.