mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Merge pull request #73040 from NousResearch/bb/context-menu-parity-2
Right-click parity for cron, webhooks, and profile rows
This commit is contained in:
commit
b6244959a6
5 changed files with 207 additions and 136 deletions
|
|
@ -1,15 +1,18 @@
|
|||
import { useStore } from '@nanostores/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { ActionsContextMenu, type MenuKit, renderActionItem } from '@/components/ui/actions-menu'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { DisclosureCaret } from '@/components/ui/disclosure-caret'
|
||||
import { GlyphSpinner } from '@/components/ui/glyph-spinner'
|
||||
import { SidebarGroup, SidebarGroupContent } from '@/components/ui/sidebar'
|
||||
import { Tip } from '@/components/ui/tooltip'
|
||||
import { getCronJobRuns, type SessionInfo } from '@/hermes'
|
||||
import { deleteCronJob, getCronJobRuns, pauseCronJob, resumeCronJob, type SessionInfo } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { fmtDayTime, relativeTime } from '@/lib/time'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { updateCronJobs } from '@/store/cron'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
import { $selectedStoredSessionId } from '@/store/session'
|
||||
import type { CronJob } from '@/types/hermes'
|
||||
|
||||
|
|
@ -191,75 +194,128 @@ function CronJobSidebarRow({
|
|||
const state = jobState(job)
|
||||
const next = nextRunMs(job)
|
||||
const label = jobTitle(job)
|
||||
const isPaused = state === 'paused'
|
||||
|
||||
const meta = INACTIVE_STATES.has(state) ? (c.states[state] ?? state) : next !== null ? relativeTime(next, nowMs) : '—'
|
||||
|
||||
// Pause/resume and delete aren't threaded through the sidebar's prop chain, so
|
||||
// drive them against the shared $cronJobs atom directly (same path the cron
|
||||
// overlay uses) — the sidebar and overlay render from that one atom, so the
|
||||
// row updates in place.
|
||||
const togglePause = async () => {
|
||||
try {
|
||||
const updated = isPaused ? await resumeCronJob(job.id) : await pauseCronJob(job.id)
|
||||
updateCronJobs(rows => rows.map(row => (row.id === job.id ? updated : row)))
|
||||
notify({ kind: 'success', title: isPaused ? c.resumed : c.paused, message: label })
|
||||
} catch (err) {
|
||||
notifyError(err, c.failedUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
const remove = async () => {
|
||||
if (!window.confirm(`${c.deleteDescPrefix}${label}${c.deleteDescSuffix}`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteCronJob(job.id)
|
||||
updateCronJobs(rows => rows.filter(row => row.id !== job.id))
|
||||
notify({ kind: 'success', title: c.deleted, message: label })
|
||||
} catch (err) {
|
||||
notifyError(err, c.failedDelete)
|
||||
}
|
||||
}
|
||||
|
||||
// One action set for both the hover buttons and the right-click menu.
|
||||
const items = (kit: MenuKit) => (
|
||||
<>
|
||||
{renderActionItem(kit, { icon: 'zap', key: 'trigger', label: c.triggerNow, onSelect: onTrigger })}
|
||||
{renderActionItem(kit, {
|
||||
icon: isPaused ? 'play' : 'debug-pause',
|
||||
key: 'pause',
|
||||
label: isPaused ? c.resume : c.pause,
|
||||
onSelect: () => void togglePause()
|
||||
})}
|
||||
{renderActionItem(kit, { icon: 'watch', key: 'manage', label: c.manage, onSelect: onManage })}
|
||||
<kit.Separator />
|
||||
{renderActionItem(kit, {
|
||||
icon: 'trash',
|
||||
key: 'delete',
|
||||
label: t.common.delete,
|
||||
onSelect: () => void remove(),
|
||||
variant: 'destructive'
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="group/cron relative grid min-h-[1.625rem] grid-cols-[minmax(0,1fr)_auto] items-center rounded-md hover:bg-(--chrome-action-hover)">
|
||||
{/* Lead with the dot in the same w-3.5 cell + pl-2 the session rows use
|
||||
so the cron dots line up with the sessions above; the caret sits next
|
||||
to the label (matching the other sidebar disclosures) and the whole
|
||||
label area toggles the run peek. */}
|
||||
<Tip label={label}>
|
||||
<button
|
||||
aria-expanded={expanded}
|
||||
aria-label={expanded ? c.hideRuns : c.showRuns}
|
||||
className="flex min-w-0 items-center gap-1.5 bg-transparent py-0.5 pl-2 pr-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
||||
onClick={onTogglePeek}
|
||||
type="button"
|
||||
>
|
||||
<span className="grid w-3.5 shrink-0 place-items-center">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
<ActionsContextMenu ariaLabel={c.actionsFor(label)} contentClassName="w-44" items={items}>
|
||||
<div className="group/cron relative grid min-h-[1.625rem] grid-cols-[minmax(0,1fr)_auto] items-center rounded-md hover:bg-(--chrome-action-hover)">
|
||||
{/* Lead with the dot in the same w-3.5 cell + pl-2 the session rows use
|
||||
so the cron dots line up with the sessions above; the caret sits next
|
||||
to the label (matching the other sidebar disclosures) and the whole
|
||||
label area toggles the run peek. */}
|
||||
<Tip label={label}>
|
||||
<button
|
||||
aria-expanded={expanded}
|
||||
aria-label={expanded ? c.hideRuns : c.showRuns}
|
||||
className="flex min-w-0 items-center gap-1.5 bg-transparent py-0.5 pl-2 pr-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
||||
onClick={onTogglePeek}
|
||||
type="button"
|
||||
>
|
||||
<span className="grid w-3.5 shrink-0 place-items-center">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'size-1 rounded-full',
|
||||
STATE_DOT[state] ?? 'bg-(--ui-text-quaternary)',
|
||||
state === 'running' && 'size-1.5 animate-pulse'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-[0.8125rem] text-(--ui-text-secondary) group-hover/cron:text-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<DisclosureCaret
|
||||
className={cn(
|
||||
'size-1 rounded-full',
|
||||
STATE_DOT[state] ?? 'bg-(--ui-text-quaternary)',
|
||||
state === 'running' && 'size-1.5 animate-pulse'
|
||||
'shrink-0 text-(--ui-text-tertiary) transition',
|
||||
expanded ? 'opacity-100' : 'opacity-0 group-hover/cron:opacity-100'
|
||||
)}
|
||||
open={expanded}
|
||||
/>
|
||||
</button>
|
||||
</Tip>
|
||||
{/* Trailing cluster: countdown by default, quick actions on hover. */}
|
||||
<div className="flex items-center gap-0.5 justify-self-end pr-1">
|
||||
<span className="text-[0.6875rem] text-(--ui-text-tertiary) tabular-nums group-hover/cron:hidden">
|
||||
{meta}
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-[0.8125rem] text-(--ui-text-secondary) group-hover/cron:text-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<DisclosureCaret
|
||||
className={cn(
|
||||
'shrink-0 text-(--ui-text-tertiary) transition',
|
||||
expanded ? 'opacity-100' : 'opacity-0 group-hover/cron:opacity-100'
|
||||
)}
|
||||
open={expanded}
|
||||
/>
|
||||
</button>
|
||||
</Tip>
|
||||
{/* Trailing cluster: countdown by default, quick actions on hover. */}
|
||||
<div className="flex items-center gap-0.5 justify-self-end pr-1">
|
||||
<span className="text-[0.6875rem] text-(--ui-text-tertiary) tabular-nums group-hover/cron:hidden">
|
||||
{meta}
|
||||
</span>
|
||||
<div className="hidden items-center gap-0.5 group-hover/cron:flex">
|
||||
<Tip label={c.triggerNow}>
|
||||
<button
|
||||
aria-label={c.triggerNow}
|
||||
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
||||
onClick={onTrigger}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="zap" size="0.75rem" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label={c.manage}>
|
||||
<button
|
||||
aria-label={c.manage}
|
||||
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
||||
onClick={onManage}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="watch" size="0.75rem" />
|
||||
</button>
|
||||
</Tip>
|
||||
<div className="hidden items-center gap-0.5 group-hover/cron:flex">
|
||||
<Tip label={c.triggerNow}>
|
||||
<button
|
||||
aria-label={c.triggerNow}
|
||||
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
||||
onClick={onTrigger}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="zap" size="0.75rem" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label={c.manage}>
|
||||
<button
|
||||
aria-label={c.manage}
|
||||
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
||||
onClick={onManage}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="watch" size="0.75rem" />
|
||||
</button>
|
||||
</Tip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ActionsContextMenu>
|
||||
{expanded && <CronJobSidebarRuns jobId={job.id} onOpenRun={onOpenRun} />}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,10 +63,10 @@ import {
|
|||
PanelHeader,
|
||||
PanelList,
|
||||
PanelListRow,
|
||||
type PanelMenuItem,
|
||||
PanelMeta,
|
||||
PanelPill,
|
||||
type PanelPillTone,
|
||||
PanelRowMenu,
|
||||
PanelSectionLabel
|
||||
} from '../overlays/panel'
|
||||
import type { SetStatusbarItemGroup } from '../shell/statusbar-controls'
|
||||
|
|
@ -501,14 +501,11 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt
|
|||
active={selectedJob?.id === job.id}
|
||||
job={job}
|
||||
key={job.id}
|
||||
menu={
|
||||
<PanelRowMenu
|
||||
items={[
|
||||
{ icon: 'edit', label: c.edit, onSelect: () => setEditor({ mode: 'edit', job }) },
|
||||
{ icon: 'trash', label: t.common.delete, onSelect: () => setPendingDelete(job), tone: 'danger' }
|
||||
]}
|
||||
/>
|
||||
}
|
||||
menuItems={[
|
||||
{ icon: 'edit', label: c.edit, onSelect: () => setEditor({ mode: 'edit', job }) },
|
||||
{ icon: 'trash', label: t.common.delete, onSelect: () => setPendingDelete(job), tone: 'danger' }
|
||||
]}
|
||||
menuLabel={c.manage}
|
||||
onSelect={() => setSelectedJobId(job.id)}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -571,12 +568,14 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt
|
|||
function CronJobListRow({
|
||||
active,
|
||||
job,
|
||||
menu,
|
||||
menuItems,
|
||||
menuLabel,
|
||||
onSelect
|
||||
}: {
|
||||
active: boolean
|
||||
job: CronJob
|
||||
menu?: React.ReactNode
|
||||
menuItems?: PanelMenuItem[]
|
||||
menuLabel?: string
|
||||
onSelect: () => void
|
||||
}) {
|
||||
const state = jobState(job)
|
||||
|
|
@ -585,7 +584,8 @@ function CronJobListRow({
|
|||
<PanelListRow
|
||||
active={active}
|
||||
dotClassName={STATE_DOT[state] ?? 'bg-muted-foreground'}
|
||||
menu={menu}
|
||||
menuItems={menuItems}
|
||||
menuLabel={menuLabel}
|
||||
onSelect={onSelect}
|
||||
rowKey={job.id}
|
||||
title={jobTitle(job)}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ReactNode } from 'react'
|
||||
|
||||
import { ActionsContextMenu, ActionsMenu, type MenuKit, renderActionItem } from '@/components/ui/actions-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { RowButton } from '@/components/ui/row-button'
|
||||
import { SearchField } from '@/components/ui/search-field'
|
||||
import { Tip } from '@/components/ui/tooltip'
|
||||
|
|
@ -149,8 +149,13 @@ interface PanelListRowProps {
|
|||
icon?: string
|
||||
// Custom leading element (colored swatch, avatar, …). Wins over dot/icon.
|
||||
lead?: ReactNode
|
||||
// Trailing per-row kebab menu (pass a <PanelRowMenu/>). Reveals on hover/focus.
|
||||
// Per-row actions. Pass `menuItems` to get BOTH the hover kebab and a matching
|
||||
// right-click menu from one array (preferred). `menu` takes a raw node for the
|
||||
// rare custom trigger; it gets no right-click parity.
|
||||
menu?: ReactNode
|
||||
menuItems?: PanelMenuItem[]
|
||||
// aria/tooltip label for the kebab + right-click menu built from `menuItems`.
|
||||
menuLabel?: string
|
||||
// Short always-visible trailing meta (a tag/time, like the trace label's duration).
|
||||
meta?: ReactNode
|
||||
onSelect: () => void
|
||||
|
|
@ -160,19 +165,22 @@ interface PanelListRowProps {
|
|||
|
||||
// A row is a container (not a <button>) so it can host both the select target
|
||||
// and a kebab menu without nesting interactive elements. Hover/active bg lives
|
||||
// on the wrapper so the whole row highlights as one.
|
||||
// on the wrapper so the whole row highlights as one. When `menuItems` is passed,
|
||||
// the whole row also answers right-click with the same actions as its kebab.
|
||||
export function PanelListRow({
|
||||
active,
|
||||
dotClassName,
|
||||
icon,
|
||||
lead,
|
||||
menu,
|
||||
menuItems,
|
||||
menuLabel,
|
||||
meta,
|
||||
onSelect,
|
||||
rowKey,
|
||||
title
|
||||
}: PanelListRowProps) {
|
||||
return (
|
||||
const row = (
|
||||
<div
|
||||
className={cn(
|
||||
'group/row row-hover relative flex h-7 w-full items-center rounded-md text-[0.78rem] hover:text-foreground',
|
||||
|
|
@ -193,9 +201,25 @@ export function PanelListRow({
|
|||
<span className="min-w-0 flex-1 truncate font-medium text-foreground/85">{title}</span>
|
||||
</RowButton>
|
||||
{meta ? <span className="shrink-0 pr-2 text-[0.62rem] tabular-nums text-muted-foreground/45">{meta}</span> : null}
|
||||
{menu ? <div className="shrink-0 pr-1">{menu}</div> : null}
|
||||
{menuItems ? (
|
||||
<div className="shrink-0 pr-1">
|
||||
<PanelRowMenu items={menuItems} label={menuLabel} />
|
||||
</div>
|
||||
) : menu ? (
|
||||
<div className="shrink-0 pr-1">{menu}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
// Right-click parity: same items as the kebab. `disabled` (no actionable
|
||||
// items) renders the row bare.
|
||||
return menuItems ? (
|
||||
<ActionsContextMenu ariaLabel={menuLabel} contentClassName="w-40" disabled={menuItems.length === 0} items={renderPanelMenuItems(menuItems)}>
|
||||
{row}
|
||||
</ActionsContextMenu>
|
||||
) : (
|
||||
row
|
||||
)
|
||||
}
|
||||
|
||||
export interface PanelMenuItem {
|
||||
|
|
@ -206,6 +230,22 @@ export interface PanelMenuItem {
|
|||
tone?: 'danger' | 'default'
|
||||
}
|
||||
|
||||
// Bridge PanelMenuItem[] → the shared actions-menu render fn, so a panel row's
|
||||
// kebab and its right-click menu render from one source.
|
||||
function renderPanelMenuItems(items: PanelMenuItem[]) {
|
||||
return (kit: MenuKit) =>
|
||||
items.map(item =>
|
||||
renderActionItem(kit, {
|
||||
disabled: item.disabled,
|
||||
icon: item.icon,
|
||||
key: item.label,
|
||||
label: item.label,
|
||||
onSelect: item.onSelect,
|
||||
variant: item.tone === 'danger' ? 'destructive' : 'default'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Per-row "⋮" actions menu — mirrors the sidebar session row's settled pattern
|
||||
// (size-5 ghost trigger + kebab-vertical codicon + w-40 content). Hidden until
|
||||
// the row is hovered/focused (or the menu is open). Returns null with no items
|
||||
|
|
@ -216,33 +256,16 @@ export function PanelRowMenu({ items, label = 'Actions' }: { items: PanelMenuIte
|
|||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<Tip label={label}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={label}
|
||||
className="size-5 rounded-[4px] bg-transparent text-(--ui-text-tertiary) opacity-0 transition-colors duration-100 hover:bg-(--ui-control-active-background) hover:text-foreground focus-visible:opacity-100 focus-visible:ring-0 group-hover/row:opacity-100 data-[state=open]:bg-(--ui-control-active-background) data-[state=open]:text-foreground data-[state=open]:opacity-100 [&_svg]:size-3.5!"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Codicon name="kebab-vertical" size="0.875rem" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</Tip>
|
||||
<DropdownMenuContent align="end" className="w-40" sideOffset={6}>
|
||||
{items.map(item => (
|
||||
<DropdownMenuItem
|
||||
disabled={item.disabled}
|
||||
key={item.label}
|
||||
onSelect={item.onSelect}
|
||||
variant={item.tone === 'danger' ? 'destructive' : undefined}
|
||||
>
|
||||
{item.icon ? <Codicon name={item.icon} size="0.875rem" /> : null}
|
||||
<span>{item.label}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ActionsMenu ariaLabel={label} contentClassName="w-40" items={renderPanelMenuItems(items)} tooltip={label}>
|
||||
<Button
|
||||
aria-label={label}
|
||||
className="size-5 rounded-[4px] bg-transparent text-(--ui-text-tertiary) opacity-0 transition-colors duration-100 hover:bg-(--ui-control-active-background) hover:text-foreground focus-visible:opacity-100 focus-visible:ring-0 group-hover/row:opacity-100 data-[state=open]:bg-(--ui-control-active-background) data-[state=open]:text-foreground data-[state=open]:opacity-100 [&_svg]:size-3.5!"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Codicon name="kebab-vertical" size="0.875rem" />
|
||||
</Button>
|
||||
</ActionsMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ import {
|
|||
PanelHeader,
|
||||
PanelList,
|
||||
PanelListRow,
|
||||
type PanelMenuItem,
|
||||
PanelMeta,
|
||||
PanelPill,
|
||||
PanelRowMenu,
|
||||
PanelSectionLabel
|
||||
} from '../overlays/panel'
|
||||
|
||||
|
|
@ -197,22 +197,18 @@ export function ProfilesView({ onClose }: ProfilesViewProps) {
|
|||
<ProfileRow
|
||||
active={selected?.name === profile.name}
|
||||
key={profile.name}
|
||||
menu={
|
||||
<PanelRowMenu
|
||||
items={
|
||||
profile.is_default
|
||||
? []
|
||||
: [
|
||||
{ icon: 'edit', label: p.renameMenu, onSelect: () => setPendingRename(profile) },
|
||||
{
|
||||
icon: 'trash',
|
||||
label: t.common.delete,
|
||||
onSelect: () => setPendingDelete(profile),
|
||||
tone: 'danger'
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
menuItems={
|
||||
profile.is_default
|
||||
? []
|
||||
: [
|
||||
{ icon: 'edit', label: p.renameMenu, onSelect: () => setPendingRename(profile) },
|
||||
{
|
||||
icon: 'trash',
|
||||
label: t.common.delete,
|
||||
onSelect: () => setPendingDelete(profile),
|
||||
tone: 'danger'
|
||||
}
|
||||
]
|
||||
}
|
||||
onSelect={() => setSelectedName(profile.name)}
|
||||
profile={profile}
|
||||
|
|
@ -281,12 +277,12 @@ export function ProfilesView({ onClose }: ProfilesViewProps) {
|
|||
|
||||
function ProfileRow({
|
||||
active,
|
||||
menu,
|
||||
menuItems,
|
||||
onSelect,
|
||||
profile
|
||||
}: {
|
||||
active: boolean
|
||||
menu?: React.ReactNode
|
||||
menuItems: PanelMenuItem[]
|
||||
onSelect: () => void
|
||||
profile: ProfileInfo
|
||||
}) {
|
||||
|
|
@ -302,7 +298,8 @@ function ProfileRow({
|
|||
name={profile.name}
|
||||
/>
|
||||
}
|
||||
menu={menu}
|
||||
menuItems={menuItems}
|
||||
menuLabel={profile.name}
|
||||
onSelect={onSelect}
|
||||
rowKey={profile.name}
|
||||
title={profile.name}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ import {
|
|||
PanelListRow,
|
||||
PanelMeta,
|
||||
PanelPill,
|
||||
PanelRowMenu,
|
||||
PanelSectionLabel
|
||||
} from '../overlays/panel'
|
||||
import { ListRow } from '../settings/primitives'
|
||||
|
|
@ -387,18 +386,14 @@ export function WebhooksView({ onClose }: WebhooksViewProps) {
|
|||
active={selectedSub?.name === sub.name}
|
||||
dotClassName={sub.enabled ? 'bg-emerald-500' : 'bg-muted-foreground/50'}
|
||||
key={sub.name}
|
||||
menu={
|
||||
<PanelRowMenu
|
||||
items={[
|
||||
{
|
||||
icon: sub.enabled ? 'circle-slash' : 'check',
|
||||
label: sub.enabled ? w.disableRow : w.enableRow,
|
||||
onSelect: () => void handleToggle(sub.name, !sub.enabled)
|
||||
},
|
||||
{ icon: 'trash', label: w.delete, onSelect: () => setPendingDelete(sub.name), tone: 'danger' }
|
||||
]}
|
||||
/>
|
||||
}
|
||||
menuItems={[
|
||||
{
|
||||
icon: sub.enabled ? 'circle-slash' : 'check',
|
||||
label: sub.enabled ? w.disableRow : w.enableRow,
|
||||
onSelect: () => void handleToggle(sub.name, !sub.enabled)
|
||||
},
|
||||
{ icon: 'trash', label: w.delete, onSelect: () => setPendingDelete(sub.name), tone: 'danger' }
|
||||
]}
|
||||
onSelect={() => setSelectedName(sub.name)}
|
||||
title={sub.name}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue