Skip to content

Commit 07049be

Browse files
committed
fix(kb-connectors): Linear projects selector loads across all selected teams
When the team selector is in multi-select mode, the basic-mode projects dropdown was passing only the first team ID into the linear.projects selector context (via readFirst in resolveDepValue), so projects from other selected teams were invisible. resolveDepValue now joins multi-value parents into a CSV string so dependent selectors receive every selected parent ID. The /api/tools/linear/projects route splits the CSV teamId, fetches projects from each team in parallel, and dedupes by project ID. Single-team configs pass through unchanged (`split(",")` on a bare ID yields a one-element array). The AND-of-filters semantics in buildIssuesQuery is intentional and matches standard GraphQL filter behavior — a user filtering on teams [A,B] and projects [X,Y] gets issues in (A or B) AND (X or Y). With this fix the project dropdown now shows every project under any selected team so the user can compose the right project set.
1 parent 2392173 commit 07049be

2 files changed

Lines changed: 42 additions & 13 deletions

File tree

apps/sim/app/api/tools/linear/projects/route.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,40 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4545
}
4646

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

50-
const team = await linearClient.team(teamId)
51-
const projectsResult = await team.projects()
52-
projects = projectsResult.nodes.map((project: Project) => ({
53-
id: project.id,
54-
name: project.name,
55-
}))
49+
/**
50+
* teamId may be a single ID or a comma-separated list when the basic-mode
51+
* team selector is in multi-select. Fetch projects from each team in
52+
* parallel and dedupe by project ID (Linear projects can be cross-team).
53+
*/
54+
const teamIds = teamId
55+
.split(',')
56+
.map((s) => s.trim())
57+
.filter(Boolean)
58+
59+
const perTeam = await Promise.all(
60+
teamIds.map(async (id) => {
61+
const team = await linearClient.team(id)
62+
const result = await team.projects()
63+
return result.nodes.map((project: Project) => ({
64+
id: project.id,
65+
name: project.name,
66+
}))
67+
})
68+
)
69+
70+
const seen = new Set<string>()
71+
const projects: Array<{ id: string; name: string }> = []
72+
for (const teamProjects of perTeam) {
73+
for (const project of teamProjects) {
74+
if (seen.has(project.id)) continue
75+
seen.add(project.id)
76+
projects.push(project)
77+
}
78+
}
5679

5780
if (projects.length === 0) {
58-
logger.info('No projects found for team', { teamId })
81+
logger.info('No projects found for team(s)', { teamIds })
5982
}
6083

6184
return NextResponse.json({ projects })

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,19 +128,25 @@ function resolveDepValue(
128128
sourceConfig: ConfigFieldMap
129129
): string {
130130
const depField = configFields.find((f) => f.id === depFieldId)
131-
const readFirst = (raw: ConfigFieldValue | undefined): string => {
132-
if (Array.isArray(raw)) return raw[0] ?? ''
131+
/**
132+
* For multi-value parent fields, pass all selected values to dependent
133+
* selectors as a comma-joined string so the downstream selector can load
134+
* options across every selected parent (e.g. Linear projects across multiple
135+
* selected teams). Single-value parents pass through unchanged.
136+
*/
137+
const readDep = (raw: ConfigFieldValue | undefined): string => {
138+
if (Array.isArray(raw)) return raw.join(',')
133139
return raw ?? ''
134140
}
135-
if (!depField?.canonicalParamId) return readFirst(sourceConfig[depFieldId])
141+
if (!depField?.canonicalParamId) return readDep(sourceConfig[depFieldId])
136142

137143
const activeMode = canonicalModes[depField.canonicalParamId] ?? 'basic'
138-
if (depField.mode === activeMode) return readFirst(sourceConfig[depFieldId])
144+
if (depField.mode === activeMode) return readDep(sourceConfig[depFieldId])
139145

140146
const activeField = configFields.find(
141147
(f) => f.canonicalParamId === depField.canonicalParamId && f.mode === activeMode
142148
)
143-
return activeField ? readFirst(sourceConfig[activeField.id]) : readFirst(sourceConfig[depFieldId])
149+
return activeField ? readDep(sourceConfig[activeField.id]) : readDep(sourceConfig[depFieldId])
144150
}
145151

146152
function getDependencyLabel(

0 commit comments

Comments
 (0)