mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-22 01:24:52 +02:00
[eric] workflows: every hub surface answers right-click through one shared row set, calendar MUI menu and its window.confirm die
This commit is contained in:
@@ -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 = (
|
||||
<Menu
|
||||
open={Boolean(ctxMenu)}
|
||||
onClose={closeMenu}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={ctxMenu ? { top: ctxMenu.y, left: ctxMenu.x } : undefined}>
|
||||
<MenuItem onClick={onRunNow}>Run now</MenuItem>
|
||||
<MenuItem onClick={onPauseToggle}>{ctxMenu?.workflow.schedule.enabled ? 'Pause schedule' : 'Resume schedule'}</MenuItem>
|
||||
<MenuItem onClick={onEdit}>Edit…</MenuItem>
|
||||
<MenuItem onClick={onDelete} sx={{ color: c.status.error }}>Delete</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
// 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)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
@@ -332,7 +292,6 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{ctxMenuEl}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -370,7 +329,7 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD
|
||||
<Box
|
||||
key={`${e.workflow.id}-${idx}`}
|
||||
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={{ 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 } }}>
|
||||
<Box sx={{ width: 6, height: 6, borderRadius: '50%', boxSizing: 'border-box', bgcolor: accent, flexShrink: 0 }} />
|
||||
<span style={{ color: c.text.muted, flexShrink: 0 }}>{formatTime(e.date.getHours(), e.date.getMinutes())}</span>
|
||||
@@ -391,7 +350,6 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
{ctxMenuEl}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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 && (
|
||||
<Box aria-hidden sx={{ height: windowing.bottomSpacer, flexShrink: 0, overflowAnchor: 'none' }} />
|
||||
)}
|
||||
{ctxMenuEl}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<typeof useClaudeTokens>): string {
|
||||
if (s === 'success') return c.status.success;
|
||||
@@ -704,6 +705,13 @@ export function HistoryList({ runs, onOpen, showWorkflow = false, workflowTitleF
|
||||
<Box key={r.id}>
|
||||
<Box
|
||||
onClick={() => 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 } }}>
|
||||
<Box sx={{ fontSize: '0.75rem', fontWeight: 700, color: statusColor(r.status, c), bgcolor: statusBg(r.status, c), px: 0.8, py: 0.3, borderRadius: c.radius.sm, minWidth: 64, textAlign: 'center' }}>
|
||||
{labelForStatus(r.status)}
|
||||
|
||||
@@ -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 }) => {
|
||||
</div>
|
||||
|
||||
{nav.calView === 'month'
|
||||
? <MonthGrid ref0={ref} now={now} occByDay={occByDay} dayKey={dayKey} onSelect={nav.selectWorkflow} openDayPop={openDayPop} />
|
||||
: <WeekGrid ref0={ref} now={now} occByDay={occByDay} dayKey={dayKey} onSelect={nav.selectWorkflow} openDayPop={openDayPop} />}
|
||||
? <MonthGrid ref0={ref} now={now} occByDay={occByDay} dayKey={dayKey} onSelect={nav.selectWorkflow} openDayPop={openDayPop} onContextWorkflow={contextWorkflow} />
|
||||
: <WeekGrid ref0={ref} now={now} occByDay={occByDay} dayKey={dayKey} onSelect={nav.selectWorkflow} openDayPop={openDayPop} onContextWorkflow={contextWorkflow} />}
|
||||
|
||||
{dayPop && createPortal(
|
||||
<div onClick={() => setDayPop(null)} style={{ position: 'fixed', inset: 0, zIndex: 2147483600 }}>
|
||||
@@ -120,7 +127,7 @@ const CalendarView: React.FC<{ nav: AppNav }> = ({ nav }) => {
|
||||
<div style={{ fontFamily: "'Newsreader',serif", fontSize: 15, fontWeight: 500, color: WC.ink, marginBottom: 10, padding: '0 2px' }}>{dayPop.title}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{dayPop.runs.map((r, i) => (
|
||||
<div key={i} onClick={() => selectFromPop(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '8px 7px', borderRadius: 8, cursor: 'pointer' }}>
|
||||
<div key={i} onClick={() => selectFromPop(r.wfId)} onContextMenu={(e) => contextWorkflow(r.wfId, e)} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '8px 7px', borderRadius: 8, cursor: 'pointer' }}>
|
||||
<div style={{ width: 7, height: 7, borderRadius: '50%', background: r.color, flex: 'none' }} />
|
||||
<span style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: 11, color: WC.ink4, flex: 'none', minWidth: 52 }}>{miniTime(r.at)}</span>
|
||||
<span style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 600, color: WC.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.title}</span>
|
||||
@@ -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<GridProps> = ({ ref0, now, occByDay, dayKey, onSelect, openDayPop }) => {
|
||||
const MonthGrid: React.FC<GridProps> = ({ 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<GridProps> = ({ ref0, now, occByDay, dayKey, onSelect,
|
||||
</div>
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: 2, overflow: 'hidden' }}>
|
||||
{shown.map((r, ri) => (
|
||||
<div key={ri} onClick={() => onSelect(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 5, cursor: 'pointer', borderRadius: 4, padding: '1px 3px' }}>
|
||||
<div key={ri} onClick={() => onSelect(r.wfId)} onContextMenu={(e) => onContextWorkflow(r.wfId, e)} style={{ display: 'flex', alignItems: 'center', gap: 5, cursor: 'pointer', borderRadius: 4, padding: '1px 3px' }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: r.color, flex: 'none' }} />
|
||||
<span style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: 10, color: WC.ink4, flex: 'none' }}>{miniTime(r.at)}</span>
|
||||
<span style={{ fontSize: 11, color: WC.ink2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.title}</span>
|
||||
@@ -233,7 +241,7 @@ const MonthGrid: React.FC<GridProps> = ({ ref0, now, occByDay, dayKey, onSelect,
|
||||
);
|
||||
};
|
||||
|
||||
const WeekGrid: React.FC<GridProps> = ({ ref0, now, occByDay, dayKey, onSelect, openDayPop }) => {
|
||||
const WeekGrid: React.FC<GridProps> = ({ 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<GridProps> = ({ ref0, now, occByDay, dayKey, onSelect,
|
||||
<div style={{ position: 'absolute', left: 0, right: 0, top: nowFrac * ROW_H, height: 2, background: WC.accent, zIndex: 3 }} />
|
||||
</>}
|
||||
{runs.slice(0, 3).map((r, ri) => (
|
||||
<div key={ri} onClick={() => 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' }}>
|
||||
<div key={ri} onClick={() => 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' }}>
|
||||
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.title}</span>
|
||||
<span style={{ marginLeft: 'auto', paddingLeft: 6, opacity: 0.85, flex: 'none' }}>{miniTime(r.at)}</span>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
<div
|
||||
onClick={entry.open}
|
||||
title={entry.open ? 'Open this run' : undefined}
|
||||
onContextMenu={(e) => 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' }}
|
||||
>
|
||||
<div style={statusDot(entry.tone, wc)} />
|
||||
|
||||
@@ -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<Set<string>>(() => 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 }) => {
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{ongoing.map((o) => (
|
||||
<div key={o.wfId} onClick={() => 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' }}>
|
||||
<div key={o.wfId} onClick={() => 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' }}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: o.color, flex: 'none' }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
|
||||
@@ -178,7 +184,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => {
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{(expandedDays.has(g.key) ? g.runs : g.runs.slice(0, COMING_CAP)).map((r, i) => (
|
||||
<div key={`${r.wfId}-${i}`} onClick={() => 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' }}>
|
||||
<div key={`${r.wfId}-${i}`} onClick={() => 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' }}>
|
||||
<div style={{ width: 3, height: 30, borderRadius: 3, background: r.color, flex: 'none' }} />
|
||||
<WorkflowTitle value={r.title} animate={animateOf(r.wfId)}>
|
||||
{(t) => <span style={{ fontSize: 14, fontWeight: 600, color: WC.ink, flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t}</span>}
|
||||
@@ -205,7 +211,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => {
|
||||
{recents.length === 0 && <div style={{ fontSize: 13, color: WC.muted }}>No runs yet.</div>}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
{recents.map((r) => (
|
||||
<div key={r.id} onClick={() => nav.selectWorkflow(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: '11px 4px', borderBottom: `1px solid rgba(${WC.inkRGB},0.05)`, cursor: 'pointer' }}>
|
||||
<div key={r.id} onClick={() => 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' }}>
|
||||
<div style={statusDot(r.status, WC)} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<WorkflowTitle value={r.title} animate={animateOf(r.wfId)}>
|
||||
|
||||
@@ -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<string | null>(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 }) => {
|
||||
<div
|
||||
key={w.id}
|
||||
onClick={() => 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' }}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
{local.map((s, i) => (
|
||||
<div key={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' }}>
|
||||
<div
|
||||
key={s.id}
|
||||
onContextMenu={(e) => {
|
||||
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' }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '9px 9px 9px 11px', opacity: s.enabled ? 1 : 0.5 }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: 11, color: WC.faint, width: 13, flex: 'none' }}>{i + 1}</span>
|
||||
<input
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { fetchDeletedWorkflows, restoreWorkflow, purgeWorkflow } from '@/shared/state/workflowsSlice';
|
||||
import { openCardContextMenu } from '@/app/pages/Dashboard/desktop/openCardContextMenu';
|
||||
import { colorForWorkflow, useWC } from './uiKit';
|
||||
import { whenText } from './model';
|
||||
|
||||
@@ -30,7 +31,17 @@ const TrashView: React.FC = () => {
|
||||
{deleted.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 9 }}>
|
||||
{deleted.map((w) => (
|
||||
<div key={w.id} 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' }}>
|
||||
<div
|
||||
key={w.id}
|
||||
onContextMenu={(e) => 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' }}
|
||||
>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', flex: 'none', background: colorForWorkflow(w) }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: WC.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{w.title || 'Untitled workflow'}</div>
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user