From f993d76874e859dbd96ab75e64d2e0fa9e640a94 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 6 Jun 2026 16:39:56 -0500 Subject: [PATCH] refactor(desktop): converge cron overlay onto profiles' split layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cron's manage overlay now uses the shared OverlaySplitLayout (sidebar list + main detail) instead of a bespoke PageSearchShell + grid, matching profiles. Extract OverlayNewButton (the "+ New …" sidebar action) so profiles and cron share one component — its hover underline is scoped to the label span so it never strokes the leading icon glyph. --- apps/desktop/src/app/cron/index.tsx | 290 ++++++++---------- .../src/app/overlays/overlay-split-layout.tsx | 27 ++ apps/desktop/src/app/profiles/index.tsx | 13 +- 3 files changed, 151 insertions(+), 179 deletions(-) diff --git a/apps/desktop/src/app/cron/index.tsx b/apps/desktop/src/app/cron/index.tsx index 5da1c62b822..63ca465fb77 100644 --- a/apps/desktop/src/app/cron/index.tsx +++ b/apps/desktop/src/app/cron/index.tsx @@ -14,6 +14,7 @@ import { DialogTitle } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' +import { SearchField } from '@/components/ui/search-field' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' import { @@ -35,8 +36,8 @@ import { $cronFocusJobId, setCronFocusJobId } from '@/store/cron' import { notify, notifyError } from '@/store/notifications' import { useRefreshHotkey } from '../hooks/use-refresh-hotkey' +import { OverlayMain, OverlayNewButton, OverlaySidebar, OverlaySplitLayout } from '../overlays/overlay-split-layout' import { OverlayView } from '../overlays/overlay-view' -import { PageSearchShell } from '../page-search-shell' import type { SetStatusbarItemGroup } from '../shell/statusbar-controls' import { jobState, jobTitle, STATE_DOT } from './job-state' @@ -243,17 +244,11 @@ interface CronViewProps extends React.ComponentProps<'section'> { setStatusbarItemGroup?: SetStatusbarItemGroup } -export function CronView({ - onClose, - onOpenSession, - setStatusbarItemGroup: _setStatusbarItemGroup, - ...props -}: CronViewProps) { +export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setStatusbarItemGroup }: CronViewProps) { const { t } = useI18n() const c = t.cron const [jobs, setJobs] = useState(null) const [query, setQuery] = useState('') - const [refreshing, setRefreshing] = useState(false) const [busyJobId, setBusyJobId] = useState(null) // Master/detail: the job whose schedule + run history fill the right pane. const [selectedJobId, setSelectedJobId] = useState(null) @@ -267,15 +262,11 @@ export function CronView({ const [deleting, setDeleting] = useState(false) const refresh = useCallback(async () => { - setRefreshing(true) - try { const result = await getCronJobs() setJobs(result) } catch (err) { notifyError(err, c.failedLoad) - } finally { - setRefreshing(false) } }, [c]) @@ -328,7 +319,6 @@ export function CronView({ }) }, [selectedJob]) - const enabledCount = jobs?.filter(job => job.enabled).length ?? 0 const totalCount = jobs?.length ?? 0 async function handlePauseResume(job: CronJob) { @@ -411,83 +401,62 @@ export function CronView({ return ( - void refresh()} - size="icon-xs" - title={refreshing ? c.refreshing : c.refresh} - type="button" - variant="ghost" - > - - - } - searchValue={query} - > - {!jobs ? ( - - ) : visibleJobs.length === 0 ? ( - // Empty state owns the primary "create" CTA — we used to also have - // one in the filters bar but it was redundant. Only show the button - // when there are zero jobs total; the search-empty case ("No - // matches") just asks the user to broaden their query. - setEditor({ mode: 'create' }) : undefined} - title={totalCount === 0 ? c.emptyTitleNew : c.emptyTitleSearch} - /> - ) : ( - // Master/detail: job list on the left, the selected job's schedule, - // actions, and run history on the right. Replaces the old accordion - // (collapse-in-row inside a modal) — fewer clicks, no nested toggles. -
-
- - {c.active(enabledCount, totalCount)} - - -
-
-
- {visibleJobs.map(job => ( - setSelectedJobId(job.id)} - /> - ))} + {!jobs ? ( + + ) : ( + + + setEditor({ mode: 'create' })} /> + {totalCount > 0 && ( + + )} + {visibleJobs.map(job => ( + setSelectedJobId(job.id)} + /> + ))} + {visibleJobs.length === 0 && ( +

+ {totalCount === 0 ? c.emptyTitleNew : c.emptyTitleSearch} +

+ )} +
+ + + {selectedJob ? ( + setPendingDelete(selectedJob)} + onEdit={() => setEditor({ mode: 'edit', job: selectedJob })} + onOpenSession={onOpenSession} + onPauseResume={() => void handlePauseResume(selectedJob)} + onTrigger={() => void handleTrigger(selectedJob)} + /> + ) : ( +
+
+ +

{totalCount === 0 ? c.emptyDescNew : c.emptyDescSearch}

+
-
- {selectedJob && ( - setPendingDelete(selectedJob)} - onEdit={() => setEditor({ mode: 'edit', job: selectedJob })} - onOpenSession={onOpenSession} - onPauseResume={() => void handlePauseResume(selectedJob)} - onTrigger={() => void handleTrigger(selectedJob)} - /> - )} -
-
-
- )} - setEditor({ mode: 'closed' })} onSave={handleEditorSave} /> + )} + + + )} + + setEditor({ mode: 'closed' })} onSave={handleEditorSave} /> !open && !deleting && setPendingDelete(null)} open={pendingDelete !== null}> @@ -513,7 +482,6 @@ export function CronView({ - ) } @@ -534,21 +502,21 @@ function CronJobListRow({ return ( ) } @@ -579,53 +547,66 @@ function CronJobDetail({ return (
-
-
- {jobTitle(job)} - {c.states[state] ?? state} - {deliver && deliver !== DEFAULT_DELIVER && ( - {c.deliveryLabels[deliver] ?? deliver} - )} +
+
+
+
+
+
+

{jobTitle(job)}

+ {c.states[state] ?? state} + {deliver && deliver !== DEFAULT_DELIVER && ( + {c.deliveryLabels[deliver] ?? deliver} + )} +
+
+ + + {jobScheduleDisplay(job)} + + + {c.last} {formatTime(job.last_run_at)} + + + {c.next} {formatTime(job.next_run_at)} + +
+
+
+ + + + +
+
+ + {prompt &&

{prompt}

} + {job.last_error && ( +

+ + {job.last_error} +

+ )} +
+ +
-
- - - {jobScheduleDisplay(job)} - - - {c.last} {formatTime(job.last_run_at)} - - - {c.next} {formatTime(job.next_run_at)} - -
- {prompt &&

{prompt}

} - {job.last_error && ( -

- - {job.last_error} -

- )} -
- - - - -
-
-
-
) @@ -731,33 +712,6 @@ function StatePill({ children, tone }: { children: string; tone: keyof typeof PI ) } -function EmptyState({ - actionLabel, - description, - onAction, - title -}: { - actionLabel?: string - description: string - onAction?: () => void - title: string -}) { - return ( -
-
-
{title}
-

{description}

- {actionLabel && onAction && ( - - )} -
-
- ) -} - function CronEditorDialog({ editor, onClose, diff --git a/apps/desktop/src/app/overlays/overlay-split-layout.tsx b/apps/desktop/src/app/overlays/overlay-split-layout.tsx index e713e4ea49e..fd562b40e28 100644 --- a/apps/desktop/src/app/overlays/overlay-split-layout.tsx +++ b/apps/desktop/src/app/overlays/overlay-split-layout.tsx @@ -1,5 +1,7 @@ import type { ReactNode } from 'react' +import { Button } from '@/components/ui/button' +import { Codicon } from '@/components/ui/codicon' import type { IconComponent } from '@/lib/icons' import { cn } from '@/lib/utils' @@ -73,6 +75,31 @@ export function OverlayMain({ children, className }: OverlayMainProps) { ) } +// Boxless "+ New …" action that tops an OverlaySidebar list (profiles, cron, …). +// The text variant underlines on hover, which also strokes the icon glyph — so +// we keep the button itself underline-free and underline only the label span. +export function OverlayNewButton({ + icon = 'add', + label, + onClick +}: { + icon?: string + label: string + onClick: () => void +}) { + return ( + + ) +} + export function OverlayNavItem({ active, icon: Icon, label, nested, onClick, trailing }: OverlayNavItemProps) { return ( + setCreateOpen(true)} /> {profiles.map(profile => (