From 4dc1a12d4491fef3b0aecbaf10787db21b09559d Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Thu, 28 May 2026 23:09:42 -0400 Subject: [PATCH] fix(app): keep removed multi-select values in the dropdown --- .../ConnectionVariableMultiSelect.tsx | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/apps/app/src/components/integrations/ConnectionVariableMultiSelect.tsx b/apps/app/src/components/integrations/ConnectionVariableMultiSelect.tsx index 12d1a49ac0..eb3476ca3b 100644 --- a/apps/app/src/components/integrations/ConnectionVariableMultiSelect.tsx +++ b/apps/app/src/components/integrations/ConnectionVariableMultiSelect.tsx @@ -3,7 +3,7 @@ import { Input, Spinner } from '@trycompai/design-system'; import { Close } from '@trycompai/design-system/icons'; import MultipleSelector from '@trycompai/ui/multiple-selector'; -import { useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import type { ConnectionVariable, VariableValue } from './ConnectionVariablesForm'; interface ConnectionVariableMultiSelectProps { @@ -31,6 +31,30 @@ export function ConnectionVariableMultiSelect({ const isGitHubRepos = variable.id === 'target_repos'; const parsedConfigs = isGitHubRepos ? normalizedSelectedValues.map(parseRepoBranch) : []; + const reposForSelector = isGitHubRepos + ? parsedConfigs.map((config) => config.repo) + : normalizedSelectedValues; + + // Remembers every value that has appeared in the multi-select during the + // lifetime of this component so that values the user removes (clicking the + // X on a tag) remain available as re-selectable options in the dropdown. + // Without this, free-form multi-selects like Google Workspace's + // `sync_excluded_emails` have an empty dropdown (no fetchOptions, no static + // options), so a removed tag can only be brought back by retyping it. + const [seenValues, setSeenValues] = useState>(() => new Set(reposForSelector)); + useEffect(() => { + setSeenValues((prev) => { + let next: Set | null = null; + for (const v of reposForSelector) { + if (v && !prev.has(v)) { + if (!next) next = new Set(prev); + next.add(v); + } + } + return next ?? prev; + }); + }, [reposForSelector]); + useEffect(() => { if ( variable.hasDynamicOptions && @@ -77,21 +101,31 @@ export function ConnectionVariableMultiSelect({ onChange(newValues); }; - const reposForSelector = isGitHubRepos - ? parsedConfigs.map((config) => config.repo) - : normalizedSelectedValues; const isCreatable = isGitHubRepos || options.length === 0; + const combinedOptions = useMemo(() => { + const map = new Map(); + for (const option of options) { + map.set(option.value, { value: option.value, label: option.label }); + } + for (const v of seenValues) { + if (!map.has(v)) { + map.set(v, { value: v, label: v }); + } + } + return Array.from(map.values()); + }, [options, seenValues]); + return (
({ value, - label: options.find((option) => option.value === value)?.label || value, + label: combinedOptions.find((option) => option.value === value)?.label || value, }))} onChange={(selected) => handleRepoSelectionChange(selected.map((item) => item.value))} - defaultOptions={options.map((option) => ({ value: option.value, label: option.label }))} - options={options.map((option) => ({ value: option.value, label: option.label }))} + defaultOptions={combinedOptions} + options={combinedOptions} placeholder={variable.placeholder || `Select ${variable.label.toLowerCase()}...`} creatable={isCreatable} emptyIndicator={