diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts index a4b1fed820e..c480deb7e9f 100644 --- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts +++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts @@ -170,6 +170,7 @@ describe('connect.schema:steps resolution', () => { const steps = resolveSteps(connectSchema, state) expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined() + expect(steps.find((s) => s.id === 'shadcn-env')).toBeDefined() expect(steps.find((s) => s.id === 'shadcn-explore')).toBeDefined() }) @@ -186,6 +187,7 @@ describe('connect.schema:steps resolution', () => { const steps = resolveSteps(connectSchema, state) expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined() + expect(steps.find((s) => s.id === 'shadcn-env')).toBeDefined() }) test('should resolve default steps for other frameworks', () => { @@ -337,6 +339,14 @@ describe('connect.schema:step content paths', () => { expect(exploreStep?.content).toBe('steps/shadcn/explore') }) + test('shadcn env step should have valid content path', () => { + const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true } + const steps = resolveSteps(connectSchema, state) + const envStep = steps.find((s) => s.id === 'shadcn-env') + + expect(envStep?.content).toBe('steps/shadcn/env') + }) + test('direct connection step should have valid content path', () => { const state: ConnectState = { mode: 'direct' } const steps = resolveSteps(connectSchema, state) diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts index 41723910b08..32f0f6f2460 100644 --- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts +++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts @@ -53,6 +53,13 @@ const frameworkShadcnStep: StepDefinition = { content: 'steps/shadcn/command', } +const frameworkShadcnEnvStep: StepDefinition = { + id: 'shadcn-env', + title: 'Set env variables', + description: 'Add the following values to your env file.', + content: 'steps/shadcn/env', +} + const frameworkShadcnExploreStep: StepDefinition = { id: 'shadcn-explore', title: 'Check out more UI components', @@ -297,6 +304,7 @@ export const connectSchema: ConnectSchema = { true: [ frameworkInstallStep, frameworkShadcnStep, + frameworkShadcnEnvStep, frameworkShadcnExploreStep, skillsInstallStep, ], @@ -308,6 +316,7 @@ export const connectSchema: ConnectSchema = { true: [ frameworkInstallStep, frameworkShadcnStep, + frameworkShadcnEnvStep, frameworkShadcnExploreStep, skillsInstallStep, ], diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/env/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/env/content.tsx new file mode 100644 index 00000000000..6bef222fdcd --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/shadcn/env/content.tsx @@ -0,0 +1,52 @@ +import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock' + +import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' + +function getEnvFile( + state: StepContentProps['state'], + projectKeys: StepContentProps['projectKeys'] +) { + if (state.framework === 'nextjs') { + return { + name: '.env.local', + language: 'bash', + code: [ + `NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys.publishableKey + ? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + } + } + + if (state.framework === 'react') { + const isCreateReactApp = state.frameworkVariant === 'create-react-app' + const envFileName = isCreateReactApp ? '.env.local' : '.env' + const keyPrefix = isCreateReactApp ? 'REACT_APP' : 'VITE' + + return { + name: envFileName, + language: 'bash', + code: [ + `${keyPrefix}_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, + projectKeys.publishableKey + ? `${keyPrefix}_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}` + : `${keyPrefix}_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, + '', + ].join('\n'), + } + } + + return null +} + +function ShadcnEnvContent({ state, projectKeys }: StepContentProps) { + const envFile = getEnvFile(state, projectKeys) + + if (!envFile) return null + + return +} + +export default ShadcnEnvContent diff --git a/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts b/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts index dbd40cd39a8..a882d8c77de 100644 --- a/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts +++ b/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts @@ -371,6 +371,7 @@ describe('useConnectState', () => { const stepIds = result.current.resolvedSteps.map((s) => s.id) expect(stepIds).toContain('shadcn-add') + expect(stepIds).toContain('shadcn-env') }) }) diff --git a/packages/ui-patterns/src/MultipleCodeBlock/index.test.tsx b/packages/ui-patterns/src/MultipleCodeBlock/index.test.tsx new file mode 100644 index 00000000000..fa9025fa192 --- /dev/null +++ b/packages/ui-patterns/src/MultipleCodeBlock/index.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { MultipleCodeBlock } from './index' + +describe('MultipleCodeBlock', () => { + it('resets the active tab when the available files change in uncontrolled mode', () => { + const { rerender } = render( + + ) + + expect(screen.getByRole('tab', { selected: true })).toHaveTextContent('.env.local') + expect(screen.getByText('NEXT_PUBLIC_SUPABASE_URL=https://next.example')).toBeVisible() + + rerender( + + ) + + expect(screen.getByRole('tab', { selected: true })).toHaveTextContent('.env') + expect(screen.getByText('VITE_SUPABASE_URL=https://react.example')).toBeVisible() + }) +}) diff --git a/packages/ui-patterns/src/MultipleCodeBlock/index.tsx b/packages/ui-patterns/src/MultipleCodeBlock/index.tsx index f9bb257ca1e..19a75e25542 100644 --- a/packages/ui-patterns/src/MultipleCodeBlock/index.tsx +++ b/packages/ui-patterns/src/MultipleCodeBlock/index.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from 'react' + import { CodeBlock, CodeBlockLang, @@ -102,17 +104,37 @@ export const MultipleCodeBlock = ({ files, value, onValueChange }: MultipleCodeB } const defaultValue = files[0]?.name ?? '' + const isControlled = value !== undefined + const [internalValue, setInternalValue] = useState(defaultValue) const trimmedFiles = files.map((file) => ({ ...file, code: typeof file.code === 'string' ? file.code.trim() : file.code, })) + useEffect(() => { + if (isControlled) return + + setInternalValue((currentValue) => { + const currentValueExists = files.some((file) => file.name === currentValue) + return currentValueExists ? currentValue : defaultValue + }) + }, [defaultValue, files, isControlled]) + + const activeValue = isControlled ? value : internalValue + + const handleValueChange = (nextValue: string) => { + if (!isControlled) { + setInternalValue(nextValue) + } + + onValueChange?.(nextValue) + } + return (