Skip to content

Commit d06aa1d

Browse files
waleedlatif1claude
andauthored
fix(connectors): align connector scopes with oauth config and fix kb modal UX (#3573)
* fix(connectors): align connector scopes with oauth config and fix kb modal UX * fix(connectors): restore onCheckedChange for keyboard accessibility * feat(connectors): add dynamic selectors to knowledge base connector config Replace manual ID text inputs with dynamic selector dropdowns that fetch options from the existing selector registry. Users can toggle between selector and manual input via canonical pairs (basic/advanced mode). Adds selector support to 12 connectors: Airtable (cascading base→table), Slack, Gmail, Google Calendar, Linear (cascading team→project), Jira, Confluence, MS Teams (cascading team→channel), Notion, Asana, Webflow, and Outlook. Dependency clearing propagates across canonical siblings to prevent stale cross-mode data on submit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * updated animated blocks UI * fix(connectors): clear canonical siblings of dependents and resolve active mode values Fixes three issues from PR review: - Dependency clearing now includes canonical siblings of dependent fields (e.g., changing base clears both tableSelector AND tableIdOrName) - Selector context and depsResolved now resolve dependency values through the active canonical mode, not just the raw depFieldId - Tooltip text changed from "Switch to manual ID" to "Switch to manual input" to correctly describe dropdown fallbacks (e.g., Outlook folder) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: linter class ordering fixes and docs link update Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(connectors): reset apiKeyFocused on connector re-selection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5b9f0d7 commit d06aa1d

File tree

22 files changed

+717
-269
lines changed

22 files changed

+717
-269
lines changed

.claude/commands/add-connector.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ export const {service}Connector: ConnectorConfig = {
117117

118118
The add-connector modal renders these automatically — no custom UI needed.
119119

120+
Three field types are supported: `short-input`, `dropdown`, and `selector`.
121+
120122
```typescript
121123
// Text input
122124
{
@@ -141,6 +143,136 @@ The add-connector modal renders these automatically — no custom UI needed.
141143
}
142144
```
143145

146+
## Dynamic Selectors (Canonical Pairs)
147+
148+
Use `type: 'selector'` to fetch options dynamically from the existing selector registry (`hooks/selectors/registry.ts`). Selectors are always paired with a manual fallback input using the **canonical pair** pattern — a `selector` field (basic mode) and a `short-input` field (advanced mode) linked by `canonicalParamId`.
149+
150+
The user sees a toggle button (ArrowLeftRight) to switch between the selector dropdown and manual text input. On submit, the modal resolves each canonical pair to the active mode's value, keyed by `canonicalParamId`.
151+
152+
### Rules
153+
154+
1. **Every selector field MUST have a canonical pair** — a corresponding `short-input` (or `dropdown`) field with the same `canonicalParamId` and `mode: 'advanced'`.
155+
2. **`required` must be set identically on both fields** in a pair. If the selector is required, the manual input must also be required.
156+
3. **`canonicalParamId` must match the key the connector expects in `sourceConfig`** (e.g. `baseId`, `channel`, `teamId`). The advanced field's `id` should typically match `canonicalParamId`.
157+
4. **`dependsOn` references the selector field's `id`**, not the `canonicalParamId`. The modal propagates dependency clearing across canonical siblings automatically — changing either field in a parent pair clears dependent children.
158+
159+
### Selector canonical pair example (Airtable base → table cascade)
160+
161+
```typescript
162+
configFields: [
163+
// Base: selector (basic) + manual (advanced)
164+
{
165+
id: 'baseSelector',
166+
title: 'Base',
167+
type: 'selector',
168+
selectorKey: 'airtable.bases', // Must exist in hooks/selectors/registry.ts
169+
canonicalParamId: 'baseId',
170+
mode: 'basic',
171+
placeholder: 'Select a base',
172+
required: true,
173+
},
174+
{
175+
id: 'baseId',
176+
title: 'Base ID',
177+
type: 'short-input',
178+
canonicalParamId: 'baseId',
179+
mode: 'advanced',
180+
placeholder: 'e.g. appXXXXXXXXXXXXXX',
181+
required: true,
182+
},
183+
// Table: selector depends on base (basic) + manual (advanced)
184+
{
185+
id: 'tableSelector',
186+
title: 'Table',
187+
type: 'selector',
188+
selectorKey: 'airtable.tables',
189+
canonicalParamId: 'tableIdOrName',
190+
mode: 'basic',
191+
dependsOn: ['baseSelector'], // References the selector field ID
192+
placeholder: 'Select a table',
193+
required: true,
194+
},
195+
{
196+
id: 'tableIdOrName',
197+
title: 'Table Name or ID',
198+
type: 'short-input',
199+
canonicalParamId: 'tableIdOrName',
200+
mode: 'advanced',
201+
placeholder: 'e.g. Tasks',
202+
required: true,
203+
},
204+
// Non-selector fields stay as-is
205+
{ id: 'maxRecords', title: 'Max Records', type: 'short-input', ... },
206+
]
207+
```
208+
209+
### Selector with domain dependency (Jira/Confluence pattern)
210+
211+
When a selector depends on a plain `short-input` field (no canonical pair), `dependsOn` references that field's `id` directly. The `domain` field's value maps to `SelectorContext.domain` automatically via `SELECTOR_CONTEXT_FIELDS`.
212+
213+
```typescript
214+
configFields: [
215+
{
216+
id: 'domain',
217+
title: 'Jira Domain',
218+
type: 'short-input',
219+
placeholder: 'yoursite.atlassian.net',
220+
required: true,
221+
},
222+
{
223+
id: 'projectSelector',
224+
title: 'Project',
225+
type: 'selector',
226+
selectorKey: 'jira.projects',
227+
canonicalParamId: 'projectKey',
228+
mode: 'basic',
229+
dependsOn: ['domain'],
230+
placeholder: 'Select a project',
231+
required: true,
232+
},
233+
{
234+
id: 'projectKey',
235+
title: 'Project Key',
236+
type: 'short-input',
237+
canonicalParamId: 'projectKey',
238+
mode: 'advanced',
239+
placeholder: 'e.g. ENG, PROJ',
240+
required: true,
241+
},
242+
]
243+
```
244+
245+
### How `dependsOn` maps to `SelectorContext`
246+
247+
The connector selector field builds a `SelectorContext` from dependency values. For the mapping to work, each dependency's `canonicalParamId` (or field `id` for non-canonical fields) must exist in `SELECTOR_CONTEXT_FIELDS` (`lib/workflows/subblocks/context.ts`):
248+
249+
```
250+
oauthCredential, domain, teamId, projectId, knowledgeBaseId, planId,
251+
siteId, collectionId, spreadsheetId, fileId, baseId, datasetId, serviceDeskId
252+
```
253+
254+
### Available selector keys
255+
256+
Check `hooks/selectors/types.ts` for the full `SelectorKey` union. Common ones for connectors:
257+
258+
| SelectorKey | Context Deps | Returns |
259+
|-------------|-------------|---------|
260+
| `airtable.bases` | credential | Base ID + name |
261+
| `airtable.tables` | credential, `baseId` | Table ID + name |
262+
| `slack.channels` | credential | Channel ID + name |
263+
| `gmail.labels` | credential | Label ID + name |
264+
| `google.calendar` | credential | Calendar ID + name |
265+
| `linear.teams` | credential | Team ID + name |
266+
| `linear.projects` | credential, `teamId` | Project ID + name |
267+
| `jira.projects` | credential, `domain` | Project key + name |
268+
| `confluence.spaces` | credential, `domain` | Space key + name |
269+
| `notion.databases` | credential | Database ID + name |
270+
| `asana.workspaces` | credential | Workspace GID + name |
271+
| `microsoft.teams` | credential | Team ID + name |
272+
| `microsoft.channels` | credential, `teamId` | Channel ID + name |
273+
| `webflow.sites` | credential | Site ID + name |
274+
| `outlook.folders` | credential | Folder ID + name |
275+
144276
## ExternalDocument Shape
145277

146278
Every document returned from `listDocuments`/`getDocument` must include:
@@ -287,6 +419,12 @@ export const CONNECTOR_REGISTRY: ConnectorRegistry = {
287419
- [ ] **Auth configured correctly:**
288420
- OAuth: `auth.provider` matches an existing `OAuthService` in `lib/oauth/types.ts`
289421
- API key: `auth.label` and `auth.placeholder` set appropriately
422+
- [ ] **Selector fields configured correctly (if applicable):**
423+
- Every `type: 'selector'` field has a canonical pair (`short-input` or `dropdown` with same `canonicalParamId` and `mode: 'advanced'`)
424+
- `required` is identical on both fields in each canonical pair
425+
- `selectorKey` exists in `hooks/selectors/registry.ts`
426+
- `dependsOn` references selector field IDs (not `canonicalParamId`)
427+
- Dependency `canonicalParamId` values exist in `SELECTOR_CONTEXT_FIELDS`
290428
- [ ] `listDocuments` handles pagination and computes content hashes
291429
- [ ] `sourceUrl` set on each ExternalDocument (full URL, not relative)
292430
- [ ] `metadata` includes source-specific data for tag mapping

0 commit comments

Comments
 (0)