From fadff0e35f0da0ff7eeec81a9846e4dd8ca697f8 Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 30 Jul 2026 14:38:49 -0700 Subject: [PATCH] fix(integrations): keep the Atlassian coverage hint visible, and trim dead surface (#6105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(integrations): keep the Atlassian coverage hint visible, and trim comments Cursor Bugbot: `ChipModalField` hides a `hint` whenever that field shows an `error`, so the multi-product coverage sentence vanished the moment the domain format check fired — exactly when someone mid-form most needs it. The sentence also describes the token, not the domain, so it read as domain guidance. Moves it to the API token field, which surfaces its errors through `ChipModalError` at the bottom rather than its own `error` prop, so the hint cannot be displaced. Also drops rationale comments that restated the code they sat above. * chore(integrations): trim the credential-display barrel to what consumers use * fix(emcn): let a custom modal field associate its hint with the control it wraps `ChipModalField` computes `aria-required`/`aria-invalid`/`aria-describedby` from its own state, but `type='custom'` returned its children untouched — so the `hint` and `error` text it renders was visible and never announced. The field cannot apply the ARIA itself here: a custom child may be a bare input or a wrapper several levels above one, which is the same reason `associatesLabel` already excludes custom from the label's `htmlFor`. Adds a function form for custom children that receives the ARIA, so the consumer — which knows where focus lands — attaches it. Existing `ReactNode` children are unaffected; all five current custom-field call sites keep working untouched. Uses it for the Atlassian API token field, whose coverage hint this branch had just relocated onto a custom field. --- .../[block]/integration-block-detail.tsx | 9 ++-- .../connect-service-account-modal.tsx | 43 ++++++++++--------- .../use-service-account-connect.ts | 7 +-- .../connected-credential-detail.tsx | 6 --- .../lib/integrations/credential-display.ts | 11 +++-- apps/sim/lib/integrations/index.ts | 5 --- .../src/components/chip-modal/chip-modal.tsx | 23 +++++++++- 7 files changed, 54 insertions(+), 50 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx index 6ad76f5996..1558d9d1ca 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx @@ -68,12 +68,9 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration useScrollRestoration(scrollContainerRef, { ready: !credentialsLoading }) /** - * Credentials that authenticate this integration. Matching goes through - * `credentialProviderMatchesService` so a family service account lists on - * every product it powers — one Atlassian token covers Jira, Jira Service - * Management, and Confluence. Comparing resolved `providerId`s instead would - * hide it from all three, since `atlassian-service-account` resolves to its - * own pseudo-service rather than to any product. + * Matches on the service's own id *or* its service-account id, so a family + * credential lists on every product it powers. Comparing resolved + * `providerId`s instead hides it from all of them. */ const connectedCredentials = useMemo(() => { if (!oauthService) return [] diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx index 22c36c6a71..e9ceb2b019 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx @@ -62,10 +62,11 @@ function openDocs(url: string): void { const ATLASSIAN_DOMAIN_HINT_REGEX = /^[a-z0-9-]+\.atlassian\.net$/i /** - * States the site-wide reach of the token up front. Users reaching this modal - * from the Jira page were left unsure whether they had connected Jira or Jira - * Service Management; the credential covers both, plus Confluence. Derived from - * the catalog so it cannot drift as Atlassian integrations are added. + * States the token's reach up front — the ambiguity this modal exists to remove. + * Sits on the API token field, not Site domain: it describes the token, and + * `ChipModalField` hides a `hint` whenever that field shows an `error`, which + * would drop it exactly while the user is correcting a domain typo. Derived + * from the catalog so it cannot drift as Atlassian integrations are added. */ const ATLASSIAN_COVERAGE_HINT = getServiceAccountCoverageSentence( ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID @@ -502,21 +503,24 @@ function AtlassianServiceAccountModal({ Add {serviceName} service account - - { - setApiToken(value) - if (error) setError(null) - }} - placeholder='Paste API token' - name='atlassian_service_account_api_token' - autoComplete='new-password' - autoCorrect='off' - autoCapitalize='off' - data-lpignore='true' - data-form-type='other' - /> + + {(aria) => ( + { + setApiToken(value) + if (error) setError(null) + }} + placeholder='Paste API token' + name='atlassian_service_account_api_token' + autoComplete='new-password' + autoCorrect='off' + autoCapitalize='off' + data-lpignore='true' + data-form-type='other' + /> + )} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx index 4ddfb4a502..13a7378f9b 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx @@ -96,12 +96,6 @@ export function ConnectedCredentialDetail({ [oauthServiceNameByProviderId] ) - /** - * Service, brand tile, and copy all come from the shared resolver so this - * page, the integrations list, and the Cmd-K search agree on how a credential - * is named and branded — a family service account reads as its family - * ("Atlassian"), not as whichever product the provider walk happened to hit. - */ const display = useMemo( () => (credential ? resolveCredentialDisplay(credential) : null), [credential] diff --git a/apps/sim/lib/integrations/credential-display.ts b/apps/sim/lib/integrations/credential-display.ts index 9b9b20d927..0dc2bcf5bf 100644 --- a/apps/sim/lib/integrations/credential-display.ts +++ b/apps/sim/lib/integrations/credential-display.ts @@ -3,12 +3,11 @@ * catalog integrations it powers, which mark and brand tile represent it, and * the sentence describing it. * - * Before this module, three surfaces (the integrations list, the Cmd-K search - * items, and the credential detail page) each re-derived this by looking the - * catalog up by the OAuth service's *display name*. That silently failed for - * family service accounts: `atlassian-service-account` resolves to a - * pseudo-service named "Atlassian Service Account", which matches no catalog - * integration, so the credential lost its brand tile and its category. + * Replaces a display-name-keyed catalog lookup that three surfaces each + * re-derived. That keying silently failed for family service accounts: + * `atlassian-service-account` resolves to a pseudo-service named "Atlassian + * Service Account", which matches no catalog entry, so the credential lost its + * brand tile and its category filter. */ import type { ComponentType } from 'react' diff --git a/apps/sim/lib/integrations/index.ts b/apps/sim/lib/integrations/index.ts index bfd42fb29c..12e58e2ff3 100644 --- a/apps/sim/lib/integrations/index.ts +++ b/apps/sim/lib/integrations/index.ts @@ -100,11 +100,6 @@ export function toIntegrationSummary(integration: Integration): IntegrationSumma export { type CredentialDisplay, - getIntegrationsForCredentialProvider, - getServiceAccountCoverageSentence, - getServiceAccountFamilyIcon, - getServiceAccountFamilyName, - isFamilyServiceAccount, resolveCredentialDisplay, } from '@/lib/integrations/credential-display' export { blockTypeToIconMap } from '@/lib/integrations/icon-mapping' diff --git a/packages/emcn/src/components/chip-modal/chip-modal.tsx b/packages/emcn/src/components/chip-modal/chip-modal.tsx index b722de672a..586c7e1fbf 100644 --- a/packages/emcn/src/components/chip-modal/chip-modal.tsx +++ b/packages/emcn/src/components/chip-modal/chip-modal.tsx @@ -549,9 +549,28 @@ export interface ChipModalEmailsFieldProps extends ChipModalFieldBaseProps { placeholder?: string } +/** + * ARIA the field derives from its own state and renders elsewhere in the row — + * the `hint`/`error` paragraph ids, plus `required`/`invalid` flags. + */ +export interface ChipModalFieldAria { + 'aria-required'?: boolean + 'aria-invalid'?: boolean + 'aria-describedby'?: string +} + interface ChipModalCustomFieldProps extends ChipModalFieldBaseProps { type: 'custom' - children: React.ReactNode + /** + * Arbitrary JSX, or a function receiving the field's {@link ChipModalFieldAria}. + * + * The owned control types wire this ARIA themselves, but a custom field can + * hold anything — a bare input, or a wrapper several levels above one — so + * the field cannot know which element should carry it. Use the function form + * whenever the child renders a focusable control, or its `hint`/`error` text + * is rendered but never announced. + */ + children: React.ReactNode | ((aria: ChipModalFieldAria) => React.ReactNode) } export type ChipModalFieldProps = @@ -718,7 +737,7 @@ function renderChipModalControl( case 'emails': return case 'custom': - return props.children + return typeof props.children === 'function' ? props.children(aria) : props.children } }