diff --git a/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.test.tsx b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.test.tsx new file mode 100644 index 00000000000..e81bc75df96 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.test.tsx @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from 'vitest' + +import { IntegrationConnection } from './IntegrationPanels' +import type { IntegrationProjectConnection } from '@/data/integrations/integrations.types' +import { render } from '@/tests/helpers' + +vi.mock('@/data/projects/project-detail-query', () => ({ + useProjectDetailQuery: () => ({ data: undefined, isPending: false }), +})) + +const connectionWithFramework = (framework: string) => + ({ + id: 'connection-1', + supabase_project_ref: 'project-ref', + foreign_project_id: 'vercel-project-1', + metadata: { id: 'vercel-project-1', name: 'my-app', framework }, + }) as IntegrationProjectConnection + +const frameworkIconSelector = 'img[src*="/img/icons/frameworks/"]' + +describe('IntegrationConnection', () => { + it('renders the fallback badge for a framework without a shipped icon', () => { + const { container } = render( + + ) + + expect(container.querySelector(frameworkIconSelector)).toBeNull() + }) + + it('renders the framework icon for a framework with a shipped icon', () => { + const { container } = render( + + ) + + expect(container.querySelector(frameworkIconSelector)?.getAttribute('src')).toMatch( + /\/img\/icons\/frameworks\/nextjs\.svg$/ + ) + }) +}) diff --git a/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx index 25c218a2134..1731a89d5d6 100644 --- a/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx +++ b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx @@ -13,7 +13,7 @@ import type { } from '@/data/integrations/integrations.types' import { useProjectDetailQuery } from '@/data/projects/project-detail-query' import { BASE_PATH } from '@/lib/constants' -import { getIntegrationConfigurationUrl } from '@/lib/integration-utils' +import { getFrameworkIconUrl, getIntegrationConfigurationUrl } from '@/lib/integration-utils' const ICON_STROKE_WIDTH = 2 const ICON_SIZE = 14 @@ -150,6 +150,7 @@ export const IntegrationConnection = forwardRef { const { data: project } = useProjectDetailQuery({ ref: connection.supabase_project_ref }) + const frameworkIconUrl = getFrameworkIconUrl(connection?.metadata?.framework) return (
  • - {!connection?.metadata?.framework ? ( + {!frameworkIconUrl ? (
    ) : ( - {`icon`} + {`icon`} )} {type === 'GitHub' ? ( { const getForeignProjectIcon = useCallback( (_project: ForeignProject) => { const project = vercelProjectsById[_project.id] + const frameworkIconUrl = getFrameworkIconUrl(project?.framework) - return !project?.framework ? ( + return !frameworkIconUrl ? ( vercelIcon ) : ( - {`icon`} + {`icon`} ) }, [vercelProjectsById] diff --git a/apps/studio/data/integrations/integrations.types.ts b/apps/studio/data/integrations/integrations.types.ts index 4e2d69fe60c..cc3344b1a46 100644 --- a/apps/studio/data/integrations/integrations.types.ts +++ b/apps/studio/data/integrations/integrations.types.ts @@ -1,50 +1,3 @@ -export type VercelFramework = - | ( - | 'blitzjs' - | 'nextjs' - | 'gatsby' - | 'remix' - | 'astro' - | 'hexo' - | 'eleventy' - | 'docusaurus-2' - | 'docusaurus' - | 'preact' - | 'solidstart' - | 'dojo' - | 'ember' - | 'vue' - | 'scully' - | 'ionic-angular' - | 'angular' - | 'polymer' - | 'svelte' - | 'sveltekit' - | 'sveltekit-1' - | 'ionic-react' - | 'create-react-app' - | 'gridsome' - | 'umijs' - | 'sapper' - | 'saber' - | 'stencil' - | 'nuxtjs' - | 'redwoodjs' - | 'hugo' - | 'jekyll' - | 'brunch' - | 'middleman' - | 'zola' - | 'hydrogen' - | 'vite' - | 'vitepress' - | 'vuepress' - | 'parcel' - | 'sanity' - | 'storybook' - ) - | null - export type VercelGitLink = | { /** @@ -142,7 +95,7 @@ export type Imetadata = { } link?: VercelGitLink name: string - framework: VercelFramework + framework: string | null } export type IntegrationProjectConnection = { diff --git a/apps/studio/lib/integration-utils.test.ts b/apps/studio/lib/integration-utils.test.ts index 070ab28e471..68767eba83a 100644 --- a/apps/studio/lib/integration-utils.test.ts +++ b/apps/studio/lib/integration-utils.test.ts @@ -1,6 +1,10 @@ +import { readdirSync } from 'node:fs' +import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { + FRAMEWORK_ICON_SLUGS, + getFrameworkIconUrl, getInitialMigrationSQLFromGitHubRepo, getIntegrationConfigurationUrl, } from './integration-utils' @@ -10,6 +14,7 @@ import type { VercelAccount, VercelTeamAccount, } from '@/data/integrations/integrations.types' +import { BASE_PATH } from '@/lib/constants' vi.mock('@/data/fetchers', () => ({ fetchHandler: vi.fn(), @@ -255,4 +260,23 @@ describe('integration-utils', () => { expect(result).toBe('') }) }) + + describe('getFrameworkIconUrl', () => { + it('returns undefined for missing or unshipped frameworks and a URL for shipped ones', () => { + expect(getFrameworkIconUrl('not-a-framework')).toBeUndefined() + expect(getFrameworkIconUrl(null)).toBeUndefined() + expect(getFrameworkIconUrl(undefined)).toBeUndefined() + expect(getFrameworkIconUrl('')).toBeUndefined() + expect(getFrameworkIconUrl('nextjs')).toBe(`${BASE_PATH}/img/icons/frameworks/nextjs.svg`) + }) + + it('stays in sync with public/img/icons/frameworks/*.svg', () => { + const slugsOnDisk = readdirSync(join(__dirname, '../public/img/icons/frameworks')) + .filter((file) => file.endsWith('.svg')) + .map((file) => file.replace(/\.svg$/, '')) + .sort() + + expect([...FRAMEWORK_ICON_SLUGS].sort()).toEqual(slugsOnDisk) + }) + }) }) diff --git a/apps/studio/lib/integration-utils.ts b/apps/studio/lib/integration-utils.ts index 3a17c746580..80e42858fee 100644 --- a/apps/studio/lib/integration-utils.ts +++ b/apps/studio/lib/integration-utils.ts @@ -3,6 +3,7 @@ import { getCreateMigrationsTableSQL, getInsertMigrationSQL } from '@supabase/pg import { isResponseOk } from './api/apiWrapper' import { fetchHandler } from '@/data/fetchers' import type { Integration } from '@/data/integrations/integrations.types' +import { BASE_PATH } from '@/lib/constants' import { ResponseError, type SupaResponse } from '@/types' async function fetchGitHub(url: string, responseJson = true): Promise> { @@ -132,3 +133,53 @@ function getGitHubConfigurationUrl(integration: GitHubIntegration) { : '' }settings/installations/${integration.metadata?.installation_id}` } + +export const FRAMEWORK_ICON_SLUGS: ReadonlySet = new Set([ + 'angular', + 'astro', + 'blitzjs', + 'brunch', + 'create-react-app', + 'docusaurus', + 'docusaurus-2', + 'dojo', + 'eleventy', + 'ember', + 'gatsby', + 'gridsome', + 'hexo', + 'hugo', + 'hydrogen', + 'ionic-angular', + 'ionic-react', + 'jekyll', + 'middleman', + 'nextjs', + 'nuxtjs', + 'parcel', + 'polymer', + 'preact', + 'redwoodjs', + 'remix', + 'saber', + 'sanity', + 'sapper', + 'scully', + 'solidstart', + 'stencil', + 'storybook', + 'svelte', + 'sveltekit', + 'sveltekit-1', + 'umijs', + 'vite', + 'vitepress', + 'vue', + 'vuepress', + 'zola', +]) + +export function getFrameworkIconUrl(framework?: string | null): string | undefined { + if (!framework || !FRAMEWORK_ICON_SLUGS.has(framework)) return undefined + return `${BASE_PATH}/img/icons/frameworks/${framework}.svg` +} diff --git a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx index ccd04468424..08c9592f422 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx @@ -25,8 +25,8 @@ import { useIntegrationVercelConnectionsCreateMutation } from '@/data/integratio import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { withAuth } from '@/hooks/misc/withAuth' -import { BASE_PATH } from '@/lib/constants' import { getErrorMessage } from '@/lib/get-error-message' +import { getFrameworkIconUrl } from '@/lib/integration-utils' import { hasVercelDeployButtonSignals } from '@/lib/integrations/vercel-install.utils' import { buildStudioPageTitle } from '@/lib/page-title' import { useProfileNameAndPicture } from '@/lib/profile' @@ -83,16 +83,12 @@ const VercelChooseProjectPage: NextPageWithLayout = () => { const getForeignProjectIcon = useCallback( (_project: ForeignProject) => { const project = vercelProjectsById[_project.id] + const frameworkIconUrl = getFrameworkIconUrl(project?.framework) - return !project?.framework ? ( + return !frameworkIconUrl ? ( vercelIcon ) : ( - Framework icon + Framework icon ) }, [vercelProjectsById]