Context menu extensions
Duplicate, delete, assign-resource, and export actions for Gantt row menus.
packages/enterprise/plugins/gantt/examples/context-menu-extensions.ts Source code
/**
* @packageDocumentation
* Example API 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 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 task',
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 task',
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, {
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' });
},
});
}
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,
}));
}