Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions apps/sim/app/api/tools/linear/projects/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,40 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}

const linearClient = new LinearClient({ accessToken })
let projects: Array<{ id: string; name: string }> = []

const team = await linearClient.team(teamId)
const projectsResult = await team.projects()
projects = projectsResult.nodes.map((project: Project) => ({
id: project.id,
name: project.name,
}))
/**
* teamId may be a single ID or a comma-separated list when the basic-mode
* team selector is in multi-select. Fetch projects from each team in
* parallel and dedupe by project ID (Linear projects can be cross-team).
*/
const teamIds = teamId
.split(',')
.map((s) => s.trim())
.filter(Boolean)

const perTeam = await Promise.all(
teamIds.map(async (id) => {
const team = await linearClient.team(id)
const result = await team.projects()
return result.nodes.map((project: Project) => ({
id: project.id,
name: project.name,
}))
})
)

const seen = new Set<string>()
const projects: Array<{ id: string; name: string }> = []
for (const teamProjects of perTeam) {
for (const project of teamProjects) {
if (seen.has(project.id)) continue
seen.add(project.id)
projects.push(project)
}
}

if (projects.length === 0) {
logger.info('No projects found for team', { teamId })
logger.info('No projects found for team(s)', { teamIds })
}

return NextResponse.json({ projects })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { OAuthModal } from '@/app/workspace/[workspaceId]/components/oauth-modal
import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field'
import { SYNC_INTERVALS } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/consts'
import { MaxBadge } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/max-badge'
import type { ConfigFieldValue } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
import { useConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation'
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
Expand Down Expand Up @@ -108,6 +109,7 @@ export function AddConnectorModal({
setCanonicalModes,
canonicalGroups,
isFieldVisible,
isFieldPopulated,
handleFieldChange,
toggleCanonicalMode,
resolveSourceConfig,
Expand Down Expand Up @@ -150,16 +152,16 @@ export function AddConnectorModal({
for (const field of connectorConfig.configFields) {
if (!field.required) continue
if (!isFieldVisible(field)) continue
if (!sourceConfig[field.id]?.trim()) return false
if (!isFieldPopulated(field)) return false
}
return true
}, [
connectorConfig,
isApiKeyMode,
apiKeyValue,
effectiveCredentialId,
sourceConfig,
isFieldVisible,
isFieldPopulated,
])

const handleSubmit = () => {
Expand All @@ -169,7 +171,13 @@ export function AddConnectorModal({

const resolvedConfig: Record<string, unknown> = {}
for (const [key, value] of Object.entries(resolveSourceConfig())) {
if (value) resolvedConfig[key] = value
if (Array.isArray(value)) {
if (value.length > 0) resolvedConfig[key] = value
} else if (typeof value === 'string') {
if (value) resolvedConfig[key] = value
} else if (value !== undefined && value !== null) {
resolvedConfig[key] = value
}
}
if (disabledTagIds.size > 0) {
resolvedConfig.disabledTagIds = Array.from(disabledTagIds)
Expand Down Expand Up @@ -370,8 +378,8 @@ export function AddConnectorModal({
{field.type === 'selector' && field.selectorKey ? (
<ConnectorSelectorField
field={field as ConnectorConfigField & { selectorKey: SelectorKey }}
value={sourceConfig[field.id] || ''}
onChange={(value) => handleFieldChange(field.id, value)}
value={sourceConfig[field.id] ?? (field.multi ? [] : '')}
onChange={(value: ConfigFieldValue) => handleFieldChange(field.id, value)}
credentialId={effectiveCredentialId}
sourceConfig={sourceConfig}
configFields={connectorConfig.configFields}
Expand All @@ -385,13 +393,21 @@ export function AddConnectorModal({
label: opt.label,
value: opt.id,
}))}
value={sourceConfig[field.id] || undefined}
value={
typeof sourceConfig[field.id] === 'string'
? (sourceConfig[field.id] as string) || undefined
: undefined
}
onChange={(value) => handleFieldChange(field.id, value)}
placeholder={field.placeholder || `Select ${field.title.toLowerCase()}`}
/>
) : (
<Input
value={sourceConfig[field.id] || ''}
value={
Array.isArray(sourceConfig[field.id])
? (sourceConfig[field.id] as string[]).join(', ')
: (sourceConfig[field.id] as string) || ''
}
onChange={(e) => handleFieldChange(field.id, e.target.value)}
placeholder={field.placeholder}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
import { useMemo } from 'react'
import { Combobox, type ComboboxOption, Loader } from '@/components/emcn'
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
import type {
ConfigFieldMap,
ConfigFieldValue,
} from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
import { getDependsOnFields } from '@/blocks/utils'
import type { ConnectorConfigField } from '@/connectors/types'
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
import { useSelectorOptions } from '@/hooks/selectors/use-selector-query'

interface ConnectorSelectorFieldProps {
field: ConnectorConfigField & { selectorKey: SelectorKey }
value: string
onChange: (value: string) => void
value: ConfigFieldValue
onChange: (value: ConfigFieldValue) => void
credentialId: string | null
sourceConfig: Record<string, string>
sourceConfig: ConfigFieldMap
configFields: ConnectorConfigField[]
canonicalModes: Record<string, 'basic' | 'advanced'>
disabled?: boolean
Expand All @@ -29,6 +33,8 @@ export function ConnectorSelectorField({
canonicalModes,
disabled,
}: ConnectorSelectorFieldProps) {
const isMulti = Boolean(field.multi)

const context = useMemo<SelectorContext>(() => {
const ctx: SelectorContext = {}
if (credentialId) ctx.oauthCredential = credentialId
Expand Down Expand Up @@ -73,11 +79,34 @@ export function ConnectorSelectorField({
)
}

if (isMulti) {
const multiValues = Array.isArray(value) ? value : value ? [value] : []
return (
<Combobox
multiSelect
options={comboboxOptions}
multiSelectValues={multiValues}
onMultiSelectChange={(values) => onChange(values)}
searchable
searchPlaceholder={`Search ${field.title.toLowerCase()}...`}
placeholder={
!credentialId
? 'Connect an account first'
: !depsResolved
? `Select ${getDependencyLabel(field, configFields)} first`
: field.placeholder || `Select ${field.title.toLowerCase()}`
}
disabled={disabled || !credentialId || !depsResolved}
/>
)
}

const singleValue = Array.isArray(value) ? value[0] : value
return (
<Combobox
options={comboboxOptions}
value={value || undefined}
onChange={onChange}
value={singleValue || undefined}
onChange={(next) => onChange(next)}
searchable
searchPlaceholder={`Search ${field.title.toLowerCase()}...`}
placeholder={
Expand All @@ -96,18 +125,28 @@ function resolveDepValue(
depFieldId: string,
configFields: ConnectorConfigField[],
canonicalModes: Record<string, 'basic' | 'advanced'>,
sourceConfig: Record<string, string>
sourceConfig: ConfigFieldMap
): string {
const depField = configFields.find((f) => f.id === depFieldId)
if (!depField?.canonicalParamId) return sourceConfig[depFieldId] ?? ''
/**
* For multi-value parent fields, pass all selected values to dependent
* selectors as a comma-joined string so the downstream selector can load
* options across every selected parent (e.g. Linear projects across multiple
* selected teams). Single-value parents pass through unchanged.
*/
const readDep = (raw: ConfigFieldValue | undefined): string => {
if (Array.isArray(raw)) return raw.join(',')
return raw ?? ''
}
if (!depField?.canonicalParamId) return readDep(sourceConfig[depFieldId])

const activeMode = canonicalModes[depField.canonicalParamId] ?? 'basic'
if (depField.mode === activeMode) return sourceConfig[depFieldId] ?? ''
if (depField.mode === activeMode) return readDep(sourceConfig[depFieldId])

const activeField = configFields.find(
(f) => f.canonicalParamId === depField.canonicalParamId && f.mode === activeMode
)
return activeField ? (sourceConfig[activeField.id] ?? '') : (sourceConfig[depFieldId] ?? '')
return activeField ? readDep(sourceConfig[activeField.id]) : readDep(sourceConfig[depFieldId])
}

function getDependencyLabel(
Expand Down
Loading
Loading