diff --git a/frontend/src/app/pages/Workflows/ScheduleCalendar.tsx b/frontend/src/app/pages/Workflows/ScheduleCalendar.tsx index 88fa09d0..5407a4d0 100644 --- a/frontend/src/app/pages/Workflows/ScheduleCalendar.tsx +++ b/frontend/src/app/pages/Workflows/ScheduleCalendar.tsx @@ -3,15 +3,14 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Tooltip from '@mui/material/Tooltip'; import Popover from '@mui/material/Popover'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { API_BASE } from '@/shared/config'; import type { Workflow } from '@/shared/state/workflowsSlice'; -import { runWorkflowNow, deleteWorkflow, updateWorkflow, openWorkflowCard } from '@/shared/state/workflowsSlice'; +import { openWorkflowCard, updateWorkflow } from '@/shared/state/workflowsSlice'; import { addWorkflowCard } from '@/shared/state/dashboardLayoutSlice'; -import { WEEKDAY_FULL, WEEKDAY_LABEL_SHORT, addDays, sameDay, startOfMonthGrid, startOfWeek, formatTime, formatHourLabel, stepsSignature } from './scheduleUtils'; +import { useWorkflowMenu } from '@/app/pages/Workflows/app/useWorkflowMenu'; +import { WEEKDAY_FULL, WEEKDAY_LABEL_SHORT, addDays, sameDay, startOfMonthGrid, startOfWeek, formatTime, formatHourLabel } from './scheduleUtils'; import { useWindowedList } from '@/shared/hooks/useWindowedList'; interface Props { @@ -51,53 +50,14 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD const id = setInterval(() => setNow(new Date()), 60_000); return () => clearInterval(id); }, []); - // Right-click menu: pinned position + the workflow whose pill was clicked. Same anchor pattern as MUI's menu examples. - const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; workflow: Workflow } | null>(null); - const closeMenu = () => setCtxMenu(null); - const onRunNow = () => { - if (!ctxMenu) return; - dispatch(runWorkflowNow({ - id: ctxMenu.workflow.id, - signature: stepsSignature(ctxMenu.workflow.steps), - })); - closeMenu(); - }; - const onPauseToggle = () => { - if (!ctxMenu) return; - const wf = ctxMenu.workflow; - dispatch(updateWorkflow({ - id: wf.id, - patch: { schedule: { ...wf.schedule, enabled: !wf.schedule.enabled } as any }, - ifMatch: wf.updated_at || null, - })); - closeMenu(); - }; - const onEdit = () => { - if (!ctxMenu) return; - dispatch(addWorkflowCard({ workflowId: ctxMenu.workflow.id })); - // Right-click "Edit" on a calendar entry opens the new Edit Agent chat view, matching the post-revamp design (Image #38). - dispatch(openWorkflowCard({ workflowId: ctxMenu.workflow.id, view: 'edit_agent' })); - closeMenu(); - }; - const onDelete = () => { - if (!ctxMenu) return; - const ok = window.confirm(`Delete "${ctxMenu.workflow.title}"? Scheduled runs will stop.`); - if (!ok) { closeMenu(); return; } - dispatch(deleteWorkflow(ctxMenu.workflow.id)); - closeMenu(); - }; - const ctxMenuEl = ( - - Run now - {ctxMenu?.workflow.schedule.enabled ? 'Pause schedule' : 'Resume schedule'} - Edit… - Delete - - ); + // Right-click goes through the one shared menu grammar (rows identical to the hub's surfaces); Edit keeps this surface's edit_agent card behavior. + const openWorkflowMenu = useWorkflowMenu(); + const openMenuFor = (ev: React.MouseEvent, wf: Workflow) => openWorkflowMenu(ev, wf, { + onEdit: () => { + dispatch(addWorkflowCard({ workflowId: wf.id })); + dispatch(openWorkflowCard({ workflowId: wf.id, view: 'edit_agent' })); + }, + }); // refDate is recreated on every render unless the caller memoizes it. Pin the calendar to a day-precision key so occurrence fetches only change when the visible day, view, or schedule set changes. const today = refDate || new Date(); const dayKey = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`; @@ -313,7 +273,7 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD maxVisible={compact ? 1 : 3} onSelectWorkflow={onSelectWorkflow} eventFontSize={EVENT_FS} - onContextWorkflow={(wf, ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, workflow: wf }); }} + onContextWorkflow={(wf, ev) => openMenuFor(ev, wf)} /> ); @@ -332,7 +292,6 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD )} - {ctxMenuEl} ); } @@ -370,7 +329,7 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD onSelectWorkflow?.(e.workflow.id, e.date)} - onContextMenu={(ev) => { ev.preventDefault(); ev.stopPropagation(); setCtxMenu({ x: ev.clientX, y: ev.clientY, workflow: e.workflow }); }} + onContextMenu={(ev) => openMenuFor(ev, e.workflow)} sx={{ mt: 0.3, display: 'flex', alignItems: 'center', gap: 0.5, fontSize: EVENT_FS, color: c.text.primary, cursor: 'pointer', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', '&:hover': { color: accent } }}> {formatTime(e.date.getHours(), e.date.getMinutes())} @@ -391,7 +350,6 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD ); })} - {ctxMenuEl} ); } @@ -447,7 +405,7 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD key={row.id} data-wl-id={row.id} onClick={() => onSelectWorkflow?.(e.workflow.id, e.date)} - onContextMenu={(ev) => { ev.preventDefault(); ev.stopPropagation(); setCtxMenu({ x: ev.clientX, y: ev.clientY, workflow: e.workflow }); }} + onContextMenu={(ev) => openMenuFor(ev, e.workflow)} sx={{ display: 'flex', alignItems: 'center', gap: 1.25, px: 2, py: 0.4, @@ -465,7 +423,6 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD {windowing.bottomSpacer > 0 && ( )} - {ctxMenuEl} ); } diff --git a/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx b/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx index 0f2e1a50..499fff55 100644 --- a/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx +++ b/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx @@ -30,6 +30,7 @@ import { isScheduleConfigured, needsScheduleTestWarning, stepsSignature } from ' import ScheduleTestWarningDialog from './ScheduleTestWarningDialog'; import { runWorkflowTest } from './runWorkflowTest'; import { useOpenSidecar } from './WorkflowCardLiveViews'; +import { openCardContextMenu } from '@/app/pages/Dashboard/desktop/openCardContextMenu'; export function statusColor(s: string, c: ReturnType): string { if (s === 'success') return c.status.success; @@ -704,6 +705,13 @@ export function HistoryList({ runs, onOpen, showWorkflow = false, workflowTitleF setExpandedId(expanded ? null : r.id)} + onContextMenu={(e) => openCardContextMenu(e, { + items: [ + { label: 'Open run', onClick: () => onOpen(r) }, + { label: expanded ? 'Collapse' : 'Expand', onClick: () => setExpandedId(expanded ? null : r.id) }, + ...(r.error ? [{ label: 'Copy error', onClick: () => { navigator.clipboard.writeText(r.error || ''); } }] : []), + ], + })} sx={{ display: 'flex', alignItems: 'center', gap: 1.25, py: 0.6, px: 0.5, cursor: 'pointer', borderRadius: c.radius.sm, '&:hover': { bgcolor: c.bg.elevated } }}> {labelForStatus(r.status)} diff --git a/frontend/src/app/pages/Workflows/app/CalendarView.tsx b/frontend/src/app/pages/Workflows/app/CalendarView.tsx index 3a7a4282..33787376 100644 --- a/frontend/src/app/pages/Workflows/app/CalendarView.tsx +++ b/frontend/src/app/pages/Workflows/app/CalendarView.tsx @@ -5,11 +5,13 @@ import { useAppSelector } from '@/shared/hooks'; import { startOfWeek, startOfMonthGrid, addDays, sameDay } from '@/app/pages/Workflows/scheduleUtils'; import { useCalendarOccurrences } from './useCalendarOccurrences'; import { colorForWorkflow, useWC, type WCPalette } from './uiKit'; +import { useWorkflowMenu } from './useWorkflowMenu'; import type { AppNav } from './types'; interface Occ { wfId: string; title: string; at: Date; color: string; } interface DayPop { title: string; runs: Occ[]; x: number; y: number; } type OpenDayPop = (title: string, runs: Occ[], e: React.MouseEvent) => void; +type ContextWorkflow = (wfId: string, e: React.MouseEvent) => void; const DOW = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']; const POP_W = 240; @@ -43,6 +45,11 @@ const CalendarView: React.FC<{ nav: AppNav }> = ({ nav }) => { const id = setInterval(() => setNow(new Date()), 60000); return () => clearInterval(id); }, []); + const openWorkflowMenu = useWorkflowMenu(); + const contextWorkflow: ContextWorkflow = (wfId, e) => { + const wf = items[wfId]; + if (wf) openWorkflowMenu(e, wf, { onEdit: () => nav.selectWorkflow(wfId) }); + }; const ref = nav.refDate; const refKey = `${ref.getFullYear()}-${ref.getMonth()}-${ref.getDate()}`; @@ -111,8 +118,8 @@ const CalendarView: React.FC<{ nav: AppNav }> = ({ nav }) => { {nav.calView === 'month' - ? - : } + ? + : } {dayPop && createPortal(
setDayPop(null)} style={{ position: 'fixed', inset: 0, zIndex: 2147483600 }}> @@ -120,7 +127,7 @@ const CalendarView: React.FC<{ nav: AppNav }> = ({ nav }) => {
{dayPop.title}
{dayPop.runs.map((r, i) => ( -
selectFromPop(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '8px 7px', borderRadius: 8, cursor: 'pointer' }}> +
selectFromPop(r.wfId)} onContextMenu={(e) => contextWorkflow(r.wfId, e)} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '8px 7px', borderRadius: 8, cursor: 'pointer' }}>
{miniTime(r.at)} {r.title} @@ -141,6 +148,7 @@ interface GridProps { dayKey: (d: Date) => string; onSelect: (id: string) => void; openDayPop: OpenDayPop; + onContextWorkflow: ContextWorkflow; } const moreStyle = (WC: WCPalette): CSSProperties => ({ @@ -148,7 +156,7 @@ const moreStyle = (WC: WCPalette): CSSProperties => ({ cursor: 'pointer', alignSelf: 'flex-start', border: 'none', background: 'transparent', }); -const MonthGrid: React.FC = ({ ref0, now, occByDay, dayKey, onSelect, openDayPop }) => { +const MonthGrid: React.FC = ({ ref0, now, occByDay, dayKey, onSelect, openDayPop, onContextWorkflow }) => { const WC = useWC(); const start = startOfMonthGrid(ref0); // Only as many weeks as the month actually spans (5 or 6), like the design, so rows aren't squashed by a dangling extra week of next-month days. @@ -212,7 +220,7 @@ const MonthGrid: React.FC = ({ ref0, now, occByDay, dayKey, onSelect,
{shown.map((r, ri) => ( -
onSelect(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 5, cursor: 'pointer', borderRadius: 4, padding: '1px 3px' }}> +
onSelect(r.wfId)} onContextMenu={(e) => onContextWorkflow(r.wfId, e)} style={{ display: 'flex', alignItems: 'center', gap: 5, cursor: 'pointer', borderRadius: 4, padding: '1px 3px' }}>
{miniTime(r.at)} {r.title} @@ -233,7 +241,7 @@ const MonthGrid: React.FC = ({ ref0, now, occByDay, dayKey, onSelect, ); }; -const WeekGrid: React.FC = ({ ref0, now, occByDay, dayKey, onSelect, openDayPop }) => { +const WeekGrid: React.FC = ({ ref0, now, occByDay, dayKey, onSelect, openDayPop, onContextWorkflow }) => { const WC = useWC(); const start = startOfWeek(ref0); const startMs = start.getTime(); @@ -279,7 +287,7 @@ const WeekGrid: React.FC = ({ ref0, now, occByDay, dayKey, onSelect,
} {runs.slice(0, 3).map((r, ri) => ( -
onSelect(r.wfId)} style={{ display: 'flex', alignItems: 'center', background: r.color, color: '#fff', borderRadius: 999, padding: '1px 7px', fontSize: 10, fontWeight: 600, cursor: 'pointer', lineHeight: 1.2, overflow: 'hidden' }}> +
onSelect(r.wfId)} onContextMenu={(e) => onContextWorkflow(r.wfId, e)} style={{ display: 'flex', alignItems: 'center', background: r.color, color: '#fff', borderRadius: 999, padding: '1px 7px', fontSize: 10, fontWeight: 600, cursor: 'pointer', lineHeight: 1.2, overflow: 'hidden' }}> {r.title} {miniTime(r.at)}
diff --git a/frontend/src/app/pages/Workflows/app/HistoryCard.tsx b/frontend/src/app/pages/Workflows/app/HistoryCard.tsx index 5cc60bfd..52d1d8fd 100644 --- a/frontend/src/app/pages/Workflows/app/HistoryCard.tsx +++ b/frontend/src/app/pages/Workflows/app/HistoryCard.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { fetchRuns } from '@/shared/state/workflowsSlice'; import { openWorkflowMonitor } from '@/shared/state/dashboardLayoutSlice'; +import { openCardContextMenu } from '@/app/pages/Dashboard/desktop/openCardContextMenu'; import { useWC, FONT_SERIF, statusChip, statusDot, statusLabel } from './uiKit'; import type { WCPalette } from './uiKit'; import { toRunRow, whenText } from './model'; @@ -18,6 +19,12 @@ const Row: React.FC<{ entry: Entry; wc: WCPalette; now: Date }> = ({ entry, wc,
openCardContextMenu(e, { + items: [ + { label: 'Open run', disabled: !entry.open, onClick: () => entry.open?.() }, + { label: 'Copy summary', onClick: () => { navigator.clipboard.writeText(entry.summary); } }, + ], + })} style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '9px 0', borderBottom: `1px solid rgba(${wc.inkRGB},0.05)`, cursor: entry.open ? 'pointer' : 'default' }} >
diff --git a/frontend/src/app/pages/Workflows/app/HomeView.tsx b/frontend/src/app/pages/Workflows/app/HomeView.tsx index 0dad456b..cd7f0cf3 100644 --- a/frontend/src/app/pages/Workflows/app/HomeView.tsx +++ b/frontend/src/app/pages/Workflows/app/HomeView.tsx @@ -6,6 +6,7 @@ import { useCalendarOccurrences } from './useCalendarOccurrences'; import { colorForWorkflow, useWC, statusChip, statusDot } from './uiKit'; import { clockOf, whenText } from './model'; import WorkflowTitle from './WorkflowTitle'; +import { useWorkflowMenu } from './useWorkflowMenu'; import type { AppNav } from './types'; interface ComingRun { wfId: string; title: string; time: string; sortKey: number; steps: number; color: string; } @@ -21,6 +22,11 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => { const missed = useAppSelector((s) => s.missedRuns.items); const [missedExpanded, setMissedExpanded] = useState(false); const [expandedDays, setExpandedDays] = useState>(() => new Set()); + const openWorkflowMenu = useWorkflowMenu(); + const menuFor = (wfId: string) => (e: React.MouseEvent) => { + const wf = items[wfId]; + if (wf) openWorkflowMenu(e, wf, { onEdit: () => nav.selectWorkflow(wfId) }); + }; const now = new Date(); const todayLabel = now.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' }); @@ -103,7 +109,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => {
{ongoing.map((o) => ( -
dispatch(openWorkflowMonitor({ workflowId: o.wfId }))} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.10)`, borderRadius: WC.radius.md, padding: '11px 15px', cursor: 'pointer' }}> +
dispatch(openWorkflowMonitor({ workflowId: o.wfId }))} onContextMenu={menuFor(o.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.10)`, borderRadius: WC.radius.md, padding: '11px 15px', cursor: 'pointer' }}>
@@ -178,7 +184,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => {
{(expandedDays.has(g.key) ? g.runs : g.runs.slice(0, COMING_CAP)).map((r, i) => ( -
nav.selectWorkflow(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.08)`, borderRadius: WC.radius.md, padding: '12px 15px', cursor: 'pointer' }}> +
nav.selectWorkflow(r.wfId)} onContextMenu={menuFor(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.08)`, borderRadius: WC.radius.md, padding: '12px 15px', cursor: 'pointer' }}>
{(t) => {t}} @@ -205,7 +211,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => { {recents.length === 0 &&
No runs yet.
}
{recents.map((r) => ( -
nav.selectWorkflow(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: '11px 4px', borderBottom: `1px solid rgba(${WC.inkRGB},0.05)`, cursor: 'pointer' }}> +
nav.selectWorkflow(r.wfId)} onContextMenu={menuFor(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: '11px 4px', borderBottom: `1px solid rgba(${WC.inkRGB},0.05)`, cursor: 'pointer' }}>
diff --git a/frontend/src/app/pages/Workflows/app/LeftRail.tsx b/frontend/src/app/pages/Workflows/app/LeftRail.tsx index d78f0536..8c4179d3 100644 --- a/frontend/src/app/pages/Workflows/app/LeftRail.tsx +++ b/frontend/src/app/pages/Workflows/app/LeftRail.tsx @@ -6,6 +6,7 @@ import { isScheduleActive, describeSchedule } from '@/app/pages/Workflows/schedu import ShareButton from '@/app/components/share/ShareButton'; import { colorForWorkflow, useWC } from './uiKit'; import WorkflowTitle from './WorkflowTitle'; +import { useWorkflowMenu } from './useWorkflowMenu'; import type { AppNav } from './types'; const navBase: CSSProperties = { @@ -20,6 +21,7 @@ const LeftRail: React.FC<{ nav: AppNav }> = ({ nav }) => { const trashCount = useAppSelector((s) => s.workflows.deleted.length); const [query, setQuery] = useState(''); const [hovered, setHovered] = useState(null); + const openWorkflowMenu = useWorkflowMenu(); const workflows = useMemo(() => Object.values(items) .filter((w) => !w.unsaved) @@ -94,6 +96,10 @@ const LeftRail: React.FC<{ nav: AppNav }> = ({ nav }) => {
nav.selectWorkflow(w.id)} + onContextMenu={(e) => openWorkflowMenu(e, w, { + onEdit: () => nav.selectWorkflow(w.id), + afterTrash: () => { if (nav.selectedId === w.id) nav.goHome(); }, + })} onMouseEnter={() => setHovered(w.id)} onMouseLeave={() => setHovered((h) => (h === w.id ? null : h))} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '5px 9px', borderRadius: 8, cursor: 'pointer', background: isSel ? WC.selBg : 'transparent' }} diff --git a/frontend/src/app/pages/Workflows/app/StepsCard.tsx b/frontend/src/app/pages/Workflows/app/StepsCard.tsx index e5aaec16..e4b2fedc 100644 --- a/frontend/src/app/pages/Workflows/app/StepsCard.tsx +++ b/frontend/src/app/pages/Workflows/app/StepsCard.tsx @@ -4,6 +4,7 @@ import { useAppDispatch } from '@/shared/hooks'; import { commitDraft } from '@/shared/state/workflowsSlice'; import type { Workflow, WorkflowStep } from '@/shared/state/workflowsSlice'; import { stepsSignature } from '@/app/pages/Workflows/scheduleUtils'; +import { openCardContextMenu, isNativeMenuTarget } from '@/app/pages/Dashboard/desktop/openCardContextMenu'; import { useWC, FONT_SERIF, FONT_SANS, track, knob } from './uiKit'; import { useWorkflowPatch } from './useWorkflowPatch'; @@ -100,7 +101,21 @@ const StepsCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => {
{local.map((s, i) => ( -
+
{ + if (isNativeMenuTarget(e)) return; + openCardContextMenu(e, { + items: [ + { label: s.open ? 'Collapse' : 'Edit prompt', onClick: () => update(s.id, { open: !s.open }) }, + { label: s.enabled ? 'Disable step' : 'Enable step', onClick: () => toggleEnabled(s.id) }, + { kind: 'separator' }, + { label: 'Remove step', danger: true, onClick: () => onDelete(s.id) }, + ], + }); + }} + style={{ border: `1px solid ${s.open ? `rgba(${WC.inkRGB},0.16)` : `rgba(${WC.inkRGB},0.10)`}`, borderRadius: WC.radius.md, background: s.open ? WC.raised : WC.paper, overflow: 'hidden' }} + >
{i + 1} { {deleted.length > 0 ? (
{deleted.map((w) => ( -
+
openCardContextMenu(e, { + items: [ + { label: 'Restore', onClick: () => dispatch(restoreWorkflow(w.id)) }, + { kind: 'separator' }, + { label: 'Delete forever', danger: true, onClick: () => onPurge(w.id, w.title || 'this workflow') }, + ], + })} + style={{ display: 'flex', alignItems: 'center', gap: 14, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.08)`, borderRadius: WC.radius.md, padding: '13px 16px' }} + >
{w.title || 'Untitled workflow'}
diff --git a/frontend/src/app/pages/Workflows/app/useWorkflowMenu.ts b/frontend/src/app/pages/Workflows/app/useWorkflowMenu.ts new file mode 100644 index 00000000..ca2524db --- /dev/null +++ b/frontend/src/app/pages/Workflows/app/useWorkflowMenu.ts @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; +import type React from 'react'; +import { useAppDispatch, useAppSelector } from '@/shared/hooks'; +import { deleteWorkflow, runWorkflowNow } from '@/shared/state/workflowsSlice'; +import type { Workflow } from '@/shared/state/workflowsSlice'; +import { openWorkflowMonitor } from '@/shared/state/dashboardLayoutSlice'; +import { stepsSignature } from '@/app/pages/Workflows/scheduleUtils'; +import { openCardContextMenu, isNativeMenuTarget } from '@/app/pages/Dashboard/desktop/openCardContextMenu'; +import { isRunning } from './model'; +import { useWorkflowPatch } from './useWorkflowPatch'; + +interface WorkflowMenuOptions { + onEdit: () => void; + afterTrash?: () => void; +} + +/** The one workflow right-click row set, so every surface (rail, home, calendars, popover) offers the same verbs. */ +export function useWorkflowMenu(): (e: React.MouseEvent, wf: Workflow, opts: WorkflowMenuOptions) => void { + const dispatch = useAppDispatch(); + const active = useAppSelector((s) => s.workflows.active); + const patch = useWorkflowPatch(); + return useCallback((e: React.MouseEvent, wf: Workflow, opts: WorkflowMenuOptions) => { + if (isNativeMenuTarget(e)) return; + const running = isRunning(wf, active); + openCardContextMenu(e, { + items: [ + { + label: 'Run now', + disabled: running, + onClick: () => { + dispatch(runWorkflowNow({ id: wf.id, signature: stepsSignature(wf.steps) })); + dispatch(openWorkflowMonitor({ workflowId: wf.id })); + }, + }, + { + label: wf.schedule.enabled ? 'Pause schedule' : 'Resume schedule', + onClick: () => patch(wf, { schedule: { ...wf.schedule, enabled: !wf.schedule.enabled } }), + }, + { label: 'Edit', onClick: opts.onEdit }, + { kind: 'separator' }, + { + label: 'Move to Trash', + danger: true, + onClick: () => { + dispatch(deleteWorkflow(wf.id)); + opts.afterTrash?.(); + }, + }, + ], + }); + }, [dispatch, active, patch]); +}