Files
Danny White 5677b0ec2a chore(studio): clarify integration settings copy (#47578)
## Summary
- Clarifies Vercel integration settings copy for org-scoped and
project-scoped contexts.
- Updates GitHub and Vercel integration section titles to sentence case
for in-page headings.
- Contributes to DEPR-565.

| Before | After |
| --- | --- |
| <img width="1242" height="759" alt="Integrations Basket Supabase"
src="https://github.com/user-attachments/assets/df33a9d4-8fb3-40cf-87d2-e87fa33195e4"
/> | <img width="1150" height="715" alt="Integrations Basket Supabase"
src="https://github.com/user-attachments/assets/45478216-c426-4bc1-9292-9a6016ac7af9"
/> |
| <img width="1242" height="759" alt="18154"
src="https://github.com/user-attachments/assets/8dfb7742-fca5-421f-88d0-4d24dad93450"
/> | <img width="1150" height="715" alt="Integrations Settings Agua
Basket Supabase"
src="https://github.com/user-attachments/assets/aa119d0e-cf5e-45e5-b5d3-cd8c0d047e34"
/> |

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **UI Text Updates**
* Updated GitHub and Vercel headings and labels to use consistent casing
(e.g., “GitHub connection”, “Vercel”).
* Adjusted success and empty-state messaging for GitHub and Vercel
integration actions.

* **UX Improvements**
* Improved GitHub and Vercel section descriptions by tailoring the text
to project-scoped vs organization-scoped contexts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-08 12:56:03 +10:00

245 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ExternalLink } from 'lucide-react'
import { useCallback, useMemo } from 'react'
import { toast } from 'sonner'
import { cn } from 'ui'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { IntegrationSectionIcon } from '../IntegrationsSettings'
import VercelIntegrationConnectionForm from './VercelIntegrationConnectionForm'
import { IntegrationConnectionItem } from '@/components/interfaces/Integrations/VercelGithub/IntegrationConnection'
import {
EmptyIntegrationConnection,
IntegrationConnectionHeader,
IntegrationInstallation,
} from '@/components/interfaces/Integrations/VercelGithub/IntegrationPanels'
import { InlineLink } from '@/components/ui/InlineLink'
import { NoPermission } from '@/components/ui/NoPermission'
import { useOrgIntegrationsQuery } from '@/data/integrations/integrations-query-org-only'
import { useIntegrationsVercelInstalledConnectionDeleteMutation } from '@/data/integrations/integrations-vercel-installed-connection-delete-mutation'
import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query'
import type {
Integration,
IntegrationName,
IntegrationProjectConnection,
} from '@/data/integrations/integrations.types'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { pluralize } from '@/lib/helpers'
import { getIntegrationConfigurationUrl } from '@/lib/integration-utils'
import { useSidePanelsStateSnapshot } from '@/state/side-panels'
export const VercelSection = ({ isProjectScoped }: { isProjectScoped: boolean }) => {
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
const { data } = useOrgIntegrationsQuery({ orgSlug: org?.slug })
const sidePanelsStateSnapshot = useSidePanelsStateSnapshot()
const isBranch = project?.parent_project_ref !== undefined
const { can: canReadVercelConnection, isLoading: isLoadingPermissions } =
useAsyncCheckPermissions(PermissionAction.READ, 'integrations.vercel_connections')
const { can: canCreateVercelConnection } = useAsyncCheckPermissions(
PermissionAction.CREATE,
'integrations.vercel_connections'
)
const { can: canUpdateVercelConnection } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'integrations.vercel_connections'
)
const { mutate: deleteVercelConnection } = useIntegrationsVercelInstalledConnectionDeleteMutation(
{
onSuccess: () => {
toast.success('Successfully deleted Vercel connection')
},
}
)
const vercelIntegrations = useMemo(() => {
return data
?.filter((integration) => integration.integration.name === 'Vercel')
.map((integration) => {
if (integration.metadata && integration.integration.name === 'Vercel') {
const avatarSrc =
!integration.metadata.account.avatar && integration.metadata.account.type === 'Team'
? `https://vercel.com/api/www/avatar?teamId=${integration.metadata.account.team_id}&s=48&format=png`
: `https://vercel.com/api/www/avatar/${integration.metadata.account.avatar}?s=48`
return {
...integration,
metadata: {
...integration.metadata,
account: {
...integration.metadata.account,
avatar: avatarSrc,
},
},
} as Integration
}
return integration
})
}, [data])
// We're only supporting one Vercel integration per org for now
// this will need to be updated when we support multiple integrations
const vercelIntegration = vercelIntegrations?.[0]
const { data: vercelProjectsData } = useVercelProjectsQuery(
{
organization_integration_id: vercelIntegration?.id,
},
{ enabled: vercelIntegration?.id !== undefined }
)
const vercelProjectCount = vercelProjectsData?.length ?? 0
const onAddVercelConnection = useCallback(
(integrationId: string) => {
sidePanelsStateSnapshot.setVercelConnectionsIntegrationId(integrationId)
sidePanelsStateSnapshot.setVercelConnectionsOpen(true)
},
[sidePanelsStateSnapshot]
)
const onDeleteVercelConnection = useCallback(
async (connection: IntegrationProjectConnection) => {
deleteVercelConnection({
id: connection.id,
organization_integration_id: connection.organization_integration_id,
orgSlug: org?.slug,
})
},
[deleteVercelConnection, org?.slug]
)
const integrationUrl =
process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
? 'https://vercel.com/integrations/supabase'
: process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
? `https://vercel.com/integrations/supabase-staging`
: 'https://vercel.com/integrations/supabase-local'
let connections =
(isProjectScoped
? vercelIntegration?.connections.filter(
(connection) => connection.supabase_project_ref === project?.ref
)
: vercelIntegration?.connections) || []
const ConnectionHeaderTitle = `${connections.length} project ${pluralize(
connections.length,
'connection'
)} `
const description = isProjectScoped
? 'Connect Vercel projects to this Supabase project. Supabase keeps environment variables up to date in each connected Vercel project.'
: 'Connect your Vercel teams to this Supabase organization. Supabase keeps environment variables up to date in each connected project. You can also link multiple Vercel projects to the same Supabase project.'
return (
<PageSection>
<PageSectionMeta>
<div className="flex flex-1 items-start gap-6">
<IntegrationSectionIcon title="vercel" />
<PageSectionSummary>
<PageSectionTitle>Vercel</PageSectionTitle>
<PageSectionDescription>{description}</PageSectionDescription>
</PageSectionSummary>
</div>
</PageSectionMeta>
<PageSectionContent>
{isLoadingPermissions ? (
<GenericSkeletonLoader />
) : !canReadVercelConnection ? (
<NoPermission resourceText="view this organization's Vercel connections" />
) : (
<div className="space-y-6">
<div>
{vercelIntegration ? (
<div key={vercelIntegration.id}>
<IntegrationInstallation title={'Vercel'} integration={vercelIntegration} />
{connections.length > 0 ? (
<>
<IntegrationConnectionHeader
title={ConnectionHeaderTitle}
markdown={`Repository connections for Vercel`}
/>
<ul className="flex flex-col">
{connections.map((connection) => (
<div
key={connection.id}
className={cn(
isProjectScoped && 'relative flex flex-col -gap-[1px] [&>li]:pb-0'
)}
>
<IntegrationConnectionItem
connection={connection}
disabled={isBranch || !canUpdateVercelConnection}
type={'Vercel' as IntegrationName}
onDeleteConnection={onDeleteVercelConnection}
className={cn(isProjectScoped && 'rounded-b-none! mb-0!')}
/>
{isProjectScoped ? (
<div className="relative pl-8 ml-6 border-l border-muted pb-6">
<div className="border-b border-l border-r rounded-b-lg">
<VercelIntegrationConnectionForm
connection={connection}
integration={vercelIntegration}
disabled={isBranch || !canUpdateVercelConnection}
/>
</div>
</div>
) : null}
</div>
))}
</ul>
</>
) : (
<IntegrationConnectionHeader
title={ConnectionHeaderTitle}
className="pb-0"
markdown={`Repository connections for Vercel`}
/>
)}
<EmptyIntegrationConnection
disabled={isBranch || !canCreateVercelConnection}
onClick={() => onAddVercelConnection(vercelIntegration.id)}
>
Add new project connection
</EmptyIntegrationConnection>
</div>
) : (
<EmptyIntegrationConnection
showNode={false}
disabled={isBranch}
href={integrationUrl}
icon={<ExternalLink />}
disabledTooltip="Install the Vercel integration on your project's main branch"
>
Install Vercel integration
</EmptyIntegrationConnection>
)}
</div>
{vercelProjectCount > 0 && vercelIntegration !== undefined && (
<p className="text-sm text-foreground-light">
Your Vercel connection can access {vercelProjectCount} Vercel projects. To change
which projects Supabase may use, open your organization’s{' '}
<InlineLink href={getIntegrationConfigurationUrl(vercelIntegration)}>
Vercel integration settings
</InlineLink>
.
</p>
)}
</div>
)}
</PageSectionContent>
</PageSection>
)
}