Gantt Context Menu
Live example
Section titled “Live example”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
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();
};
}
<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>
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>
);
}
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?.();
}
}
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, },};Row Menu
Section titled “Row Menu”Right-click a task-table row or task bar to open task actions:
AddConvert to milestoneIndentOutdentDelete
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.
Confirming Delete
Section titled “Confirming Delete”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.
Column Menu
Section titled “Column Menu”Right-click main grid headers to open column actions:
Sort ascendingSort descendingFilterHide columnColumns
Visibility follows the active column:
- Sort actions appear only when the target column has
sortable: true. - Filter appears only when the target column has
filterconfigured. - 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.
Timeline Menu
Section titled “Timeline Menu”Right-click the pinned Gantt timeline/canvas header to open timeline actions:
ZoomDate rangeShow todayShow flagswhen 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.
Read-Only And Locked State
Section titled “Read-Only And Locked State”The row menu respects packaged Gantt protection state:
gantt.readOnly: truehides mutating row actions.- Selected locked tasks hide hierarchy actions.
- Mutation services still guard direct calls, so blocked task operations return
readonly-task.
Extending The Menu
Section titled “Extending The Menu”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); }, }, ],};Customization
Section titled “Customization”Gantt context menu customization has two layers:
gantt.contextMenucontrols the generated Gantt row, column, and timeline actions.rowContextMenuandcolumnContextMenuhold 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],};Legacy Context Menu
Section titled “Legacy Context Menu”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.