-
Notifications
You must be signed in to change notification settings - Fork 66
Ask for a name when adding another integration account #1544
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8582418
8fa5680
1810472
787f3cb
272d732
338ec7b
0c5097c
71e4964
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,9 +39,14 @@ import { | |
| } from '#client/routes/record-table.tsx' | ||
| import { renderByokExplainer } from '#client/routes/byok-explainer.tsx' | ||
| import { | ||
| addAccountAnchorId, | ||
| buildAddAccountHref, | ||
| buildCustomIntegrationSetupPrompt, | ||
| buildIntegrationSetupPrompt, | ||
| integrationProviderSuggestions, | ||
| isAddAccountFormOpen, | ||
| nextSuggestedConnectionName, | ||
| resolveAddAccountConnectionName, | ||
| } from '#client/routes/integration-provider-catalog.ts' | ||
| import { integrationDisplayName } from '#client/routes/integration-filter.ts' | ||
| import { matchesSearchQuery } from '#client/search-filter.ts' | ||
|
|
@@ -258,8 +263,11 @@ function buildConnectOauthHref(input: { | |
| appSlug?: string | ||
| }) { | ||
| const params = new URLSearchParams({ provider: input.name }) | ||
| const appSlug = input.appSlug?.trim() | ||
| if (input.platform) { | ||
| params.set('platform', input.appSlug?.trim() || '1') | ||
| params.set('platform', appSlug || '1') | ||
| } else if (appSlug) { | ||
| params.set('app', appSlug) | ||
| } | ||
| return `/connect/oauth?${params.toString()}` | ||
| } | ||
|
|
@@ -268,6 +276,132 @@ function connectActionLabel(status: 'Connected' | 'Needs setup') { | |
| return status === 'Connected' ? 'Reconnect' : 'Connect' | ||
| } | ||
|
|
||
| const addAccountLinkCss = { | ||
| ...primaryLinkCss, | ||
| justifySelf: 'start', | ||
| width: 'fit-content', | ||
| } | ||
|
|
||
| function AddAccountForm( | ||
| handle: Handle<{ | ||
| slug: string | ||
| platform: boolean | ||
| existingNames: ReadonlyArray<string> | ||
| open: boolean | ||
| openHref: string | ||
| }>, | ||
| ) { | ||
| let nameError: string | null = null | ||
| let editedName: string | null = null | ||
| let boundSlug = handle.props.slug | ||
|
|
||
| function connectHref(connectionName: string) { | ||
| return buildConnectOauthHref({ | ||
| name: connectionName, | ||
| platform: handle.props.platform, | ||
| appSlug: handle.props.slug, | ||
| }) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| return () => { | ||
| if (handle.props.slug !== boundSlug) { | ||
| boundSlug = handle.props.slug | ||
| editedName = null | ||
| nameError = null | ||
| } | ||
| const suggested = nextSuggestedConnectionName( | ||
| handle.props.slug, | ||
| handle.props.existingNames, | ||
| ) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| const name = editedName ?? suggested | ||
| if (!handle.props.open) { | ||
| return ( | ||
| <a | ||
| href={handle.props.openHref} | ||
| data-testid="add-account-open" | ||
| data-prevent-scroll-reset | ||
| mix={css(addAccountLinkCss)} | ||
| > | ||
| Add another account | ||
| </a> | ||
| ) | ||
| } | ||
| return ( | ||
| <form | ||
| id={addAccountAnchorId} | ||
| data-testid="add-account-form" | ||
| mix={[ | ||
| on('submit', (event) => { | ||
| event.preventDefault() | ||
| const resolved = resolveAddAccountConnectionName({ | ||
| name, | ||
| suggested, | ||
| existingNames: handle.props.existingNames, | ||
| }) | ||
| if (!resolved.ok) { | ||
| nameError = resolved.error | ||
| handle.update() | ||
| return | ||
| } | ||
| nameError = null | ||
| window.location.assign(connectHref(resolved.name)) | ||
|
Comment on lines
+334
to
+347
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the OAuth connection persistence path and its uniqueness checks.
fd -i 'account-secrets*' packages
rg -n -C 12 --glob '*.ts' \
'connect_oauth|upsertIntegration|createIntegration|connection.*name|oauth.*app.*slug|unique' \
packages/workerRepository: kentcdodds/kody Length of output: 50372 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- integration persistence symbols ---'
rg -n -C 18 --glob '*.ts' \
'function upsertIntegrationConnection|export async function upsertIntegrationConnection|function getJoinedIntegrationByName|resolveOrCreateOauthApp|CREATE TABLE.*integration|integration_connections|oauth_apps' \
packages/worker/src/integrations packages/worker/migrations packages/worker/src/app packages/worker/src/routes 2>/dev/null | head -n 1200
printf '%s\n' '--- OAuth connect route symbols ---'
rg -n -C 20 --glob '*.{ts,tsx}' \
'connect/oauth|upsertIntegration\(|upsertIntegrationConnection\(|name.*searchParams|connectionName|provider' \
packages/worker/src packages/worker/client 2>/dev/null | head -n 1600Repository: kentcdodds/kody Length of output: 50371 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(service|repo|.*migration.*|.*oauth.*|account-integrations|connect).*' | rg 'packages/worker|packages/mcp' | head -n 500
printf '%s\n' '--- persistence implementation locations ---'
rg -n --glob '*.ts' \
'upsertIntegrationConnection|getJoinedIntegrationByName|resolveOrCreateOauthApp|connect/oauth|upsertIntegration\(' \
packages/worker | head -n 300
printf '%s\n' '--- schema and index declarations ---'
rg -n --glob '*.{sql,ts}' \
'CREATE TABLE (user_integrations|user_oauth_apps)|CREATE UNIQUE INDEX|UNIQUE\(.*(name|slug)|user_integrations.*name|user_oauth_apps.*slug' \
. | head -n 500Repository: kentcdodds/kody Length of output: 37258 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- integration repository ---'
sed -n '100,190p;380,480p' packages/worker/src/integrations/repo.ts
printf '%s\n' '--- integration service save paths ---'
sed -n '230,310p;338,460p;865,1015p' packages/worker/src/integrations/service.ts
printf '%s\n' '--- database schema ---'
sed -n '350,405p;550,610p' packages/worker/migrations/0001-squashed-init.sql
sed -n '1,90p' packages/worker/migrations/0004-platform-oauth-apps.sql
printf '%s\n' '--- connect OAuth handler persistence calls ---'
sed -n '1,280p' packages/worker/src/app/handlers/connect-oauth.ts
sed -n '850,935p' packages/worker/src/app/handlers/account-secrets.tsRepository: kentcdodds/kody Length of output: 29296 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- slug allocation and normalization ---'
rg -n -C 20 --glob '*.ts' \
'function allocateAppSlug|allocateAppSlug|canonicalIntegrationName|normalizeIntegrationName|existingNames' \
packages/worker/src/integrations packages/worker/src/app packages/worker/client/routes/account-integrations.tsx packages/worker/client/routes/connect-oauth.tsx
printf '%s\n' '--- OAuth persistence call graph ---'
rg -n -C 25 --glob '*.ts' \
'saveOAuth|save.*Integration|persist.*OAuth|upsertIntegration|oauth.*persist|exchange.*token|connect.*integration' \
packages/worker/src/app/handlers packages/worker/src/integrations packages/worker/client/routes/connect-oauth.tsx
printf '%s\n' '--- relevant connect-oauth client sections ---'
rg -n 'function |const .* =|fetch\(|location\.|sessionStorage|name|provider|accountLabel' \
packages/worker/client/routes/connect-oauth.tsx | head -n 350Repository: kentcdodds/kody Length of output: 50372 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- exact allocator and canonicalization ---'
rg -n 'allocateAppSlug|canonicalIntegrationName' \
packages/worker/src/integrations/service.ts \
packages/worker/src/mcp/capabilities/integrations/integration-shared.ts \
packages/worker/src/app/account-integrations-data.ts \
packages/worker/client/routes/account-integrations.tsx
printf '%s\n' '--- service tail ---'
sed -n '1000,1175p' packages/worker/src/integrations/service.ts
printf '%s\n' '--- connect OAuth and callback persistence references ---'
rg -n -C 12 --glob '*.ts' \
'upsertOauthAppWithoutConnection|saveOAuthIntegration|saveOAuth|exchange.*persist|persist.*connect|upsertIntegration' \
packages/worker/src/app packages/worker/src/oauth-handlers.ts packages/worker/src/integrations/oauth-token-exchange.ts
printf '%s\n' '--- client connect form handlers ---'
rg -n -C 15 --glob '*.tsx' \
'upsertOauthAppWithoutConnection|fetch\(|sessionStorage|window\.location|location\.assign|name:' \
packages/worker/client/routes/connect-oauth.tsxRepository: kentcdodds/kody Length of output: 50371 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- canonical name and UI uniqueness behavior ---'
sed -n '1,35p;120,155p' packages/worker/src/mcp/capabilities/integrations/integration-shared.ts
rg -n -C 18 \
'existingNames|resolveAddAccountConnectionName|nameError|connectHref' \
packages/worker/client/routes/account-integrations.tsx
printf '%s\n' '--- account integration name sources ---'
sed -n '330,415p' packages/worker/src/app/account-integrations-data.ts
sed -n '1,145p' packages/worker/src/app/account-integrations-data.ts
printf '%s\n' '--- connect_oauth request handler and save action ---'
rg -n -C 20 \
'action === .connect_oauth.|case .connect_oauth.|connect_oauth|save_oauth_app' \
packages/worker/src/app/handlers/account-secrets.ts | head -n 500
printf '%s\n' '--- transaction/batch use in integration save code ---'
rg -n -C 8 \
'APP_DB\.batch|db\.batch|BEGIN|transaction|upsertIntegrationConnection|allocateAppSlug' \
packages/worker/src/integrations/service.ts packages/worker/src/integrations/repo.tsRepository: kentcdodds/kody Length of output: 26811 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- add-name resolver ---'
sed -n '1,260p' packages/worker/client/routes/integration-provider-catalog.ts
printf '%s\n' '--- app and connection list construction ---'
rg -n -C 18 \
'listOauthApps|listJoinedIntegrations|connections:|apps:' \
packages/worker/src/app/account-integrations-data.ts \
packages/worker/src/integrations/service.ts
printf '%s\n' '--- exact provider lookup order ---'
rg -n -C 25 \
'export async function getJoinedIntegration|async function getJoinedIntegration|loadAccountIntegrationByName|getOauthApp\(' \
packages/worker/src/integrations/service.ts \
packages/worker/src/app/account-integrations-data.tsRepository: kentcdodds/kody Length of output: 50371 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import sqlite3
db = sqlite3.connect(':memory:')
db.executescript("""
CREATE TABLE user_oauth_apps (
user_id TEXT NOT NULL,
slug TEXT NOT NULL,
PRIMARY KEY (user_id, slug)
);
CREATE TABLE user_integrations (
user_id TEXT NOT NULL,
name TEXT NOT NULL,
app_slug TEXT NOT NULL,
PRIMARY KEY (user_id, name),
FOREIGN KEY (user_id, app_slug) REFERENCES user_oauth_apps(user_id, slug)
);
""")
# Reproduce the repository's independent keys and upsert shape.
db.execute(
"INSERT INTO user_oauth_apps(user_id, slug) VALUES (?, ?)",
("u1", "alpha"),
)
db.execute(
"INSERT INTO user_oauth_apps(user_id, slug) VALUES (?, ?)",
("u1", "other"),
)
# A connection named like another OAuth app's slug is accepted.
db.execute(
"INSERT INTO user_integrations(user_id, name, app_slug) VALUES (?, ?, ?)",
("u1", "alpha", "other"),
)
# Reproduce ON CONFLICT(user_id, name) DO UPDATE: same-name submissions
# update one row instead of being rejected.
db.execute("""
INSERT INTO user_integrations(user_id, name, app_slug)
VALUES (?, ?, ?)
ON CONFLICT(user_id, name) DO UPDATE SET app_slug = excluded.app_slug
""", ("u1", "alpha", "alpha"))
count = db.execute(
"SELECT COUNT(*) FROM user_integrations WHERE user_id = ? AND name = ?",
("u1", "alpha"),
).fetchone()[0]
cross_collision = db.execute("""
SELECT COUNT(*)
FROM user_integrations i
JOIN user_oauth_apps a
ON a.user_id = i.user_id AND a.slug = i.name
WHERE i.user_id = ?
""", ("u1",)).fetchone()[0]
print({"same_name_row_count": count, "cross_table_name_collision_count": cross_collision})
assert count == 1
assert cross_collision == 1
PYRepository: kentcdodds/kody Length of output: 219 Enforce the normalized connection-name invariant at the server save boundary.
🤖 Prompt for AI Agents |
||
| }), | ||
| css({ | ||
| display: 'grid', | ||
| gap: spacing.sm, | ||
| justifyItems: 'start', | ||
| scrollMarginTop: '5.5rem', | ||
| }), | ||
| ]} | ||
| > | ||
| <label mix={css(fieldCss)}> | ||
| <span mix={css(fieldLabelCss)}>Connection name</span> | ||
| <input | ||
| type="text" | ||
| name="connectionName" | ||
| data-field-ring | ||
| required | ||
| value={name} | ||
| aria-invalid={nameError ? 'true' : undefined} | ||
| aria-describedby={nameError ? 'add-account-name-error' : undefined} | ||
| {...passwordManagerIgnoreProps} | ||
| mix={[ | ||
| on('input', (event) => { | ||
| editedName = event.currentTarget.value | ||
| nameError = null | ||
| handle.update() | ||
| }), | ||
| css(accountInputCss), | ||
| ]} | ||
| /> | ||
| {nameError ? ( | ||
| <p | ||
| id="add-account-name-error" | ||
| role="alert" | ||
| data-testid="add-account-name-error" | ||
| mix={css({ | ||
| ...descriptionCss, | ||
| color: colors.error, | ||
| })} | ||
| > | ||
| {nameError} | ||
| </p> | ||
| ) : null} | ||
| </label> | ||
| <button | ||
| type="submit" | ||
| mix={css({ | ||
| ...getPillButtonCss({ size: 'sm' }), | ||
| display: 'inline-flex', | ||
| })} | ||
| > | ||
| Connect | ||
| </button> | ||
| </form> | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| function PlugIcon() { | ||
| return ( | ||
| <svg | ||
|
|
@@ -913,6 +1047,16 @@ export function AccountIntegrationsRoute(handle: Handle) { | |
| </article> | ||
| ) | ||
| })} | ||
| <AddAccountForm | ||
| slug={selectedApp.slug} | ||
| platform={isBuiltInApp(selectedApp)} | ||
| existingNames={[ | ||
| ...integrations.map((entry) => entry.name), | ||
| ...apps.map((app) => app.slug), | ||
| ]} | ||
| open={isAddAccountFormOpen(getCurrentHref())} | ||
| openHref={buildAddAccountHref(getCurrentHref())} | ||
| /> | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| </div> | ||
| )} | ||
| </section> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.