From f951ce21faa61fb7b00a353bf2486cd89add5ef5 Mon Sep 17 00:00:00 2001 From: Roomote Date: Fri, 17 Jul 2026 13:34:12 +0000 Subject: [PATCH 1/8] [Improve] Clarify Review Code copy on Automations Update Review Code toggle labels and helpers, and remove the redundant "Which PRs get reviewed?" info box. --- .../automations/AutomationsSettings.tsx | 169 +++++++----------- 1 file changed, 61 insertions(+), 108 deletions(-) diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.tsx index 746643ebd..ba6c57ba7 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.tsx @@ -2181,22 +2181,14 @@ export function AutomationsSettings() { commsStatus.data?.invocationIdentities.find( (identity) => identity.provider === 'slack', ) ?? null; - const githubInvocationIdentity = - commsStatus.data?.invocationIdentities.find( - (identity) => identity.provider === 'github', - ) ?? null; const slackAppMention = slackInvocationIdentity?.mentionText ?? slackInvocationIdentity?.nativeMention ?? 'the Slack app'; - const githubAppMention = - githubInvocationIdentity?.mentionText ?? 'the GitHub app'; const channelAutoStartLaunchModeOptions = CHANNEL_AUTO_START_LAUNCH_MODE_OPTIONS; const showChannelAutoStartLaunchModePicker = false; const reviewerIsEnabled = formState?.reviewerEnabled ?? false; - const reviewerReviewsAllPrs = - formState?.reviewerReviewAllPullRequestAuthors ?? false; const conflictResolverIsEnabled = formState?.conflictResolverFrequency !== 'off'; const channelAutoStartIsEnabled = hasConfiguredChannelAutoStartRows( @@ -3806,114 +3798,75 @@ If unclear, send to manager channel.`} } /> {reviewerIsEnabled ? ( -
- - - Which PRs get reviewed? - -
-

- When background auto-review is on, reviews run on{' '} - {reviewerReviewsAllPrs - ? 'all pull requests in connected repositories' - : `pull requests opened by ${PRODUCT_NAME}`} - {formState.reviewerReviewOnCommit - ? ' as they open or receive new commits.' - : '. Right now, Review Code only runs when someone mentions it on a PR.'} -

- {formState.reviewerReviewOnCommit ? ( -

- {formState.reviewerReviewDraftPrs - ? 'Draft PRs are included in automatic reviews.' - : 'Draft PRs wait until they are marked ready for review.'} -

- ) : null} -

- {reviewerReviewsAllPrs - ? 'You can also comment' - : 'For PRs outside that scope, comment'}{' '} - - {githubAppMention} review this PR - {' '} - to request an on-demand review. -

-
-
-
- -
-
- - setFormState((prev) => - prev ? { ...prev, reviewerReviewOnCommit } : prev, - ) - } - /> -
-

- Auto-review on open and new commits -

-

- Turn this off to keep Review Code background work - disabled. -

-
+
+
+ + setFormState((prev) => + prev ? { ...prev, reviewerReviewOnCommit } : prev, + ) + } + /> +
+

+ Automatically review any new PRs and follow-up commits. +

+

+ Disable to only get reviews by asking @-mentioning{' '} + {PRODUCT_NAME} +

+
-
- - setFormState((prev) => - prev - ? { - ...prev, - reviewerReviewAllPullRequestAuthors, - } - : prev, - ) - } - /> -
-

- Review PRs from other authors -

-

- Include pull requests opened by people or bots outside - {` ${PRODUCT_NAME}`} in automatic reviews. -

-
+
+ + setFormState((prev) => + prev + ? { + ...prev, + reviewerReviewAllPullRequestAuthors, + } + : prev, + ) + } + /> +
+

+ Review PRs not created by {PRODUCT_NAME} +

+

+ Include pull requests opened by people or others +

+
-
- - setFormState((prev) => - prev ? { ...prev, reviewerReviewDraftPrs } : prev, - ) - } - /> -
-

Review draft PRs

-

- Keep draft pull requests in scope before they are - marked ready for review when Review Code is enabled. -

-
+
+ + setFormState((prev) => + prev ? { ...prev, reviewerReviewDraftPrs } : prev, + ) + } + /> +
+

Review draft PRs

+

+ Turn off to only review PRs marked as ready (and save + tokens) +

From fc70d2963750b779d3693f7f7c670e8aca6a74db Mon Sep 17 00:00:00 2001 From: Roomote Date: Fri, 17 Jul 2026 13:40:45 +0000 Subject: [PATCH 2/8] [Improve] Put Review draft PRs above other-authors toggle Swap nested Review Code toggle order so draft PRs appear before non-Roomote authors. --- .../automations/AutomationsSettings.tsx | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.tsx index ba6c57ba7..5b64aaf66 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.tsx @@ -3828,25 +3828,18 @@ If unclear, send to manager channel.`}
+ checked={formState.reviewerReviewDraftPrs} + onCheckedChange={(reviewerReviewDraftPrs) => setFormState((prev) => - prev - ? { - ...prev, - reviewerReviewAllPullRequestAuthors, - } - : prev, + prev ? { ...prev, reviewerReviewDraftPrs } : prev, ) } />
-

- Review PRs not created by {PRODUCT_NAME} -

+

Review draft PRs

- Include pull requests opened by people or others + Turn off to only review PRs marked as ready (and save + tokens)

@@ -3854,18 +3847,25 @@ If unclear, send to manager channel.`}
+ aria-label={`Review PRs not created by ${PRODUCT_NAME}`} + checked={formState.reviewerReviewAllPullRequestAuthors} + onCheckedChange={(reviewerReviewAllPullRequestAuthors) => setFormState((prev) => - prev ? { ...prev, reviewerReviewDraftPrs } : prev, + prev + ? { + ...prev, + reviewerReviewAllPullRequestAuthors, + } + : prev, ) } />
-

Review draft PRs

+

+ Review PRs not created by {PRODUCT_NAME} +

- Turn off to only review PRs marked as ready (and save - tokens) + Include pull requests opened by people or others

From 4a90478c6e345e449a9975dcf8053d151b1f6c49 Mon Sep 17 00:00:00 2001 From: Roomote Date: Fri, 17 Jul 2026 13:56:03 +0000 Subject: [PATCH 3/8] [Improve] Group source-control automations and dynamic provider badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Source Code automations section after Slack, rename Other to Meta, and render “For X only” capability badges from configured providers only. --- ...AutomationsSettings.render.client.test.tsx | 75 +- .../automations/AutomationsSettings.tsx | 3113 +++++++++-------- .../commands/automations/settings-read.ts | 36 + 3 files changed, 1772 insertions(+), 1452 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 1d279b3f7..791995981 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 @@ -7,6 +7,10 @@ import { waitFor, } from '@testing-library/react'; import { FeatureFlag } from '@roomote/feature-flags'; +import type { + CommunicationProvider, + SourceControlProvider, +} from '@roomote/types'; const groupedRoutingPlaceholder = /Ideas about incidents, reliability, alerts, and monitoring/; @@ -37,6 +41,12 @@ const state = vi.hoisted(() => ({ missingScopes: [], slackWorkspaceDomain: 'acme', sentryConnected: false, + connectedCommunicationProviders: ['slack'] as CommunicationProvider[], + connectedSourceControlProviders: [ + 'github', + 'gitlab', + 'ado', + ] as SourceControlProvider[], }, settings: { reviewer: { @@ -381,6 +391,14 @@ describe('AutomationsSettings', () => { mutations.latestSettingsOptions = null; mutations.latestTriggerOptions = null; state.settingsQuery.data.capabilities.discordConnected = false; + state.settingsQuery.data.capabilities.connectedCommunicationProviders = [ + 'slack', + ]; + state.settingsQuery.data.capabilities.connectedSourceControlProviders = [ + 'github', + 'gitlab', + 'ado', + ]; state.discordChannelsQuery.data.channels = []; state.settingsQuery.data.settings.managerStatsDiscordChannelId = null; state.settingsQuery.data.settings.suggesterDiscordChannelId = null; @@ -572,23 +590,50 @@ describe('AutomationsSettings', () => { it('shows exception-only capability badges from the shared descriptors', async () => { render(); - // Dependabot, CodeQL, and CI failure triage stay GitHub-only; manager - // stats is provider-neutral now and shows no source-control badge. - expect((await screen.findAllByText('GitHub only')).length).toBe(3); - // The suggester supports Slack and Discord destinations; the other - // manager automations post to all configured communication providers. - expect(screen.queryByText('Slack only')).toBeNull(); - expect(screen.getAllByText('Slack · Discord only').length).toBe(1); - // conflict_resolver supports GitHub, GitLab, and Azure DevOps (no - // Gitea/Bitbucket conflict signal). + // Deployment runs multi-SCM; Dependabot/CodeQL/CI stay GitHub-only among + // the configured providers, so they share "For GitHub only". + expect((await screen.findAllByText('For GitHub only')).length).toBe(3); + // Only Slack is configured for comms in the default fixture, so no comms + // capability badge is needed (including for the Slack+Discord suggester). + expect(screen.queryByText('For Slack only')).toBeNull(); + expect(screen.queryByText('For Slack + Discord only')).toBeNull(); + // conflict_resolver covers all three configured SCM providers → no badge. expect( - screen.getAllByText('GitHub · GitLab · Azure DevOps only').length, - ).toBe(1); + screen.queryByText('For GitHub + GitLab + Azure DevOps only'), + ).toBeNull(); // Full coverage shows nothing — absence of a warning is the signal. expect(screen.queryByText('All chat channels')).toBeNull(); expect(screen.queryByText('All source control')).toBeNull(); }); + it('shows communication provider badges only when multiple are configured', async () => { + state.settingsQuery.data.capabilities.discordConnected = true; + state.settingsQuery.data.capabilities.connectedCommunicationProviders = [ + 'slack', + 'discord', + 'teams', + ]; + + render(); + + // Suggest Ideas only targets Slack + Discord destinations among the three + // connected chat providers. + expect( + await screen.findByText('For Slack + Discord only'), + ).toBeInTheDocument(); + expect(screen.queryByText('For Slack only')).toBeNull(); + }); + + it('renders the Source Code and Meta automation sections', async () => { + render(); + + expect( + await screen.findByText('Source Code automations'), + ).toBeInTheDocument(); + expect(screen.getByText('Meta automations')).toBeInTheDocument(); + expect(screen.queryByText('Other automations')).toBeNull(); + }); + it('reflects the reviewer all-author setting in the review scope copy', async () => { state.settingsQuery.data.reviewer.enabled = true; state.settingsQuery.data.reviewer.reviewAllPullRequestAuthors = true; @@ -598,13 +643,15 @@ describe('AutomationsSettings', () => { await openReviewerCard(); expect( - screen.getByRole('switch', { name: /review prs from other authors/i }), + screen.getByRole('switch', { name: /review prs not created by/i }), ).toBeChecked(); expect( - screen.getByText(/all pull requests in connected repositories/), + screen.getByText( + /Automatically review any new PRs and follow-up commits/i, + ), ).toBeInTheDocument(); expect( - screen.queryByText(/pull requests opened by Roomote/), + screen.queryByText(/Which PRs get reviewed/i), ).not.toBeInTheDocument(); }); diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.tsx index 5b64aaf66..ae4c41f60 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.tsx @@ -2,7 +2,15 @@ import Link from 'next/link'; import type { ComponentType } from 'react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { FeatureFlag } from '@roomote/feature-flags'; @@ -10,7 +18,6 @@ import { AUTOMATION_DESTINATION_DESCRIPTORS, type BackgroundAutomationKey, type CommunicationProvider, - communicationProviders, CONFLICT_RESOLUTION_MAX_PR_AGE_DAYS_OPTIONS, DEFAULT_CHANNEL_AUTO_START_LAUNCH_MODE, DEFAULT_CONFLICT_RESOLUTION_MAX_PR_AGE_DAYS, @@ -18,7 +25,6 @@ import { getCommunicationProviderDisplayName, getSourceControlProviderLabel, getTriggerableBackgroundAutomationDescriptorByKey, - sourceControlProviders, type ChannelAutoStartLaunchMode, type ConflictResolverMaxPrAgeDays, PRODUCT_NAME, @@ -331,13 +337,30 @@ const TRIGGERABLE_AUTOMATION_SCHEDULE_LABELS = { } as const; /** - * Exception-only capability badges derived from the automation descriptor: - * a badge appears only when an automation is LIMITED relative to full - * provider coverage. Full coverage (or no applicable surface at all) shows - * nothing — the absence of a warning is the signal. + * Capability badges relative to what this deployment actually has + * configured. Shown only when more than one provider of that kind is + * connected and the automation does not cover every configured one. */ -function getAutomationCapabilityBadges( +export function formatConfiguredProviderOnlyBadge( + labels: readonly string[], +): string | undefined { + if (labels.length === 0) { + return undefined; + } + + if (labels.length === 1) { + return `For ${labels[0]} only`; + } + + return `For ${labels.join(' + ')} only`; +} + +export function getAutomationCapabilityBadges( automationKey: BackgroundAutomationKey, + options: { + configuredCommunicationProviders: readonly CommunicationProvider[]; + configuredSourceControlProviders: readonly SourceControlProvider[]; + }, ): Pick { const descriptor = getTriggerableBackgroundAutomationDescriptorByKey(automationKey); @@ -346,29 +369,47 @@ function getAutomationCapabilityBadges( return {}; } - const comms: readonly CommunicationProvider[] = - descriptor.supportedCommunicationProviders; - const commsLimited = - comms.length > 0 && comms.length < communicationProviders.length; - const commsBadge = commsLimited - ? comms.length === 1 && comms[0] === 'slack' - ? 'Slack only' - : `${comms.map(getCommunicationProviderDisplayName).join(' · ')} only` - : undefined; - - const scm: readonly SourceControlProvider[] = - descriptor.supportedSourceControlProviders; - const scmLimited = - scm.length > 0 && scm.length < sourceControlProviders.length; - const scmBadge = scmLimited - ? scm.length === 1 && scm[0] === 'github' - ? 'GitHub only' - : `${scm.map(getSourceControlProviderLabel).join(' · ')} only` - : undefined; + const configuredComms = options.configuredCommunicationProviders; + const configuredScm = options.configuredSourceControlProviders; + + const supportedComms = new Set( + descriptor.supportedCommunicationProviders, + ); + const supportedScm = new Set( + descriptor.supportedSourceControlProviders, + ); + + const commsBadge = + configuredComms.length > 1 + ? formatConfiguredProviderOnlyBadge( + configuredComms + .filter((provider) => supportedComms.has(provider)) + .map(getCommunicationProviderDisplayName), + ) + : undefined; + const coversAllConfiguredComms = + configuredComms.length > 0 && + configuredComms.every((provider) => supportedComms.has(provider)); + const resolvedCommsBadge = + coversAllConfiguredComms || !commsBadge ? undefined : commsBadge; + + const scmBadge = + configuredScm.length > 1 + ? formatConfiguredProviderOnlyBadge( + configuredScm + .filter((provider) => supportedScm.has(provider)) + .map(getSourceControlProviderLabel), + ) + : undefined; + const coversAllConfiguredScm = + configuredScm.length > 0 && + configuredScm.every((provider) => supportedScm.has(provider)); + const resolvedScmBadge = + coversAllConfiguredScm || !scmBadge ? undefined : scmBadge; return { - ...(commsBadge ? { commsBadge } : {}), - ...(scmBadge ? { scmBadge } : {}), + ...(resolvedCommsBadge ? { commsBadge: resolvedCommsBadge } : {}), + ...(resolvedScmBadge ? { scmBadge: resolvedScmBadge } : {}), }; } @@ -390,7 +431,6 @@ function getAutomationDefinition( description: TRIGGERABLE_AUTOMATION_DESCRIPTIONS[automationKey], icon, availability: descriptor.availability, - ...getAutomationCapabilityBadges(automationKey), }; } @@ -455,7 +495,6 @@ const SCHEDULE_ONLY_AUTOMATION_DEFINITIONS = Object.fromEntries( SCHEDULE_ONLY_AUTOMATION_UI_DEFINITIONS[automation.id].description, icon: SCHEDULE_ONLY_AUTOMATION_UI_DEFINITIONS[automation.id].icon, availability: automation.availability, - ...getAutomationCapabilityBadges(automation.automationKey), }, ]), ) as unknown as Record< @@ -1555,21 +1594,38 @@ function AutomationCard({ ); } +const ConfiguredProvidersContext = createContext<{ + configuredCommunicationProviders: readonly CommunicationProvider[]; + configuredSourceControlProviders: readonly SourceControlProvider[]; +}>({ + configuredCommunicationProviders: [], + configuredSourceControlProviders: [], +}); + function AutomationTitle({ automation }: { automation: AutomationDefinition }) { + const configuredProviders = useContext(ConfiguredProvidersContext); + const automationKey = AUTOMATION_RUN_KEYS_BY_ID[automation.id]; + const badges = automationKey + ? getAutomationCapabilityBadges(automationKey, configuredProviders) + : { + commsBadge: automation.commsBadge, + scmBadge: automation.scmBadge, + }; + return ( {automation.label} {automation.availability === 'beta' ? ( Beta ) : null} - {automation.commsBadge ? ( + {badges.commsBadge ? ( - {automation.commsBadge} + {badges.commsBadge} ) : null} - {automation.scmBadge ? ( + {badges.scmBadge ? ( - {automation.scmBadge} + {badges.scmBadge} ) : null} @@ -2480,442 +2536,1183 @@ export function AutomationsSettings() { isEditingManagerChannel || isDirty.managerChannel; + const configuredCommunicationProviders = useMemo( + () => + capabilities?.connectedCommunicationProviders ?? + ([ + ...(capabilities?.slackConnected ? (['slack'] as const) : []), + ...(capabilities?.discordConnected ? (['discord'] as const) : []), + ] as CommunicationProvider[]), + [capabilities], + ); + const configuredSourceControlProviders = useMemo( + () => capabilities?.connectedSourceControlProviders ?? [], + [capabilities], + ); + const configuredProvidersValue = useMemo( + () => ({ + configuredCommunicationProviders, + configuredSourceControlProviders, + }), + [configuredCommunicationProviders, configuredSourceControlProviders], + ); + return ( -
- {!settingsQuery.isPending && - capabilities && - !capabilities.slackConnected ? ( - - -
- - - You need to connect Slack for automations that post updates into - channels. - - +
+
+
+ ) : null} + + {!settingsQuery.isPending && + capabilities?.requiresSlackReconnect && + capabilities.missingScopes.length > 0 ? ( + + +
+ + + Your Slack installation is missing some permissions for + automations to post their findings. + + +
+
+
+ ) : null} + + {fieldErrors.general ? ( + + + Unable to save settings + {fieldErrors.general} + + ) : null} + + {fieldErrors.managerSlackChannel ? ( + + + Manager Channel required + + {fieldErrors.managerSlackChannel} + + + ) : null} + + {settingsQuery.isPending || !formState ? ( + + ) : ( +
+
+

+ Slack automations +

+ + + setAutomationOpen('channelAutoStart', open) + } + iconEnabled={iconEnabled.channelAutoStart} + footer={ + saveAgent('channelAutoStart')} + onReset={() => resetAgent('channelAutoStart')} + /> + } > - - {connectSlack.isPending ? 'Connecting...' : 'Connect Slack'} - -
- - - ) : null} - - {!settingsQuery.isPending && - capabilities?.requiresSlackReconnect && - capabilities.missingScopes.length > 0 ? ( - - -
- - - Your Slack installation is missing some permissions for - automations to post their findings. - - -
-
-
- ) : null} +
+
+ + setFormState((prev) => + prev ? { ...prev, reviewerEnabled } : prev, + ) + } + /> + +
- {fieldErrors.general ? ( - - - Unable to save settings - {fieldErrors.general} - - ) : null} + {reviewerIsEnabled ? ( +
+
+ + setFormState((prev) => + prev ? { ...prev, reviewerReviewOnCommit } : prev, + ) + } + /> +
+

+ Automatically review any new PRs and follow-up + commits. +

+

+ Disable to only get reviews by asking @-mentioning{' '} + {PRODUCT_NAME} +

+
+
- {fieldErrors.managerSlackChannel ? ( - - - Manager Channel required - {fieldErrors.managerSlackChannel} - - ) : null} +
+ + setFormState((prev) => + prev ? { ...prev, reviewerReviewDraftPrs } : prev, + ) + } + /> +
+

+ Review draft PRs +

+

+ Turn off to only review PRs marked as ready (and + save tokens) +

+
+
- {settingsQuery.isPending || !formState ? ( - - ) : ( -
-
-

- Slack automations -

+
+ + setFormState((prev) => + prev + ? { + ...prev, + reviewerReviewAllPullRequestAuthors, + } + : prev, + ) + } + /> +
+

+ Review PRs not created by {PRODUCT_NAME} +

+

+ Include pull requests opened by people or others +

+
+
+
+ ) : null} +
+ - - setAutomationOpen('channelAutoStart', open) - } - iconEnabled={iconEnabled.channelAutoStart} - footer={ - saveAgent('channelAutoStart')} - onReset={() => resetAgent('channelAutoStart')} - /> - } - > - - ) : undefined + + setAutomationOpen('conflictResolver', open) } - channelFieldError={fieldErrors.channelAutoStartSlackChannels} - instructionsFieldError={ - fieldErrors.channelAutoStartInstructions + iconEnabled={iconEnabled.conflictResolver} + debugSection={renderDebugRunsSection('conflictResolver')} + runAction={ + + + } - onRowsChange={(rows) => - setFormState((prev) => - prev - ? { - ...prev, - channelAutoStartSlackChannels: rows, - } - : prev, - ) + footer={ + saveAgent('conflictResolver')} + onReset={() => resetAgent('conflictResolver')} + /> } - /> - - -

- Automations for Roomote Managers -

- -
- {showManagerChannelForm ? ( - <> - -

- Make sure {slackAppMention} is added to the channel. -

-
-
- + setFormState((prev) => + prev + ? { + ...prev, + conflictResolverFrequency: + value as ConflictResolverFrequency, + } + : prev, + ) + } + > + + + + + {CONFLICT_RESOLVER_FREQUENCY_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + + {conflictResolverIsEnabled ? ( +
+
+ + +

+ Sets the maximum PR age that Resolve PR Conflicts will + consider. Labeled PRs older than this are skipped. +

+ {fieldErrors.conflictResolverMaxPrAgeDays ? ( +

+ {fieldErrors.conflictResolverMaxPrAgeDays} +

+ ) : null} +
- if (value === CUSTOM_MANAGER_CHANNEL_SELECT_VALUE) { - setIsEnteringCustomManagerChannel(true); +
+ + setFormState((prev) => - prev && selectedManagerChannelOption + prev ? { ...prev, - managerSlackChannel: '', + conflictResolverLabel: event.target.value, } : prev, - ); - return; + ) } + placeholder="roomote:auto-resolve-conflicts" + className="max-w-80" + /> +

+ Make sure this label exists in your repos. It will be + added automatically to all new agent PRs. Remove it + whenever you do not want conflicts resolved. +

+ {fieldErrors.conflictResolverLabel ? ( +

+ {fieldErrors.conflictResolverLabel} +

+ ) : null} +
- const selectedChannel = managerChannelOptions.find( - (channel) => channel.id === value, - ); - - if (!selectedChannel) { - return; +
+ +