diff --git a/apps/docs/content/docs/search/coda.mdx b/apps/docs/content/docs/search/coda.mdx index c2a276f7cf5..23f937b18b6 100644 --- a/apps/docs/content/docs/search/coda.mdx +++ b/apps/docs/content/docs/search/coda.mdx @@ -19,14 +19,27 @@ Sign in to Coda with a dedicated integration account that can read the documents Use a standard REST API token. Do not select the MCP restriction. -Coda API connections with the Generate new token dialog +Coda Generate new token dialog with a name and no restriction selected -### Add the source +### Add the credential -In Sim, open **Settings → Sources → Add source → Coda**. Add or select a Coda service account credential and enter the token when prompted. +In Sim, open **Settings → Sources → Add source → Coda**. If Coda is already listed, open it and select **Add connection**. + +Open **Service account → Add API token**. Paste the token, give it a recognizable display name, then select **Add API token**. You can also choose an existing Coda credential. + +Add Coda API token form with token, display name, and optional description fields + + + + +### Choose documents + +Select the documents to index, or leave the selection empty for discovery. + +Coda connection setup with a saved credential, Documents selector, and optional Enterprise organization ID | Field | What to enter | |---|---| @@ -40,7 +53,7 @@ The picker shows accessible documents the token owner has opened. Use IDs for ot ### Sync and verify -Select **Connect & Sync**. Open the source's **Documents** and **Sync history** to check indexing. Content and permission changes appear after synchronization. +Select **Connect & Sync**. Open the source's **Documents** and **Sync history** to check indexing. Content and permission changes appear after synchronization. Have a teammate with a matching verified email search for a shared document, and confirm that an unshared teammate cannot find it. To rotate the token, add the replacement credential, update the source, and verify a sync before revoking the old token. @@ -59,8 +72,17 @@ Ordinary connections index visible canvas text and base-table rows. Enterprise c Enterprise mode excludes deactivated and deleted users. Direct-share guests absent from the organization directory are excluded; guests explicitly listed in a group or workspace may receive that membership's access. Ordinary connections cannot check Coda organization deactivation, so manage departing users' Sim membership too. -Sim refreshes permissions even when content is unchanged. Unverified permissions never become broad access, and Sim admin status does not override Coda sharing. For setup failures or missing results, check the token's access, Enterprise admin role if applicable, the teammate's verified email, and **Sync history**. +Sim refreshes permissions even when content is unchanged. Unverified permissions never become broad access, and Sim admin status does not override Coda sharing. + +These access rules apply to organization Search and **Admin access** sources. In a regular knowledge base, **Workspace access** shares all indexed content with that Sim workspace and ignores the Enterprise organization ID. -In a regular knowledge base, **Workspace access** intentionally shares all indexed content with that Sim workspace and does not use the Enterprise organization ID. The permissions above apply to organization Search and **Admin access** sources. +## Troubleshooting + +| Problem | Next step | +|---|---| +| Document missing from the picker | Open it in Coda with the token owner, or enter its document ID. | +| Enterprise setup fails | Confirm the organization ID and Enterprise org-admin role. A workspace admin token is insufficient. | +| Teammate sees no results | Check organization membership, their verified Sim email, the Coda share, and **Sync history**. | +| Token expired or revoked | Replace the credential in the connection's **Settings**, then verify a sync. | See Coda's [public API](https://coda.io/developers/apis/v1) and [Enterprise Admin API](https://coda.io/developers/apis/admin/v1). diff --git a/apps/docs/public/static/search/coda-api-token.jpg b/apps/docs/public/static/search/coda-api-token.jpg deleted file mode 100644 index 09aa99e08eb..00000000000 Binary files a/apps/docs/public/static/search/coda-api-token.jpg and /dev/null differ diff --git a/apps/docs/public/static/search/coda-credential.jpg b/apps/docs/public/static/search/coda-credential.jpg new file mode 100644 index 00000000000..7ba5fdb9e5e Binary files /dev/null and b/apps/docs/public/static/search/coda-credential.jpg differ diff --git a/apps/docs/public/static/search/coda-setup.jpg b/apps/docs/public/static/search/coda-setup.jpg new file mode 100644 index 00000000000..76d390bd56d Binary files /dev/null and b/apps/docs/public/static/search/coda-setup.jpg differ diff --git a/apps/docs/public/static/search/coda-token-creation.jpg b/apps/docs/public/static/search/coda-token-creation.jpg new file mode 100644 index 00000000000..f6a1bd64475 Binary files /dev/null and b/apps/docs/public/static/search/coda-token-creation.jpg differ diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx index a8ca013792d..e9b3be36d43 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx @@ -6,7 +6,10 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { Credential } from '@/lib/oauth' import type { SourceSelectionLabels } from '@/lib/sim-search/source-identity' -import type { ServiceAccountConnectTarget } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal' +import type { + ServiceAccountConnectTarget, + useServiceAccountConnectTarget, +} from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal' import type { ConnectorConfigFieldsProps } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-config-fields/connector-config-fields' import type { ConfigFieldMap } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields' @@ -26,6 +29,7 @@ const mocks = vi.hoisted(() => ({ refetchCredentials: vi.fn(), oauthModal: vi.fn(), serviceAccountModal: vi.fn(), + serviceAccountTargetInput: vi.fn(), githubSetup: vi.fn(), serviceAccountTarget: null as ServiceAccountConnectTarget | null, memberAccess: true, @@ -77,6 +81,7 @@ vi.mock('@/hooks/use-permission-config', () => ({ ['gmail_v2', { oauthAvailable: true, state: 'ready' }], ['google_calendar_v2', { oauthAvailable: true, state: 'ready' }], ['confluence_v2', { oauthAvailable: true, state: 'ready' }], + ['coda', { oauthAvailable: false, state: 'ready' }], ]), oauthServiceAvailability: new Map( [ @@ -173,7 +178,12 @@ vi.mock( ) : null }, - useServiceAccountConnectTarget: () => mocks.serviceAccountTarget, + useServiceAccountConnectTarget: ( + args: Parameters[0] + ) => { + mocks.serviceAccountTargetInput(args) + return args.serviceAccountProviderId ? mocks.serviceAccountTarget : null + }, }) ) vi.mock('@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-config-fields', () => ({ @@ -223,6 +233,7 @@ vi.mock('@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config })) import { AddConnectorModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal' +import { codaConnectorMeta } from '@/connectors/coda/meta' import { confluenceConnectorMeta } from '@/connectors/confluence/meta' import { googleDriveConnectorMeta } from '@/connectors/google-drive/meta' import { useConnectorSetupStore } from '@/stores/connector-setup/store' @@ -916,6 +927,55 @@ describe('Account connection dropdown', () => { ) } ) + it.each([true, false])( + 'creates a Coda token credential inline (Search: %s)', + async (isSearchIndex) => { + mocks.credentials = [] + mocks.serviceAccountTarget = { + serviceAccountProviderId: 'coda-service-account', + serviceName: 'Coda', + serviceIcon: codaConnectorMeta.icon, + label: 'Add API token', + hidden: false, + } + await render({ + initialConnectorType: 'coda', + lockedAccessMode: 'admin', + isSearchIndex, + scope: isSearchIndex + ? { kind: 'organization', organizationId: 'org-1' } + : { kind: 'workspace', workspaceId: 'workspace-1' }, + }) + expect(mocks.serviceAccountTargetInput).toHaveBeenLastCalledWith( + expect.objectContaining({ serviceAccountProviderId: 'coda-service-account' }) + ) + expect(button('Connect & Sync')).toBeDisabled() + await act(async () => combobox('Select a service account').click()) + const options = Array.from(document.querySelectorAll('[role="option"]')) + expect(options.map((option) => option.textContent?.trim())).toEqual(['Add API token']) + await act(async () => + options[0].dispatchEvent(new MouseEvent('mousedown', { bubbles: true })) + ) + expect(mocks.serviceAccountModal).toHaveBeenLastCalledWith( + expect.objectContaining({ + serviceAccountProviderId: 'coda-service-account', + ...(isSearchIndex ? { organizationId: 'org-1' } : { workspaceId: 'workspace-1' }), + }) + ) + await act(async () => button('Finish service account setup').click()) + await act(async () => button('Connect & Sync').click()) + expect(mocks.create).toHaveBeenCalledWith( + expect.objectContaining({ + connectorType: 'coda', + credentialId: 'new-service-account', + accessMode: 'admin', + }), + expect.any(Object) + ) + expect(mocks.oauthModal).not.toHaveBeenCalled() + } + ) + it.each(['google_drive', 'gmail', 'google_calendar'])( 'opens only service-account creation for a central %s source and submits that credential', async (connectorType) => { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx index 92da9801726..b8b1a7c334f 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx @@ -19,6 +19,7 @@ import { import { ArrowLeft, ChevronDown, ChevronRight, Plus, Search } from '@sim/emcn/icons' import type { ConnectorData } from '@/lib/api/contracts/knowledge/connectors' import { type ResourceScope, resourceScopeFields } from '@/lib/core/resource-scope' +import { asServiceAccountProviderId } from '@/lib/credentials/service-account-provider-ids' import { getIntegrationsForCredentialProvider } from '@/lib/integrations/credential-display' import { initialConnectorAccessMode } from '@/lib/knowledge/connectors/access-modes' import { @@ -247,10 +248,8 @@ export function AddConnectorModal({ !isConnectorCredentialTypeAllowed(connectorConfig.auth, access.accessMode, 'oauth') const serviceAccountTarget = useServiceAccountConnectTarget({ serviceAccountProviderId: - (isSearchIndex || requiresServiceAccount) && - (serviceAccountProviderId === 'google-service-account' || - serviceAccountProviderId === 'atlassian-service-account') - ? serviceAccountProviderId + isSearchIndex || requiresServiceAccount + ? asServiceAccountProviderId(serviceAccountProviderId) : undefined, serviceName: connectorConfig?.name, serviceIcon: connectorConfig?.icon, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.test.tsx index d5776048408..e3eef736633 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.test.tsx @@ -113,6 +113,7 @@ vi.mock( ) import { ConnectorSettingsFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields' +import { codaConnectorMeta } from '@/connectors/coda/meta' import { confluenceConnectorMeta } from '@/connectors/confluence/meta' import { githubConnectorMeta } from '@/connectors/github/meta' import { googleDriveConnectorMeta } from '@/connectors/google-drive/meta' @@ -507,6 +508,7 @@ describe('connector settings service-account choices', () => { product: 'confluence', }, { meta: googleDriveConnectorMeta, provider: 'google-service-account', product: undefined }, + { meta: codaConnectorMeta, provider: 'coda-service-account', product: undefined }, ])( 'opens the correct $meta.name service-account setup from Search settings', async ({ meta, provider, product }) => { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx index a149de2e6be..d880808a513 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx @@ -12,6 +12,7 @@ import { import { ChevronDown, ChevronRight, Plus } from '@sim/emcn/icons' import type { ConnectorAccessMode } from '@/lib/api/contracts/knowledge/connectors' import { type ResourceScope, resourceScopeFields } from '@/lib/core/resource-scope' +import { asServiceAccountProviderId } from '@/lib/credentials/service-account-provider-ids' import { getProviderIdFromServiceId, getServiceAccountProviderForProviderId, @@ -174,10 +175,8 @@ export function ConnectorSettingsFields({ : undefined const serviceAccountTarget = useServiceAccountConnectTarget({ serviceAccountProviderId: - (isSearchIndex || requiresServiceAccount) && - (serviceAccountProviderId === 'google-service-account' || - serviceAccountProviderId === 'atlassian-service-account') - ? serviceAccountProviderId + isSearchIndex || requiresServiceAccount + ? asServiceAccountProviderId(serviceAccountProviderId) : undefined, serviceName: connectorConfig?.name, serviceIcon: connectorConfig?.icon, diff --git a/apps/sim/connectors/coda/README.md b/apps/sim/connectors/coda/README.md index 1b2ae294e9b..205452cb1c6 100644 --- a/apps/sim/connectors/coda/README.md +++ b/apps/sim/connectors/coda/README.md @@ -72,7 +72,7 @@ The application test uses disposable PostgreSQL/Redis, real credentials, Coda, s - Focused connector, selector, credential, navigation, configuration, and integration-availability tests passed. App and deployment-config type checks, Biome, and the strict API-contract audit passed. - Live provider tests cover page/table hydration, page edits, and row edits. Coda's real initialization `409` led to an explicit retry path. - Real application setup and ingestion passed in both workspace and organization scope (the live sharing cases require a workspace that allows the intended cross-domain share). Organization scope creates the token credential through the authorized application use case. Owner search/chunks were allowed; another verified user, unverified owner, and workspace API key were denied. Embeddings were substituted, so external embedding-provider behavior was not tested. -- Sim's browser showed the Coda source Active, both indexed documents, the live document picker, the saved organization credential, and preserved selection across connection/input modes. Browser checks caught and fixed missing Search URL registration, scope loss when switching modes, and deployment metadata rejecting Coda's token credential. Coda's official browser app showed the fixture content. +- Sim's browser showed the Coda source Active, both indexed documents, the live document picker, inline API-token creation, credential replacement followed by a successful sync, and preserved selection across connection/input modes. Browser checks caught and fixed missing Search URL registration, scope loss when switching modes, deployment metadata rejecting Coda's token credential, and a setup allowlist hiding inline Coda credential creation. Coda's official browser app showed the fixture content. - Live sharing to the requested second account was rejected by the source policy: “Cross domain sharing is prohibited.” Grant/revoke tests remain unverified live, with unit coverage for ACL changes and incomplete responses. - The available test account returned no Enterprise organizations; the official UI offers sales-assisted Enterprise setup rather than a self-service trial. Admin API behavior is checked against published schemas and mocked responses; organization-wide crawling, deactivation, and group/workspace/domain permission parity still require an Enterprise tenant test before being considered release-verified. diff --git a/apps/sim/lib/credentials/token-service-accounts/descriptors.ts b/apps/sim/lib/credentials/token-service-accounts/descriptors.ts index 1ff8453881c..76945ec3c7b 100644 --- a/apps/sim/lib/credentials/token-service-accounts/descriptors.ts +++ b/apps/sim/lib/credentials/token-service-accounts/descriptors.ts @@ -424,7 +424,7 @@ export const TOKEN_SERVICE_ACCOUNT_DESCRIPTORS: Record< ], docsUrl: 'https://docs.sim.ai/integrations/coda', helpText: - 'Create a token under Account settings → API settings. A token restricted to specific docs or tables can only read and write those.', + 'Create a token under Account settings → API connections. A token restricted to specific docs or tables can only read and write those.', }, [SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID]: { providerId: SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID,