From 253f41e63c7e20ff654e4c07eb20548d3c7857e4 Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 3 Aug 2026 15:46:34 +0000 Subject: [PATCH 1/2] feat: improve automations filtering and editing --- .../settings/DeploymentTimeZoneSetting.tsx | 5 + ...AutomationsSettings.render.client.test.tsx | 61 ++++- .../automations/AutomationsSettings.tsx | 187 ++++++++++++- .../automations/CustomAutomationsSection.tsx | 246 +++++++++--------- 4 files changed, 369 insertions(+), 130 deletions(-) diff --git a/apps/web/src/components/settings/DeploymentTimeZoneSetting.tsx b/apps/web/src/components/settings/DeploymentTimeZoneSetting.tsx index e152c9441..6f39e6445 100644 --- a/apps/web/src/components/settings/DeploymentTimeZoneSetting.tsx +++ b/apps/web/src/components/settings/DeploymentTimeZoneSetting.tsx @@ -21,6 +21,7 @@ import { Popover, PopoverContent, PopoverTrigger, + Skeleton, } from '@/components/system'; const FALLBACK_TIME_ZONES = [ @@ -83,6 +84,10 @@ export function DeploymentTimeZoneSetting() { const effectiveTimeZone = settings.data?.effectiveTimeZone ?? 'UTC'; + if (settings.isPending) { + return ; + } + if (!isEditing) { return (

diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx index 18219b032..d3d1a0568 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx @@ -10,6 +10,7 @@ const managerInstructionsPlaceholder = /Optional guidance for which ideas to prioritize or avoid/; const state = vi.hoisted(() => ({ + customAutomationsPending: false, customAutomations: [] as Array<{ id: string; name: string; @@ -284,7 +285,10 @@ vi.mock('@tanstack/react-query', () => ({ } if (key1 === 'listCustomAutomations') { - return { isPending: false, data: state.customAutomations }; + return { + isPending: state.customAutomationsPending, + data: state.customAutomations, + }; } if (queryOptions.queryKey?.[0] === 'taskModels') { @@ -522,6 +526,8 @@ describe('AutomationsSettings', () => { state.settingsQuery.data.reviewer.relayReviewResultsToTask = false; state.settingsQuery.data.reviewer.relayUsers = []; state.customAutomations = []; + state.customAutomationsPending = false; + state.settingsQuery.isPending = false; state.environments = []; for (const key of Object.keys( state.settingsQuery.data.resolvedDestinations, @@ -776,6 +782,50 @@ describe('AutomationsSettings', () => { expect(screen.queryByText('Meta automations')).toBeNull(); }); + it('filters available automations by category and provider-aware search', async () => { + render(); + + const categoryFilter = await screen.findByRole('combobox', { + name: 'Filter available automations by category', + }); + expect(categoryFilter).toHaveTextContent('All'); + + fireEvent.change( + screen.getByRole('textbox', { name: 'Search available automations' }), + { target: { value: 'Discord' } }, + ); + + expect(screen.getByText('Auto-respond to channels')).toBeInTheDocument(); + expect(screen.queryByText('Review Code')).not.toBeInTheDocument(); + fireEvent.click( + screen.getByRole('button', { name: 'Clear automation filters' }), + ); + expect(screen.getByText('Review Code')).toBeInTheDocument(); + + fireEvent.click(categoryFilter); + fireEvent.click(await screen.findByRole('option', { name: 'Operations' })); + expect(screen.getByText('Triage Sentry Issues')).toBeInTheDocument(); + expect(screen.queryByText('Review Code')).not.toBeInTheDocument(); + }); + + it('shows independent structural skeletons for custom and built-in automations', () => { + state.customAutomationsPending = true; + state.settingsQuery.isPending = true; + + render(); + + expect( + screen + .getByTestId('custom-automations-skeleton') + .querySelectorAll('[data-slot="skeleton"]'), + ).toHaveLength(6); + expect( + screen + .getByTestId('built-in-automations-skeleton') + .querySelectorAll('[data-slot="skeleton"]'), + ).toHaveLength(17); + }); + it('uses plain text empty states for built-in and custom automations', async () => { state.settingsQuery.data.settings.channelAutoStartSlackChannels = []; state.settingsQuery.data.settings.managerSlackChannelId = null as never; @@ -857,6 +907,15 @@ describe('AutomationsSettings', () => { expect( await screen.findByRole('dialog', { name: 'Edit custom automation' }), ).toBeInTheDocument(); + expect(screen.getByText('Frequency')).toBeInTheDocument(); + expect(screen.getByText('Destination')).toBeInTheDocument(); + expect(screen.queryByText('Cadence')).not.toBeInTheDocument(); + expect(screen.queryByText('Destination provider')).not.toBeInTheDocument(); + expect( + screen.queryByText( + 'Configure what runs, when it runs, and where the result is sent.', + ), + ).not.toBeInTheDocument(); }); it('reflects the reviewer all-author setting in the review scope copy', async () => { diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.tsx index da31d3ca0..a189f22a9 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.tsx @@ -95,6 +95,7 @@ import { Play, Plus, RefreshCcw, + Search, Select, SelectContent, SelectItem, @@ -111,6 +112,7 @@ import { Textarea, TriangleAlert, Users, + X, } from '@/components/system'; type FieldErrors = Partial< @@ -228,12 +230,26 @@ type AutomationDefinition = { label: string; description: string; icon: ComponentType<{ className?: string }>; + category: AutomationCategory; + searchTerms?: string[]; /** Compact label for the chat surfaces the automation can report to. */ commsBadge?: string; /** Compact label for the source-control providers the automation supports. */ scmBadge?: string; }; +type AutomationCategory = 'source-code' | 'communication' | 'operations'; + +const AUTOMATION_CATEGORY_OPTIONS: Array<{ + value: AutomationCategory | 'all'; + label: string; +}> = [ + { value: 'all', label: 'All' }, + { value: 'source-code', label: 'Source code' }, + { value: 'communication', label: 'Communication' }, + { value: 'operations', label: 'Operations' }, +]; + /** * Where an automation's next run will report, as resolved server-side through * the destination waterfall (own target -> Manager Channel -> primary @@ -335,12 +351,12 @@ const TRIGGERABLE_AUTOMATION_SCHEDULE_LABELS = { */ function getAutomationCapabilityBadges( automationKey: BackgroundAutomationKey, -): Pick { +): Pick { const descriptor = getTriggerableBackgroundAutomationDescriptorByKey(automationKey); if (!descriptor) { - return {}; + return { searchTerms: [] }; } const comms: readonly CommunicationProvider[] = @@ -364,6 +380,10 @@ function getAutomationCapabilityBadges( : undefined; return { + searchTerms: [ + ...comms.map(getCommunicationProviderDisplayName), + ...scm.map(getSourceControlProviderLabel), + ], ...(commsBadge ? { commsBadge } : {}), ...(scmBadge ? { scmBadge } : {}), }; @@ -386,6 +406,7 @@ function getAutomationDefinition( label: descriptor.label, description: TRIGGERABLE_AUTOMATION_DESCRIPTIONS[automationKey], icon, + category: automationKey === 'sentry_triage' ? 'operations' : 'source-code', ...getAutomationCapabilityBadges(automationKey), }; } @@ -450,6 +471,7 @@ const SCHEDULE_ONLY_AUTOMATION_DEFINITIONS = Object.fromEntries( description: SCHEDULE_ONLY_AUTOMATION_UI_DEFINITIONS[automation.id].description, icon: SCHEDULE_ONLY_AUTOMATION_UI_DEFINITIONS[automation.id].icon, + category: 'source-code', ...getAutomationCapabilityBadges(automation.automationKey), }, ]), @@ -475,6 +497,8 @@ const AUTOMATION_DEFINITIONS: Record = { description: 'Start tasks from selected Slack or Discord channels, each with its own custom instructions.', icon: MessagesSquare, + category: 'communication', + searchTerms: ['Slack', 'Discord'], }, managerChannel: { id: 'managerChannel', @@ -482,6 +506,8 @@ const AUTOMATION_DEFINITIONS: Record = { description: 'Shared Slack or Discord channel for manager-facing Roomote asks, summaries, and alerts.', icon: Users, + category: 'communication', + searchTerms: ['Slack', 'Discord'], }, managerStats: { ...getAutomationDefinition( @@ -489,6 +515,7 @@ const AUTOMATION_DEFINITIONS: Record = { 'manager_stats', ChartColumnIncreasing, ), + category: 'communication', }, sentryTriage: { ...getAutomationDefinition('sentryTriage', 'sentry_triage', SentryIcon), @@ -509,6 +536,8 @@ const AUTOMATION_DEFINITIONS: Record = { label: 'Review Code', description: `Review PRs automatically and on-demand.`, icon: GitPullRequest, + category: 'source-code', + searchTerms: sourceControlProviders.map(getSourceControlProviderLabel), }, conflictResolver: { ...getAutomationDefinition( @@ -519,9 +548,11 @@ const AUTOMATION_DEFINITIONS: Record = { }, suggester: { ...getAutomationDefinition('suggester', 'suggester', Lightbulb), + category: 'communication', }, announcer: { ...getAutomationDefinition('announcer', 'announcer', Megaphone), + category: 'communication', }, platformIssueAlerts: { id: 'platformIssueAlerts', @@ -529,6 +560,8 @@ const AUTOMATION_DEFINITIONS: Record = { description: 'Alert on Slack or Discord when a task runs into admin-fixable issues.', icon: BellElectric, + category: 'operations', + searchTerms: ['Slack', 'Discord'], }, }; @@ -1176,8 +1209,12 @@ function shouldShowChannelAutoStartWarning(params: { function LoadingSkeleton() { return ( -

- {Array.from({ length: 6 }).map((_, index) => ( +
+ + {Array.from({ length: 4 }).map((_, index) => (
@@ -1434,6 +1471,7 @@ function AutomationCard({ isOpen, onOpenChange, iconEnabled, + isAvailableMatch = true, runAction, debugSection, footer, @@ -1445,6 +1483,7 @@ function AutomationCard({ isOpen: boolean; onOpenChange: (open: boolean) => void; iconEnabled: boolean; + isAvailableMatch?: boolean; runAction?: React.ReactNode; debugSection?: React.ReactNode; footer?: React.ReactNode; @@ -1458,6 +1497,10 @@ function AutomationCard({ ? `Configure ${automation.label}` : `Set up ${automation.label}`; + if (!iconEnabled && !isAvailableMatch) { + return null; + } + return (
({ isOpen, onOpenChange, iconEnabled, + isAvailableMatch, disabled = false, debugSection, runTooltip, @@ -1556,6 +1600,7 @@ function ScheduledAutomationCard({ isOpen: boolean; onOpenChange: (open: boolean) => void; iconEnabled: boolean; + isAvailableMatch: boolean; disabled?: boolean; debugSection?: React.ReactNode; runTooltip: string; @@ -1579,6 +1624,7 @@ function ScheduledAutomationCard({ isOpen={isOpen} onOpenChange={onOpenChange} iconEnabled={iconEnabled} + isAvailableMatch={isAvailableMatch} disabled={disabled} debugSection={debugSection} runAction={ @@ -1655,6 +1701,10 @@ export function AutomationsSettings() { const [isEditingManagerChannel, setIsEditingManagerChannel] = useState(false); const [isEnteringCustomManagerChannel, setIsEnteringCustomManagerChannel] = useState(false); + const [availableCategory, setAvailableCategory] = useState< + AutomationCategory | 'all' + >('all'); + const [availableSearch, setAvailableSearch] = useState(''); const formStateRef = useRef(null); const savedStateRef = useRef(null); const didApplyInitialHashRef = useRef(false); @@ -2539,6 +2589,28 @@ export function AutomationsSettings() { announcer: announcerIsEnabled, platformIssueAlerts: isPlatformIssueAlertsEnabled(formState), } satisfies Record; + const normalizedAvailableSearch = availableSearch.trim().toLowerCase(); + const availableAutomationMatches = new Set( + Object.values(AUTOMATION_DEFINITIONS) + .filter( + (automation) => + !iconEnabled[automation.id] && + (availableCategory === 'all' || + automation.category === availableCategory) && + (!normalizedAvailableSearch || + [ + automation.label, + automation.description, + ...(automation.searchTerms ?? []), + ] + .join(' ') + .toLowerCase() + .includes(normalizedAvailableSearch)), + ) + .map((automation) => automation.id), + ); + const hasAvailableFilters = + availableCategory !== 'all' || Boolean(normalizedAvailableSearch); const isAutomationSaving = (automationId: AutomationId) => updateMutation.isPending && savingAutomation === automationId; @@ -2652,6 +2724,8 @@ export function AutomationsSettings() { ) : null} + + {settingsQuery.isPending || !formState ? ( ) : ( @@ -2665,11 +2739,71 @@ export function AutomationsSettings() { No built-in automations enabled yet.

)} -

- Available -

+
+

+ Available +

+
+ +
+ + + setAvailableSearch(event.currentTarget.value) + } + placeholder="Search" + aria-label="Search available automations" + className="h-8 w-44 pl-8 text-sm" + /> +
+ {hasAvailableFilters ? ( + + + + ) : null} +
+
+ {availableAutomationMatches.size === 0 ? ( +

+ No available automations match these filters. +

+ ) : null} setAutomationOpen('reviewer', open)} iconEnabled={iconEnabled.reviewer} @@ -2818,6 +2952,9 @@ export function AutomationsSettings() { setAutomationOpen(automation.id, open) @@ -2894,6 +3031,9 @@ export function AutomationsSettings() { setAutomationOpen('conflictResolver', open) @@ -3100,6 +3240,9 @@ export function AutomationsSettings() { setAutomationOpen(automation.id, open) @@ -3212,6 +3355,9 @@ export function AutomationsSettings() { setAutomationOpen('dependabotTriage', open) @@ -3287,6 +3433,7 @@ export function AutomationsSettings() { setAutomationOpen('codeqlTriage', open)} iconEnabled={iconEnabled.codeqlTriage} @@ -3377,6 +3524,9 @@ export function AutomationsSettings() { setAutomationOpen(automation.id, open) @@ -3487,10 +3637,11 @@ export function AutomationsSettings() { ); })} - - setAutomationOpen('channelAutoStart', open) @@ -3546,6 +3697,9 @@ export function AutomationsSettings() { setAutomationOpen('managerChannel', open)} iconEnabled={iconEnabled.managerChannel} @@ -3814,6 +3968,7 @@ export function AutomationsSettings() { setAutomationOpen('managerStats', open)} iconEnabled={iconEnabled.managerStats} @@ -3826,7 +3981,9 @@ export function AutomationsSettings() { variant="ghost" size="icon" onClick={() => - triggerMutation.mutate({ automationKey: 'manager_stats' }) + triggerMutation.mutate({ + automationKey: 'manager_stats', + }) } disabled={isRunDisabled( 'managerStats', @@ -3898,6 +4055,7 @@ export function AutomationsSettings() { setAutomationOpen('sentryTriage', open)} iconEnabled={iconEnabled.sentryTriage} @@ -3914,7 +4072,9 @@ export function AutomationsSettings() { variant="ghost" size="icon" onClick={() => - triggerMutation.mutate({ automationKey: 'sentry_triage' }) + triggerMutation.mutate({ + automationKey: 'sentry_triage', + }) } disabled={isRunDisabled( 'sentryTriage', @@ -4074,6 +4234,7 @@ export function AutomationsSettings() { setAutomationOpen('suggester', open)} iconEnabled={iconEnabled.suggester} @@ -4197,6 +4358,7 @@ export function AutomationsSettings() { setAutomationOpen('announcer', open)} iconEnabled={iconEnabled.announcer} @@ -4310,6 +4472,9 @@ export function AutomationsSettings() { setAutomationOpen('platformIssueAlerts', open) diff --git a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx index a4673d6ff..432838973 100644 --- a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx +++ b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx @@ -20,7 +20,6 @@ import { CardContent, Dialog, DialogContent, - DialogDescription, DialogHeader, DialogTitle, Input, @@ -31,7 +30,8 @@ import { SelectItem, SelectTrigger, SelectValue, - SlidersHorizontal, + Settings2, + Skeleton, Switch, Textarea, Trash2, @@ -440,14 +440,11 @@ export function CustomAutomationsSection() { }; const renderEditor = () => ( - + {editingId ? 'Edit custom automation' : 'New custom automation'} - - Configure what runs, when it runs, and where the result is sent. -
@@ -482,9 +479,9 @@ export function CustomAutomationsSection() { />
-
-
- +
+ +
-
- {form.scheduleMode === 'cron' ? ( -
- + {form.scheduleMode === 'cron' ? ( - {resolveScheduleMutation.isPending ? ( -

- Interpreting schedule... -

- ) : effectiveScheduleSummary ? ( -

- {effectiveScheduleSummary} -

- ) : null} -
+ ) : null} +
+ {resolveScheduleMutation.isPending ? ( +

+ Interpreting schedule... +

+ ) : effectiveScheduleSummary ? ( +

+ {effectiveScheduleSummary} +

) : null}
@@ -598,9 +597,9 @@ export function CustomAutomationsSection() {
-
-
- +
+ +
-
- {form.targetProvider === 'none' ? ( -
- -

+ {form.targetProvider === 'none' ? ( +

Results appear only in the task view.

-
- ) : ( -
- - {form.targetProvider === 'slack' ? ( - - setForm((current) => ({ - ...current, - targetChannelId: value ?? '', - })) - } - /> - ) : form.targetProvider === 'discord' ? ( - + setForm((current) => ({ + ...current, + targetChannelId: value, + })) + } + > + - - - - - {discordOptions.map((channel) => ( - - {channel.label} - - ))} - - - ) : ( - - setForm((current) => ({ - ...current, - targetChannelId: event.target.value, - })) - } - placeholder={ - form.targetProvider === 'teams' - ? 'Teams conversation ID' - : 'Telegram chat ID' - } - /> - )} -
- )} -
- - {form.targetProvider === 'teams' ? ( -
- - - setForm((current) => ({ - ...current, - targetServiceUrl: event.target.value, - })) - } - placeholder="https://smba.trafficmanager.net/..." - /> + + + + {discordOptions.map((channel) => ( + + {channel.label} + + ))} + + + ) : ( + + setForm((current) => ({ + ...current, + targetChannelId: event.target.value, + })) + } + placeholder={ + form.targetProvider === 'teams' + ? 'Teams conversation ID' + : 'Telegram chat ID' + } + /> + )} + {form.targetProvider === 'teams' ? ( + + setForm((current) => ({ + ...current, + targetServiceUrl: event.target.value, + })) + } + placeholder="Service URL (optional)" + /> + ) : null}
- ) : null} +
@@ -747,10 +745,7 @@ export function CustomAutomationsSection() { ); return ( -
+

- Create your own scheduled agent runs with a prompt, cadence, + Create your own scheduled agent runs with a prompt, frequency, environment, and optional report channel.

@@ -801,9 +796,24 @@ export function CustomAutomationsSection() { {listQuery.isPending ? ( -

- Loading custom automations… -

+ + +
+ {Array.from({ length: 2 }).map((_, index) => ( +
+ +
+ + +
+
+ ))} +
+
+
) : rows.length === 0 && !isCreating ? (

No custom automations created yet. @@ -876,7 +886,7 @@ export function CustomAutomationsSection() { aria-label={`Configure ${row.name}`} onClick={() => editAutomation(row)} > - + From 23b312de6f27453f5055f53abe9ba42143ec74ef Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 3 Aug 2026 16:02:56 +0000 Subject: [PATCH 2/2] improve: refine automations editor layout --- ...AutomationsSettings.render.client.test.tsx | 4 +- .../automations/AutomationsSettings.tsx | 2 +- .../automations/CustomAutomationsSection.tsx | 229 ++++++++++-------- .../src/components/system/primitives/icons.ts | 1 - 4 files changed, 129 insertions(+), 107 deletions(-) diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx index d3d1a0568..eede21ef9 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx @@ -907,9 +907,11 @@ describe('AutomationsSettings', () => { expect( await screen.findByRole('dialog', { name: 'Edit custom automation' }), ).toBeInTheDocument(); - expect(screen.getByText('Frequency')).toBeInTheDocument(); + expect(screen.getByText('Schedule')).toBeInTheDocument(); expect(screen.getByText('Destination')).toBeInTheDocument(); + expect(screen.getByText('Channel')).toBeInTheDocument(); expect(screen.queryByText('Cadence')).not.toBeInTheDocument(); + expect(screen.queryByText('Frequency')).not.toBeInTheDocument(); expect(screen.queryByText('Destination provider')).not.toBeInTheDocument(); expect( screen.queryByText( diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.tsx index a189f22a9..e054eff12 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.tsx @@ -2774,7 +2774,7 @@ export function AutomationsSettings() { } placeholder="Search" aria-label="Search available automations" - className="h-8 w-44 pl-8 text-sm" + className="h-8 w-36 pl-8 text-sm" />

{hasAvailableFilters ? ( diff --git a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx index 432838973..e23fff447 100644 --- a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx +++ b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx @@ -480,7 +480,7 @@ export function CustomAutomationsSection() {
- +
- + @@ -584,7 +587,6 @@ export function CustomAutomationsSection() {
@@ -639,78 +641,99 @@ export function CustomAutomationsSection() { Results appear only in the task view.

) : form.targetProvider === 'slack' ? ( - - setForm((current) => ({ - ...current, - targetChannelId: value ?? '', - })) - } - /> - ) : form.targetProvider === 'discord' ? ( - + setForm((current) => ({ + ...current, + targetChannelId: value, + })) + } > - - - - {discordOptions.map((channel) => ( - - {channel.label} - - ))} - - + + + + + {discordOptions.map((channel) => ( + + {channel.label} + + ))} + + +
) : ( - - setForm((current) => ({ - ...current, - targetChannelId: event.target.value, - })) - } - placeholder={ - form.targetProvider === 'teams' - ? 'Teams conversation ID' - : 'Telegram chat ID' - } - /> +
+ + + setForm((current) => ({ + ...current, + targetChannelId: event.target.value, + })) + } + placeholder={ + form.targetProvider === 'teams' + ? 'Teams conversation ID' + : 'Telegram chat ID' + } + /> +
)} {form.targetProvider === 'teams' ? ( - - setForm((current) => ({ - ...current, - targetServiceUrl: event.target.value, - })) - } - placeholder="Service URL (optional)" - /> +
+ + + setForm((current) => ({ + ...current, + targetServiceUrl: event.target.value, + })) + } + placeholder="Optional" + /> +
) : null}
@@ -846,37 +869,35 @@ export function CustomAutomationsSection() { return (
-
- - toggleMutation.mutate({ - id: row.id, - ...writeInputFromRow(row), - enabled, - }) - } - /> -
-

{row.name}

-

- {row.scheduleMode === 'cron' - ? row.cronExpression - : scheduleLabel(row.scheduleMode)}{' '} - · {environmentName} ·{' '} - {target.provider === 'none' - ? destinationLabel - : `${target.provider}:${destinationLabel}`}{' '} - · Created by {row.createdByName ?? 'Unknown'} -

-
+ + toggleMutation.mutate({ + id: row.id, + ...writeInputFromRow(row), + enabled, + }) + } + /> +
+

{row.name}

+

+ {row.scheduleMode === 'cron' + ? row.cronExpression + : scheduleLabel(row.scheduleMode)}{' '} + · {environmentName} ·{' '} + {target.provider === 'none' + ? destinationLabel + : `${target.provider}:${destinationLabel}`}{' '} + · Created by {row.createdByName ?? 'Unknown'} +

-
+